udf: honor ICB allocation-descriptor type (Short/Long/Extended AD)
read_icb_extents hardcoded an 8-byte Short-AD stride for every file. Large BD-ROM .m2ts streams use 16-byte Long ADs; striding them as Short ADs reads descriptor #0 correctly (length+lba align) but lands #1 in the middle of the first Long AD (its zero impl_use bytes). The AD-list terminator (data_len==0 => break) then fired on that zero and stopped after the first extent, so every multi-extent title truncated at ~1 GiB. The same reader backs read_file, so disc AACS-input files (/AACS/*.inf) and the m2ts mux extents were both affected. Read the ICB Tag flags (AD type) and stride 8/16/20 bytes for Short/Long/ Extended ADs accordingly; Extended ADs carry the lba at off+12. aacs: extract trim_mkb and restore its guard so an MKB whose content length the parser cannot determine (mkb_content_len == 0) is returned intact instead of truncated to empty. Regression tests: Long-AD read_icb_extents returns all extents; Long-AD read_file returns full content; trim_mkb never zeroes an unrecognised MKB.
This commit is contained in:
@@ -563,6 +563,21 @@ pub fn mkb_content_len(mkb: &[u8]) -> usize {
|
||||
pos
|
||||
}
|
||||
|
||||
/// Trim an MKB's trailing fixed-region padding to its real content length —
|
||||
/// but ONLY when [`mkb_content_len`] actually found one. It returns 0 for an
|
||||
/// MKB whose first record cannot be parsed; truncating to 0 in that case would
|
||||
/// hand downstream consumers (and the online key service) an EMPTY MKB that can
|
||||
/// never resolve. So a 0 (or a length that isn't strictly inside the buffer)
|
||||
/// leaves the MKB untouched. A 0.31.0 regression dropped this guard and
|
||||
/// `truncate`-d unconditionally, zeroing unrecognised MKBs.
|
||||
pub fn trim_mkb(mut mkb: Vec<u8>) -> Vec<u8> {
|
||||
let n = mkb_content_len(&mkb);
|
||||
if n > 0 && n < mkb.len() {
|
||||
mkb.truncate(n);
|
||||
}
|
||||
mkb
|
||||
}
|
||||
|
||||
/// Get MKB version from Type and Version Record (type 0x10).
|
||||
/// Version is a BE u32 at offset 8 of the record body (offset 12 from `pos`).
|
||||
pub fn mkb_version(mkb: &[u8]) -> Option<u32> {
|
||||
@@ -1519,6 +1534,44 @@ mod tests {
|
||||
assert_eq!(mkb_content_len(&[]), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_mkb_never_zeroes_an_unrecognised_mkb() {
|
||||
// Regression: the 0.31.0 read_aacs_inputs path truncated the MKB to
|
||||
// mkb_content_len() unconditionally. For an MKB whose first record the
|
||||
// parser can't read, mkb_content_len() returns 0 → an unconditional
|
||||
// truncate zeroed the MKB, so autorip sent an EMPTY MKB to the key
|
||||
// service (or skipped the request). trim_mkb must leave it intact.
|
||||
let unrecognised = vec![0xFFu8; 4096]; // first "rec_type" 0xFF, rec_len huge → content_len 0
|
||||
assert_eq!(
|
||||
mkb_content_len(&unrecognised),
|
||||
0,
|
||||
"precondition: unparseable → 0"
|
||||
);
|
||||
assert_eq!(
|
||||
trim_mkb(unrecognised.clone()),
|
||||
unrecognised,
|
||||
"unrecognised MKB must be returned untouched, never zeroed"
|
||||
);
|
||||
|
||||
// A parseable MKB with trailing padding IS trimmed to its records.
|
||||
let mut mkb = vec![
|
||||
0x10, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4D,
|
||||
];
|
||||
mkb.extend_from_slice(&[0x86, 0x00, 0x00, 0x18]);
|
||||
mkb.extend_from_slice(&[0xAB; 16]);
|
||||
mkb.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
let records_len = mkb.len();
|
||||
mkb.extend(std::iter::repeat(0u8).take(1024));
|
||||
assert_eq!(
|
||||
trim_mkb(mkb).len(),
|
||||
records_len,
|
||||
"padded MKB trims to records"
|
||||
);
|
||||
|
||||
// Empty stays empty (n==0 → untouched).
|
||||
assert!(trim_mkb(Vec::new()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mkb_version_returns_none_on_empty() {
|
||||
assert_eq!(mkb_version(&[]), None);
|
||||
|
||||
@@ -34,6 +34,7 @@ pub use keys::{
|
||||
derive_media_key_from_dk, derive_media_key_from_pk, derive_media_key_from_pk_walked,
|
||||
derive_vuk, disc_hash, disc_hash_hex, mkb_content_len, mkb_version, parse_content_cert,
|
||||
parse_unit_key_ro, read_mkb_from_drive, resolve_keys_v1, resolve_keys_v2, resolve_keys_v21,
|
||||
trim_mkb,
|
||||
};
|
||||
pub use provider::KeyProvider;
|
||||
pub use variants::{
|
||||
|
||||
+6
-4
@@ -1307,13 +1307,15 @@ impl Disc {
|
||||
.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let mut mkb = udf_fs
|
||||
let mkb = udf_fs
|
||||
.read_file(reader, "/AACS/MKB_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RW.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let n = crate::aacs::mkb_content_len(&mkb);
|
||||
mkb.truncate(n);
|
||||
Ok((inf, mkb))
|
||||
// Trim trailing padding to the real MKB content length, never zeroing
|
||||
// an unrecognised MKB (see `crate::aacs::trim_mkb` — restores the
|
||||
// pre-0.31.0 guard so the online key service never receives an empty
|
||||
// MKB).
|
||||
Ok((inf, crate::aacs::trim_mkb(mkb)))
|
||||
}
|
||||
|
||||
/// Read a disc's AACS key-input files from an ISO image: returns
|
||||
|
||||
+138
-7
@@ -437,6 +437,33 @@ impl UdfFs {
|
||||
}
|
||||
};
|
||||
|
||||
// Allocation-descriptor type lives in the ICB Tag flags (low 3
|
||||
// bits). The ICB Tag immediately follows the 16-byte descriptor
|
||||
// tag, and its `flags` u16 is the last field at ICB-tag offset 18
|
||||
// → absolute offset 34, for both File Entry (261) and Extended
|
||||
// File Entry (266). 0 = Short AD (8 bytes), 1 = Long AD (16 bytes),
|
||||
// 2 = Extended AD (20 bytes), 3 = data embedded inline in the ICB.
|
||||
//
|
||||
// This MUST be honoured: a Short AD and a Long AD both carry
|
||||
// length+lba in their first 8 bytes, so hardcoding an 8-byte
|
||||
// stride reads descriptor #0 of a Long-AD file correctly but lands
|
||||
// descriptor #1 in the middle of the first Long AD (its impl_use
|
||||
// bytes) — garbage that trips the terminator/unknown-type break.
|
||||
// Large BD-ROM .m2ts streams use Long ADs, so that bug truncated
|
||||
// every multi-extent title at its first extent (~973 MB-1 GiB).
|
||||
let icb_flags = u16::from_le_bytes([icb[34], icb[35]]);
|
||||
let ad_type = (icb_flags & 0x07) as usize;
|
||||
let ad_size: usize = match ad_type {
|
||||
0 => 8, // Short AD
|
||||
1 => 16, // Long AD
|
||||
2 => 20, // Extended AD
|
||||
// 3 = inline/embedded data (no out-of-line extents) — never
|
||||
// used for large stream files. Anything else is unexpected;
|
||||
// fall back to the historical 8-byte stride rather than fail
|
||||
// the whole title.
|
||||
_ => 8,
|
||||
};
|
||||
|
||||
let mut extents = Vec::new();
|
||||
|
||||
// Parse the first allocation-descriptor list from the ICB. A type-3
|
||||
@@ -450,12 +477,12 @@ impl UdfFs {
|
||||
const MAX_AD_BLOCKS: usize = 256;
|
||||
|
||||
for _ in 0..MAX_AD_BLOCKS {
|
||||
let num_descriptors = ad_bytes / 8; // Short Allocation Descriptor = 8 bytes
|
||||
let num_descriptors = ad_bytes / ad_size;
|
||||
let mut next_block: Option<u32> = None;
|
||||
|
||||
for i in 0..num_descriptors {
|
||||
let off = ad_start + i * 8;
|
||||
if off + 8 > block.len() {
|
||||
let off = ad_start + i * ad_size;
|
||||
if off + ad_size > block.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -467,11 +494,15 @@ impl UdfFs {
|
||||
]);
|
||||
let extent_type = raw_len >> 30;
|
||||
let data_len = raw_len & 0x3FFF_FFFF;
|
||||
// Short and Long ADs carry the extent LBA at off+4. Extended
|
||||
// ADs (20 bytes) place their extent_location lb_addr after
|
||||
// three length fields, at off+12.
|
||||
let lba_off = if ad_size == 20 { off + 12 } else { off + 4 };
|
||||
let data_lba = u32::from_le_bytes([
|
||||
block[off + 4],
|
||||
block[off + 5],
|
||||
block[off + 6],
|
||||
block[off + 7],
|
||||
block[lba_off],
|
||||
block[lba_off + 1],
|
||||
block[lba_off + 2],
|
||||
block[lba_off + 3],
|
||||
]);
|
||||
|
||||
match extent_type {
|
||||
@@ -1278,6 +1309,35 @@ mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
/// Build an Extended File Entry (tag 266) ICB whose allocation
|
||||
/// descriptors are LONG ADs (16 bytes: len(4) | lba(4) | part_ref(2) |
|
||||
/// impl_use(6)). Sets the ICB Tag flags (abs offset 34) low bits to 1
|
||||
/// so the parser must select the 16-byte stride. This is the layout
|
||||
/// large BD-ROM .m2ts streams actually use.
|
||||
fn build_efe_long(info_length: u64, ads: &[(u32, u32, u32)]) -> [u8; 2048] {
|
||||
let mut s = [0u8; 2048];
|
||||
s[0..2].copy_from_slice(&266u16.to_le_bytes()); // tag
|
||||
// ICB Tag flags at abs offset 34: AD type 1 = Long AD.
|
||||
s[34..36].copy_from_slice(&1u16.to_le_bytes());
|
||||
s[56..64].copy_from_slice(&info_length.to_le_bytes());
|
||||
let l_ea: u32 = 0;
|
||||
let l_ad: u32 = (ads.len() * 16) as u32;
|
||||
s[208..212].copy_from_slice(&l_ea.to_le_bytes());
|
||||
s[212..216].copy_from_slice(&l_ad.to_le_bytes());
|
||||
let mut off = 216 + l_ea as usize;
|
||||
for &(etype, dlen, dlba) in ads {
|
||||
let raw_len = (etype << 30) | (dlen & 0x3FFF_FFFF);
|
||||
s[off..off + 4].copy_from_slice(&raw_len.to_le_bytes());
|
||||
s[off + 4..off + 8].copy_from_slice(&dlba.to_le_bytes());
|
||||
// off+8..off+10 = partition reference (0), off+10..off+16 =
|
||||
// impl_use (0). Leaving these zero is what trips the old
|
||||
// 8-byte-stride parser into reading a bogus zero-length
|
||||
// terminator as descriptor #1.
|
||||
off += 16;
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// A continuation block: a bare list of short ADs from byte 0.
|
||||
fn build_cont_block(ads: &[(u32, u32, u32)]) -> [u8; 2048] {
|
||||
let mut s = [0u8; 2048];
|
||||
@@ -1335,6 +1395,42 @@ mod tests {
|
||||
assert_eq!(extents, vec![(10, 4096), (20, 2048)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icb_extents_long_ad_returns_all_extents_not_just_first() {
|
||||
// Regression: BD-ROM large .m2ts files use Long ADs (16-byte
|
||||
// descriptors). The pre-fix parser hardcoded an 8-byte stride, so
|
||||
// it read descriptor #0 (length+lba align in both layouts) then
|
||||
// misread descriptor #1 from the middle of the first Long AD —
|
||||
// a zero terminator — and returned ONLY the first extent. That
|
||||
// truncated every multi-extent title at ~973 MB-1 GiB.
|
||||
//
|
||||
// Four Long ADs, each a near-max Short-AD-sized extent. The fix
|
||||
// must return all four; the old code returned exactly one.
|
||||
let icb = build_efe_long(
|
||||
4 * 1_000_000_000,
|
||||
&[
|
||||
(0, 0x3FFF_F800, 100), // ~1 GiB extent
|
||||
(0, 0x3FFF_F800, 600_000), // next extent
|
||||
(0, 0x3FFF_F800, 1_100_000),
|
||||
(0, 0x1000_0000, 1_600_000), // shorter tail extent
|
||||
],
|
||||
);
|
||||
let mut reader = MapReader::new();
|
||||
reader.put(5, icb);
|
||||
let fs = fs_with(0, 0, file_entry("BIG", 5, 4 * 1_000_000_000));
|
||||
let extents = fs.read_icb_extents(&mut reader, 5).expect("extents");
|
||||
assert_eq!(
|
||||
extents,
|
||||
vec![
|
||||
(100, 0x3FFF_F800),
|
||||
(600_000, 0x3FFF_F800),
|
||||
(1_100_000, 0x3FFF_F800),
|
||||
(1_600_000, 0x1000_0000),
|
||||
],
|
||||
"Long-AD file must return ALL extents, not just the first"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_spans_multiple_extents() {
|
||||
let part_start = 0;
|
||||
@@ -1360,6 +1456,41 @@ mod tests {
|
||||
assert!(data[2048..].iter().all(|&b| b == 0xBB));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_long_ad_returns_full_content_not_truncated() {
|
||||
// Regression for BOTH 0.31.0 bugs through `read_file`: this is the
|
||||
// exact path `Disc::read_aacs_inputs_from_reader` uses to read
|
||||
// `/AACS/MKB_RO.inf` + `Unit_Key_RO.inf`. A Long-AD, multi-extent
|
||||
// file (Dunkirk-class UHD layout) must return ALL its bytes. With the
|
||||
// pre-fix Short-AD-only parser this read stopped after the first
|
||||
// extent, which (a) truncated the mux and (b) made autorip's
|
||||
// `key_files()` see a short/garbage AACS file → `MissingInputs` →
|
||||
// the online key request was never sent.
|
||||
let icb = build_efe_long(6144, &[(0, 2048, 10), (0, 2048, 30), (0, 2048, 50)]);
|
||||
let mut reader = MapReader::new();
|
||||
reader.put(5, icb);
|
||||
reader.put(10, [0xAA; 2048]);
|
||||
reader.put(30, [0xBB; 2048]);
|
||||
reader.put(50, [0xCC; 2048]);
|
||||
let root = DirEntry {
|
||||
name: String::new(),
|
||||
is_dir: true,
|
||||
meta_lba: 0,
|
||||
size: 0,
|
||||
entries: vec![file_entry("MKB", 5, 6144)],
|
||||
};
|
||||
let fs = fs_with(0, 0, root);
|
||||
let data = fs.read_file(&mut reader, "/MKB").expect("read");
|
||||
assert_eq!(
|
||||
data.len(),
|
||||
6144,
|
||||
"Long-AD file must not truncate at extent #0"
|
||||
);
|
||||
assert!(data[..2048].iter().all(|&b| b == 0xAA));
|
||||
assert!(data[2048..4096].iter().all(|&b| b == 0xBB));
|
||||
assert!(data[4096..].iter().all(|&b| b == 0xCC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_ranges_saturates_near_u32_max() {
|
||||
// Adjacent ranges near u32::MAX must not panic (debug) or wrap.
|
||||
|
||||
Reference in New Issue
Block a user