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:
Matthew Jackson
2026-08-01 15:00:01 -07:00
parent f8ed0b99f4
commit ff18d4c3c8
7 changed files with 1290 additions and 33 deletions
+177
View File
@@ -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));
}
}
+222
View File
@@ -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);
}
}