mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
source: open_source opens every input incl. disc://; MuxSource replaced by Source; one image probe; disc-named output titles
This commit is contained in:
+3
-1
@@ -5,9 +5,10 @@
|
||||
### Breaking
|
||||
|
||||
- **AACS keys:** `keys::KeyRing` is the only door to AACS keys. Removed: crate-root `AacsKeyMap`, `DecryptKeys`, `resolve_and_apply`, `KeyFetch`; `resolve_and_apply_traced`, `keysource::{fetch_unit_keys, fetch_fmts_indexes, key_fetch}`, `sector::{KeyFetch, KeyFetchFn}`, `session::{resolve_keys_for, ResolvedKeys}`, `DiscSession::{resolve_keys, key_fetch}`, `Disc::{decrypt_keys, decrypt_keys_for_title, decrypt_with, ensure_decryptable, ensure_decryptable_keys, ensure_title_decryptable}`, `InputOptions::{unit_keys, key_fetch}`, `DiscStream::with_key_map`. `DecryptKeys` is `#[non_exhaustive]`; `AacsKeyMap::from_ranges*`, `DecryptingSectorSource::{with_key_map, set_key_map}` and `aacs::content::{decrypt_unit, decrypt_bus}` are no longer public. Acquire once with `DiscSession::resolve_key_set` / `KeyRing::acquire`.
|
||||
- **Mux entry point:** `mux_stream` and `MuxInput` are replaced by `mux_with_keys(MuxSource, Option<&KeyRing>, dest_url, opts, ctx)`; `MuxSource` (`Session`, `Iso`, `Live`, `Url`) carries no key material. `InputOptions::keys: Option<KeyRing>` replaces `unit_keys` / `key_fetch`; an AACS image opened without a set is refused before any output. Crate-root `build_iso_pipeline` and `resolve_mux_key_map` are removed.
|
||||
- **Mux entry point:** `mux_stream` and `MuxInput` are replaced by `mux_with_keys(Source, Option<&KeyRing>, dest_url, opts, ctx)`; a `Source` carries no key material. `InputOptions::keys: Option<KeyRing>` replaces `unit_keys` / `key_fetch`; an AACS image opened without a set is refused before any output. Crate-root `build_iso_pipeline` and `resolve_mux_key_map` are removed.
|
||||
- **One run context for every stage:** `Ctx { halt, events, stats, diag }` is passed to `input(url, opts, ctx)`, `mux_with_keys(.., ctx)`, `Disc::extract_tree(reader, dest, opts, ctx)`, `write_image(reader, dest, sectors, ctx)`, `DiscStream::new(.., ctx)` and `PrefetchedSectorSource::new(.., ctx)`. One `Events` trait over one `Event` enum (`BytesRead`, `SectorSkipped`, `UnitBlanked`, `BatchSizeChanged`, `Pass`, `BytesWritten`, `BytesDurable`, `OutputOpened`) replaces `MuxEvents`, `event::{Event, EventKind}`, `progress::Progress`, `EventFn` / `PrefetchedSectorSource::new_with_events`, `DiscStream::{on_event, with_halt}` and `Drive::on_event`; `ExtractOptions::{halt, progress}` are removed (Stop is `ctx.halt`; extract progress is `PassKind::Extract`). `Stats` / `LossReport` count read skips, lost bytes, blanked units and resync drops across the run. `halt::Progress` is renamed `halt::Liveness`. `FREEMKV_SKIP_PARSE` / `FREEMKV_PROFILE` take effect only through `Diag::from_env()` on the caller's `Ctx`.
|
||||
- **Key acquisition is not bound to a scanned disc:** `ResolvedKeySet` is renamed `KeyRing` and `ResolveKeysOptions` `AcquireOptions` (its `halt` is the run's `Ctx`; `resolve_with_progress` is `AcquireOptions::liveness`). `KeyRing::acquire(&KeyEvidence, &mut Sampler, sources, opts, ctx)` takes what a source can tell (`KeyEvidence`: `MediaId`, AACS structures, `Piece`s in scope, main title, `Detector`), and `KeyRing::acquire_for_disc(disc, reader, scope, sources, opts, ctx)` builds that evidence from a scanned disc (`KeyEvidence::from_disc`). `KeyRing::is_for` takes a `&MediaId` (`Disc::media_id()`), which includes the Volume ID fingerprint. Evidence with no disc hash derives it from the title-key file, so a keydb lookup no longer misses silently. The legacy `aacs::resolve` / `aacs::provider` ladder and `disc::Key` are removed; `KeyOrigin` lives in `aacs::trace` (still re-exported at the crate root).
|
||||
- **One way to open any input:** `open_source(url, ScanOptions, ctx)` opens every input scheme, `disc://` included (the drive is brought up and scanned; `input("disc://…")` now opens it instead of returning `DiscUrlNotDirect`), and `Source::layout()` is the probe's scanned `Disc` for `disc://`, `iso://` and `dir://`. `MuxSource` is removed: `Source::from_session(&mut DiscSession)`, `Source::from_image(path, ScannedTitle)` (a title scanned elsewhere, never rescanned) and `Source::from_reader(reader, ScannedTitle)` replace `Session`, `Iso` and `Live`, and `mux_url(url, keys, dest, opts, ctx)` replaces `Url`. `MuxOptions` gains `title_index` (it replaces `MuxSource::Session::title_index` and `InputOptions::title_index` for a mux), and `MuxOptions::selection` applies to every source. `scan_iso` / `scan_dir` share the one image probe.
|
||||
- **Stop is the only send bound:** `MuxOptions::send_deadline`, `NO_SEND_DEADLINE` and `halt::POLL_INTERVAL` are removed (use `halt::WAIT_SLICE`); a slow sink blocks the mux until it drains or Stop.
|
||||
- `Drive::clear_halt`, `Drive::scsi_mut` and `Drive::is_unlocked` and the path-based `platform::fs_type::detect` are removed; use `Drive::attach` / `detach` / `halt` / `token` and `DiscSession::source_mut`.
|
||||
- `DvdAudioAttr::sub_stream_id` (positional id) is removed.
|
||||
@@ -29,6 +30,7 @@
|
||||
- **`json://`** audio `sample_rate` is a number in Hz (`null` when unknown), not a string such as `"48kHz"`; a `sample_rates` array lists every rate a stream carries.
|
||||
- **FMKV (`network://`, `stdio://`) header version 2** is used when a track carries decoder timing (Opus CodecDelay/SeekPreRoll) and adds per-frame DiscardPadding; streams without timing stay version 1. Headers now also carry display aspect, CICP, stream purpose/qualifier, chapters and content format, and PIDs actually written by M2TS. A header declaring more than 256 streams is refused with E9008. An older receiver omits the MKV Channels element for the new channel-layout strings; keep both ends on one version.
|
||||
- **`dir://` image bytes:** the PVD declares interchange level 3, Unique IDs start at 16 and the volume id round-trips through the d-string encoder; `dir://` sources are stamped as images in the `fvi://` provenance header.
|
||||
- **One output title name:** a title read from a disc layout (`disc://`, `iso://`, `dir://`, a staged image, a server live rip) is named by the disc's meta title (else its volume id) in every container's title field and in `fvi://` provenance; 1.7.7 named image and server-live rips by the MPLS playlist file. `fvi://` provenance of an `iso://`/`dir://` source now carries the volume id.
|
||||
- **Frames dropped by the resync gate count as errors** in `MuxOutcome::errors` / `lost_bytes`, together with AACS units blanked as damaged.
|
||||
- **A failed or stopped `network://` / `stdio://` sender resets the connection** (`Stream::finish_incomplete`), so the receiver errors instead of finishing a truncated title.
|
||||
- **Key-service 404 / 422 answers are E7022** (no key opens the title), and a key-service outage or unreachable source is a source failure, never "no key for this disc".
|
||||
|
||||
@@ -750,12 +750,8 @@ fn a_dir_url_mux_reports_read_progress_to_the_ctx() {
|
||||
}
|
||||
}));
|
||||
let url = format!("dir://{}", s.path().display());
|
||||
let src = crate::MuxSource::Url {
|
||||
url: &url,
|
||||
opts: crate::InputOptions::default(),
|
||||
};
|
||||
// The fixture's TS carries no PES, so the drain refuses as NoStreams after reading it all.
|
||||
let _ = crate::mux_with_keys(src, None, "null://", &crate::MuxOptions::default(), &ctx);
|
||||
let _ = crate::mux_url(&url, None, "null://", &crate::MuxOptions::default(), &ctx);
|
||||
assert_eq!(
|
||||
read.load(std::sync::atomic::Ordering::Relaxed),
|
||||
4096 * 192,
|
||||
|
||||
+4
-5
@@ -1904,11 +1904,10 @@ fn resolve_retains_no_source_and_nothing_asks_after() {
|
||||
measured_cicp: None,
|
||||
}));
|
||||
let r = crate::mux::mux_with_keys(
|
||||
crate::mux::MuxSource::Live {
|
||||
reader: Box::new(src.clone()),
|
||||
title,
|
||||
format: ContentFormat::BdTs,
|
||||
},
|
||||
crate::mux::Source::from_reader(
|
||||
Box::new(src.clone()),
|
||||
crate::mux::ScannedTitle::new(title, ContentFormat::BdTs),
|
||||
),
|
||||
Some(&set),
|
||||
"null://",
|
||||
&crate::mux::MuxOptions {
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
//! see [`Golden`](crate::test_util::Golden) for the re-bless command.
|
||||
|
||||
use super::*;
|
||||
use crate::mux::driver::{MuxOptions, MuxSource, mux_with_keys};
|
||||
use crate::mux::driver::{MuxOptions, mux_with_keys};
|
||||
use crate::mux::parity_tests::{golden, record_run, record_tree};
|
||||
use crate::mux::select::{PidFilter, StreamSelection};
|
||||
use crate::mux::source::{ScannedTitle, Source};
|
||||
use crate::test_util::{CLIP_AUDIO_PIDS, Golden, synthetic_bd_clip};
|
||||
|
||||
fn refused<T>(g: &mut Golden, key: &str, r: &Result<T>) -> bool {
|
||||
@@ -139,17 +140,9 @@ fn mux_cells(g: &mut Golden, fx: &Fx, set: Option<&KeyRing>, dead: Option<(u32,
|
||||
if let Some((a, b)) = dead {
|
||||
src.kill(a, b);
|
||||
}
|
||||
MuxSource::Live {
|
||||
reader: Box::new(src),
|
||||
title: title.clone(),
|
||||
format,
|
||||
}
|
||||
Source::from_reader(Box::new(src), ScannedTitle::new(title.clone(), format))
|
||||
} else {
|
||||
MuxSource::Iso {
|
||||
path: &iso,
|
||||
title: title.clone(),
|
||||
format,
|
||||
}
|
||||
Source::from_image(&iso, ScannedTitle::new(title.clone(), format))
|
||||
};
|
||||
let r = mux_with_keys(source, set, &dest, opts, &crate::ctx::Ctx::default());
|
||||
record_run(g, tag, &r);
|
||||
|
||||
@@ -6,8 +6,9 @@ use super::*;
|
||||
use crate::dirimage::tests::{minimal_clpi, one_item_mpls};
|
||||
use crate::disc::ScanOptions;
|
||||
use crate::drive::Drive;
|
||||
use crate::mux::driver::{MuxOptions, MuxSource, mux_with_keys};
|
||||
use crate::mux::driver::{MuxOptions, mux_with_keys};
|
||||
use crate::mux::parity_tests::{golden, record_run, record_tree};
|
||||
use crate::mux::source::Source;
|
||||
use crate::session::{DiscSession, KeySpec};
|
||||
use crate::test_util::{FakeMode, FakeTransport, Golden, synthetic_bd_clip};
|
||||
|
||||
@@ -79,10 +80,7 @@ fn mux(
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = format!("mkv://{}", dir.path().join("o.mkv").display());
|
||||
let r = mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session: s,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(s),
|
||||
set,
|
||||
&dest,
|
||||
opts,
|
||||
@@ -231,10 +229,7 @@ fn parity_live_bd_stop_mid_read() {
|
||||
armed.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let r = mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session: &mut s,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(&mut s),
|
||||
Some(&set),
|
||||
&format!("mkv://{}", dir.path().join("o.mkv").display()),
|
||||
&MuxOptions {
|
||||
|
||||
+1
-1
@@ -254,7 +254,7 @@ pub use mux::{Mp4FitReport, Mp4Sink, Mp4SkipReason, mp4_fit_report};
|
||||
#[cfg(feature = "rip")]
|
||||
pub use mux::select::{PidFilter, StreamSelection};
|
||||
#[cfg(feature = "rip")]
|
||||
pub use mux::{MuxOptions, MuxOutcome, MuxSource, mux_with_keys};
|
||||
pub use mux::{MuxOptions, MuxOutcome, ScannedTitle, Source, mux_url, mux_with_keys, open_source};
|
||||
pub use scsi::{
|
||||
DiscPresence, DriveInfo, ScsiSense, ScsiTransport, SenseFamily, disc_presence, drive_has_disc,
|
||||
list_drives,
|
||||
|
||||
+295
-457
@@ -5,7 +5,6 @@
|
||||
//! [`mux_with_keys`] DRIVES the existing pipeline via the same ISO pipeline builders
|
||||
//! and a [`WRITE_PIPELINE_DEPTH`]-deep write [`Pipeline`]; it does not replace either.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
@@ -22,14 +21,15 @@ use crate::pes::{CountingStream, PesFrame, Stream};
|
||||
use crate::sector::{FileSectorSource, SectorSource};
|
||||
use crate::session::DiscSession;
|
||||
|
||||
use super::resolve::{
|
||||
InputOptions, StreamUrl, build_iso_pipeline, input, output, output_with, parse_url,
|
||||
};
|
||||
use super::resolve::{InputOptions, StreamUrl, build_iso_pipeline, output, output_with, parse_url};
|
||||
#[cfg(test)]
|
||||
use super::source::ScannedTitle;
|
||||
use super::source::{Origin, Source};
|
||||
use super::videomap::{Medium, SourceInfo};
|
||||
|
||||
// The source medium a parsed input URL denotes, used only for provenance. Exhaustive, so a
|
||||
// new scheme must be placed; sink-only schemes never reach here and take the `File` default.
|
||||
fn url_medium(parsed: &StreamUrl) -> Medium {
|
||||
pub(crate) fn url_medium(parsed: &StreamUrl) -> Medium {
|
||||
match parsed {
|
||||
StreamUrl::Disc { .. } => Medium::Disc,
|
||||
// `dir://` is an image-level source: a UDF volume synthesized over the folder.
|
||||
@@ -60,7 +60,7 @@ pub(crate) const HEADER_BUFFER_CAP_BYTES: usize = 512 * 1024 * 1024;
|
||||
/// `Default` = keep-everything, no-skip, decrypt — the archival default. Added
|
||||
/// so callers set only the fields they care about (and so an additive field
|
||||
/// doesn't churn every constructor).
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MuxOptions {
|
||||
/// Skip past read errors (zero-fill + continue) on the live-drive path
|
||||
/// instead of aborting. Wired onto `DiscStream::skip_errors`.
|
||||
@@ -74,11 +74,11 @@ pub struct MuxOptions {
|
||||
/// demux pipeline is built, so track headers, `codec_privates`, and frame
|
||||
/// routing all follow the pruned list. See [`crate::StreamSelection`].
|
||||
///
|
||||
/// Applies to the `Iso`, `Session` and `Live` inputs. It does NOT apply to
|
||||
/// [`MuxSource::Url`], which builds its demux inside `input()` — a Url-source
|
||||
/// caller sets `InputOptions::selection` instead. Setting this field for a Url
|
||||
/// input has no effect.
|
||||
/// Applies to every [`Source`].
|
||||
pub selection: crate::StreamSelection,
|
||||
/// 0-based index of the title to mux in the source's layout (a container source has
|
||||
/// one title, 0). Out of range is [`Error::MuxTrackRange`].
|
||||
pub title_index: usize,
|
||||
}
|
||||
|
||||
/// The result of a [`mux_with_keys`] run.
|
||||
@@ -128,232 +128,17 @@ impl MuxOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
// The mux of a source with no AACS key set (clear, CSS, `raw`, or a URL that scans):
|
||||
// construct the source stream, open the `dest_url` sink and pump it (`drive_mux`).
|
||||
fn mux_unkeyed(
|
||||
input_src: MuxSource,
|
||||
dest_url: &str,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<MuxOutcome> {
|
||||
// Construct the source stream (ISO/live/URL) under `ctx`. Each arm also derives
|
||||
// SourceInfo since `output()` has no access to source.
|
||||
let (stream, playlist_name, mut source): (Box<dyn Stream>, Option<String>, SourceInfo) =
|
||||
match input_src {
|
||||
// The Url path builds its demux INSIDE `input()`, pruned via
|
||||
// `InputOptions.selection`. `MuxOptions.selection` does NOT apply
|
||||
// here — that's the File/Session arms' field.
|
||||
MuxSource::Url { url, opts: in_opts } => {
|
||||
// Provenance: source URL verbatim, scheme's medium, and the title
|
||||
// `input()` will open. `playlist` fills in below from the opened
|
||||
// stream's scanned title — not known until the scan runs.
|
||||
let source = SourceInfo {
|
||||
medium: url_medium(&parse_url(url)),
|
||||
path: url.to_string(),
|
||||
title: in_opts.title_index.unwrap_or(0),
|
||||
..SourceInfo::default()
|
||||
};
|
||||
let stream = input(url, &in_opts, ctx)?;
|
||||
let source = SourceInfo {
|
||||
playlist: stream.info().playlist.clone(),
|
||||
..source
|
||||
};
|
||||
(stream, None, source)
|
||||
}
|
||||
MuxSource::Iso {
|
||||
path,
|
||||
title,
|
||||
format,
|
||||
} => {
|
||||
// Prune to selected audio/subtitle streams BEFORE the highway
|
||||
// builds demux state (and before the DVD AC-3 channel probe).
|
||||
// Video is always kept; no-op for default All/All.
|
||||
let mut title = title;
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
// Provenance: staged image as an `iso://` URL, plus playlist. Title
|
||||
// INDEX is NOT reachable here — `MuxSource::Iso` carries an already-
|
||||
// scanned `DiscTitle` with no index, so it stays 0 rather than guessed.
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Iso,
|
||||
path: format!("iso://{}", path.display()),
|
||||
playlist: title.playlist.clone(),
|
||||
..SourceInfo::default()
|
||||
};
|
||||
let reader = FileSectorSource::open(path)?;
|
||||
let stream = build_iso_pipeline(
|
||||
reader,
|
||||
title,
|
||||
DecryptKeys::None,
|
||||
opts.batch_sectors,
|
||||
format,
|
||||
opts.raw,
|
||||
ctx,
|
||||
)?;
|
||||
(Box::new(stream), None, source)
|
||||
}
|
||||
MuxSource::Session {
|
||||
session,
|
||||
title_index,
|
||||
} => {
|
||||
// Pull everything we need out of the disc as owned values so the
|
||||
// immutable disc borrow is released before the mutable
|
||||
// `take_reader` below.
|
||||
let (mut title, format, keys, playlist, source) = {
|
||||
let disc = session.disc().ok_or_else(|| Error::DeviceNotReady {
|
||||
path: session.device_path().to_string(),
|
||||
})?;
|
||||
let title =
|
||||
disc.titles
|
||||
.get(title_index)
|
||||
.cloned()
|
||||
.ok_or(Error::MuxTrackRange {
|
||||
track: title_index,
|
||||
tracks: disc.titles.len(),
|
||||
})?;
|
||||
let playlist = disc
|
||||
.meta_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| disc.volume_id.clone());
|
||||
// Provenance: every member is reachable on this arm — the device
|
||||
// the session is bound to, the title index the caller named, the
|
||||
// title's own playlist, and the scanned volume id.
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Disc,
|
||||
path: format!("disc://{}", session.device_path()),
|
||||
title: title_index,
|
||||
playlist: title.playlist.clone(),
|
||||
volume_id: disc.volume_id.clone(),
|
||||
};
|
||||
// DVD CSS is per-VTS: resolve the per-title key via the pipeline
|
||||
// (see `session_mux_keys`), never the whole-disc `decrypt_keys()`.
|
||||
(
|
||||
title,
|
||||
disc.content_format,
|
||||
session_mux_keys(disc),
|
||||
playlist,
|
||||
source,
|
||||
)
|
||||
};
|
||||
// Prune to selected streams before `DiscStream::new` builds demux tables.
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
// A missing staged reader ("already consumed" / never staged) is a
|
||||
// clean error, not a panic (contract Q2).
|
||||
let reader = session.take_reader().ok_or_else(|| Error::DeviceNotReady {
|
||||
path: session.device_path().to_string(),
|
||||
})?;
|
||||
let mut stream = crate::mux::DiscStream::new(
|
||||
reader,
|
||||
title,
|
||||
keys,
|
||||
opts.batch_sectors,
|
||||
format,
|
||||
opts.raw,
|
||||
ctx,
|
||||
)?;
|
||||
if opts.raw {
|
||||
stream.set_raw();
|
||||
}
|
||||
stream.skip_errors = opts.skip_errors;
|
||||
(Box::new(stream), Some(playlist), source)
|
||||
}
|
||||
MuxSource::Live {
|
||||
reader,
|
||||
title,
|
||||
format,
|
||||
} => {
|
||||
// Prune to selected streams, exactly as the Iso/Session arms do —
|
||||
// without this, selection was silently ignored on the live-drive
|
||||
// path despite the field's doc saying it was applied.
|
||||
let mut title = title;
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
// Provenance: medium is certain, playlist is on hand. Device PATH
|
||||
// and title INDEX are NOT reachable — `MuxSource::Live` hands an
|
||||
// opaque reader/title with neither, so both stay empty/0.
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Disc,
|
||||
playlist: title.playlist.clone(),
|
||||
..SourceInfo::default()
|
||||
};
|
||||
// INLINE `DiscStream`, same constructor as the `Session` arm, NOT
|
||||
// `build_iso_pipeline` — the highway would bypass the adaptive
|
||||
// batch-retry that lives in `DiscStream::fill_extents`.
|
||||
let mut stream = crate::mux::DiscStream::new(
|
||||
reader,
|
||||
title,
|
||||
DecryptKeys::None,
|
||||
opts.batch_sectors,
|
||||
format,
|
||||
opts.raw,
|
||||
ctx,
|
||||
)?;
|
||||
if opts.raw {
|
||||
stream.set_raw();
|
||||
}
|
||||
stream.skip_errors = opts.skip_errors;
|
||||
(Box::new(stream), None, source)
|
||||
}
|
||||
};
|
||||
|
||||
// The consumer-supplied disc name overrides the title's own playlist for the
|
||||
// muxed title (see `drive_mux`); mirror that into the provenance so a
|
||||
// `fvi://` header and its sibling MKV agree on what the playlist was called.
|
||||
if let Some(name) = playlist_name.as_deref() {
|
||||
source.playlist = name.to_string();
|
||||
}
|
||||
|
||||
drive_mux(
|
||||
stream,
|
||||
dest_url,
|
||||
ctx,
|
||||
playlist_name.as_deref(),
|
||||
Some(&source),
|
||||
)
|
||||
}
|
||||
|
||||
/// Where [`mux_with_keys`] reads its PES frames from (KU §3.1). No variant carries key
|
||||
/// material: keys come only from the rip's [`KeyRing`](crate::keys::KeyRing).
|
||||
pub enum MuxSource<'a> {
|
||||
/// Live single-pass mux off an opened [`DiscSession`] (its reader staged).
|
||||
Session {
|
||||
session: &'a mut DiscSession,
|
||||
/// Index into `session.disc().titles`.
|
||||
title_index: usize,
|
||||
},
|
||||
/// An image file and a title the caller ALREADY scanned (the drive scan, J14): the
|
||||
/// image is never rescanned, so a sweep's unreadable UDF/MPLS/AACS sectors do not
|
||||
/// matter. The set must cover the title and be for a sector-exact image of its disc.
|
||||
Iso {
|
||||
path: &'a Path,
|
||||
title: DiscTitle,
|
||||
format: crate::disc::ContentFormat,
|
||||
},
|
||||
/// Live single-pass mux off a raw disc reader (the inline `DiscStream`).
|
||||
Live {
|
||||
reader: Box<dyn SectorSource>,
|
||||
title: DiscTitle,
|
||||
format: crate::disc::ContentFormat,
|
||||
},
|
||||
/// Any URL-addressed source; the only variant that scans. `keys` goes into
|
||||
/// [`InputOptions::keys`].
|
||||
Url { url: &'a str, opts: InputOptions },
|
||||
}
|
||||
|
||||
/// Run the decrypt + mux pipeline end-to-end with the rip's up-front key set (KU §3.1):
|
||||
/// every AACS read goes through the set's readers (its map, and the on-arrival proof for
|
||||
/// pieces it left unproven), with no key lookup. `keys == None`, a non-AACS set, or
|
||||
/// `opts.raw`: no AACS decryption (CSS and clear discs decrypt as before). E7013 when the
|
||||
/// set is not for the disc or does not cover the title; E7026 when the title needs
|
||||
/// forensic keys that are Pending. Unresolved codec headers are [`Error::MkvInvalid`], a
|
||||
/// zero-output drain [`Error::NoStreams`]. Every stage runs under `ctx`; a Stop of its halt,
|
||||
/// during the open or the pump alike, yields `completed = false, halted = true`, not an error.
|
||||
/// Run the decrypt + mux pipeline end-to-end over `source` with the rip's up-front key set
|
||||
/// (KU §3.1): every AACS read goes through the set's readers (its map, and the on-arrival
|
||||
/// proof for pieces it left unproven), with no key lookup. `keys == None`, a non-AACS set, or
|
||||
/// `opts.raw`: no AACS decryption (CSS and clear discs decrypt as before; a BD-TS disc with
|
||||
/// no set refuses its first flagged unit, E7022). E7013 when the set is not for the disc or
|
||||
/// does not cover the title; E7026 when the title needs forensic keys that are Pending.
|
||||
/// Unresolved codec headers are [`Error::MkvInvalid`], a zero-output drain
|
||||
/// [`Error::NoStreams`]. Every stage runs under `ctx`; a Stop of its halt, during the open
|
||||
/// or the pump alike, yields `completed = false, halted = true`, not an error.
|
||||
pub fn mux_with_keys(
|
||||
source: MuxSource,
|
||||
source: Source,
|
||||
keys: Option<&crate::keys::KeyRing>,
|
||||
dest_url: &str,
|
||||
opts: &MuxOptions,
|
||||
@@ -367,154 +152,244 @@ pub fn mux_with_keys(
|
||||
}
|
||||
}
|
||||
|
||||
// `mux_with_keys` before its one halt rule: picks the keyed or unkeyed arms.
|
||||
fn mux_routed(
|
||||
source: MuxSource,
|
||||
/// [`open_source`](super::source::open_source) then [`mux_with_keys`]: mux title
|
||||
/// `opts.title_index` of the input `url`. A Stop during the open is a stopped outcome, as
|
||||
/// during the pump.
|
||||
pub fn mux_url(
|
||||
url: &str,
|
||||
keys: Option<&crate::keys::KeyRing>,
|
||||
dest_url: &str,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<MuxOutcome> {
|
||||
let set = keys.filter(|s| s.is_aacs() && !opts.raw);
|
||||
// KU §3.1: "`keys` must be `Some` for AACS". A BD-TS Iso/Live mux with no AACS set reads
|
||||
// through a keyless set: the first AACS-flagged unit is E7022, never muxed as content.
|
||||
// MPEG-PS keeps the old path (a DVD cracks its own CSS key; `MuxSource` has no disc).
|
||||
let keyless = match (&source, set) {
|
||||
(MuxSource::Iso { title, format, .. } | MuxSource::Live { title, format, .. }, None)
|
||||
if !opts.raw && *format == crate::disc::ContentFormat::BdTs =>
|
||||
{
|
||||
Some(crate::keys::KeyRing::keyless_for(title, *format))
|
||||
let source = match super::source::open_source(url, crate::disc::ScanOptions::default(), ctx) {
|
||||
Ok(s) => s,
|
||||
Err(Error::Halted) if ctx.halt.is_cancelled() => {
|
||||
return Ok(MuxOutcome::stopped_before_output(0, 0));
|
||||
}
|
||||
// A Session over an AACS disc with no set: BD-TS as above; HD DVD is never
|
||||
// probed (KU §2.6), so it refuses up front.
|
||||
(
|
||||
MuxSource::Session {
|
||||
session,
|
||||
title_index,
|
||||
},
|
||||
None,
|
||||
) if !opts.raw => match session.disc() {
|
||||
Some(d) if d.aacs.is_some() => {
|
||||
if d.content_format != crate::disc::ContentFormat::BdTs {
|
||||
return Err(Error::NoDiscKey {
|
||||
disc_hash: d.aacs_disc_hash(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
crate::keys::KeyRing::keyless_for_disc(d, *title_index)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let set = set.or(keyless.as_ref());
|
||||
match (source, set) {
|
||||
(MuxSource::Url { url, opts: mut o }, _) => {
|
||||
if let Some(k) = keys {
|
||||
o.keys = Some(k.clone());
|
||||
}
|
||||
mux_unkeyed(MuxSource::Url { url, opts: o }, dest_url, opts, ctx)
|
||||
}
|
||||
(src, None) => mux_unkeyed(src, dest_url, opts, ctx),
|
||||
(src, Some(set)) => mux_keyed(src, set, dest_url, opts, ctx),
|
||||
}
|
||||
mux_with_keys(source, keys, dest_url, opts, ctx)
|
||||
}
|
||||
|
||||
// The keyed arms of `mux_with_keys`: the set's readers, never a resolve.
|
||||
fn mux_keyed(
|
||||
source: MuxSource,
|
||||
set: &crate::keys::KeyRing,
|
||||
// `mux_with_keys` before its one halt rule.
|
||||
fn mux_routed(
|
||||
source: Source,
|
||||
keys: Option<&crate::keys::KeyRing>,
|
||||
dest_url: &str,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<MuxOutcome> {
|
||||
let (stream, playlist, source): (Box<dyn Stream>, Option<String>, SourceInfo) = match source {
|
||||
MuxSource::Iso { path, title, .. } => {
|
||||
let name = source.title_name();
|
||||
let mut info = source.provenance(opts.title_index);
|
||||
let from_stream = matches!(source.origin, Origin::Stream { .. });
|
||||
let stream = open_pes(source, keys, opts, ctx)?;
|
||||
if from_stream {
|
||||
info.playlist = stream.info().playlist.clone();
|
||||
}
|
||||
if let Some(n) = &name {
|
||||
info.playlist = n.clone();
|
||||
}
|
||||
drive_mux(stream, dest_url, ctx, name.as_deref(), Some(&info))
|
||||
}
|
||||
|
||||
// The PES stream of `opts.title_index` read from `source`: the one place that turns any
|
||||
// opened input into frames (decrypting with `keys` unless `opts.raw`).
|
||||
pub(crate) fn open_pes(
|
||||
source: Source,
|
||||
keys: Option<&crate::keys::KeyRing>,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<Box<dyn Stream>> {
|
||||
let set = keys.filter(|s| s.is_aacs() && !opts.raw);
|
||||
let idx = opts.title_index;
|
||||
let input_opts = || InputOptions {
|
||||
title_index: Some(idx),
|
||||
raw: opts.raw,
|
||||
selection: opts.selection.clone(),
|
||||
keys: keys.cloned(),
|
||||
};
|
||||
match source.origin {
|
||||
Origin::Stream { url } => super::resolve::open_stream_url(&url, &input_opts(), ctx),
|
||||
Origin::Image {
|
||||
path,
|
||||
folder,
|
||||
reader,
|
||||
disc,
|
||||
} => {
|
||||
let reopen = move || super::resolve::reopen_image(&path, folder);
|
||||
Ok(Box::new(super::resolve::image_input_scanned(
|
||||
reader,
|
||||
disc,
|
||||
&input_opts(),
|
||||
reopen,
|
||||
ctx,
|
||||
)?))
|
||||
}
|
||||
Origin::Prescanned { path, title: t } => {
|
||||
let (title, format) = (t.title, t.format);
|
||||
let keyless = keyless_ring(&title, format, set, opts);
|
||||
let set = set.or(keyless.as_ref());
|
||||
let mut title = title;
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Iso,
|
||||
path: format!("iso://{}", path.display()),
|
||||
playlist: title.playlist.clone(),
|
||||
..SourceInfo::default()
|
||||
};
|
||||
let reader = FileSectorSource::open(path)?;
|
||||
let stream = super::resolve::build_iso_pipeline_keyed(
|
||||
reader,
|
||||
title,
|
||||
set,
|
||||
opts.batch_sectors,
|
||||
ctx,
|
||||
)?;
|
||||
(Box::new(stream), None, source)
|
||||
let reader = FileSectorSource::open(&path)?;
|
||||
match set {
|
||||
Some(set) => Ok(Box::new(super::resolve::build_iso_pipeline_keyed(
|
||||
reader,
|
||||
title,
|
||||
set,
|
||||
opts.batch_sectors,
|
||||
ctx,
|
||||
)?)),
|
||||
None => Ok(Box::new(build_iso_pipeline(
|
||||
reader,
|
||||
title,
|
||||
DecryptKeys::None,
|
||||
opts.batch_sectors,
|
||||
format,
|
||||
opts.raw,
|
||||
ctx,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
MuxSource::Session {
|
||||
session,
|
||||
title_index,
|
||||
} => {
|
||||
let (title, format, playlist, source) = {
|
||||
let disc = session.disc().ok_or_else(|| Error::DeviceNotReady {
|
||||
path: session.device_path().to_string(),
|
||||
})?;
|
||||
let scope = crate::keys::KeyScope::Titles(vec![title_index]);
|
||||
if !set.is_for(&disc.media_id()) || !set.covers(&scope) {
|
||||
tracing::error!(target: "freemkv::keys", "key set is not for this session's title");
|
||||
return Err(Error::DecryptFailed.into());
|
||||
Origin::Live { reader, title: t } => {
|
||||
let (title, format) = (t.title, t.format);
|
||||
let keyless = keyless_ring(&title, format, set, opts);
|
||||
match set.or(keyless.as_ref()) {
|
||||
Some(set) => {
|
||||
let title = live_keyed_title(&*reader, title, set, opts)?;
|
||||
live_keyed(reader, title, format, set, opts, ctx)
|
||||
}
|
||||
let title = disc
|
||||
.titles
|
||||
.get(title_index)
|
||||
.cloned()
|
||||
.ok_or(Error::MuxTrackRange {
|
||||
track: title_index,
|
||||
tracks: disc.titles.len(),
|
||||
})?;
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Disc,
|
||||
path: format!("disc://{}", session.device_path()),
|
||||
title: title_index,
|
||||
playlist: title.playlist.clone(),
|
||||
volume_id: disc.volume_id.clone(),
|
||||
};
|
||||
let playlist = disc
|
||||
.meta_title
|
||||
.clone()
|
||||
.unwrap_or_else(|| disc.volume_id.clone());
|
||||
(title, disc.content_format, playlist, source)
|
||||
};
|
||||
// Refuse the selection / the set's gate BEFORE taking the reader, so a
|
||||
// refused mux leaves the session retryable (as mux_unkeyed does).
|
||||
let path = session.device_path().to_string();
|
||||
let not_ready = || Error::DeviceNotReady { path: path.clone() };
|
||||
None => {
|
||||
let mut title = title;
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
live_unkeyed(reader, title, format, DecryptKeys::None, opts, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
Origin::Session(session) => session_pes(session, set, opts, ctx),
|
||||
Origin::Drive(mut session) => session_pes(&mut session, set, opts, ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// Title `idx` of a scanned layout, and the layout's container format.
|
||||
fn title_of(
|
||||
disc: &crate::disc::Disc,
|
||||
idx: usize,
|
||||
) -> std::io::Result<(DiscTitle, crate::disc::ContentFormat)> {
|
||||
let title = disc.titles.get(idx).cloned().ok_or(Error::MuxTrackRange {
|
||||
track: idx,
|
||||
tracks: disc.titles.len(),
|
||||
})?;
|
||||
Ok((title, disc.content_format))
|
||||
}
|
||||
|
||||
// KU §3.1: "`keys` must be `Some` for AACS". A BD-TS read with no AACS set reads through a
|
||||
// keyless set: the first AACS-flagged unit is E7022, never muxed as content. MPEG-PS keeps
|
||||
// its own path (a DVD cracks its CSS key in the stream).
|
||||
fn keyless_ring(
|
||||
title: &DiscTitle,
|
||||
format: crate::disc::ContentFormat,
|
||||
set: Option<&crate::keys::KeyRing>,
|
||||
opts: &MuxOptions,
|
||||
) -> Option<crate::keys::KeyRing> {
|
||||
(set.is_none() && !opts.raw && format == crate::disc::ContentFormat::BdTs)
|
||||
.then(|| crate::keys::KeyRing::keyless_for(title, format))
|
||||
}
|
||||
|
||||
// A live mux off a drive session: the set's readers when keyed; else the session's own
|
||||
// CSS/clear keys, and a keyless set for an AACS disc (HD DVD refuses up front).
|
||||
fn session_pes(
|
||||
session: &mut DiscSession,
|
||||
set: Option<&crate::keys::KeyRing>,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<Box<dyn Stream>> {
|
||||
let idx = opts.title_index;
|
||||
let path = session.device_path().to_string();
|
||||
let not_ready = || Error::DeviceNotReady { path: path.clone() };
|
||||
let disc = session.disc().ok_or_else(not_ready)?;
|
||||
let (title, format) = title_of(disc, idx)?;
|
||||
let keyless = match set {
|
||||
None if !opts.raw && disc.aacs.is_some() => {
|
||||
if disc.content_format != crate::disc::ContentFormat::BdTs {
|
||||
return Err(Error::NoDiscKey {
|
||||
disc_hash: disc.aacs_disc_hash(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
crate::keys::KeyRing::keyless_for_disc(disc, idx)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let keys = session_mux_keys(disc);
|
||||
let set = set.or(keyless.as_ref());
|
||||
if let Some(set) = set {
|
||||
let scope = crate::keys::KeyScope::Titles(vec![idx]);
|
||||
if !set.is_for(&disc.media_id()) || !set.covers(&scope) {
|
||||
tracing::error!(target: "freemkv::keys", "key set is not for this session's title");
|
||||
return Err(Error::DecryptFailed.into());
|
||||
}
|
||||
}
|
||||
let batch = match opts.batch_sectors {
|
||||
0 => crate::disc::detect_max_batch_sectors(&path),
|
||||
n => n,
|
||||
};
|
||||
let opts = &MuxOptions {
|
||||
batch_sectors: batch,
|
||||
..opts.clone()
|
||||
};
|
||||
if session.staged_reader().is_none() {
|
||||
session.stage_drive_as_reader();
|
||||
}
|
||||
match set {
|
||||
Some(set) => {
|
||||
// Refuse the selection / the set's gate BEFORE taking the reader, so a refused
|
||||
// mux leaves the session retryable.
|
||||
let staged = session.staged_reader().ok_or_else(not_ready)?;
|
||||
let title = live_keyed_title(staged, title, set, opts)?;
|
||||
let reader = session.take_reader().ok_or_else(not_ready)?;
|
||||
let stream = live_keyed(reader, title, format, set, opts, ctx)?;
|
||||
(stream, Some(playlist), source)
|
||||
live_keyed(reader, title, format, set, opts, ctx)
|
||||
}
|
||||
MuxSource::Live {
|
||||
reader,
|
||||
title,
|
||||
format,
|
||||
} => {
|
||||
let source = SourceInfo {
|
||||
medium: Medium::Disc,
|
||||
playlist: title.playlist.clone(),
|
||||
..SourceInfo::default()
|
||||
};
|
||||
let title = live_keyed_title(&*reader, title, set, opts)?;
|
||||
let stream = live_keyed(reader, title, format, set, opts, ctx)?;
|
||||
(stream, None, source)
|
||||
None => {
|
||||
let mut title = title;
|
||||
opts.selection
|
||||
.apply(&mut title)
|
||||
.map_err(std::io::Error::from)?;
|
||||
let reader = session.take_reader().ok_or_else(not_ready)?;
|
||||
live_unkeyed(reader, title, format, keys, opts, ctx)
|
||||
}
|
||||
MuxSource::Url { .. } => unreachable!("mux_with_keys routes Url to mux_unkeyed"),
|
||||
};
|
||||
let mut source = source;
|
||||
if let Some(name) = playlist.as_deref() {
|
||||
source.playlist = name.to_string();
|
||||
}
|
||||
drive_mux(stream, dest_url, ctx, playlist.as_deref(), Some(&source))
|
||||
}
|
||||
|
||||
// The inline live-drive `DiscStream` with no AACS set: `keys` (CSS cracks per title when
|
||||
// `None` on a DVD), or ciphertext under `opts.raw`.
|
||||
fn live_unkeyed(
|
||||
reader: Box<dyn SectorSource>,
|
||||
title: DiscTitle,
|
||||
format: crate::disc::ContentFormat,
|
||||
keys: DecryptKeys,
|
||||
opts: &MuxOptions,
|
||||
ctx: &Ctx,
|
||||
) -> std::io::Result<Box<dyn Stream>> {
|
||||
let mut stream = crate::mux::DiscStream::new(
|
||||
reader,
|
||||
title,
|
||||
keys,
|
||||
opts.batch_sectors,
|
||||
format,
|
||||
opts.raw,
|
||||
ctx,
|
||||
)?;
|
||||
if opts.raw {
|
||||
stream.set_raw();
|
||||
}
|
||||
stream.skip_errors = opts.skip_errors;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
// The live title after the selection, once the set's gate admits it over `reader`.
|
||||
@@ -1638,11 +1513,10 @@ mod tests {
|
||||
..DiscTitle::empty()
|
||||
};
|
||||
let reader = crate::test_util::MemSource::new(vec![0u8; 16 * 2048]);
|
||||
let src = MuxSource::Live {
|
||||
reader: Box::new(reader),
|
||||
title,
|
||||
format: crate::disc::ContentFormat::MpegPs,
|
||||
};
|
||||
let src = Source::from_reader(
|
||||
Box::new(reader),
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::MpegPs),
|
||||
);
|
||||
let opts = MuxOptions {
|
||||
batch_sectors: 16,
|
||||
..Default::default()
|
||||
@@ -1868,14 +1742,15 @@ mod tests {
|
||||
batch_sectors: 8192,
|
||||
raw: false,
|
||||
selection: Default::default(),
|
||||
title_index: 0,
|
||||
};
|
||||
let halt = Halt::new();
|
||||
let out = mux_unkeyed(
|
||||
MuxSource::Iso {
|
||||
path: &iso_path,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
let out = mux_with_keys(
|
||||
Source::from_image(
|
||||
&iso_path,
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
None,
|
||||
"null://",
|
||||
&opts,
|
||||
&crate::ctx::Ctx::new(halt.clone()).with_events(events.clone()),
|
||||
@@ -1993,13 +1868,12 @@ mod tests {
|
||||
batch_sectors: 3,
|
||||
raw: false,
|
||||
selection: Default::default(),
|
||||
title_index: 0,
|
||||
};
|
||||
let halt = Halt::new();
|
||||
let err = mux_unkeyed(
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: 0,
|
||||
},
|
||||
let err = mux_with_keys(
|
||||
Source::from_session(&mut session),
|
||||
None,
|
||||
"null://",
|
||||
&opts,
|
||||
&crate::ctx::Ctx::new(halt.clone()),
|
||||
@@ -2066,13 +1940,12 @@ mod tests {
|
||||
batch_sectors: 3,
|
||||
raw: false,
|
||||
selection: Default::default(),
|
||||
title_index: num_titles + 5,
|
||||
};
|
||||
let halt = Halt::new();
|
||||
let err = mux_unkeyed(
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: num_titles + 5, // out of range
|
||||
},
|
||||
let err = mux_with_keys(
|
||||
Source::from_session(&mut session),
|
||||
None,
|
||||
"null://",
|
||||
&opts,
|
||||
&crate::ctx::Ctx::new(halt.clone()),
|
||||
@@ -3011,6 +2884,7 @@ mod tests {
|
||||
batch_sectors: 3,
|
||||
raw: false,
|
||||
selection: Default::default(),
|
||||
title_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3048,11 +2922,10 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let out_path = dir.path().join("live.mkv");
|
||||
let out = mux_with_keys(
|
||||
MuxSource::Live {
|
||||
Source::from_reader(
|
||||
reader,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
Some(&set),
|
||||
&format!("mkv://{}", out_path.display()),
|
||||
&keyed_opts(),
|
||||
@@ -3076,10 +2949,7 @@ mod tests {
|
||||
let set = crate::keys::KeyRing::keyed_for_test(&disc, key, &[(0, 3)]);
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), Some(reader));
|
||||
let out = mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(&mut session),
|
||||
Some(&set),
|
||||
"null://",
|
||||
&keyed_opts(),
|
||||
@@ -3103,10 +2973,7 @@ mod tests {
|
||||
bad.selection.audio = crate::mux::select::PidFilter::Only(vec![0x0FFF]);
|
||||
let run = |session: &mut DiscSession, opts: &MuxOptions| {
|
||||
mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(session),
|
||||
Some(&set),
|
||||
"null://",
|
||||
opts,
|
||||
@@ -3136,10 +3003,7 @@ mod tests {
|
||||
let set = crate::keys::KeyRing::keyed_for_test(&other, key, &[(0, 3)]);
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), Some(reader));
|
||||
let err = mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(&mut session),
|
||||
Some(&set),
|
||||
"null://",
|
||||
&keyed_opts(),
|
||||
@@ -3163,11 +3027,10 @@ mod tests {
|
||||
std::fs::write(&path, &image).unwrap();
|
||||
let out_path = dir.path().join("iso.mkv");
|
||||
let out = mux_with_keys(
|
||||
MuxSource::Iso {
|
||||
path: &path,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
Source::from_image(
|
||||
&path,
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
Some(&set),
|
||||
&format!("mkv://{}", out_path.display()),
|
||||
&keyed_opts(),
|
||||
@@ -3180,11 +3043,8 @@ mod tests {
|
||||
"the muxed audio is the plaintext"
|
||||
);
|
||||
let url = format!("iso://{}", path.display());
|
||||
let rescanned = mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &url,
|
||||
opts: InputOptions::default(),
|
||||
},
|
||||
let rescanned = mux_url(
|
||||
&url,
|
||||
Some(&set),
|
||||
"null://",
|
||||
&keyed_opts(),
|
||||
@@ -3215,11 +3075,10 @@ mod tests {
|
||||
..keyed_opts()
|
||||
};
|
||||
mux_with_keys(
|
||||
MuxSource::Live {
|
||||
Source::from_reader(
|
||||
reader,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
keys,
|
||||
"null://",
|
||||
&opts,
|
||||
@@ -3250,11 +3109,10 @@ mod tests {
|
||||
image.resize(16 * 2048, 0);
|
||||
std::fs::write(&path, &image).unwrap();
|
||||
let iso = mux_with_keys(
|
||||
MuxSource::Iso {
|
||||
path: &path,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
Source::from_image(
|
||||
&path,
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
None,
|
||||
"null://",
|
||||
&keyed_opts(),
|
||||
@@ -3278,10 +3136,7 @@ mod tests {
|
||||
});
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), Some(reader));
|
||||
let r = mux_with_keys(
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: 0,
|
||||
},
|
||||
Source::from_session(&mut session),
|
||||
None,
|
||||
"null://",
|
||||
&keyed_opts(),
|
||||
@@ -3335,11 +3190,10 @@ mod tests {
|
||||
dest: &str,
|
||||
) -> std::io::Result<MuxOutcome> {
|
||||
mux_with_keys(
|
||||
MuxSource::Iso {
|
||||
Source::from_image(
|
||||
path,
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
Some(set),
|
||||
dest,
|
||||
&MuxOptions {
|
||||
@@ -3463,22 +3317,20 @@ mod tests {
|
||||
..keyed_opts()
|
||||
};
|
||||
let iso = mux_with_keys(
|
||||
MuxSource::Iso {
|
||||
path: &path,
|
||||
title: title.clone(),
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
Source::from_image(
|
||||
&path,
|
||||
ScannedTitle::new(title.clone(), crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
Some(&set),
|
||||
"null://",
|
||||
&opts,
|
||||
&crate::ctx::Ctx::default(),
|
||||
);
|
||||
let live = mux_with_keys(
|
||||
MuxSource::Live {
|
||||
reader: Box::new(ImageReader(std::fs::read(&path).unwrap())),
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
Source::from_reader(
|
||||
Box::new(ImageReader(std::fs::read(&path).unwrap())),
|
||||
ScannedTitle::new(title, crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
Some(&set),
|
||||
"null://",
|
||||
&opts,
|
||||
@@ -3618,15 +3470,14 @@ mod tests {
|
||||
title
|
||||
}
|
||||
|
||||
fn clear_live(units: usize, bad: Option<u32>) -> MuxSource<'static> {
|
||||
MuxSource::Live {
|
||||
reader: Box::new(BadSectorImage {
|
||||
fn clear_live(units: usize, bad: Option<u32>) -> Source<'static> {
|
||||
Source::from_reader(
|
||||
Box::new(BadSectorImage {
|
||||
data: clear_audio_image(units),
|
||||
bad,
|
||||
}),
|
||||
title: clear_title(units),
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
}
|
||||
ScannedTitle::new(clear_title(units), crate::disc::ContentFormat::BdTs),
|
||||
)
|
||||
}
|
||||
|
||||
// `raw` routes a clear Live source through mux_unkeyed; otherwise mux_keyed runs it
|
||||
@@ -3702,13 +3553,7 @@ mod tests {
|
||||
let (src, raw) = match arm {
|
||||
"live-unkeyed" => (clear_live(2, Some(4)), true),
|
||||
"live-keyed" => (clear_live(2, Some(4)), false),
|
||||
_ => (
|
||||
MuxSource::Session {
|
||||
session: &mut s,
|
||||
title_index: 0,
|
||||
},
|
||||
false,
|
||||
),
|
||||
_ => (Source::from_session(&mut s), false),
|
||||
};
|
||||
let opts = MuxOptions {
|
||||
skip_errors: skip,
|
||||
@@ -3761,19 +3606,12 @@ mod tests {
|
||||
let (src, raw) = match arm {
|
||||
"live-unkeyed" => (clear_live(2, None), true),
|
||||
"live-keyed" => (clear_live(2, None), false),
|
||||
"session" => (
|
||||
MuxSource::Session {
|
||||
session: &mut session,
|
||||
title_index: 0,
|
||||
},
|
||||
false,
|
||||
),
|
||||
"session" => (Source::from_session(&mut session), false),
|
||||
_ => (
|
||||
MuxSource::Iso {
|
||||
path: &iso,
|
||||
title: clear_title(2),
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
},
|
||||
Source::from_image(
|
||||
&iso,
|
||||
ScannedTitle::new(clear_title(2), crate::disc::ContentFormat::BdTs),
|
||||
),
|
||||
arm == "iso-unkeyed",
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! QA explicitly runs ignored `ffmpeg_` tests with software codecs installed.
|
||||
//! Fixtures are generated locally and never downloaded from a media corpus.
|
||||
|
||||
use super::driver::{MuxOptions, MuxSource, mux_with_keys};
|
||||
use super::driver::MuxOptions;
|
||||
use serde_json::Value;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
@@ -190,11 +190,8 @@ fn generate_with(
|
||||
fn remux(source: &Path, output: &Path, transport: bool) {
|
||||
let scheme = if transport { "m2ts" } else { "mkv" };
|
||||
let url = format!("{scheme}://{}", source.display());
|
||||
let result = mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &url,
|
||||
opts: Default::default(),
|
||||
},
|
||||
let result = crate::mux::mux_url(
|
||||
&url,
|
||||
None,
|
||||
&format!("mkv://{}", output.display()),
|
||||
&MuxOptions::default(),
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ pub mod driver;
|
||||
pub mod pipelined_stream;
|
||||
pub mod resolve;
|
||||
pub mod select;
|
||||
pub mod source;
|
||||
|
||||
// Internal-only modules (referenced only via `crate::mux::…`; not public API).
|
||||
// `#[allow(dead_code)]`: narrowing `pub`→`pub(crate)` surfaces helpers only
|
||||
@@ -84,13 +85,14 @@ pub(crate) mod videomap;
|
||||
// The provenance types ARE public: `output()` takes a `SourceInfo` so an `fvi://`
|
||||
// destination records the INPUT it was built from (§6.2), not the file written.
|
||||
pub use disc::DiscStream;
|
||||
pub use driver::{MuxOptions, MuxOutcome, MuxSource, mux_with_keys};
|
||||
pub use driver::{MuxOptions, MuxOutcome, mux_url, mux_with_keys};
|
||||
pub use fit::{FitReport, SkipReason, fit_report};
|
||||
pub use m2ts::M2tsStream;
|
||||
pub use mkvstream::{
|
||||
MkvProbe, MkvProbeTrack, MkvStream, MkvTrackKind, parse_freemkv_version, probe_mkv,
|
||||
probe_mkv_with_cues,
|
||||
};
|
||||
pub use source::{ScannedTitle, Source, open_source};
|
||||
pub use videomap::{Medium, SourceInfo};
|
||||
// `Mp4Sink` is public so a caller driving the sink can ask `final_report()` what
|
||||
// the finished file contains — the pre-mux `mp4_fit_report` is only a prediction,
|
||||
|
||||
@@ -2059,7 +2059,7 @@ fn a_map_with_a_bad_crc_is_ignored() {
|
||||
// offset); the extension is the mkv sink's to exclude (M1, J23).
|
||||
#[test]
|
||||
fn an_mpg_source_remuxes_to_mkv() {
|
||||
use crate::mux::driver::{MuxOptions, MuxSource, mux_with_keys};
|
||||
use crate::mux::driver::MuxOptions;
|
||||
let fx = fixture(&Opts {
|
||||
spu_tracks: 0,
|
||||
..Opts::default()
|
||||
@@ -2074,14 +2074,12 @@ fn an_mpg_source_remuxes_to_mkv() {
|
||||
batch_sectors: 8192,
|
||||
raw: false,
|
||||
selection: Default::default(),
|
||||
title_index: 0,
|
||||
};
|
||||
let out = {
|
||||
let _g = crate::sector::prefetched::holder_test_lock();
|
||||
mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &url,
|
||||
opts: Default::default(),
|
||||
},
|
||||
crate::mux::mux_url(
|
||||
&url,
|
||||
None,
|
||||
&format!("mkv://{}", mkv.display()),
|
||||
&opts,
|
||||
|
||||
+15
-18
@@ -2,7 +2,7 @@
|
||||
//! every sink driven through `mux_with_keys`, over the synthetic BD clip. Pinned as output
|
||||
//! hashes, loss counters and refusal codes (see [`Golden`]); cells are `parity_*`.
|
||||
|
||||
use super::driver::{MuxOptions, MuxOutcome, MuxSource, mux_with_keys};
|
||||
use super::driver::{MuxOptions, MuxOutcome};
|
||||
use super::resolve::{InputOptions, input};
|
||||
use super::select::{PidFilter, StreamSelection};
|
||||
use crate::aacs::content::{ALIGNED_UNIT_LEN, encrypt_unit};
|
||||
@@ -119,14 +119,17 @@ fn sinks(
|
||||
"null" => "null://".to_string(),
|
||||
s => format!("{s}://{}", out.join(dest_name(s)).display()),
|
||||
};
|
||||
let r = mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &url,
|
||||
opts: iopts.clone(),
|
||||
},
|
||||
None,
|
||||
let m = MuxOptions {
|
||||
title_index: iopts.title_index.unwrap_or(0),
|
||||
raw: iopts.raw,
|
||||
selection: iopts.selection.clone(),
|
||||
..mopts.clone()
|
||||
};
|
||||
let r = crate::mux::mux_url(
|
||||
&url,
|
||||
iopts.keys.as_ref(),
|
||||
&dest,
|
||||
mopts,
|
||||
&m,
|
||||
&crate::ctx::Ctx::default(),
|
||||
);
|
||||
record_run(g, sink, &r);
|
||||
@@ -212,11 +215,8 @@ fn parity_sinks_from_mkv_mpg_and_fmkv_sources() {
|
||||
std::fs::write(&src, &clip).unwrap();
|
||||
for (scheme, name) in [("mkv", "a.mkv"), ("mpg", "a.mpg"), ("m2ts", "a.fmkv.m2ts")] {
|
||||
let p = dir.path().join(name);
|
||||
mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &format!("m2ts://{}", src.display()),
|
||||
opts: Default::default(),
|
||||
},
|
||||
crate::mux::mux_url(
|
||||
&format!("m2ts://{}", src.display()),
|
||||
None,
|
||||
&format!("{scheme}://{}", p.display()),
|
||||
&MuxOptions::default(),
|
||||
@@ -396,11 +396,8 @@ pub(crate) fn clear_mpg() -> Vec<u8> {
|
||||
let src = dir.path().join("c.m2ts");
|
||||
std::fs::write(&src, &clip).unwrap();
|
||||
let dst = dir.path().join("c.mpg");
|
||||
mux_with_keys(
|
||||
MuxSource::Url {
|
||||
url: &format!("m2ts://{}", src.display()),
|
||||
opts: Default::default(),
|
||||
},
|
||||
crate::mux::mux_url(
|
||||
&format!("m2ts://{}", src.display()),
|
||||
None,
|
||||
&format!("mpg://{}", dst.display()),
|
||||
&MuxOptions::default(),
|
||||
|
||||
+90
-80
@@ -2,12 +2,8 @@
|
||||
//!
|
||||
//! Format: `scheme://path`.
|
||||
//!
|
||||
//! For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy` instead.
|
||||
//!
|
||||
//! Note: `disc://` cannot be opened through [`input`]; it returns
|
||||
//! [`crate::error::Error::DiscUrlNotDirect`]. Live-disc input must go
|
||||
//! through `Drive::open()` + `Disc::scan()` + `DiscStream::new()`, not
|
||||
//! the URL resolver.
|
||||
//! Every input scheme, `disc://` included, opens through [`super::source::open_source`];
|
||||
//! [`input`] is that plus the PES stream of one title.
|
||||
|
||||
use super::network::NetworkStream;
|
||||
use super::null::NullStream;
|
||||
@@ -381,60 +377,50 @@ impl std::fmt::Debug for InputOptions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a PES input stream (produces PES frames) for the run `ctx`. Its halt reaches the
|
||||
/// image/PS/TS pipelines' scan, crack, reads and demux and the network receive (`stdio://`,
|
||||
/// `mkv://`, `mp4://` read inline); image and PS reads report `BytesRead`; stats count loss.
|
||||
/// Open a PES input stream (produces PES frames) for the run `ctx`: [`open_source`] then the
|
||||
/// PES stream of `opts.title_index`. Every input scheme opens here, `disc://` included (the
|
||||
/// drive is brought up and scanned with default [`crate::disc::ScanOptions`]). Its halt
|
||||
/// reaches the scan, crack, reads and demux and the network receive (`stdio://`, `mkv://`,
|
||||
/// `mp4://` read inline); image and PS reads report `BytesRead`; stats count loss.
|
||||
///
|
||||
/// [`open_source`]: super::source::open_source
|
||||
pub fn input(
|
||||
url: &str,
|
||||
opts: &InputOptions,
|
||||
ctx: &crate::ctx::Ctx,
|
||||
) -> io::Result<Box<dyn crate::pes::Stream>> {
|
||||
let source = super::source::open_source(url, crate::disc::ScanOptions::default(), ctx)?;
|
||||
let mux = super::driver::MuxOptions {
|
||||
raw: opts.raw,
|
||||
selection: opts.selection.clone(),
|
||||
title_index: opts.title_index.unwrap_or(0),
|
||||
..Default::default()
|
||||
};
|
||||
super::driver::open_pes(source, opts.keys.as_ref(), &mux, ctx)
|
||||
}
|
||||
|
||||
// A probed image's reader opened again, for the TrueHD header probe.
|
||||
pub(crate) fn reopen_image(path: &Path, folder: bool) -> io::Result<Box<dyn SectorSource>> {
|
||||
Ok(if folder {
|
||||
Box::new(crate::dirimage::DirImage::open(path)?)
|
||||
} else {
|
||||
Box::new(crate::io::file_sector_source::FileSectorSource::open(path)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn validate_path(path: &Path, scheme: &str) -> crate::error::Result<()> {
|
||||
validate_file_path(path, scheme).map_err(crate::error::Error::from)
|
||||
}
|
||||
|
||||
// The container and stream inputs (`m2ts://`, `mkv://`, `mp4://`, `mpg://`, `network://`,
|
||||
// `stdio://`), each through the one decryption stage.
|
||||
pub(crate) fn open_stream_url(
|
||||
url: &str,
|
||||
opts: &InputOptions,
|
||||
ctx: &crate::ctx::Ctx,
|
||||
) -> io::Result<Box<dyn crate::pes::Stream>> {
|
||||
let parsed = parse_url(url);
|
||||
match parsed {
|
||||
StreamUrl::Disc { .. } => {
|
||||
// Disc sources require live SCSI state — caller must use
|
||||
// `Drive::open() + Disc::scan() + DiscStream::new()` directly.
|
||||
// Typed error only; the CLI/UI explains the entry point.
|
||||
Err(crate::error::Error::DiscUrlNotDirect.into())
|
||||
}
|
||||
StreamUrl::Iso { ref path } => {
|
||||
validate_file_path(path, "iso")?;
|
||||
// Sole file-backed sector source: carries the platform SEQUENTIAL
|
||||
// fadvise hint (widens kernel readahead) plus periodic DONTNEED
|
||||
// eviction bounding memory pressure during same-disk mux output.
|
||||
let reader = crate::io::file_sector_source::FileSectorSource::open(path)?;
|
||||
let probe_path = path.clone();
|
||||
let stream = image_input(
|
||||
reader,
|
||||
opts,
|
||||
move || {
|
||||
crate::io::file_sector_source::FileSectorSource::open(&probe_path)
|
||||
.map_err(|e| -> io::Error { e.into() })
|
||||
},
|
||||
false,
|
||||
ctx,
|
||||
)?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
// `dir://` as a SOURCE: an extracted disc folder presented as a
|
||||
// synthetic UDF image (`crate::dirimage`), reaching the exact same
|
||||
// body as `iso://` since `DirImage` is just another `SectorSource`.
|
||||
StreamUrl::Dir { ref path } => {
|
||||
validate_file_path(path, "dir")?;
|
||||
let reader = crate::dirimage::DirImage::open(path)?;
|
||||
let probe_path = path.clone();
|
||||
let stream = image_input(
|
||||
reader,
|
||||
opts,
|
||||
move || {
|
||||
crate::dirimage::DirImage::open(&probe_path)
|
||||
.map_err(|e| -> io::Error { e.into() })
|
||||
},
|
||||
true,
|
||||
ctx,
|
||||
)?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
StreamUrl::M2ts { ref path } => {
|
||||
validate_file_path(path, "m2ts")?;
|
||||
let len = std::fs::metadata(path)?.len();
|
||||
@@ -489,41 +475,31 @@ pub fn input(
|
||||
| StreamUrl::Json { .. } => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
// `fvi://` is an output-only sink (per-picture video index); never a source.
|
||||
StreamUrl::Fvi { .. } => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||
StreamUrl::Disc { .. } | StreamUrl::Iso { .. } | StreamUrl::Dir { .. } => {
|
||||
Err(crate::error::Error::StreamUrlInvalid {
|
||||
url: url.to_string(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
StreamUrl::Unknown { ref raw } => {
|
||||
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared body of every IMAGE-level PES source (`iso://`/`dir://`).
|
||||
fn image_input<S, F>(
|
||||
mut reader: S,
|
||||
// Shared body of every IMAGE-level PES source (`iso://`/`dir://`) over the probe's reader
|
||||
// and layout (`open_source` scanned it, and judged a folder's AACS verdict from content).
|
||||
pub(crate) fn image_input_scanned<S, F>(
|
||||
reader: S,
|
||||
mut disc: crate::disc::Disc,
|
||||
opts: &InputOptions,
|
||||
reopen: F,
|
||||
// A FOLDER's `encrypted` flag comes from tree shape and can be wrong; an
|
||||
// image's cannot. See `session::apply_folder_encryption_verdict`.
|
||||
is_folder: bool,
|
||||
ctx: &crate::ctx::Ctx,
|
||||
) -> io::Result<PipelinedPesStream>
|
||||
where
|
||||
S: SectorSource + Send + 'static,
|
||||
F: FnOnce() -> io::Result<S>,
|
||||
{
|
||||
let capacity = reader.capacity_sectors();
|
||||
// BUG-4: the scan and every pipeline stage below observe the run's halt.
|
||||
let scan = crate::disc::ScanOptions {
|
||||
halt: Some(ctx.halt.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut disc = crate::disc::Disc::scan_image(&mut reader, capacity, &scan)
|
||||
.map_err(|e| -> io::Error { e.into() })?;
|
||||
// Without this a folder reached here with a tree-shape verdict while
|
||||
// `session::scan_dir` reached the opposite one from its CONTENT, so the
|
||||
// same folder ripped through one door and failed through the other.
|
||||
if is_folder {
|
||||
crate::session::apply_folder_encryption_verdict(&mut reader, &mut disc)
|
||||
.map_err(|e| -> io::Error { e.into() })?;
|
||||
}
|
||||
if let Some(set) = opts.keys.as_ref().filter(|s| s.is_aacs() && !opts.raw) {
|
||||
return keyed_image_input(reader, disc, opts, set, reopen, ctx);
|
||||
}
|
||||
@@ -1438,12 +1414,22 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// The resolver doc table marks disc:// as input-only via the
|
||||
/// `Drive::open` path — input("disc://") must surface DiscUrlNotDirect
|
||||
/// (E9009 → Unsupported), never attempt to open a stream.
|
||||
/// `disc://` is an ordinary input: `input()` opens the named drive (a missing device
|
||||
/// fails as the drive open does), never refusing the scheme.
|
||||
#[test]
|
||||
fn input_disc_url_is_not_direct() {
|
||||
assert_eq!(input_err_kind("disc://"), std::io::ErrorKind::Unsupported);
|
||||
fn input_disc_url_opens_the_drive() {
|
||||
let err = super::input(
|
||||
"disc:///nonexistent/freemkv-test-drive",
|
||||
&super::InputOptions::default(),
|
||||
&crate::ctx::Ctx::default(),
|
||||
)
|
||||
.err()
|
||||
.expect("no such drive");
|
||||
assert_ne!(
|
||||
crate::error::error_code(&err),
|
||||
Some(crate::error::E_DISC_URL_NOT_DIRECT),
|
||||
"got {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// null:// is write-only per the table — input() must reject it with
|
||||
@@ -1926,7 +1912,31 @@ mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod stop_tests {
|
||||
use super::{InputOptions, image_input};
|
||||
use super::{InputOptions, PipelinedPesStream, image_input_scanned};
|
||||
|
||||
// `open_source`'s image probe, then the image stream, over a test reader.
|
||||
fn image_input<S, F>(
|
||||
mut reader: S,
|
||||
opts: &InputOptions,
|
||||
reopen: F,
|
||||
folder: bool,
|
||||
ctx: &crate::ctx::Ctx,
|
||||
) -> std::io::Result<PipelinedPesStream>
|
||||
where
|
||||
S: SectorSource + Send + 'static,
|
||||
F: FnOnce() -> std::io::Result<S>,
|
||||
{
|
||||
let cap = reader.capacity_sectors();
|
||||
let scan = crate::disc::ScanOptions {
|
||||
halt: Some(ctx.halt.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut disc = crate::disc::Disc::scan_image(&mut reader, cap, &scan)?;
|
||||
if folder {
|
||||
crate::session::apply_folder_encryption_verdict(&mut reader, &mut disc)?;
|
||||
}
|
||||
image_input_scanned(reader, disc, opts, reopen, ctx)
|
||||
}
|
||||
use crate::pes::Stream as _;
|
||||
use crate::sector::SectorSource;
|
||||
use std::sync::mpsc;
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
//! One way to open any input (pipeline design §2.2, slice 4): [`open_source`] takes a URL
|
||||
//! for every input scheme, `disc://` included, and probes it once. The [`Source`] carries
|
||||
//! that probe's [`Disc`] layout (for `disc://`, `iso://` and `dir://`), so listing titles,
|
||||
//! acquiring keys and muxing read the same scan. A caller that already holds a scanned
|
||||
//! layout (a drive scan reused over a staged image, J14) or an opened session builds the
|
||||
//! same `Source` with [`Source::from_image`], [`Source::from_reader`] or
|
||||
//! [`Source::from_session`]; [`crate::mux_with_keys`] and [`crate::input`] take it.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::ctx::Ctx;
|
||||
use crate::disc::{Disc, ScanOptions};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::sector::SectorSource;
|
||||
use crate::session::{DeviceTarget, DiscSession, KeySpec};
|
||||
|
||||
use super::resolve::{StreamUrl, parse_url};
|
||||
use super::videomap::{Medium, SourceInfo};
|
||||
|
||||
/// One title of a disc the caller already scanned, for [`Source::from_image`] and
|
||||
/// [`Source::from_reader`]: the title, its container format, and the disc's name.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScannedTitle {
|
||||
pub title: crate::disc::DiscTitle,
|
||||
pub format: crate::disc::ContentFormat,
|
||||
/// The disc's name (meta title, else volume id), which names the output title; `None`
|
||||
/// keeps the title's own playlist name.
|
||||
pub disc_name: Option<String>,
|
||||
pub volume_id: String,
|
||||
}
|
||||
|
||||
impl ScannedTitle {
|
||||
/// Title `idx` of `disc`, named by the disc. `None` when out of range.
|
||||
pub fn of(disc: &Disc, idx: usize) -> Option<Self> {
|
||||
Some(ScannedTitle {
|
||||
title: disc.titles.get(idx)?.clone(),
|
||||
format: disc.content_format,
|
||||
disc_name: Some(disc_name(disc)),
|
||||
volume_id: disc.volume_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// `title` alone: no disc name or volume id.
|
||||
pub fn new(title: crate::disc::DiscTitle, format: crate::disc::ContentFormat) -> Self {
|
||||
ScannedTitle {
|
||||
title,
|
||||
format,
|
||||
disc_name: None,
|
||||
volume_id: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A disc's name: its meta title, else its volume id.
|
||||
fn disc_name(d: &Disc) -> String {
|
||||
d.meta_title.clone().unwrap_or_else(|| d.volume_id.clone())
|
||||
}
|
||||
|
||||
/// An opened input: where PES frames come from, and the layout probed from it.
|
||||
pub struct Source<'a> {
|
||||
pub(crate) origin: Origin<'a>,
|
||||
}
|
||||
|
||||
pub(crate) enum Origin<'a> {
|
||||
// A live drive session the caller keeps (tray lock, eject, finish stay with it).
|
||||
Session(&'a mut DiscSession),
|
||||
// `disc://`: a session opened and scanned here.
|
||||
Drive(Box<DiscSession>),
|
||||
// An image (`iso://`, `dir://`) and the probe's reader.
|
||||
Image {
|
||||
path: PathBuf,
|
||||
folder: bool,
|
||||
reader: Box<dyn SectorSource>,
|
||||
disc: Disc,
|
||||
},
|
||||
// An image file read with a title the caller scanned elsewhere (J14): opened at read
|
||||
// time, never rescanned.
|
||||
Prescanned {
|
||||
path: PathBuf,
|
||||
title: ScannedTitle,
|
||||
},
|
||||
// A raw live reader the caller already holds, with the title scanned off it.
|
||||
Live {
|
||||
reader: Box<dyn SectorSource>,
|
||||
title: ScannedTitle,
|
||||
},
|
||||
// A container or stream input, opened when it is read.
|
||||
Stream {
|
||||
url: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Open the input `url` and probe it: a drive is brought up and scanned (`disc://`, with
|
||||
/// `probe`'s credentials and key sources for the handshake), an image or folder is scanned
|
||||
/// (`iso://`, `dir://`; a folder's AACS verdict is judged from its content). Container and
|
||||
/// stream inputs (`m2ts://`, `mkv://`, `mp4://`, `mpg://`, `network://`, `stdio://`) open
|
||||
/// when read. `ctx.halt` stops the bring-up and the scan.
|
||||
pub fn open_source(url: &str, mut probe: ScanOptions, ctx: &Ctx) -> Result<Source<'static>> {
|
||||
if probe.halt.is_none() {
|
||||
probe.halt = Some(ctx.halt.clone());
|
||||
}
|
||||
let origin = match parse_url(url) {
|
||||
StreamUrl::Disc { device } => {
|
||||
let target = device.map_or(DeviceTarget::Autodetect, DeviceTarget::Path);
|
||||
let spec = KeySpec {
|
||||
credentials: probe.credentials.take(),
|
||||
key_sources: std::mem::take(&mut probe.key_sources),
|
||||
..KeySpec::default()
|
||||
};
|
||||
let mut session = DiscSession::open_with(target, spec, &ctx.halt)?;
|
||||
session.scan_with(probe)?;
|
||||
Origin::Drive(Box::new(session))
|
||||
}
|
||||
StreamUrl::Iso { path } => {
|
||||
super::resolve::validate_path(&path, "iso")?;
|
||||
let (disc, reader) = probe_image(&path, false, &probe)?;
|
||||
Origin::Image {
|
||||
path,
|
||||
folder: false,
|
||||
reader,
|
||||
disc,
|
||||
}
|
||||
}
|
||||
StreamUrl::Dir { path } => {
|
||||
super::resolve::validate_path(&path, "dir")?;
|
||||
let (disc, reader) = probe_image(&path, true, &probe)?;
|
||||
Origin::Image {
|
||||
path,
|
||||
folder: true,
|
||||
reader,
|
||||
disc,
|
||||
}
|
||||
}
|
||||
StreamUrl::M2ts { .. }
|
||||
| StreamUrl::Mkv { .. }
|
||||
| StreamUrl::Mp4 { .. }
|
||||
| StreamUrl::Mpg { .. }
|
||||
| StreamUrl::Network { .. }
|
||||
| StreamUrl::Stdio => Origin::Stream {
|
||||
url: url.to_string(),
|
||||
},
|
||||
StreamUrl::Null
|
||||
| StreamUrl::Demux { .. }
|
||||
| StreamUrl::Video { .. }
|
||||
| StreamUrl::Audio { .. }
|
||||
| StreamUrl::Sub { .. }
|
||||
| StreamUrl::Fvi { .. }
|
||||
| StreamUrl::Chapters { .. }
|
||||
| StreamUrl::Json { .. } => return Err(Error::StreamWriteOnly),
|
||||
StreamUrl::Unknown { raw } => return Err(Error::StreamUrlInvalid { url: raw }),
|
||||
};
|
||||
Ok(Source { origin })
|
||||
}
|
||||
|
||||
/// The one image probe (`iso://` file or `dir://` folder): scan the image, and judge a
|
||||
/// folder's AACS verdict from its content (tree shape alone can be wrong for a decrypted
|
||||
/// folder that kept `AACS/`). Returns the layout and the probe's reader.
|
||||
pub(crate) fn probe_image(
|
||||
path: &Path,
|
||||
folder: bool,
|
||||
probe: &ScanOptions,
|
||||
) -> Result<(Disc, Box<dyn SectorSource>)> {
|
||||
let mut reader: Box<dyn SectorSource> = if folder {
|
||||
Box::new(crate::dirimage::DirImage::open(path)?)
|
||||
} else {
|
||||
Box::new(crate::io::file_sector_source::FileSectorSource::open(path)?)
|
||||
};
|
||||
let cap = reader.capacity_sectors();
|
||||
let mut disc = Disc::scan_image(&mut reader, cap, probe)?;
|
||||
if folder {
|
||||
crate::session::apply_folder_encryption_verdict(&mut reader, &mut disc)?;
|
||||
}
|
||||
Ok((disc, reader))
|
||||
}
|
||||
|
||||
impl<'a> Source<'a> {
|
||||
/// A live drive the caller opened and scanned; the session stays the caller's, so it
|
||||
/// still locks, ejects and finishes the tray. Its drive is staged as the reader here.
|
||||
pub fn from_session(session: &'a mut DiscSession) -> Source<'a> {
|
||||
Source {
|
||||
origin: Origin::Session(session),
|
||||
}
|
||||
}
|
||||
|
||||
/// The image file at `path` read with a title the caller already scanned (the drive's,
|
||||
/// J14): it is never rescanned, so a sweep's unread filesystem sectors do not matter.
|
||||
/// The mux reads this one title whatever `MuxOptions::title_index` says.
|
||||
pub fn from_image(path: &Path, title: ScannedTitle) -> Source<'static> {
|
||||
Source {
|
||||
origin: Origin::Prescanned {
|
||||
path: path.to_path_buf(),
|
||||
title,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw live reader the caller holds, with the title scanned off it (read like
|
||||
/// [`Self::from_image`]'s, whatever `MuxOptions::title_index` says).
|
||||
pub fn from_reader(reader: Box<dyn SectorSource>, title: ScannedTitle) -> Source<'static> {
|
||||
Source {
|
||||
origin: Origin::Live { reader, title },
|
||||
}
|
||||
}
|
||||
|
||||
/// The probed layout: the scanned disc for `disc://`, `iso://`, `dir://` and the
|
||||
/// `from_*` forms; `None` for a container or stream input.
|
||||
pub fn layout(&self) -> Option<&Disc> {
|
||||
match &self.origin {
|
||||
Origin::Session(s) => s.disc(),
|
||||
Origin::Drive(s) => s.disc(),
|
||||
Origin::Image { disc, .. } => Some(disc),
|
||||
Origin::Prescanned { .. } | Origin::Live { .. } | Origin::Stream { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The opened drive session, for `disc://` and [`Self::from_session`].
|
||||
pub fn session_mut(&mut self) -> Option<&mut DiscSession> {
|
||||
match &mut self.origin {
|
||||
Origin::Session(s) => Some(s),
|
||||
Origin::Drive(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The drive session `open_source("disc://…")` opened, so the caller can finish it
|
||||
/// (unlock, eject). `None` for every other input.
|
||||
pub fn into_session(self) -> Option<DiscSession> {
|
||||
match self.origin {
|
||||
Origin::Drive(s) => Some(*s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The probe's layout and raw reader, for a caller that keys or copies the image itself
|
||||
/// (`iso://`, `dir://`). `None` for every other input.
|
||||
pub fn into_image(self) -> Option<(Disc, Box<dyn SectorSource>)> {
|
||||
match self.origin {
|
||||
Origin::Image { reader, disc, .. } => Some((disc, reader)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw reader the layout was probed through, for key acquisition before a mux.
|
||||
pub fn reader_mut(&mut self) -> Option<&mut dyn SectorSource> {
|
||||
match &mut self.origin {
|
||||
Origin::Session(s) => s.source_mut(),
|
||||
Origin::Drive(s) => s.source_mut(),
|
||||
Origin::Image { reader: r, .. } | Origin::Live { reader: r, .. } => Some(r.as_mut()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// The provenance of title `title` read from this source (one place, SO15).
|
||||
pub(crate) fn provenance(&self, title: usize) -> SourceInfo {
|
||||
let (medium, path) = match &self.origin {
|
||||
Origin::Session(s) => (Medium::Disc, format!("disc://{}", s.device_path())),
|
||||
Origin::Drive(s) => (Medium::Disc, format!("disc://{}", s.device_path())),
|
||||
Origin::Image { path, folder, .. } => (
|
||||
Medium::Iso,
|
||||
format!(
|
||||
"{}://{}",
|
||||
if *folder { "dir" } else { "iso" },
|
||||
path.display()
|
||||
),
|
||||
),
|
||||
Origin::Prescanned { path, .. } => (Medium::Iso, format!("iso://{}", path.display())),
|
||||
Origin::Live { .. } => (Medium::Disc, String::new()),
|
||||
Origin::Stream { url } => (super::driver::url_medium(&parse_url(url)), url.clone()),
|
||||
};
|
||||
let (playlist, volume_id) = match (&self.origin, self.layout()) {
|
||||
(Origin::Prescanned { title: t, .. } | Origin::Live { title: t, .. }, _) => {
|
||||
(t.title.playlist.clone(), t.volume_id.clone())
|
||||
}
|
||||
(_, Some(d)) => (
|
||||
d.titles
|
||||
.get(title)
|
||||
.map(|t| t.playlist.clone())
|
||||
.unwrap_or_default(),
|
||||
d.volume_id.clone(),
|
||||
),
|
||||
(_, None) => (String::new(), String::new()),
|
||||
};
|
||||
// A pre-scanned title carries no index; it is reported as 0.
|
||||
let title = match &self.origin {
|
||||
Origin::Prescanned { .. } | Origin::Live { .. } => 0,
|
||||
_ => title,
|
||||
};
|
||||
SourceInfo {
|
||||
medium,
|
||||
path,
|
||||
title,
|
||||
playlist,
|
||||
volume_id,
|
||||
}
|
||||
}
|
||||
|
||||
// The output title's name (SO14): every disc layout names it by the disc (its meta
|
||||
// title, else its volume id); a container keeps its own.
|
||||
pub(crate) fn title_name(&self) -> Option<String> {
|
||||
match &self.origin {
|
||||
Origin::Prescanned { title, .. } | Origin::Live { title, .. } => {
|
||||
title.disc_name.clone()
|
||||
}
|
||||
_ => self.layout().map(disc_name),
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-11
@@ -12,7 +12,7 @@ use crate::drive::{Drive, find_drive};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::halt::{Halt, Liveness};
|
||||
use crate::keysource::KeySource;
|
||||
use crate::sector::{FileSectorSource, SectorSource};
|
||||
use crate::sector::SectorSource;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -522,10 +522,7 @@ impl DiscSession {
|
||||
/// carries. The returned reader is a fresh handle at the start of the image,
|
||||
/// reusable by callers that need to sample ciphertext or feed a mux.
|
||||
pub fn scan_iso(path: &Path, opts: ScanOptions) -> Result<(Disc, Box<dyn SectorSource>)> {
|
||||
let mut reader = FileSectorSource::open(path)?;
|
||||
let capacity = reader.capacity_sectors();
|
||||
let disc = Disc::scan_image(&mut reader, capacity, &opts)?;
|
||||
Ok((disc, Box::new(reader)))
|
||||
crate::mux::source::probe_image(path, false, &opts)
|
||||
}
|
||||
|
||||
// Sampled 6144-byte aligned units when judging whether a folder with `AACS/`
|
||||
@@ -543,12 +540,7 @@ const AACS_PROBE_UNITS: usize = 8;
|
||||
/// * none need decryption → `encrypted` is forced false, reason logged.
|
||||
/// * any unit does → [`Error::DirImageEncrypted`] (`dir://` doesn't support it).
|
||||
pub fn scan_dir(path: &Path, opts: ScanOptions) -> Result<(Disc, Box<dyn SectorSource>)> {
|
||||
let mut reader = crate::dirimage::DirImage::open(path)?;
|
||||
let capacity = reader.capacity_sectors();
|
||||
let mut disc = Disc::scan_image(&mut reader, capacity, &opts)?;
|
||||
|
||||
apply_folder_encryption_verdict(&mut reader, &mut disc)?;
|
||||
Ok((disc, Box::new(reader)))
|
||||
crate::mux::source::probe_image(path, true, &opts)
|
||||
}
|
||||
|
||||
// Re-judge a FOLDER's encryption verdict from its CONTENT (tree shape alone can be wrong for an
|
||||
|
||||
Reference in New Issue
Block a user