Close the MEDIUM mutation gaps across transport, labels and codecs
The remaining triage items after tonight's HIGH fixes: 1,290 lines, almost all tests. Covers disc/mod.rs's DVD scan path (with real minimal VMG/VTS IFO fixtures rather than mocks), drive/mod.rs, labels/class_reader.rs and labels/mod.rs — the two biggest untriaged survivor clusters in the crate — plus hevc.rs and ps.rs. One production change, and it is an extraction rather than a behaviour change: MacScsiTransport::open mapped the shim's negative failure sentinels to typed errors inline, where nothing could reach it without a real IOKit FFI call. It is now map_shim_open_error, so the mapping can be pinned. It matters because collapsing -5 into the DeviceNotFound catch-all turns "another process holds the drive" into "no such drive", and an operator chasing the wrong problem is worse than a blunt error. Gate green on the pinned toolchain including the secrets scanner.
This commit is contained in:
@@ -1546,4 +1546,181 @@ mod tests {
|
||||
assert_eq!(cf.member_descriptor(&m), Some("()V"));
|
||||
assert_ne!(cf.member_descriptor(&m), Some("doStuff"));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Reader::u16/u32/u64 boundary + value correctness
|
||||
//
|
||||
// Mirrors `slice_boundary_is_inclusive_of_the_final_byte`: an
|
||||
// exact-fit read must succeed, one byte short must fail. Plus
|
||||
// positive-value tests so a scrambled byte assembly (not just an
|
||||
// out-of-bounds read) would be caught.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn u16_boundary_is_inclusive_of_the_final_byte() {
|
||||
let data = [0xAB, 0xCD];
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u16("exact fit").expect("2 bytes available"), 0xABCD);
|
||||
|
||||
let data = [0xAB];
|
||||
let mut r = Reader::new(&data);
|
||||
assert!(matches!(
|
||||
r.u16("one byte short"),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u16_decodes_big_endian_value() {
|
||||
let data = [0x01, 0x02];
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u16("value").unwrap(), 0x0102);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u32_boundary_is_inclusive_of_the_final_byte() {
|
||||
let data = [0x00, 0x00, 0x00, 0x2A];
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u32("exact fit").expect("4 bytes available"), 42);
|
||||
|
||||
let data = [0x00, 0x00, 0x00];
|
||||
let mut r = Reader::new(&data);
|
||||
assert!(matches!(
|
||||
r.u32("one byte short"),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u32_decodes_big_endian_value() {
|
||||
let data = [0x00, 0x00, 0x05, 0x39]; // 1337
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u32("value").unwrap(), 1337);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u64_boundary_is_inclusive_of_the_final_byte() {
|
||||
// pos == 0, buffer exactly 8 bytes: must succeed.
|
||||
let data = [0, 0, 0, 0, 0, 0, 0, 0x7B]; // 123
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u64("exact fit").expect("8 bytes available"), 123);
|
||||
|
||||
// pos == 0, buffer one byte short of 8: must fail cleanly, not
|
||||
// panic on the internal self.data[self.pos + 7] index.
|
||||
let data = [0u8; 7];
|
||||
let mut r = Reader::new(&data);
|
||||
assert!(matches!(
|
||||
r.u64("one byte short"),
|
||||
Err(Error::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u64_decodes_big_endian_value() {
|
||||
let data = [0, 0, 0, 0, 0, 0, 0x05, 0x39]; // 1337
|
||||
let mut r = Reader::new(&data);
|
||||
assert_eq!(r.u64("value").unwrap(), 1337);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// decode_modified_utf8: 3-byte (BMP) decode path
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn modified_utf8_three_byte_cjk() {
|
||||
// U+3042 (hiragana あ) in modified UTF-8: 1110xxxx 10xxxxxx 10xxxxxx
|
||||
// = 0xE3 0x81 0x82.
|
||||
let s = decode_modified_utf8(&[0xE3, 0x81, 0x82]).unwrap();
|
||||
assert_eq!(s, "\u{3042}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_utf8_three_byte_rejects_bad_first_continuation() {
|
||||
// Valid lead byte (0xE3), but the first continuation byte is not
|
||||
// 10xxxxxx (0x01 instead) — must be rejected, proving the first
|
||||
// `& 0xC0 != 0x80` check is live.
|
||||
assert!(decode_modified_utf8(&[0xE3, 0x01, 0x82]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modified_utf8_three_byte_rejects_bad_second_continuation() {
|
||||
// Valid lead + first continuation, but the second continuation
|
||||
// byte is not 10xxxxxx — proves the second check is independently
|
||||
// live (not short-circuited by the first).
|
||||
assert!(decode_modified_utf8(&[0xE3, 0x81, 0x01]).is_err());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// read_constant_pool: Long/Double two-slot skip, real byte parsing
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn constant_pool_long_entry_occupies_two_slots_via_real_parse() {
|
||||
// Real class-file bytes (not the `from_entries` synthetic ctor):
|
||||
// magic + minor/major + cp_count=4 + tag=5 (Long, 8-byte payload
|
||||
// at index 1, reserved slot at index 2) + tag=1 (Utf8 at index 3)
|
||||
// + empty access_flags/this/super/interfaces/fields/methods/attrs.
|
||||
let mut buf = vec![
|
||||
0xCA, 0xFE, 0xBA, 0xBE, // magic
|
||||
0x00, 0x00, // minor
|
||||
0x00, 0x34, // major
|
||||
0x00, 0x04, // cp_count = 4 (0=Empty,1=Long,2=Empty tail,3=Utf8)
|
||||
5, // Long tag
|
||||
];
|
||||
buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_be_bytes()); // 8-byte payload
|
||||
buf.push(1); // Utf8 tag
|
||||
let name = b"marker";
|
||||
buf.extend_from_slice(&(name.len() as u16).to_be_bytes());
|
||||
buf.extend_from_slice(name);
|
||||
// access_flags, this_class, super_class, interfaces_count
|
||||
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
// fields_count, methods_count, attributes_count
|
||||
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
|
||||
|
||||
let cf = ClassFile::parse(&buf).expect("well-formed synthetic class file");
|
||||
assert_eq!(cf.constant_pool.len(), 4);
|
||||
// The Long occupies indices 1 AND 2 (its reserved tail slot).
|
||||
// The Utf8 must resolve at index 3 = long_index(1) + 2, NOT +1.
|
||||
assert_eq!(cf.constant_pool.utf8(3), Some("marker"));
|
||||
// Index 2 is the reserved tail slot: not a Utf8, must not
|
||||
// resolve as one (guards against the Utf8 landing one slot early).
|
||||
assert_eq!(cf.constant_pool.utf8(2), None);
|
||||
match cf.constant_pool.get(1) {
|
||||
Some(CpInfo::Long(v)) => assert_eq!(*v, 0x1122_3344_5566_7788u64 as i64),
|
||||
other => panic!("expected Long at index 1, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// instruction_size: tableswitch/lookupswitch with non-degenerate
|
||||
// low/high/npairs (the existing tests only cover low==high==0 and
|
||||
// npairs==0, which can't distinguish `-` from `+` in the entry-count
|
||||
// arithmetic).
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn instruction_size_tableswitch_non_degenerate_range() {
|
||||
// low=1, high=4 -> 4 entries (high-low+1 = 4). A `-`->`+` mutation
|
||||
// on that arithmetic would instead compute high+low+1 = 6.
|
||||
let mut code = vec![TABLESWITCH];
|
||||
code.extend_from_slice(&[0, 0, 0]); // padding
|
||||
code.extend_from_slice(&[0, 0, 0, 0]); // default offset
|
||||
code.extend_from_slice(&1i32.to_be_bytes()); // low = 1
|
||||
code.extend_from_slice(&4i32.to_be_bytes()); // high = 4
|
||||
code.extend_from_slice(&[0; 16]); // 4 jump entries * 4 bytes
|
||||
// total = 1 (opcode) + 3 (pad) + 12 (default/low/high) + 16 (entries) = 32
|
||||
assert_eq!(instruction_size(&code, 0), Some(32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instruction_size_lookupswitch_non_degenerate_npairs() {
|
||||
// npairs = 3 -> 3 * 8 = 24 bytes of pairs.
|
||||
let mut code = vec![LOOKUPSWITCH];
|
||||
code.extend_from_slice(&[0, 0, 0]); // padding
|
||||
code.extend_from_slice(&[0, 0, 0, 0]); // default
|
||||
code.extend_from_slice(&3i32.to_be_bytes()); // npairs = 3
|
||||
code.extend_from_slice(&[0; 24]); // 3 pairs
|
||||
// total = 1 + 3 + 8 (default/npairs) + 24 = 36
|
||||
assert_eq!(instruction_size(&code, 0), Some(36));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2363,3 +2363,225 @@ mod fill_gaps_sort_tests {
|
||||
assert_eq!(framework[1].stream_number, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── append_clpi_orphans ─────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod clpi_orphan_tests {
|
||||
use super::*;
|
||||
use crate::udf::fixture::*;
|
||||
|
||||
fn label(t: StreamLabelType, n: u16, lang: &str, codec: &str) -> StreamLabel {
|
||||
StreamLabel {
|
||||
stream_number: n,
|
||||
stream_type: t,
|
||||
language: lang.into(),
|
||||
name: String::new(),
|
||||
purpose: LabelPurpose::Normal,
|
||||
qualifier: LabelQualifier::None,
|
||||
codec_hint: codec.into(),
|
||||
variant: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a CLPI ProgramInfo section for one program with the given
|
||||
/// (pid, stream_coding_info) pairs. Layout mirrors
|
||||
/// `crate::clpi::parse_program_info`'s expectations: length(4) +
|
||||
/// reserved(1) + num_programs(1), then per-program
|
||||
/// spn(4)+pmt_pid(2)+num_streams(1)+num_groups(1), then per-stream
|
||||
/// pid(2)+sci_len(1)+sci.
|
||||
fn build_program_info(streams: &[(u16, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
body.push(0); // reserved
|
||||
body.push(1); // num_programs = 1
|
||||
body.extend_from_slice(&0u32.to_be_bytes()); // spn_program_sequence_start
|
||||
body.extend_from_slice(&0u16.to_be_bytes()); // program_map_pid
|
||||
body.push(streams.len() as u8); // num_streams
|
||||
body.push(0); // num_groups
|
||||
for (pid, sci) in streams {
|
||||
body.extend_from_slice(&pid.to_be_bytes());
|
||||
body.push(sci.len() as u8);
|
||||
body.extend_from_slice(sci);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(&body);
|
||||
out
|
||||
}
|
||||
|
||||
/// Build a full CLPI byte buffer (HDMV header + ProgramInfo) declaring
|
||||
/// the given (pid, coding_type, lang) streams. `sci` layout follows
|
||||
/// `crate::clpi::parse_program_info`'s per-coding-type match arms:
|
||||
/// PG/IG = coding_type + 3-byte lang; audio (primary or secondary) =
|
||||
/// coding_type + format/rate byte + 3-byte lang.
|
||||
fn build_clpi(streams: &[(u16, u8, &str)]) -> Vec<u8> {
|
||||
use crate::consts::coding_type as c;
|
||||
let sci_streams: Vec<(u16, Vec<u8>)> = streams
|
||||
.iter()
|
||||
.map(|(pid, coding, lang)| {
|
||||
let lang_bytes = lang.as_bytes();
|
||||
let sci = match *coding {
|
||||
c::PG | c::IG => {
|
||||
let mut v = vec![*coding];
|
||||
v.extend_from_slice(lang_bytes);
|
||||
v
|
||||
}
|
||||
_ => {
|
||||
let mut v = vec![*coding, 0x61];
|
||||
v.extend_from_slice(lang_bytes);
|
||||
v
|
||||
}
|
||||
};
|
||||
(*pid, sci)
|
||||
})
|
||||
.collect();
|
||||
let pi = build_program_info(&sci_streams);
|
||||
let mut buf = vec![0u8; 60];
|
||||
buf[0..4].copy_from_slice(b"HDMV");
|
||||
buf[4..8].copy_from_slice(b"0200");
|
||||
let prog_info_start: u32 = 60;
|
||||
buf[12..16].copy_from_slice(&prog_info_start.to_be_bytes());
|
||||
buf[56..60].copy_from_slice(&1000u32.to_be_bytes()); // source_packet_count
|
||||
buf.extend_from_slice(&pi);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Lay a minimal BDMV/CLIPINF/00001.clpi tree on `disc`, with the CLPI
|
||||
/// declaring the given synthetic streams, and return the parsed UdfFs.
|
||||
fn fs_with_clpi(disc: &mut MemDisc, streams: &[(u16, u8, &str)]) -> crate::udf::UdfFs {
|
||||
let clpi_data = build_clpi(streams);
|
||||
let clipinf = DirSpec {
|
||||
name: "CLIPINF".to_string(),
|
||||
icb_lba: 24,
|
||||
dir_data_lba: 25,
|
||||
files: vec![file_with("00001.clpi", 26, 8000, clpi_data, false)],
|
||||
subdirs: vec![],
|
||||
};
|
||||
let bdmv = DirSpec {
|
||||
name: "BDMV".to_string(),
|
||||
icb_lba: 20,
|
||||
dir_data_lba: 21,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![clipinf],
|
||||
};
|
||||
let root = DirSpec {
|
||||
name: String::new(),
|
||||
icb_lba: 10,
|
||||
dir_data_lba: 11,
|
||||
files: Vec::new(),
|
||||
subdirs: vec![bdmv],
|
||||
};
|
||||
build_udf_skeleton(disc, 10);
|
||||
lay_dir(disc, &root);
|
||||
crate::udf::read_filesystem(disc).expect("fs")
|
||||
}
|
||||
|
||||
/// (a) A PG-coded CLPI orphan becomes a Subtitle label.
|
||||
#[test]
|
||||
fn pg_orphan_becomes_subtitle() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[(0x1200, crate::consts::coding_type::PG, "eng")],
|
||||
);
|
||||
let mut labels: Vec<StreamLabel> = Vec::new();
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 1);
|
||||
assert_eq!(labels.len(), 1);
|
||||
assert_eq!(labels[0].stream_type, StreamLabelType::Subtitle);
|
||||
assert_eq!(labels[0].stream_number, 1);
|
||||
}
|
||||
|
||||
/// (b) An audio-range-coded orphan (here DTS-HD MA, the top of the
|
||||
/// `LPCM..=DTS_HD_MA` primary-audio range) becomes an Audio label.
|
||||
#[test]
|
||||
fn audio_range_orphan_becomes_audio() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[(0x1100, crate::consts::coding_type::DTS_HD_MA, "eng")],
|
||||
);
|
||||
let mut labels: Vec<StreamLabel> = Vec::new();
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 1);
|
||||
assert_eq!(labels[0].stream_type, StreamLabelType::Audio);
|
||||
}
|
||||
|
||||
/// (b, secondary) AC3_PLUS_SECONDARY is outside the primary
|
||||
/// `LPCM..=DTS_HD_MA` range and must be classified through the
|
||||
/// dedicated secondary-audio arm.
|
||||
#[test]
|
||||
fn secondary_audio_orphan_becomes_audio() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[(
|
||||
0x1A00,
|
||||
crate::consts::coding_type::AC3_PLUS_SECONDARY,
|
||||
"eng",
|
||||
)],
|
||||
);
|
||||
let mut labels: Vec<StreamLabel> = Vec::new();
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 1);
|
||||
assert_eq!(labels[0].stream_type, StreamLabelType::Audio);
|
||||
}
|
||||
|
||||
/// (c) IG (0x91, BD-J menu overlay) is not a user-facing subtitle and
|
||||
/// must be skipped entirely, not appended as anything.
|
||||
#[test]
|
||||
fn ig_orphan_is_skipped() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[(0x1201, crate::consts::coding_type::IG, "eng")],
|
||||
);
|
||||
let mut labels: Vec<StreamLabel> = Vec::new();
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 0);
|
||||
assert!(labels.is_empty());
|
||||
}
|
||||
|
||||
/// (d) Numbering continues from `max(existing) + 1` and increments once
|
||||
/// per new orphan stream, independently of PID order.
|
||||
#[test]
|
||||
fn numbering_continues_from_max_existing_and_increments() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[
|
||||
(0x1100, crate::consts::coding_type::TRUEHD, "eng"),
|
||||
(0x1101, crate::consts::coding_type::AC3, "fra"),
|
||||
],
|
||||
);
|
||||
let mut labels = vec![label(StreamLabelType::Audio, 3, "jpn", "DTS")];
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 2);
|
||||
let mut nums: Vec<u16> = labels
|
||||
.iter()
|
||||
.filter(|l| l.stream_type == StreamLabelType::Audio && l.language != "jpn")
|
||||
.map(|l| l.stream_number)
|
||||
.collect();
|
||||
nums.sort();
|
||||
assert_eq!(
|
||||
nums,
|
||||
vec![4, 5],
|
||||
"orphans must number 4 and 5 after the existing max of 3"
|
||||
);
|
||||
}
|
||||
|
||||
/// (e) A CLPI stream whose (type, language, codec) tuple already exists
|
||||
/// in `existing` is a duplicate and must be skipped, not double-listed.
|
||||
#[test]
|
||||
fn duplicate_type_lang_codec_already_in_existing_is_skipped() {
|
||||
let mut disc = MemDisc::new();
|
||||
let udf = fs_with_clpi(
|
||||
&mut disc,
|
||||
&[(0x1100, crate::consts::coding_type::TRUEHD, "eng")],
|
||||
);
|
||||
let mut labels = vec![label(StreamLabelType::Audio, 1, "eng", "TrueHD")];
|
||||
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
|
||||
assert_eq!(added, 0);
|
||||
assert_eq!(labels.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user