Files
Matthew Jackson 661ab138c6 Fix audit findings: DTS AMODE bound, key-fetch negative memoization, PGS probe coverage
- dts: accept all 16 legal AMODE channel-arrangement codes (0-15), not just
  0-9. Per ETSI TS 102 114 the 6-bit AMODE field has 16 defined arrangements;
  only 16-63 are reserved. ffmpeg's ff_dca_channels[16] confirms 10-15 are
  decodable 6/7/8-channel layouts. The old bound of 10 dropped spec-legal
  multichannel core frames as undecodable, silencing recoverable audio. Add a
  regression test (literal 0..16 range) that fails if the bound reverts to 10.

- keysource: only memoize a NEGATIVE (empty) key-fetch result when every source
  genuinely ran and none held the key — never when a source Err'd (network down,
  unreachable). A transient outage was being cached as a permanent "no key" for
  the fingerprint, permanently dropping a unit that could be recovered once the
  source came back. Thread an `errored` flag out of the drivers and gate the
  cache insert on it. Tests cover both the recover-after-outage case and that a
  genuine absence is still memoized.

- pgs_forced_probe: add happy-path coverage feeding real synthetic BD-TS PGS
  display sets through the full demux -> parse -> observe -> apply path, both a
  forced verdict landing and a non-forced verdict clearing a vendor flag.

- mp4: correct fit_report doc (audio carried is AC-3/E-AC-3 AND DTS/DTS-HD).

- scan_iso test: add independent fixture expectations (volume id) so the parity
  test is no longer purely tautological against a re-run of the same composition.
2026-07-24 08:32:37 -07:00

198 lines
7.7 KiB
Rust

//! Tests for the `libfreemkv::scan_iso` entry point — the file-backed scan seam
//! that replaced consumers hand-rolling `FileSectorSource::open` +
//! `capacity_sectors` + `Disc::scan_image`.
//!
//! Uses a minimal synthetic UDF image (the same byte-level fixture the
//! `disc_tests.rs` `scan_image` tests build, but materialised to a real file on
//! disk so the file-backed `FileSectorSource` path is exercised end to end).
use libfreemkv::{Disc, ScanOptions, SectorSource};
use std::collections::BTreeMap;
use std::io::Write;
const SECTOR_SIZE: usize = 2048;
// ── Minimal UDF sector builders (mirrors disc_tests.rs) ─────────────────────
fn make_avdp_sector(vds_lba: u32) -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&2u16.to_le_bytes());
s[16..20].copy_from_slice(&vds_lba.to_le_bytes());
s[20..24].copy_from_slice(&(6u32 * SECTOR_SIZE as u32).to_le_bytes());
s
}
fn make_pvd_sector(volume_id: &str) -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&1u16.to_le_bytes());
if !volume_id.is_empty() {
let id_bytes = volume_id.as_bytes();
s[24] = 8;
let copy_len = id_bytes.len().min(30);
s[25..25 + copy_len].copy_from_slice(&id_bytes[..copy_len]);
s[55] = (1 + copy_len) as u8;
}
s
}
fn make_partition_desc(partition_start: u32) -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&5u16.to_le_bytes());
s[188..192].copy_from_slice(&partition_start.to_le_bytes());
s
}
fn make_lvd_sector_simple() -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&6u16.to_le_bytes());
s[268..272].copy_from_slice(&1u32.to_le_bytes());
s
}
fn make_terminator() -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&8u16.to_le_bytes());
s
}
fn make_fsd_sector(root_meta_lba: u32) -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&256u16.to_le_bytes());
s[400..404].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
s[404..408].copy_from_slice(&root_meta_lba.to_le_bytes());
s
}
fn make_dir_icb(data_meta_lba: u32, data_len: u32) -> Vec<u8> {
let mut s = vec![0u8; SECTOR_SIZE];
s[0..2].copy_from_slice(&266u16.to_le_bytes());
s[56..64].copy_from_slice(&(data_len as u64).to_le_bytes());
s[208..212].copy_from_slice(&0u32.to_le_bytes());
s[212..216].copy_from_slice(&8u32.to_le_bytes());
s[216..220].copy_from_slice(&data_len.to_le_bytes());
s[220..224].copy_from_slice(&data_meta_lba.to_le_bytes());
s
}
fn make_parent_fid() -> Vec<u8> {
let fid_len = (38 + 3) & !3;
let mut fid = vec![0u8; fid_len];
fid[0..2].copy_from_slice(&257u16.to_le_bytes());
fid[18] = 0x08;
fid[19] = 0;
fid
}
/// Build the minimal UDF image as an LBA→sector map (empty root directory).
fn minimal_udf_sectors() -> BTreeMap<u32, Vec<u8>> {
let partition_start: u32 = 512;
let mut sectors: BTreeMap<u32, Vec<u8>> = BTreeMap::new();
sectors.insert(256, make_avdp_sector(32));
sectors.insert(32, make_pvd_sector("TEST_DISC"));
sectors.insert(33, make_partition_desc(partition_start));
sectors.insert(34, make_lvd_sector_simple());
sectors.insert(35, make_terminator());
sectors.insert(partition_start, make_fsd_sector(1));
let parent_fid = make_parent_fid();
let dir_data_len = parent_fid.len() as u32;
sectors.insert(partition_start + 1, make_dir_icb(2, dir_data_len));
let mut sector = vec![0u8; SECTOR_SIZE];
sector[..parent_fid.len()].copy_from_slice(&parent_fid);
sectors.insert(partition_start + 2, sector);
sectors
}
/// Materialise an LBA→sector map to a real ISO file (zero-filled gaps) and
/// return its path (kept alive by the returned tempfile handle).
fn write_iso(sectors: &BTreeMap<u32, Vec<u8>>) -> tempfile::NamedTempFile {
let max_lba = *sectors.keys().max().unwrap();
let mut image = vec![0u8; (max_lba as usize + 1) * SECTOR_SIZE];
for (&lba, data) in sectors {
let off = lba as usize * SECTOR_SIZE;
image[off..off + SECTOR_SIZE].copy_from_slice(data);
}
let mut tmp = tempfile::Builder::new()
.suffix(".iso")
.tempfile()
.expect("tempfile create");
tmp.write_all(&image).expect("write iso");
tmp.flush().expect("flush iso");
tmp
}
// ── Tests ───────────────────────────────────────────────────────────────────
#[test]
fn scan_iso_matches_manual_scan_image_path() {
let sectors = minimal_udf_sectors();
let expected_capacity = *sectors.keys().max().unwrap() + 1;
let tmp = write_iso(&sectors);
// The new entry point.
let (disc, mut reader) =
libfreemkv::scan_iso(tmp.path(), ScanOptions::default()).expect("scan_iso succeeds");
// Parity with the old hand-rolled triple: open a fresh reader and run the
// exact composition scan_iso encapsulates. The resulting Disc must match.
let mut manual_reader = libfreemkv::FileSectorSource::open(tmp.path()).expect("manual open");
let manual_capacity = manual_reader.capacity_sectors();
let manual = Disc::scan_image(&mut manual_reader, manual_capacity, &ScanOptions::default())
.expect("manual scan_image succeeds");
assert_eq!(disc.capacity_sectors, manual.capacity_sectors, "capacity");
assert_eq!(disc.titles.len(), manual.titles.len(), "title count");
assert_eq!(disc.encrypted, manual.encrypted, "encrypted flag");
assert_eq!(disc.format, manual.format, "disc format");
// Independent expectations (not parity against a re-run of the same
// composition): the scanned Disc must match KNOWN properties of the fixture
// itself — its capacity (max LBA + 1), its PVD volume id ("TEST_DISC"), and
// that a UDF with no /AACS directory is unencrypted. These would fail even if
// scan_iso and the manual path drifted together.
assert_eq!(disc.capacity_sectors, expected_capacity, "capacity value");
assert_eq!(
disc.volume_id, "TEST_DISC",
"PVD volume id from the fixture"
);
assert!(!disc.encrypted, "minimal UDF (no /AACS) is not encrypted");
// The returned reader is usable: correct capacity and a real read of sector
// 256 (the AVDP) returns the bytes we wrote — proves it is not consumed /
// exhausted by the scan.
assert_eq!(
reader.capacity_sectors(),
expected_capacity,
"reader capacity"
);
let mut buf = vec![0u8; SECTOR_SIZE];
let n = reader
.read_sectors(256, 1, &mut buf, false)
.expect("read AVDP sector");
assert_eq!(n, SECTOR_SIZE);
assert_eq!(&buf[..], &sectors[&256][..], "AVDP sector bytes round-trip");
}
#[test]
fn scan_iso_propagates_open_error() {
// A path that does not exist must surface an Err (not a panic) — kills a
// mutant that ignores the open failure.
let missing = std::path::Path::new("/nonexistent/does-not-exist.iso");
let result = libfreemkv::scan_iso(missing, ScanOptions::default());
assert!(result.is_err(), "missing file must error");
}
#[test]
fn scan_iso_propagates_scan_error() {
// A readable file with no valid UDF (no AVDP at sector 256) must surface the
// scan failure — kills a mutant that swallows the scan_image error.
let mut tmp = tempfile::Builder::new()
.suffix(".iso")
.tempfile()
.expect("tempfile create");
tmp.write_all(&vec![0u8; 8 * SECTOR_SIZE]).expect("write");
tmp.flush().expect("flush");
let result = libfreemkv::scan_iso(tmp.path(), ScanOptions::default());
assert!(result.is_err(), "non-UDF image must error");
}