diff --git a/docs/api-design.md b/docs/api-design.md index d37d5d9..88564cd 100644 --- a/docs/api-design.md +++ b/docs/api-design.md @@ -85,10 +85,10 @@ All URLs require a `scheme://path` format. Bare paths are rejected. // PES pipeline (frame-level) — input() returns Box, // output() returns Box. let input = libfreemkv::input("disc:///dev/sg4", &opts)?; // DiscStream -let input = libfreemkv::input("iso://Dune.iso", &opts)?; // IsoStream -let output = libfreemkv::output("mkv://Dune.mkv", &title)?; // MkvOutputStream -let output = libfreemkv::output("m2ts://Dune.m2ts", &title)?; // M2tsOutputStream -let output = libfreemkv::output("network://10.0.0.1:9000", &title)?; // NetworkOutputStream +let input = libfreemkv::input("iso://Movie.iso", &opts)?; // IsoStream +let output = libfreemkv::output("mkv://Movie.mkv", &title)?; // MkvOutputStream +let output = libfreemkv::output("m2ts://Movie.m2ts", &title)?; // M2tsOutputStream +let output = libfreemkv::output("network://192.0.2.10:9000", &title)?; // NetworkOutputStream let output = libfreemkv::output("null://", &title)?; // NullOutputStream ``` diff --git a/src/aacs/keydb.rs b/src/aacs/keydb.rs index bb2de3a..f3a8c34 100644 --- a/src/aacs/keydb.rs +++ b/src/aacs/keydb.rs @@ -448,9 +448,9 @@ mod tests { #[test] fn test_parse_disc_entry() { - let line = r#"***REMOVED*** = DUNE_PART_TWO (Dune: Part Two) | D | 2024-04-02 | M | ***REMOVED*** | I | ***REMOVED*** | V | ***REMOVED*** | U | 1-***REMOVED*** ; MKBv77"#; + let line = r#"0x000102030405060708090A0B0C0D0E0F10111213 = SAMPLE_FILM (Sample Film) | D | 2024-01-01 | M | 0x000102030405060708090A0B0C0D0E0F | I | 0x101112131415161718191A1B1C1D1E1F | V | 0x202122232425262728292A2B2C2D2E2F | U | 1-0x303132333435363738393A3B3C3D3E3F ; MKBv77"#; let entry = KeyDb::parse_disc_entry(line).unwrap(); - assert_eq!(entry.title, "Dune: Part Two"); + assert_eq!(entry.title, "Sample Film"); assert!(entry.media_key.is_some()); assert!(entry.vuk.is_some()); assert_eq!(entry.unit_keys.len(), 1); @@ -459,7 +459,7 @@ mod tests { #[test] fn test_parse_device_key() { - let line = "| DK | DEVICE_KEY ***REMOVED*** | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; MKBv01-MKBv48"; + let line = "| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; MKBv01-MKBv48"; let dk = KeyDb::parse_device_key(line).unwrap(); assert_eq!(dk.node, 0x0800); assert_eq!(dk.u_mask_shift, 0x17); @@ -472,9 +472,9 @@ mod tests { // candidate: it lands in `processing_keys` and the brute walker // handles it. let cfg = r#" -| DK | DEVICE_KEY ***REMOVED*** ; orphan from HKD\x02 corpus -| DK | DEVICE_KEY ***REMOVED*** | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; positioned MKBv01-MKBv48 -| PK | ***REMOVED*** ; legacy PK row still works +| DK | DEVICE_KEY 0xDEADBEEF0001020304050607080900AA ; orphan, no position fields +| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17 ; positioned MKBv01-MKBv48 +| PK | 0xCAFEBABE0001020304050607080900BB ; legacy PK row still works "#; let db = KeyDb::parse(cfg); assert_eq!( @@ -488,29 +488,29 @@ mod tests { 2, "orphan DK row + legacy PK row both belong in processing_keys" ); - assert_eq!(db.processing_keys[0][..4], [0xC5, 0xDD, 0xB5, 0xB4]); - assert_eq!(db.processing_keys[1][..4], [0x76, 0xDD, 0xD7, 0x09]); + assert_eq!(db.processing_keys[0][..4], [0xDE, 0xAD, 0xBE, 0xEF]); + assert_eq!(db.processing_keys[1][..4], [0xCA, 0xFE, 0xBA, 0xBE]); } #[test] fn test_parse_orphan_dk_rejects_lines_with_position_fields() { // The parser must NOT pick up a positioned DK row as an orphan // (that would double-count). parse_orphan_dk explicitly checks. - let positioned = "| DK | DEVICE_KEY ***REMOVED*** | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17"; + let positioned = "| DK | DEVICE_KEY 0x000102030405060708090A0B0C0D0E0F | DEVICE_NODE 0x0800 | KEY_UV 0x00000400 | KEY_U_MASK_SHIFT 0x17"; assert!( KeyDb::parse_orphan_dk(positioned).is_none(), "positioned DK must not match orphan parser" ); - let orphan = "| DK | DEVICE_KEY ***REMOVED***"; + let orphan = "| DK | DEVICE_KEY 0xDEADBEEF0001020304050607080900AA"; let key = KeyDb::parse_orphan_dk(orphan).expect("orphan should parse"); - assert_eq!(key[..4], [0xC5, 0xDD, 0xB5, 0xB4]); + assert_eq!(key[..4], [0xDE, 0xAD, 0xBE, 0xEF]); } #[test] fn test_parse_host_cert() { - let line = "| HC | HOST_PRIV_KEY ***REMOVED*** | HOST_CERT ***REMOVED*** ; Revoked"; + let line = "| HC | HOST_PRIV_KEY 0xDEADBEEF000102030405060708090A0B0C0D0E0F | HOST_CERT 0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B ; Revoked"; let hc = KeyDb::parse_host_cert(line).unwrap(); - assert_eq!(hc.private_key[0], 0x90); + assert_eq!(hc.private_key[0], 0xDE); assert_eq!(hc.certificate.len(), 92); } @@ -528,15 +528,15 @@ mod tests { assert!(!db.host_certs.is_empty()); assert!(db.disc_entries.len() > 170000); - // Look up Dune: Part Two - let dune = db + // Look up any disc entry carrying a full key set. + let entry = db .disc_entries .values() - .find(|e| e.title.contains("Dune: Part Two") && e.vuk.is_some()) - .expect("Dune: Part Two not found"); - assert!(dune.media_key.is_some()); - assert!(dune.vuk.is_some()); - assert!(!dune.unit_keys.is_empty()); + .find(|e| e.vuk.is_some() && e.media_key.is_some() && !e.unit_keys.is_empty()) + .expect("no disc entry with a full key set"); + assert!(entry.media_key.is_some()); + assert!(entry.vuk.is_some()); + assert!(!entry.unit_keys.is_empty()); eprintln!( "Parsed {} disc entries, {} DK, {} PK", diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index 1645f83..63c65af 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -1163,8 +1163,7 @@ mod tests { #[test] fn test_vuk_derivation() { - // Civil War UHD: known MK, VID, VUK from KEYDB - // MK = 15665F98..., VID (disc_id) = from entry, VUK = F96D7908... + // Pick any UHD entry with a known MK, VID, and VUK from KEYDB. // VUK = AES-DEC(MK, VID) XOR VID let path = match keydb_path() { Some(p) => p, @@ -1237,7 +1236,7 @@ mod tests { #[test] fn test_decrypt_real_unit() { - // Try decrypting a real encrypted aligned unit from Civil War UHD + // Try decrypting a real encrypted aligned unit from a UHD sample. // This disc is AACS 2.0 (BEE) so unit key alone won't work — // we need bus decryption first. But this verifies the pipeline. let unit_path = std::path::Path::new("/tmp/encrypted_unit.bin"); @@ -1258,20 +1257,17 @@ mod tests { }; let db = KeyDb::load(&kp).unwrap(); - // Civil War UHD entries - let civil_war_entries: Vec<&DiscEntry> = db + // Candidate entries: any UHD entry that carries unit keys. + let candidate_entries: Vec<&DiscEntry> = db .disc_entries .values() - .filter(|e| e.title.contains("CIVIL WAR") && !e.unit_keys.is_empty()) + .filter(|e| !e.unit_keys.is_empty()) .collect(); - eprintln!( - "Found {} Civil War entries with unit keys", - civil_war_entries.len() - ); + eprintln!("Found {} entries with unit keys", candidate_entries.len()); // Try each entry's unit keys - for entry in &civil_war_entries { + for entry in &candidate_entries { let keys: Vec<[u8; 16]> = entry.unit_keys.iter().map(|(_, k)| *k).collect(); let mut unit = original.clone(); @@ -1304,10 +1300,11 @@ mod tests { #[test] fn test_disc_hash_hex() { let hash = [ - ***REMOVED***, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, ]; let hex = disc_hash_hex(&hash); - assert_eq!(hex, "***REMOVED***"); + assert_eq!(hex, "0x000102030405060708090A0B0C0D0E0F10111213"); } #[test] @@ -1601,29 +1598,26 @@ mod tests { assert_eq!(selected, only07, "fallback returns the 0x07 body"); } - /// Locate a captured MKB research sample, if the private research tree - /// is checked out alongside the crate. Returns `None` (skip) otherwise. + /// Locate a captured MKB sample under the optional `MKB_SAMPLE_DIR`. + /// Returns `None` (skip) when the directory or file is absent. fn mkb_sample(rel: &str) -> Option { - let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .parent()? // freemkv/ - .join("(internal)/research/aacs/mkb-samples") - .join(rel); + let dir = std::env::var("MKB_SAMPLE_DIR").ok()?; + let p = std::path::Path::new(&dir).join(rel); if p.exists() { Some(p) } else { None } } #[test] fn real_aacs2_samples_select_large_0x05_not_small_0x07() { - // Real in-drive AACS 2.x UHD MKBs (Wicked / Civil War / MOVIE) - // carry BOTH a small 0x07 Explicit-Subset-Difference record (96 - // 16-byte entries) AND the large 0x05 Media Key Data / cvalue table - // (181270 entries, 1:1 with the 0x04 index). The production selector - // must return the LARGE 0x05 body, not the small 0x07 one. This is - // the exact regression #259 found. Skips when the research tree is - // absent. + // Real in-drive AACS 2.x UHD MKBs carry BOTH a small 0x07 + // Explicit-Subset-Difference record (96 16-byte entries) AND the + // large 0x05 Media Key Data / cvalue table (181270 entries, 1:1 + // with the 0x04 index). The production selector must return the + // LARGE 0x05 body, not the small 0x07 one. This is the exact + // regression #259 found. Skips when no sample dir is present. let samples = [ - "wicked/MKB_RO.inf", - "civilwar-uhd/MKB_RO.inf", - "movie-uhd-2.1/MKB_RO.inf", + "sample-a/MKB_RO.inf", + "sample-b/MKB_RO.inf", + "sample-c/MKB_RO.inf", ]; let mut checked = 0; for rel in samples { @@ -1707,29 +1701,32 @@ mod tests { }; let db = KeyDb::load(&path).unwrap(); - // Find V for Vendetta BD — has VUK and unit keys - // hash: ***REMOVED*** - let entry = db.find_disc("***REMOVED***"); + // Find any BD entry that carries a VUK and unit keys, then exercise + // the lookup-by-hash + VUK-derivation chain against it. + let entry = db + .disc_entries + .values() + .find(|e| e.vuk.is_some() && !e.unit_keys.is_empty() && e.disc_id.is_some()); if entry.is_none() { return; } let entry = entry.unwrap(); let vuk = entry.vuk.unwrap(); let vid = entry.disc_id.unwrap(); + let hash_hex = format!("0x{}", entry.disc_hash.trim_start_matches("0x")); // We need the actual Unit_Key_RO.inf from the disc to compute disc hash. // Since we don't have it, we can at least test that the KEYDB lookup // works with a known hash. - let hash_hex = "***REMOVED***"; - let found = db.find_disc(hash_hex); + let found = db.find_disc(&hash_hex); assert!(found.is_some()); assert_eq!(found.unwrap().vuk, Some(vuk)); // Verify VUK derivation if we have MK + VID if let Some(mk) = entry.media_key { let derived = derive_vuk(&mk, &vid); - assert_eq!(derived, vuk, "VUK derivation mismatch for V for Vendetta"); - eprintln!("V for Vendetta VUK derivation verified"); + assert_eq!(derived, vuk, "VUK derivation mismatch"); + eprintln!("VUK derivation verified"); } } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index c747895..a21dc3e 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -981,9 +981,9 @@ impl ScanOptions { /// Quick disc identification — name, format, capacity. No title/stream parsing. #[derive(Debug)] pub struct DiscId { - /// UDF Volume Identifier (always present, e.g. "V_FOR_VENDETTA") + /// UDF Volume Identifier (always present, e.g. "SAMPLE_FILM") pub volume_id: String, - /// Disc title from META/DL/bdmt_eng.xml (e.g. "V for Vendetta") + /// Disc title from META/DL/bdmt_eng.xml (e.g. "Sample Film") pub meta_title: Option, /// Disc format (BD, UHD, DVD) — UHD vs BD requires full scan to confirm pub format: DiscFormat, diff --git a/src/disc/read_error.rs b/src/disc/read_error.rs index a6beb12..a708add 100644 --- a/src/disc/read_error.rs +++ b/src/disc/read_error.rs @@ -334,7 +334,7 @@ const FAIL_PAUSE_SECS: u64 = 5; /// FIRST read failure after a clean run, before the drive has had a /// chance to cycle in retries that push it toward fast-fail). /// -/// Empirical: 2026-05-11 Dune Pt 2 wedge incident showed 7 medium +/// Empirical: a 2026-05-11 wedge incident showed 7 medium /// errors in 6.5 seconds (~1s per attempt + ~1s pause) push the /// BU40N's firmware into IllegalRequest fast-fail mode permanently. /// Once there, only physical eject + reload clears it. Giving the diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 291bab1..44ecf15 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -529,7 +529,7 @@ impl Drive { // /dev/sr0 pread fallback (Linux only). The kernel // sr_mod driver auto-retries failed reads (~5× per - // command). Empirically (BU40N + Dune Part 2 UHD, + // command). Empirically (BU40N + a UHD disc, // 2026-05-08) dd via /dev/sr0 recovers ~50% of bad // sectors that a single-shot SG_IO READ misses. #[cfg(target_os = "linux")] diff --git a/src/io/byte_channel.rs b/src/io/byte_channel.rs index ef92f96..80a3693 100644 --- a/src/io/byte_channel.rs +++ b/src/io/byte_channel.rs @@ -16,9 +16,6 @@ //! The underlying mpsc channel is created with a very large slot count //! so the byte cap (not the slot count) is the real backpressure. Slot //! count is only there to give the kernel a small chunk to wake on. -//! -//! See `(internal)/memory/project_buffering_architecture.md` § -//! Pipeline channel — sizing. use std::sync::mpsc::{Receiver as MpscReceiver, RecvError, SendError, SyncSender, sync_channel}; use std::sync::{Arc, Condvar, Mutex}; diff --git a/src/io/mod.rs b/src/io/mod.rs index 993502c..8c10a4e 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -23,8 +23,7 @@ //! bounded channel + dedicated consumer thread. //! //! `byte_channel` is a byte-sized producer/consumer channel for the -//! mux pipeline, sized to absorb worst-case input read stalls (see -//! `(internal)/memory/project_buffering_architecture.md`). +//! mux pipeline, sized to absorb worst-case input read stalls. pub(crate) mod bounded; pub mod byte_channel; diff --git a/src/io/pipeline.rs b/src/io/pipeline.rs index 2916797..180c5a4 100644 --- a/src/io/pipeline.rs +++ b/src/io/pipeline.rs @@ -9,7 +9,6 @@ //! sweep (migrated to `disc/sweep.rs::SweepSink`), patch, and mux. //! 0.18 collapses all three onto this primitive; sweep is in, //! patch and mux migrate in later 0.18 slices. -//! See `(internal)/memory/0_18_redesign.md` for the full picture. //! //! ## Cancellation and error semantics //! @@ -68,9 +67,7 @@ pub const JOIN_TIMEOUT_SECS: u64 = 600; /// /// 0.21.7 replaced an old `std::sync::mpsc::sync_channel` + 50 ms /// `thread::sleep` polling loop that capped mux throughput at -/// ~20 frames/sec ≈ 1 MB/s on saturated channels. See -/// (internal)/memory/feedback_send_with_halt_poll_throttle.md -/// for the multi-day diagnostic that surfaced it. +/// ~20 frames/sec ≈ 1 MB/s on saturated channels. use crate::halt::POLL_INTERVAL; const SEND_HALT_CHECK_INTERVAL: Duration = POLL_INTERVAL; diff --git a/src/io/sink/mod.rs b/src/io/sink/mod.rs index 5284278..8299c9c 100644 --- a/src/io/sink/mod.rs +++ b/src/io/sink/mod.rs @@ -20,9 +20,6 @@ //! 4 MiB buffer for the common local-disk case; `WritebackFile` //! (separate module) wraps a `File` with the adaptive-chunk //! `sync_file_range` machinery for the Linux+NFS case. -//! -//! See `(internal)/memory/project_buffering_architecture.md` for -//! the full design and the source/sink matrix. use std::io::{Seek, Write}; diff --git a/src/io/sink/socket.rs b/src/io/sink/socket.rs index 411ac25..312a6bf 100644 --- a/src/io/sink/socket.rs +++ b/src/io/sink/socket.rs @@ -47,7 +47,7 @@ impl SocketSink { /// writing. `sndbuf_bytes`, when present, is forwarded to /// `setsockopt(SO_SNDBUF)` as a kernel hint — the OS may clamp it. /// - /// `addr` accepts anything `ToSocketAddrs` does: `"10.0.0.1:1234"`, + /// `addr` accepts anything `ToSocketAddrs` does: `"192.0.2.1:1234"`, /// `("host", 1234)`, a `SocketAddr`, etc. pub fn connect(addr: A, sndbuf_bytes: Option) -> io::Result { let stream = TcpStream::connect(addr)?; diff --git a/src/labels/bdmt.rs b/src/labels/bdmt.rs index 90d78b1..d462f40 100644 --- a/src/labels/bdmt.rs +++ b/src/labels/bdmt.rs @@ -38,7 +38,7 @@ use std::collections::BTreeMap; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] pub struct DiscMetadata { /// Localized titles, keyed by 3-char ISO 639-2 lang code - /// (e.g. "eng" → "Dune Part Two") + /// (e.g. "eng" → "Aurora Drift") pub titles: BTreeMap, /// First-line / short description, per lang pub descriptions: BTreeMap, @@ -146,7 +146,7 @@ pub(crate) fn parse_bdmt_xml(_lang_code: &str, xml_text: &str) -> Option` contained `` child /// elements and no actual prose. Surfacing that raw to the JSON /// output is worse than dropping the field entirely. @@ -210,10 +210,10 @@ mod tests { // carrier inside a root. let xml = r#" - Dune Part Two + Aurora Drift "#; let (title, desc, set) = parse_bdmt_xml("eng", xml).expect("title should parse"); - assert_eq!(title, "Dune Part Two"); + assert_eq!(title, "Aurora Drift"); assert_eq!(desc, None); assert_eq!(set, None); } @@ -223,12 +223,12 @@ mod tests { // is the alternate carrier; should be picked up // when is absent. let xml = r#" - The Matrix - A film about computers. + Echo Chamber + A film about machines. "#; let (title, desc, _) = parse_bdmt_xml("eng", xml).unwrap(); - assert_eq!(title, "The Matrix"); - assert_eq!(desc.as_deref(), Some("A film about computers.")); + assert_eq!(title, "Echo Chamber"); + assert_eq!(desc.as_deref(), Some("A film about machines.")); } #[test] @@ -238,17 +238,17 @@ mod tests { // titleName inside tableOfContents. let xml = r#" - Inside Out 2 + Feelings Two "#; let (title, _, _) = parse_bdmt_xml("eng", xml).unwrap(); - assert_eq!(title, "Inside Out 2"); + assert_eq!(title, "Feelings Two"); } #[test] fn extract_box_set_position() { let xml = r#" - LOTR Disc 2 + Box Set Disc 2 2 5 "#; @@ -287,11 +287,11 @@ mod tests { // would. This exercises the BTreeMap key handling without // needing a UdfFs. let eng_xml = r#" - Dune Part Two + Aurora Drift "#; let fra_xml = r#" - Dune Deuxième Partie - Suite du film de 2021. + Aurora Drift (Partie Deux) + Suite du film fictif. "#; let mut meta = DiscMetadata::default(); @@ -310,16 +310,16 @@ mod tests { assert_eq!( meta.titles.get("eng").map(String::as_str), - Some("Dune Part Two") + Some("Aurora Drift") ); assert_eq!( meta.titles.get("fra").map(String::as_str), - Some("Dune Deuxième Partie") + Some("Aurora Drift (Partie Deux)") ); assert!(meta.descriptions.get("eng").is_none()); assert_eq!( meta.descriptions.get("fra").map(String::as_str), - Some("Suite du film de 2021.") + Some("Suite du film fictif.") ); assert_eq!(meta.disc_number, None); } @@ -342,21 +342,21 @@ mod tests { #[test] fn description_with_only_child_xml_is_dropped() { - // Real-world bug from disc-04 (Top Gun: Maverick, 2026-05-11 + // Real-world bug from a captured disc (2026-05-11 // capture): contained only // child elements with no actual prose. The previous parser // surfaced the raw XML fragment as the description string. // Now we reject candidates that begin with `<`. let xml = r#" - Top Gun: Maverick + Skyline Run - - + + "#; let (title, description, _) = parse_bdmt_xml("eng", xml).expect("title is present so parse must succeed"); - assert_eq!(title, "Top Gun: Maverick"); + assert_eq!(title, "Skyline Run"); assert!( description.is_none(), "description containing only XML children must be dropped, got {description:?}" @@ -381,10 +381,10 @@ mod tests { #[test] fn whitespace_in_title_is_trimmed() { let xml = r#" - Dune Part Two + Aurora Drift "#; let (title, _, _) = parse_bdmt_xml("eng", xml).unwrap(); - assert_eq!(title, "Dune Part Two"); + assert_eq!(title, "Aurora Drift"); } #[test] diff --git a/src/labels/dbp.rs b/src/labels/dbp.rs index 812aa26..ac5f1bd 100644 --- a/src/labels/dbp.rs +++ b/src/labels/dbp.rs @@ -7,8 +7,7 @@ //! Stream labels live as plain ASCII strings inside compiled `.class` //! files in the jar — a quirk of the menu-rendering layer encoding //! its TextField positions and content as constant strings the -//! Java compiler retained in the class string pool. Format observed -//! in the corpus (Civil War UHD, 2024): +//! Java compiler retained in the class string pool. Observed format: //! //! ```text //! LTextField,Audio1,English Dolby Atmos,Fontstrip_Composite,... diff --git a/src/labels/deluxe.rs b/src/labels/deluxe.rs index 7819a08..3cd2f46 100644 --- a/src/labels/deluxe.rs +++ b/src/labels/deluxe.rs @@ -1,83 +1,65 @@ //! Deluxe BD-J framework — `com/bydeluxe/bluray/` package signature. //! -//! Used by major studios (Disney, Warner, others) for their UHD -//! BD-J authoring. Detected on discs whose `/BDMV/JAR/.jar` -//! contains a `com/bydeluxe/` directory entry. +//! Detected on discs whose `/BDMV/JAR/.jar` contains a +//! `com/bydeluxe/` directory entry. //! -//! ## Why this parser exists +//! ## What this parser reads //! -//! Deluxe-authored discs store stream labels as **ordinal references -//! into obfuscated enum classes**. The label text isn't a literal -//! string in any anchor pattern (unlike dbp's `TextField,...` rows). -//! Instead, the binding code is roughly: -//! -//! ```java -//! streamTable.put(1, new AudioSlot(LanguageEnum.English, -//! CodecEnum.ATMOS_HD_AUDIO, -//! PurposeEnum.Normal)); -//! ``` -//! -//! The class names `LanguageEnum`, `CodecEnum`, `PurposeEnum`, and -//! `AudioSlot` are obfuscated per-disc (`be.class`, `ma.class`, -//! `lp.class`, etc.) — no name pattern survives the obfuscator. But -//! the **shape of ``** is framework-stable: +//! Deluxe-authored discs store stream labels as ordinal references into +//! enum classes whose names are obfuscated per-disc, so a name-based +//! match won't work. The label data is instead recovered by matching on +//! the **shape of each enum's ``**, which is framework-stable: //! //! | Enum | Signature | //! |---|---| //! | Language | 70 `ldc` operations in ``, sequence starts `English, French, Spanish, Dutch, ...` | //! | Purpose | 8 ldcs starting `Normal, Commentary, PiP, Trivia, ...` | //! | VideoFormat | 7 ldcs starting `HD, HDR10 Plus, HD Dolby, ...` | -//! | Region | 22 ldcs starting `USA_D1, LIC1, LIC2, LIC3, ...` (Disney only) | -//! | Studio | 6 ldcs starting `Disney, Marvel, Pixar, ...` (Disney only) | -//! | Codec | ~46 `new` instructions, 0 ldcs in `` (codec strings live in subclasses) | +//! | Region | 22 ldcs starting `USA_D1, LIC1, LIC2, LIC3, ...` | +//! | Studio | 6 ldcs starting `Disney, Marvel, Pixar, ...` | +//! | Codec | many `new` instructions, 0 ldcs in `` (codec strings live in subclasses) | //! -//! Match on the SHAPE, not the name, and the parser survives obfuscation. +//! Matching on the shape rather than the class name keeps the parser +//! working across obfuscation variants. //! //! ## Implementation phases //! //! - **Phase A** — master enum identification (`identify_master_enums`). //! Walks every `.class`'s `` ldc sequence and matches against //! the framework-stable fingerprints. Output: `Vec<(label, MasterEnum)>` -//! with full ordinal → string-value tables. **Empirically verified** -//! on disc-01 (Disney) + disc-09 (Warner). +//! with full ordinal → string-value tables. //! //! - **Phase B** — codec enum subclass walk (`decode_codec_enum`). -//! The codec enum's `` has ~46 `new` instructions and zero +//! The codec enum's `` has many `new` instructions and zero //! string ldcs — codec name strings live in the subclasses each //! `new` constructs. Walks every referenced subclass's constant -//! pool, extracts the codec name string. **Structural shape -//! verified** on disc-01 (ma.class, 41 `new` ops) + disc-09 -//! (ea.class, 46 `new` ops); per-subclass string extraction -//! designed against the published Java enum compilation convention -//! (each enum value's `` is called with its name string as -//! the first arg). +//! pool, extracts the codec name string, following the standard Java +//! enum compilation convention (each enum value's `` is called +//! with its name string as the first arg). //! //! - **Phase C** — binding-class identification (`find_binding_classes`). //! The per-stream table is built by some class via repeated //! `getstatic` references to the master enums identified in A. //! That class has the highest such `getstatic` count in the jar. -//! **Heuristic shape**; precise threshold may need tuning. +//! Heuristic shape; precise threshold may need tuning. //! //! - **Phase D** — binding-class bytecode decoder (`decode_binding`). //! Walks the binding class's `` with a tiny symbolic stack //! machine. For each `new X / dup / ... / invokespecial X.` //! sequence, collects the int values and enum-reference operands //! between the `dup` and the constructor call, then emits a -//! `DecodedStream`. **Mechanism verified** in unit tests against -//! synthetic class fixtures; the **signal-to-StreamLabel mapping** -//! (which arg is stream index? which is language? audio vs -//! subtitle?) uses a documented heuristic that needs corpus-disc -//! verification — see `interpret_stream` for the mapping rules. +//! `DecodedStream`. The signal-to-StreamLabel mapping (which arg is +//! stream index? which is language? audio vs subtitle?) uses a +//! heuristic — see `interpret_streams` for the mapping rules. //! //! ## Confidence //! //! [`parse`] returns `Some(ParseResult::medium(labels))` when Phases A //! through D produce at least one stream — `Medium` because the -//! signal-to-label mapping is heuristic until real disc bytecode -//! confirms the binding pattern. Once verified the parser can promote -//! to `High`. `None` when the disc isn't Deluxe-authored or when -//! decoding produces zero streams (a recognized-but-broken state that -//! the analyzer still surfaces via `parsers_detected`). +//! signal-to-label mapping is heuristic. `None` when the disc isn't +//! Deluxe-authored or when decoding produces zero streams (a +//! recognized-but-broken state that the analyzer still surfaces via +//! `parsers_detected`). use super::class_reader::{ AASTORE, BIPUSH, ClassFile, CodeAttribute, ConstantPool, CpInfo, GETSTATIC, ICONST_0, ICONST_1, @@ -559,10 +541,9 @@ fn clinit_news_and_ldcs( /// candidates ordered by descending getstatic count, filtered to a /// minimum concentration of master-enum references. /// -/// Empirically (POC v0.3 dumps): on disc-01 the audio binding class -/// (`ma.class`) has ~82 getstatic refs, the subtitle binding -/// (`ko.class`) has ~63. Both share the master Language + Purpose -/// enums. +/// A disc that splits the table commonly has one audio binding class +/// with the most getstatic refs and a subtitle binding class with +/// somewhat fewer; both share the master Language + Purpose enums. pub(crate) fn find_binding_classes( archive: &mut jar::Jar, master_enum_classes: &HashSet<&str>, @@ -641,10 +622,9 @@ pub(crate) enum StackVal { /// name (e.g. `DOLBY_AC3_AUDIO`, `DOLBY_LOSSLESS_AUDIO`) is the /// codec identifier. Deluxe binding constructors take a /// `LCodingType;` arg directly — codecs are NOT a Deluxe-internal - /// enum (Phase B's codec-subclass walk was based on a wrong - /// assumption; the actual codec source is the standard BD-J API - /// enum). Discovered via deluxe-poc v0.3 binding-bytecode dump - /// against disc-01 (Disney) + disc-09 (Warner) on 2026-05-10. + /// enum; the codec source is the standard BD-J API `CodingType` + /// enum, so the binding constructor's codec arg is read straight + /// from that getstatic operand. CodingType(String), /// An uninitialized `new` object — popped by the matching /// invokespecial. @@ -988,11 +968,10 @@ impl MasterEnumTable { // ── interpret_streams: Constructions → StreamLabels ───────────────────────── /// Convert the per-construction tuples from Phase D into -/// [`StreamLabel`]s. Pattern verified against corpus discs via -/// deluxe-poc v0.3 binding-bytecode dump (2026-05-10): +/// [`StreamLabel`]s. Two binding-constructor shapes are handled: /// -/// Disney binding (5-arg): `BindingType.(I, Lbe;, Llp;, I, LCodingType;)V` -/// Warner binding (4-arg): `BindingType.(I, Law;, Lgp;, LCodingType;)V` +/// 5-arg: `BindingType.(I, Lang;, Lpurpose;, I, LCodingType;)V` +/// 4-arg: `BindingType.(I, Lang;, Lpurpose;, LCodingType;)V` /// /// Args are identified by **TYPE**, not position: /// - First `EnumRef{kind: "Language"}` → audio/subtitle language @@ -1098,11 +1077,10 @@ fn interpret_streams(constructions: &[Construction], master: &MasterEnumTable) - /// getstatic operands on Deluxe binding classes) to a human-readable /// codec hint string. /// -/// CodingType is the standard BD-J API enum; values are documented -/// in the BD-J specification and verified empirically against the -/// binding-bytecode dumps in `(internal)/research/deluxe-poc/data/`. -/// Unknown field names pass through unchanged so unfamiliar codecs -/// still surface something rather than going silent. +/// CodingType is the standard BD-J API enum; values are documented in +/// the BD-J specification. Unknown field names pass through unchanged +/// so unfamiliar codecs still surface something rather than going +/// silent. fn coding_type_to_codec_hint(field: &str) -> &str { match field { // Lossless / hi-res. diff --git a/src/labels/mod.rs b/src/labels/mod.rs index 30fa0e7..fb4a056 100644 --- a/src/labels/mod.rs +++ b/src/labels/mod.rs @@ -954,7 +954,7 @@ mod gap_fill_tests { #[test] fn partial_yield_fills_gaps_keeps_framework() { - // Oppenheimer-style: framework matched but only labeled 2 of 6 audios. + // Partial-coverage case: framework matched but only labeled 2 of 6 audios. let mut framework = vec![ label(StreamLabelType::Audio, 1, "eng", "Atmos"), label(StreamLabelType::Audio, 4, "eng", "Commentary"), diff --git a/src/labels/pixelogic.rs b/src/labels/pixelogic.rs index 7549cc8..4073435 100644 --- a/src/labels/pixelogic.rs +++ b/src/labels/pixelogic.rs @@ -300,7 +300,7 @@ mod tests { #[test] fn assign_labels_numbers_commentary_behind_placeholders() { - // Wicked: the FPL_MainFeature playlist lists three unlabelled main + // Observed case: the FPL_MainFeature playlist lists three unlabelled main // audio tracks as `Audio Stream N` placeholders, then a lone // `eng_ACOM_` commentary at STN slot 4. The commentary must land on // audio #4, not collapse onto #1 (which would tag the main feature diff --git a/src/labels/vocab.rs b/src/labels/vocab.rs index 76bd319..e6ab56d 100644 --- a/src/labels/vocab.rs +++ b/src/labels/vocab.rs @@ -10,8 +10,7 @@ //! - English text → [`LabelPurpose`] (Commentary / Descriptive / etc.). //! - English text → [`LabelQualifier`] (SDH / Forced / Descriptive Service). //! -//! Rules of engagement (carried over from -//! `(internal)/memory/feedback_label_data_rules.md`): +//! Rules of engagement: //! //! 1. Only map values we are 100% certain about — published codec //! names, well-known ISO 639-2 mappings, vendor-documented purpose diff --git a/src/mux/mod.rs b/src/mux/mod.rs index e95d105..77a35a8 100644 --- a/src/mux/mod.rs +++ b/src/mux/mod.rs @@ -6,7 +6,7 @@ //! ```text //! let mut input = input("iso://Disc.iso", &opts)?; //! let title = input.info().clone(); -//! let mut output = output("mkv://Dune.mkv", &title)?; +//! let mut output = output("mkv://Movie.mkv", &title)?; //! while let Ok(Some(frame)) = input.read() { //! output.write(&frame)?; //! } diff --git a/tests/scsi_error_decoding.rs b/tests/scsi_error_decoding.rs index f97b90d..3ab36fe 100644 --- a/tests/scsi_error_decoding.rs +++ b/tests/scsi_error_decoding.rs @@ -9,9 +9,7 @@ //! [`ScsiSense::is_*`]. //! //! The actual `ioctl(SG_IO, ...)` call is impossible to mock without a -//! kernel — see -//! `(internal)/docs/audits/2026-04-26-scsi-architecture-research.md` -//! for why the audit recommends against libc shims here. These tests +//! kernel, so libc shims are deliberately avoided here. These tests //! therefore pin the *contract* every backend must satisfy via a mock //! `ScsiTransport`: //! diff --git a/tests/streams.rs b/tests/streams.rs index d3964ab..c0ca526 100644 --- a/tests/streams.rs +++ b/tests/streams.rs @@ -77,21 +77,21 @@ fn parse_url_disc_device() { #[test] fn parse_url_mkv() { - let u = parse_url("mkv://Dune.mkv"); + let u = parse_url("mkv://Movie.mkv"); assert_eq!(u.scheme(), "mkv"); - assert_eq!(u.path_str(), "Dune.mkv"); + assert_eq!(u.path_str(), "Movie.mkv"); } #[test] fn parse_url_network() { - let u = parse_url("network://10.0.0.1:9000"); + let u = parse_url("network://192.0.2.10:9000"); assert_eq!(u.scheme(), "network"); - assert_eq!(u.path_str(), "10.0.0.1:9000"); + assert_eq!(u.path_str(), "192.0.2.10:9000"); } #[test] fn parse_url_bare_path_rejected() { - let u = parse_url("Dune.mkv"); + let u = parse_url("Movie.mkv"); assert_eq!(u.scheme(), "unknown"); } @@ -104,21 +104,21 @@ fn parse_url_null() { #[test] fn parse_url_m2ts_with_path() { - let u = parse_url("m2ts:///tmp/Dune.m2ts"); + let u = parse_url("m2ts:///tmp/Movie.m2ts"); assert_eq!(u.scheme(), "m2ts"); - assert_eq!(u.path_str(), "/tmp/Dune.m2ts"); + assert_eq!(u.path_str(), "/tmp/Movie.m2ts"); } #[test] fn parse_url_m2ts_relative() { - let u = parse_url("m2ts://Dune.m2ts"); + let u = parse_url("m2ts://Movie.m2ts"); assert_eq!(u.scheme(), "m2ts"); - assert_eq!(u.path_str(), "Dune.m2ts"); + assert_eq!(u.path_str(), "Movie.m2ts"); } #[test] fn open_input_bare_path_errors() { - let result = libfreemkv::input("Dune.mkv", &libfreemkv::InputOptions::default()); + let result = libfreemkv::input("Movie.mkv", &libfreemkv::InputOptions::default()); assert!(result.is_err()); let msg = match result { Err(e) => e.to_string(), @@ -134,7 +134,7 @@ fn open_input_bare_path_errors() { #[test] fn open_output_bare_path_errors() { let dt = sample_disc_title(); - let result = libfreemkv::output("Dune.mkv", &dt); + let result = libfreemkv::output("Movie.mkv", &dt); assert!(result.is_err()); let msg = match result { Err(e) => e.to_string(), @@ -195,7 +195,7 @@ fn open_output_disc_errors() { #[test] fn open_input_network_no_port_errors() { - let result = libfreemkv::input("network://10.0.0.1", &libfreemkv::InputOptions::default()); + let result = libfreemkv::input("network://192.0.2.1", &libfreemkv::InputOptions::default()); assert!(result.is_err()); let msg = match result { Err(e) => e.to_string(),