mux: add high-level mux_stream driver (layering step 4a)
Add the shared decrypt+mux driver both consumers hand-roll today, as a
library-only API. `mux_stream(input, dest_url, opts, halt, events)` runs
the construct -> headers gate -> open sink -> pump -> finish pipeline:
- MuxInput::{Session, Iso, Url} selects the source. The file/ISO path
calls the untouched build_iso_pipeline highway (zero added copies —
the driver reads frames exactly where consumers call stream.read());
the live path builds a DiscStream; a URL source goes through input().
- chapters:// / json:// metadata sinks short-circuit BEFORE the header
pump/gate. They write their whole file from the scanned title and need
no codec headers; the CLI placed this after the gate, so a metadata
export on a title whose video headers never resolved failed with
MkvInvalid. Fixed by construction.
- The header gate refuses a stream with no resolved codec_private
(MkvInvalid); the zero-output gate refuses an empty/undecryptable drain
(NoStreams); a halt mid-pump yields completed=false, never a success.
- Frames are written through a WRITE_PIPELINE_DEPTH consumer pipeline so
the latency-bound sink write overlaps the next read.
Add error::is_skippable_title_stub(&io::Error) so consumers can drop the
E7023/E6008 string-match, and DiscSession::take_reader for the live arm.
Driver body unit-tested in isolation via a synthetic Stream against
null:// / chapters:// / json:// sinks (short-circuit, header gate,
zero-output gate, halt, happy path); each gate mutation-verified.
Consumers are NOT migrated yet (steps 4b/4c).
This commit is contained in:
@@ -879,6 +879,39 @@ impl From<Error> for std::io::Error {
|
||||
/// Convenience alias for `Result<T, Error>`.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The numeric error code carried by an [`io::Error`](std::io::Error) that was
|
||||
/// produced from an [`Error`], or `None` if it carries none.
|
||||
///
|
||||
/// Two shapes are recognised: an `io::Error` that still wraps the typed
|
||||
/// [`Error`] (via `io::Error::new(kind, Error)`), and the round-tripped form
|
||||
/// produced by [`From<Error> for io::Error`] whose message is the `Error`'s
|
||||
/// `E<code>[: …]` [`Display`](std::fmt::Display) string.
|
||||
fn io_error_code(e: &std::io::Error) -> Option<u16> {
|
||||
// Direct: the io::Error still holds the typed Error.
|
||||
if let Some(err) = e.get_ref().and_then(|r| r.downcast_ref::<Error>()) {
|
||||
return Some(err.code());
|
||||
}
|
||||
// Round-tripped: `From<Error> for io::Error` stringifies as "E<code>[: …]".
|
||||
let s = e.to_string();
|
||||
let digits = s.strip_prefix('E')?;
|
||||
let end = digits
|
||||
.find(|c: char| !c.is_ascii_digit())
|
||||
.unwrap_or(digits.len());
|
||||
digits.get(..end)?.parse::<u16>().ok()
|
||||
}
|
||||
|
||||
/// Whether a per-title mux failure is a *skippable title stub* — a
|
||||
/// copy-protected-but-uncrackable title ([`Error::CssKeyMissing`]) or a title
|
||||
/// that produced no muxable frames ([`Error::MkvInvalid`], an empty nav/menu
|
||||
/// PGC stub). An all-titles rip skips such a title and finishes the rest;
|
||||
/// every other error stays fatal.
|
||||
///
|
||||
/// This replaces the CLI's `E7023`/`E6008` string-match with a typed check on
|
||||
/// the [`io::Error`](std::io::Error) `mux_stream` returns.
|
||||
pub fn is_skippable_title_stub(e: &std::io::Error) -> bool {
|
||||
matches!(io_error_code(e), Some(E_MKV_INVALID | E_CSS_KEY_MISSING))
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Borrow the drive-returned SPC-4 sense triple if this error is a
|
||||
/// [`Error::ScsiError`] carrying sense data. `None` for any other
|
||||
@@ -973,6 +1006,25 @@ mod tests {
|
||||
//! match arms in `code()` / the From impl could silently miscategorize.
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_skippable_title_stub_matches_only_the_two_stub_codes() {
|
||||
// The two skippable per-title stub codes, round-tripped through io::Error
|
||||
// exactly as `mux_stream` returns them.
|
||||
let mkv: std::io::Error = Error::MkvInvalid.into();
|
||||
let css: std::io::Error = Error::CssKeyMissing.into();
|
||||
assert!(is_skippable_title_stub(&mkv));
|
||||
assert!(is_skippable_title_stub(&css));
|
||||
|
||||
// A different coded error is NOT skippable (kills a "match anything with
|
||||
// an E-code" mutant).
|
||||
let nostreams: std::io::Error = Error::NoStreams.into();
|
||||
assert!(!is_skippable_title_stub(&nostreams));
|
||||
|
||||
// A plain io::Error with no E-code prefix is not skippable.
|
||||
let plain = std::io::Error::from(std::io::ErrorKind::BrokenPipe);
|
||||
assert!(!is_skippable_title_stub(&plain));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_variants_have_distinct_codes() {
|
||||
let codes = [
|
||||
|
||||
Reference in New Issue
Block a user