mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Lands the code-audit branches for aacs, css, keys, labels, udf, ifo and dvd, bdnav/mpls/clpi, disc/bluray, extract, io, scsi, the mkv, mp4, network and mpg muxers, the audio and video codecs, and CI. Fixes include MKB and FMTS validation, UDF bounds, IFO fallback to the BUP, DTS Express framing, MPEG-2 field pairing and mpg re-basing; the rest pins existing behaviour with tests.
421 lines
16 KiB
Rust
421 lines
16 KiB
Rust
//! UDF parser tests using a MockSectorReader.
|
|
|
|
use libfreemkv::error::Error;
|
|
use libfreemkv::error::Result;
|
|
use libfreemkv::{SectorSource, read_filesystem};
|
|
use std::collections::HashMap;
|
|
|
|
const SECTOR_SIZE: usize = 2048;
|
|
|
|
/// In-memory sector reader backed by a HashMap<LBA, sector_data>.
|
|
/// Any LBA not in the map returns zeroed sectors.
|
|
struct MockSectorReader {
|
|
sectors: HashMap<u32, Vec<u8>>,
|
|
}
|
|
|
|
impl MockSectorReader {
|
|
fn new() -> Self {
|
|
Self {
|
|
sectors: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Write a full 2048-byte sector at the given LBA.
|
|
fn set_sector(&mut self, lba: u32, data: Vec<u8>) {
|
|
assert_eq!(
|
|
data.len(),
|
|
SECTOR_SIZE,
|
|
"sector data must be exactly 2048 bytes"
|
|
);
|
|
self.sectors.insert(lba, data);
|
|
}
|
|
|
|
/// Write partial data into a sector (rest is zeroed).
|
|
fn set_sector_partial(&mut self, lba: u32, data: &[u8]) {
|
|
let mut sector = vec![0u8; SECTOR_SIZE];
|
|
let len = data.len().min(SECTOR_SIZE);
|
|
sector[..len].copy_from_slice(&data[..len]);
|
|
self.sectors.insert(lba, sector);
|
|
}
|
|
}
|
|
|
|
impl SectorSource for MockSectorReader {
|
|
fn read_sectors(
|
|
&mut self,
|
|
lba: u32,
|
|
count: u16,
|
|
buf: &mut [u8],
|
|
_recovery: bool,
|
|
) -> Result<usize> {
|
|
let total = count as usize * SECTOR_SIZE;
|
|
assert!(buf.len() >= total, "buffer too small");
|
|
for i in 0..count as u32 {
|
|
let offset = i as usize * SECTOR_SIZE;
|
|
if let Some(data) = self.sectors.get(&(lba + i)) {
|
|
buf[offset..offset + SECTOR_SIZE].copy_from_slice(data);
|
|
} else {
|
|
// Return zeros for unmapped sectors
|
|
buf[offset..offset + SECTOR_SIZE].fill(0);
|
|
}
|
|
}
|
|
Ok(total)
|
|
}
|
|
}
|
|
|
|
// ── Helper: build raw sector data ──────────────────────────────────────────
|
|
|
|
/// Build an AVDP sector (tag_id=2) pointing to VDS at the given LBA.
|
|
fn make_avdp_sector(vds_lba: u32) -> Vec<u8> {
|
|
let mut s = vec![0u8; SECTOR_SIZE];
|
|
// Tag ID = 2 (AVDP) at bytes [0..2]
|
|
s[0..2].copy_from_slice(&2u16.to_le_bytes());
|
|
// Main VDS extent location at bytes [16..20]
|
|
s[16..20].copy_from_slice(&vds_lba.to_le_bytes());
|
|
// Main VDS extent length at bytes [20..24] (arbitrary, say 6 sectors)
|
|
s[20..24].copy_from_slice(&(6u32 * SECTOR_SIZE as u32).to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build a Primary Volume Descriptor (tag_id=1) with the given volume ID.
|
|
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());
|
|
// Volume ID at offset 24, d-string format:
|
|
// compression_id(1 byte) + ASCII chars + length byte at position 55
|
|
if !volume_id.is_empty() {
|
|
let id_bytes = volume_id.as_bytes();
|
|
s[24] = 8; // compression ID = ASCII
|
|
let copy_len = id_bytes.len().min(30);
|
|
s[25..25 + copy_len].copy_from_slice(&id_bytes[..copy_len]);
|
|
// d-string length byte at end of 32-byte field (offset 55)
|
|
s[55] = (1 + copy_len) as u8; // compression byte + chars
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Build a Partition Descriptor (tag_id=5) with partition_start.
|
|
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());
|
|
// Partition start at bytes [188..192]
|
|
s[188..192].copy_from_slice(&partition_start.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build a Logical Volume Descriptor (tag_id=6) with a single partition map
|
|
/// (no metadata partition -- simplest case).
|
|
fn make_lvd_sector_simple() -> Vec<u8> {
|
|
let mut s = vec![0u8; SECTOR_SIZE];
|
|
s[0..2].copy_from_slice(&6u16.to_le_bytes());
|
|
// num_partition_maps at bytes [268..272] = 1 (no metadata partition)
|
|
s[268..272].copy_from_slice(&1u32.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build a Terminating Descriptor (tag_id=8).
|
|
fn make_terminator() -> Vec<u8> {
|
|
let mut s = vec![0u8; SECTOR_SIZE];
|
|
s[0..2].copy_from_slice(&8u16.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build a File Set Descriptor (tag_id=256) with root ICB at the given meta LBA.
|
|
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());
|
|
// Root directory ICB long_ad at offset 400:
|
|
// [400..404] = extent_length (one sector = 2048)
|
|
s[400..404].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
// [404..408] = extent_location (LBA within metadata partition)
|
|
s[404..408].copy_from_slice(&root_meta_lba.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build an Extended File Entry (tag_id=266) for a directory with the given
|
|
/// allocation extent pointing to directory data.
|
|
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());
|
|
// info_length at offset 56 (u64)
|
|
s[56..64].copy_from_slice(&(data_len as u64).to_le_bytes());
|
|
// L_EA at offset 208 = 0 (no extended attributes)
|
|
s[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
// L_AD at offset 212 = 8 (one short allocation descriptor)
|
|
s[212..216].copy_from_slice(&8u32.to_le_bytes());
|
|
// Short allocation descriptor at offset 216:
|
|
// [216..220] = extent_length (type=0 in top 2 bits, length in low 30 bits)
|
|
s[216..220].copy_from_slice(&data_len.to_le_bytes());
|
|
// [220..224] = extent_position (LBA within metadata partition)
|
|
s[220..224].copy_from_slice(&data_meta_lba.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
/// Build a File Identifier Descriptor (tag_id=257) for a named entry.
|
|
/// Returns raw bytes (not padded to full sector).
|
|
fn make_fid(name: &str, icb_meta_lba: u32, is_dir: bool) -> Vec<u8> {
|
|
// FID name: compression_id(1) + ASCII bytes
|
|
let mut name_bytes = vec![8u8]; // compression ID = 8 (ASCII)
|
|
name_bytes.extend_from_slice(name.as_bytes());
|
|
let l_fi = name_bytes.len() as u8;
|
|
|
|
let file_chars: u8 = if is_dir { 0x02 } else { 0x00 };
|
|
|
|
// Fixed header = 38 bytes, L_IU = 0
|
|
let fid_len = (38 + l_fi as usize + 3) & !3; // 4-byte aligned
|
|
let mut fid = vec![0u8; fid_len];
|
|
// tag_id = 257
|
|
fid[0..2].copy_from_slice(&257u16.to_le_bytes());
|
|
// file_characteristics at offset 18
|
|
fid[18] = file_chars;
|
|
// L_FI at offset 19
|
|
fid[19] = l_fi;
|
|
// ICB long_ad at offset 20:
|
|
// [20..24] = extent_length (2048)
|
|
fid[20..24].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
// [24..28] = extent_location (LBA)
|
|
fid[24..28].copy_from_slice(&icb_meta_lba.to_le_bytes());
|
|
// L_IU at offset 36 = 0
|
|
fid[36..38].copy_from_slice(&0u16.to_le_bytes());
|
|
// Name starts at offset 38
|
|
fid[38..38 + name_bytes.len()].copy_from_slice(&name_bytes);
|
|
fid
|
|
}
|
|
|
|
/// Build a parent FID (file_chars = 0x08, no name).
|
|
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; // parent
|
|
fid[19] = 0; // L_FI = 0
|
|
fid
|
|
}
|
|
|
|
/// Build a File Entry ICB (tag_id=261) for a file, with the given extent.
|
|
fn make_file_icb(data_lba: u32, data_len: u32, file_size: u64) -> Vec<u8> {
|
|
let mut s = vec![0u8; SECTOR_SIZE];
|
|
s[0..2].copy_from_slice(&261u16.to_le_bytes());
|
|
// info_length at offset 56 (u64)
|
|
s[56..64].copy_from_slice(&file_size.to_le_bytes());
|
|
// L_EA at offset 168 = 0
|
|
s[168..172].copy_from_slice(&0u32.to_le_bytes());
|
|
// L_AD at offset 172 = 8
|
|
s[172..176].copy_from_slice(&8u32.to_le_bytes());
|
|
// Short allocation descriptor at offset 176:
|
|
s[176..180].copy_from_slice(&data_len.to_le_bytes());
|
|
s[180..184].copy_from_slice(&data_lba.to_le_bytes());
|
|
s
|
|
}
|
|
|
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn read_filesystem_no_avdp() {
|
|
// Empty reader — sector 256 is all zeros, tag_id=0 != 2
|
|
let mut reader = MockSectorReader::new();
|
|
let result = read_filesystem(&mut reader);
|
|
assert!(
|
|
matches!(result, Err(Error::UdfNotFilesystem)),
|
|
"no AVDP at sector 256 is the deterministic not-UDF verdict"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_filesystem_bad_avdp_tag() {
|
|
// Put a sector at 256 with wrong tag_id
|
|
let mut reader = MockSectorReader::new();
|
|
let mut bad = vec![0u8; SECTOR_SIZE];
|
|
bad[0..2].copy_from_slice(&99u16.to_le_bytes()); // tag_id=99, not 2
|
|
reader.set_sector(256, bad);
|
|
|
|
let result = read_filesystem(&mut reader);
|
|
assert!(
|
|
matches!(result, Err(Error::UdfNotFilesystem)),
|
|
"AVDP tag_id != 2 is the not-UDF verdict"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_filesystem_no_partition_descriptor() {
|
|
// Valid AVDP but VDS has no Partition Descriptor (tag 5)
|
|
let mut reader = MockSectorReader::new();
|
|
reader.set_sector(256, make_avdp_sector(32));
|
|
// Put a terminator immediately at sector 32
|
|
reader.set_sector(32, make_terminator());
|
|
|
|
let result = read_filesystem(&mut reader);
|
|
assert!(
|
|
matches!(result, Err(Error::UdfNotFilesystem)),
|
|
"no partition descriptor is the not-UDF verdict"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_filesystem_bad_fsd_tag() {
|
|
// Valid AVDP + VDS with partition desc + LVD, but FSD at metadata_start has wrong tag
|
|
let mut reader = MockSectorReader::new();
|
|
let partition_start = 512;
|
|
|
|
reader.set_sector(256, make_avdp_sector(32));
|
|
reader.set_sector(32, make_pvd_sector("TEST_DISC"));
|
|
reader.set_sector(33, make_partition_desc(partition_start));
|
|
reader.set_sector(34, make_lvd_sector_simple());
|
|
reader.set_sector(35, make_terminator());
|
|
// With 1 partition map, metadata_start = partition_start.
|
|
// FSD should be at sector partition_start but we leave it as zeros (tag_id=0 != 256).
|
|
|
|
let result = read_filesystem(&mut reader);
|
|
assert!(
|
|
matches!(result, Err(Error::UdfNotFilesystem)),
|
|
"FSD tag_id != 256 is the not-UDF verdict"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn read_filesystem_minimal_valid() {
|
|
// Build a minimal valid UDF image: AVDP -> VDS -> FSD -> empty root dir
|
|
let mut reader = MockSectorReader::new();
|
|
let partition_start: u32 = 512;
|
|
let root_icb_meta_lba: u32 = 1; // relative to metadata_start
|
|
let root_data_meta_lba: u32 = 2;
|
|
|
|
// Sector 256: AVDP
|
|
reader.set_sector(256, make_avdp_sector(32));
|
|
|
|
// VDS at sectors 32..35
|
|
reader.set_sector(32, make_pvd_sector("MY_DISC"));
|
|
reader.set_sector(33, make_partition_desc(partition_start));
|
|
reader.set_sector(34, make_lvd_sector_simple());
|
|
reader.set_sector(35, make_terminator());
|
|
|
|
// FSD at partition_start (since single partition map, metadata_start = partition_start)
|
|
reader.set_sector(partition_start, make_fsd_sector(root_icb_meta_lba));
|
|
|
|
// Root directory ICB at metadata_start + root_icb_meta_lba
|
|
// Points to directory data at root_data_meta_lba, length = 40 (one parent FID)
|
|
let parent_fid = make_parent_fid();
|
|
let dir_data_len = parent_fid.len() as u32;
|
|
reader.set_sector(
|
|
partition_start + root_icb_meta_lba,
|
|
make_dir_icb(root_data_meta_lba, dir_data_len),
|
|
);
|
|
|
|
// Root directory data: just a parent FID (empty directory)
|
|
reader.set_sector_partial(partition_start + root_data_meta_lba, &parent_fid);
|
|
|
|
let fs = read_filesystem(&mut reader).expect("should parse minimal UDF");
|
|
assert_eq!(fs.volume_id, "MY_DISC");
|
|
assert!(fs.root.is_dir);
|
|
assert!(fs.root.entries.is_empty(), "root should have no children");
|
|
}
|
|
|
|
#[test]
|
|
fn read_filesystem_with_subdirectory() {
|
|
// Build a UDF image with root -> BDMV (dir) -> test.mpls (file)
|
|
let mut reader = MockSectorReader::new();
|
|
let partition_start: u32 = 512;
|
|
|
|
// Layout, meta LBAs relative to partition_start (== metadata_start): 0=FSD,
|
|
// 1=root ICB, 2=root dir data, 3=BDMV ICB, 4=BDMV dir data, 5=test.mpls
|
|
// ICB, 10=test.mpls file data.
|
|
|
|
reader.set_sector(256, make_avdp_sector(32));
|
|
reader.set_sector(32, make_pvd_sector("DISC_WITH_BDMV"));
|
|
reader.set_sector(33, make_partition_desc(partition_start));
|
|
reader.set_sector(34, make_lvd_sector_simple());
|
|
reader.set_sector(35, make_terminator());
|
|
|
|
// FSD -> root ICB at meta LBA 1
|
|
reader.set_sector(partition_start, make_fsd_sector(1));
|
|
|
|
// Root dir: parent FID + BDMV dir FID
|
|
let parent_fid = make_parent_fid();
|
|
let bdmv_fid = make_fid("BDMV", 3, true);
|
|
let mut root_data = Vec::new();
|
|
root_data.extend_from_slice(&parent_fid);
|
|
root_data.extend_from_slice(&bdmv_fid);
|
|
let root_data_len = root_data.len() as u32;
|
|
|
|
reader.set_sector(partition_start + 1, make_dir_icb(2, root_data_len));
|
|
reader.set_sector_partial(partition_start + 2, &root_data);
|
|
|
|
// BDMV dir: parent FID + test.mpls file FID
|
|
let bdmv_parent = make_parent_fid();
|
|
let mpls_fid = make_fid("test.mpls", 5, false);
|
|
let mut bdmv_data = Vec::new();
|
|
bdmv_data.extend_from_slice(&bdmv_parent);
|
|
bdmv_data.extend_from_slice(&mpls_fid);
|
|
let bdmv_data_len = bdmv_data.len() as u32;
|
|
|
|
reader.set_sector(partition_start + 3, make_dir_icb(4, bdmv_data_len));
|
|
reader.set_sector_partial(partition_start + 4, &bdmv_data);
|
|
|
|
// test.mpls file ICB (File Entry tag 261)
|
|
reader.set_sector(partition_start + 5, make_file_icb(10, 1024, 1024));
|
|
|
|
let fs = read_filesystem(&mut reader).expect("should parse UDF with subdir");
|
|
assert_eq!(fs.volume_id, "DISC_WITH_BDMV");
|
|
|
|
// Root should have one child: BDMV
|
|
assert_eq!(fs.root.entries.len(), 1);
|
|
let bdmv = &fs.root.entries[0];
|
|
assert_eq!(bdmv.name, "BDMV");
|
|
assert!(bdmv.is_dir);
|
|
|
|
// BDMV should have one child: test.mpls
|
|
assert_eq!(bdmv.entries.len(), 1);
|
|
let mpls = &bdmv.entries[0];
|
|
assert_eq!(mpls.name, "test.mpls");
|
|
assert!(!mpls.is_dir);
|
|
assert_eq!(mpls.size, 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn find_dir_case_insensitive() {
|
|
// Build a UDF image with BDMV/PLAYLIST directories, then search with various cases
|
|
let mut reader = MockSectorReader::new();
|
|
let partition_start: u32 = 512;
|
|
|
|
reader.set_sector(256, make_avdp_sector(32));
|
|
reader.set_sector(32, make_pvd_sector("CASE_TEST"));
|
|
reader.set_sector(33, make_partition_desc(partition_start));
|
|
reader.set_sector(34, make_lvd_sector_simple());
|
|
reader.set_sector(35, make_terminator());
|
|
|
|
// FSD
|
|
reader.set_sector(partition_start, make_fsd_sector(1));
|
|
|
|
// Root -> BDMV
|
|
let root_data = [make_parent_fid(), make_fid("BDMV", 3, true)].concat();
|
|
reader.set_sector(partition_start + 1, make_dir_icb(2, root_data.len() as u32));
|
|
reader.set_sector_partial(partition_start + 2, &root_data);
|
|
|
|
// BDMV -> PLAYLIST
|
|
let bdmv_data = [make_parent_fid(), make_fid("PLAYLIST", 5, true)].concat();
|
|
reader.set_sector(partition_start + 3, make_dir_icb(4, bdmv_data.len() as u32));
|
|
reader.set_sector_partial(partition_start + 4, &bdmv_data);
|
|
|
|
// PLAYLIST (empty)
|
|
let playlist_data = make_parent_fid();
|
|
reader.set_sector(
|
|
partition_start + 5,
|
|
make_dir_icb(6, playlist_data.len() as u32),
|
|
);
|
|
reader.set_sector_partial(partition_start + 6, &playlist_data);
|
|
|
|
let fs = read_filesystem(&mut reader).expect("should parse");
|
|
|
|
// Exact case
|
|
assert!(fs.find_dir("BDMV/PLAYLIST").is_some());
|
|
// Lower case
|
|
assert!(fs.find_dir("bdmv/playlist").is_some());
|
|
// Mixed case
|
|
assert!(fs.find_dir("Bdmv/Playlist").is_some());
|
|
assert!(fs.find_dir("BdMv/PlayList").is_some());
|
|
// Leading/trailing slashes
|
|
assert!(fs.find_dir("/BDMV/PLAYLIST/").is_some());
|
|
// Nonexistent
|
|
assert!(fs.find_dir("BDMV/STREAM").is_none());
|
|
assert!(fs.find_dir("NONEXISTENT").is_none());
|
|
}
|