- 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.
198 lines
7.7 KiB
Rust
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(§ors);
|
|
|
|
// 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[..], §ors[&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");
|
|
}
|