- MKV muxer: EBML header, segment, cluster, cues, multi-track, keyframe flags — 6 tests - MkvStream: BD-TS roundtrip, metadata preservation — 2 tests - IsoWriter: valid UDF, file size update, custom names, empty content — 4 tests - Disc pipeline: format detection (UHD/BD/DVD), content format, capacity, duration — 5 tests - Network: listen/connect roundtrip, metadata flow — 2 tests (ignored for CI) - Encryption: no AACS dir, no keydb — 2 tests - 297 tests total, all passing
624 lines
23 KiB
Rust
624 lines
23 KiB
Rust
//! UDF ISO writer — creates Blu-ray disc images.
|
|
//!
|
|
//! Writes a minimal UDF 2.50 filesystem containing BDMV/STREAM/*.m2ts.
|
|
//! The ISO can be mounted or read back via IsoStream.
|
|
//!
|
|
//! Layout:
|
|
//! Sector 0-15: System area (zeros)
|
|
//! Sector 16-18: Volume Recognition Sequence (BEA01, NSR03, TEA01)
|
|
//! Sector 32-37: Volume Descriptor Sequence
|
|
//! Sector 256: Anchor Volume Descriptor Pointer
|
|
//! Sector 260-271: Metadata partition (FSD, ICBs, directories)
|
|
//! Sector 288+: File data (m2ts content)
|
|
//! Last-256: Reserve AVDP
|
|
|
|
use std::io::{self, Seek, SeekFrom, Write};
|
|
|
|
const SECTOR_SIZE: u64 = 2048;
|
|
|
|
// Layout constants
|
|
const VRS_START: u32 = 16; // Volume Recognition Sequence
|
|
const VDS_START: u32 = 32; // Volume Descriptor Sequence
|
|
const AVDP_SECTOR: u32 = 256; // Anchor Volume Descriptor Pointer
|
|
const PARTITION_START: u32 = 257; // Physical partition start
|
|
const METADATA_START: u32 = 260; // Metadata partition content
|
|
const FSD_SECTOR: u32 = 260; // File Set Descriptor
|
|
const ROOT_ICB_SECTOR: u32 = 261; // Root directory ICB
|
|
const ROOT_DIR_SECTOR: u32 = 262; // Root directory data
|
|
const BDMV_ICB_SECTOR: u32 = 263; // BDMV/ ICB
|
|
const BDMV_DIR_SECTOR: u32 = 264; // BDMV/ directory data
|
|
const STREAM_ICB_SECTOR: u32 = 265; // BDMV/STREAM/ ICB
|
|
const STREAM_DIR_SECTOR: u32 = 266; // BDMV/STREAM/ directory data
|
|
const M2TS_ICB_SECTOR: u32 = 267; // m2ts file ICB
|
|
const DATA_START: u32 = 288; // Start of file data (aligned)
|
|
|
|
/// Write a complete BD ISO image.
|
|
///
|
|
/// Writes UDF structure, then streams m2ts content from the writer.
|
|
/// Call `start()` first, then write BD-TS bytes, then call `finish()`.
|
|
pub struct IsoWriter<W: Write + Seek> {
|
|
writer: W,
|
|
volume_id: String,
|
|
m2ts_name: String,
|
|
data_start_sector: u32,
|
|
bytes_written: u64,
|
|
}
|
|
|
|
impl<W: Write + Seek> IsoWriter<W> {
|
|
/// Create a new ISO writer. Call `start()` to write the UDF header.
|
|
pub fn new(writer: W, volume_id: &str, m2ts_name: &str) -> Self {
|
|
Self {
|
|
writer,
|
|
volume_id: volume_id.to_string(),
|
|
m2ts_name: m2ts_name.to_string(),
|
|
data_start_sector: DATA_START,
|
|
bytes_written: 0,
|
|
}
|
|
}
|
|
|
|
/// Update volume ID and m2ts filename. Must be called before `start()`.
|
|
pub fn with_names(mut self, volume_id: &str, m2ts_name: &str) -> Self {
|
|
self.volume_id = volume_id.to_string();
|
|
self.m2ts_name = m2ts_name.to_string();
|
|
self
|
|
}
|
|
|
|
/// Write UDF filesystem header. After this, write m2ts content bytes.
|
|
pub fn start(&mut self) -> io::Result<()> {
|
|
// System area: sectors 0-15 (zeros)
|
|
let zero_sector = [0u8; SECTOR_SIZE as usize];
|
|
for _ in 0..VRS_START {
|
|
self.writer.write_all(&zero_sector)?;
|
|
}
|
|
|
|
// Volume Recognition Sequence
|
|
self.write_vrs()?;
|
|
|
|
// Pad sectors 19-31
|
|
for _ in 19..VDS_START {
|
|
self.writer.write_all(&zero_sector)?;
|
|
}
|
|
|
|
// Volume Descriptor Sequence (sectors 32-37)
|
|
self.write_vds()?;
|
|
|
|
// Pad sectors 38-255
|
|
for _ in 38..AVDP_SECTOR {
|
|
self.writer.write_all(&zero_sector)?;
|
|
}
|
|
|
|
// AVDP at sector 256
|
|
self.write_avdp()?;
|
|
|
|
// Partition area: metadata file ICB at partition_start
|
|
self.write_metadata_file_icb()?;
|
|
|
|
// Pad to metadata start
|
|
for _ in (PARTITION_START + 1)..METADATA_START {
|
|
self.writer.write_all(&zero_sector)?;
|
|
}
|
|
|
|
// Metadata partition
|
|
self.write_fsd()?;
|
|
self.write_root_icb()?;
|
|
self.write_root_dir()?;
|
|
self.write_bdmv_icb()?;
|
|
self.write_bdmv_dir()?;
|
|
self.write_stream_icb()?;
|
|
self.write_stream_dir()?;
|
|
self.write_m2ts_icb(0)?; // placeholder size, updated in finish()
|
|
|
|
// Pad to data start
|
|
for _ in (M2TS_ICB_SECTOR + 1)..self.data_start_sector {
|
|
self.writer.write_all(&zero_sector)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Write m2ts content bytes. Call after `start()`.
|
|
pub fn write_data(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
let n = self.writer.write(buf)?;
|
|
self.bytes_written += n as u64;
|
|
Ok(n)
|
|
}
|
|
|
|
/// Finalize the ISO: pad to sector boundary, update file sizes, write reserve AVDP.
|
|
pub fn finish(&mut self) -> io::Result<()> {
|
|
// Pad to sector boundary
|
|
let remainder = (self.bytes_written % SECTOR_SIZE) as usize;
|
|
if remainder > 0 {
|
|
let pad = SECTOR_SIZE as usize - remainder;
|
|
let zeros = vec![0u8; pad];
|
|
self.writer.write_all(&zeros)?;
|
|
self.bytes_written += pad as u64;
|
|
}
|
|
|
|
let total_data_sectors = (self.bytes_written / SECTOR_SIZE) as u32;
|
|
let total_sectors = self.data_start_sector + total_data_sectors;
|
|
|
|
// Seek back and update m2ts file ICB with actual size
|
|
self.writer
|
|
.seek(SeekFrom::Start(M2TS_ICB_SECTOR as u64 * SECTOR_SIZE))?;
|
|
self.write_m2ts_icb(self.bytes_written)?;
|
|
|
|
// Seek to end and write reserve AVDP
|
|
let reserve_sector = total_sectors;
|
|
self.writer
|
|
.seek(SeekFrom::Start(reserve_sector as u64 * SECTOR_SIZE))?;
|
|
self.write_avdp()?;
|
|
|
|
self.writer.flush()?;
|
|
Ok(())
|
|
}
|
|
|
|
// ── UDF structure writers ──────────────────────────────────────────────
|
|
|
|
fn write_vrs(&mut self) -> io::Result<()> {
|
|
// BEA01 at sector 16
|
|
let mut bea = [0u8; SECTOR_SIZE as usize];
|
|
bea[0] = 0; // structure type
|
|
bea[1..6].copy_from_slice(b"BEA01");
|
|
bea[6] = 1; // structure version
|
|
self.writer.write_all(&bea)?;
|
|
|
|
// NSR03 at sector 17 (UDF 2.50)
|
|
let mut nsr = [0u8; SECTOR_SIZE as usize];
|
|
nsr[0] = 0;
|
|
nsr[1..6].copy_from_slice(b"NSR03");
|
|
nsr[6] = 1;
|
|
self.writer.write_all(&nsr)?;
|
|
|
|
// TEA01 at sector 18
|
|
let mut tea = [0u8; SECTOR_SIZE as usize];
|
|
tea[0] = 0;
|
|
tea[1..6].copy_from_slice(b"TEA01");
|
|
tea[6] = 1;
|
|
self.writer.write_all(&tea)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_vds(&mut self) -> io::Result<()> {
|
|
// Primary Volume Descriptor (tag 1) at sector 32
|
|
let mut pvd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut pvd, 1, VDS_START);
|
|
// Volume Identifier at offset 24 (32-byte d-string)
|
|
write_dstring(&mut pvd[24..56], &self.volume_id);
|
|
self.writer.write_all(&pvd)?;
|
|
|
|
// Partition Descriptor (tag 5) at sector 33
|
|
let mut pd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut pd, 5, VDS_START + 1);
|
|
// Partition starting location at offset 188
|
|
pd[188..192].copy_from_slice(&PARTITION_START.to_le_bytes());
|
|
// Partition length (large enough for everything)
|
|
let part_len: u32 = 0xFFFFFFFF;
|
|
pd[192..196].copy_from_slice(&part_len.to_le_bytes());
|
|
self.writer.write_all(&pd)?;
|
|
|
|
// Logical Volume Descriptor (tag 6) at sector 34
|
|
let mut lvd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut lvd, 6, VDS_START + 2);
|
|
// Logical block size at offset 212
|
|
lvd[212..216].copy_from_slice(&2048u32.to_le_bytes());
|
|
// Number of partition maps at offset 268
|
|
lvd[268..272].copy_from_slice(&2u32.to_le_bytes());
|
|
// Partition map 1: Type 1 (physical), 6 bytes
|
|
lvd[440] = 1; // type
|
|
lvd[441] = 6; // length
|
|
// Partition map 2: Type 2 (metadata), 64 bytes
|
|
lvd[446] = 2; // type
|
|
lvd[447] = 64; // length
|
|
// Entity ID for metadata partition
|
|
lvd[450..473].copy_from_slice(b"*UDF Metadata Partition");
|
|
self.writer.write_all(&lvd)?;
|
|
|
|
// Unallocated Space Descriptor (tag 7) at sector 35
|
|
let mut usd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut usd, 7, VDS_START + 3);
|
|
self.writer.write_all(&usd)?;
|
|
|
|
// Implementation Use Volume Descriptor (tag 4) at sector 36
|
|
let mut iuvd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut iuvd, 4, VDS_START + 4);
|
|
self.writer.write_all(&iuvd)?;
|
|
|
|
// Terminating Descriptor (tag 8) at sector 37
|
|
let mut td = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut td, 8, VDS_START + 5);
|
|
self.writer.write_all(&td)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_avdp(&mut self) -> io::Result<()> {
|
|
let mut avdp = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut avdp, 2, AVDP_SECTOR);
|
|
// Main VDS extent_ad: {length, location} per UDF spec
|
|
avdp[16..20].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
|
|
avdp[20..24].copy_from_slice(&VDS_START.to_le_bytes()); // location
|
|
// Reserve VDS extent_ad (same as main for simplicity)
|
|
avdp[24..28].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length
|
|
avdp[28..32].copy_from_slice(&VDS_START.to_le_bytes()); // location
|
|
self.writer.write_all(&avdp)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_metadata_file_icb(&mut self) -> io::Result<()> {
|
|
// Extended File Entry (tag 266) at partition_start
|
|
// Points to metadata content at METADATA_START
|
|
let mut icb = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut icb, 266, PARTITION_START);
|
|
// ICB tag at offset 16
|
|
icb[16..20].copy_from_slice(&0u32.to_le_bytes()); // prior recorded
|
|
icb[20..22].copy_from_slice(&0u16.to_le_bytes()); // strategy type
|
|
icb[22..24].copy_from_slice(&0u16.to_le_bytes()); // strategy parameter
|
|
// File type at offset 27: 250 = metadata file
|
|
icb[27] = 250;
|
|
// Information length at offset 56
|
|
let meta_len: u64 = 12 * SECTOR_SIZE; // 12 sectors of metadata
|
|
icb[56..64].copy_from_slice(&meta_len.to_le_bytes());
|
|
// Extended attribute length at offset 208
|
|
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
// Allocation descriptor at offset 216: short_ad (length + position)
|
|
let ad_len = meta_len as u32;
|
|
let ad_pos = METADATA_START - PARTITION_START; // relative to partition
|
|
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
|
|
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
|
|
self.writer.write_all(&icb)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_fsd(&mut self) -> io::Result<()> {
|
|
let mut fsd = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut fsd, 256, FSD_SECTOR);
|
|
// Root Directory ICB (long_ad at offset 400)
|
|
let root_lba = ROOT_ICB_SECTOR - METADATA_START; // metadata-relative
|
|
fsd[400..404].copy_from_slice(&SECTOR_SIZE.to_le_bytes()[..4]); // extent length
|
|
fsd[404..408].copy_from_slice(&root_lba.to_le_bytes());
|
|
self.writer.write_all(&fsd)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_root_icb(&mut self) -> io::Result<()> {
|
|
let mut icb = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut icb, 266, ROOT_ICB_SECTOR);
|
|
icb[27] = 4; // file type: directory
|
|
let dir_len: u64 = SECTOR_SIZE;
|
|
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
|
|
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
let ad_pos = ROOT_DIR_SECTOR - METADATA_START;
|
|
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
|
|
self.writer.write_all(&icb)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_root_dir(&mut self) -> io::Result<()> {
|
|
let mut dir = [0u8; SECTOR_SIZE as usize];
|
|
let mut offset = 0;
|
|
// Parent entry (.. points to self)
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
ROOT_ICB_SECTOR - METADATA_START,
|
|
"",
|
|
true,
|
|
);
|
|
// BDMV directory entry
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
BDMV_ICB_SECTOR - METADATA_START,
|
|
"BDMV",
|
|
false,
|
|
);
|
|
let _ = offset;
|
|
self.writer.write_all(&dir)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_bdmv_icb(&mut self) -> io::Result<()> {
|
|
let mut icb = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut icb, 266, BDMV_ICB_SECTOR);
|
|
icb[27] = 4; // directory
|
|
let dir_len: u64 = SECTOR_SIZE;
|
|
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
|
|
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
let ad_pos = BDMV_DIR_SECTOR - METADATA_START;
|
|
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
|
|
self.writer.write_all(&icb)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_bdmv_dir(&mut self) -> io::Result<()> {
|
|
let mut dir = [0u8; SECTOR_SIZE as usize];
|
|
let mut offset = 0;
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
ROOT_ICB_SECTOR - METADATA_START,
|
|
"",
|
|
true,
|
|
);
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
STREAM_ICB_SECTOR - METADATA_START,
|
|
"STREAM",
|
|
false,
|
|
);
|
|
let _ = offset;
|
|
self.writer.write_all(&dir)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_stream_icb(&mut self) -> io::Result<()> {
|
|
let mut icb = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut icb, 266, STREAM_ICB_SECTOR);
|
|
icb[27] = 4; // directory
|
|
let dir_len: u64 = SECTOR_SIZE;
|
|
icb[56..64].copy_from_slice(&dir_len.to_le_bytes());
|
|
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
let ad_pos = STREAM_DIR_SECTOR - METADATA_START;
|
|
icb[216..220].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
icb[220..224].copy_from_slice(&ad_pos.to_le_bytes());
|
|
self.writer.write_all(&icb)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_stream_dir(&mut self) -> io::Result<()> {
|
|
let mut dir = [0u8; SECTOR_SIZE as usize];
|
|
let mut offset = 0;
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
BDMV_ICB_SECTOR - METADATA_START,
|
|
"",
|
|
true,
|
|
);
|
|
offset += write_fid(
|
|
&mut dir[offset..],
|
|
M2TS_ICB_SECTOR - METADATA_START,
|
|
&self.m2ts_name,
|
|
false,
|
|
);
|
|
let _ = offset;
|
|
self.writer.write_all(&dir)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn write_m2ts_icb(&mut self, file_size: u64) -> io::Result<()> {
|
|
let mut icb = [0u8; SECTOR_SIZE as usize];
|
|
write_descriptor_tag(&mut icb, 266, M2TS_ICB_SECTOR);
|
|
icb[27] = 5; // file type: regular file
|
|
icb[56..64].copy_from_slice(&file_size.to_le_bytes());
|
|
icb[208..212].copy_from_slice(&0u32.to_le_bytes());
|
|
// Allocation: data starts at DATA_START in the physical partition
|
|
let data_offset = self.data_start_sector - PARTITION_START;
|
|
// Cap allocation length at u32::MAX for files >4GB (UDF short_ad limitation)
|
|
let ad_len = if file_size > u32::MAX as u64 { u32::MAX } else { file_size as u32 };
|
|
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
|
|
icb[220..224].copy_from_slice(&data_offset.to_le_bytes());
|
|
self.writer.write_all(&icb)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ── UDF primitives ─────────────────────────────────────────────────────────
|
|
|
|
/// Write a UDF Descriptor Tag at the start of a sector.
|
|
fn write_descriptor_tag(buf: &mut [u8], tag_id: u16, sector: u32) {
|
|
buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
|
|
// Descriptor version: 3 (UDF 2.50)
|
|
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
|
|
// Tag serial number
|
|
buf[4] = 0;
|
|
// Descriptor CRC (simplified — set to 0, most implementations accept this)
|
|
buf[8..10].copy_from_slice(&0u16.to_le_bytes());
|
|
// Descriptor CRC length
|
|
buf[10..12].copy_from_slice(&0u16.to_le_bytes());
|
|
// Tag location
|
|
buf[12..16].copy_from_slice(§or.to_le_bytes());
|
|
}
|
|
|
|
/// Write a UDF d-string (compressed unicode string with length prefix).
|
|
fn write_dstring(buf: &mut [u8], s: &str) {
|
|
let max = buf.len() - 1; // last byte is length
|
|
let bytes = s.as_bytes();
|
|
let len = bytes.len().min(max);
|
|
if len > 0 {
|
|
buf[0] = 8; // compression ID: 8 = Latin-1
|
|
buf[1..1 + len].copy_from_slice(&bytes[..len]);
|
|
buf[buf.len() - 1] = (len + 1) as u8; // d-string length including comp ID
|
|
}
|
|
}
|
|
|
|
/// Write a File Identifier Descriptor. Returns bytes written (4-byte aligned).
|
|
fn write_fid(buf: &mut [u8], icb_lba: u32, name: &str, is_parent: bool) -> usize {
|
|
// Tag 257 = File Identifier Descriptor
|
|
let name_bytes = name.as_bytes();
|
|
let name_len = if is_parent { 0 } else { name_bytes.len() + 1 }; // +1 for comp ID
|
|
let fid_len = 38 + name_len; // fixed header + identifier
|
|
let padded = (fid_len + 3) & !3; // 4-byte align
|
|
|
|
if padded > buf.len() {
|
|
return 0;
|
|
}
|
|
|
|
// Tag
|
|
buf[0..2].copy_from_slice(&257u16.to_le_bytes());
|
|
// File version number at offset 16
|
|
buf[16..18].copy_from_slice(&1u16.to_le_bytes());
|
|
// File characteristics at offset 18
|
|
buf[18] = if is_parent { 0x0A } else { 0x02 }; // parent | directory
|
|
if !is_parent && !name.contains('.') {
|
|
buf[18] = 0x02; // directory
|
|
} else if !is_parent {
|
|
buf[18] = 0x00; // file
|
|
}
|
|
// ICB (long_ad at offset 20): extent length + location
|
|
buf[20..24].copy_from_slice(&(SECTOR_SIZE as u32).to_le_bytes());
|
|
buf[24..28].copy_from_slice(&icb_lba.to_le_bytes());
|
|
// Identifier length at offset 36
|
|
buf[36] = name_len as u8;
|
|
// Implementation use length at offset 37
|
|
buf[37] = 0;
|
|
// File identifier at offset 38
|
|
if !is_parent && !name_bytes.is_empty() {
|
|
buf[38] = 8; // compression ID: Latin-1
|
|
buf[39..39 + name_bytes.len()].copy_from_slice(name_bytes);
|
|
}
|
|
|
|
padded
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::io::Cursor;
|
|
|
|
/// Read a little-endian u16 from a byte slice at the given offset.
|
|
fn le_u16(data: &[u8], off: usize) -> u16 {
|
|
u16::from_le_bytes([data[off], data[off + 1]])
|
|
}
|
|
|
|
/// Read a little-endian u32 from a byte slice at the given offset.
|
|
fn le_u32(data: &[u8], off: usize) -> u32 {
|
|
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
|
|
}
|
|
|
|
/// Read a little-endian u64 from a byte slice at the given offset.
|
|
fn le_u64(data: &[u8], off: usize) -> u64 {
|
|
u64::from_le_bytes([
|
|
data[off],
|
|
data[off + 1],
|
|
data[off + 2],
|
|
data[off + 3],
|
|
data[off + 4],
|
|
data[off + 5],
|
|
data[off + 6],
|
|
data[off + 7],
|
|
])
|
|
}
|
|
|
|
/// Get the sector at a given sector number from the output data.
|
|
fn sector(data: &[u8], num: u32) -> &[u8] {
|
|
let start = num as usize * SECTOR_SIZE as usize;
|
|
&data[start..start + SECTOR_SIZE as usize]
|
|
}
|
|
|
|
#[test]
|
|
fn isowriter_creates_valid_udf() {
|
|
let buf = Cursor::new(Vec::new());
|
|
let mut w = IsoWriter::new(buf, "TEST_VOL", "00001.m2ts");
|
|
w.start().unwrap();
|
|
w.write_data(&[0xAA; 4096]).unwrap();
|
|
w.finish().unwrap();
|
|
let data = w.writer.into_inner();
|
|
|
|
// AVDP at sector 256 should have tag ID = 2
|
|
let avdp = sector(&data, AVDP_SECTOR);
|
|
assert_eq!(le_u16(avdp, 0), 2, "AVDP tag ID should be 2");
|
|
|
|
// VRS at sector 16 should contain "BEA01"
|
|
let vrs = sector(&data, VRS_START);
|
|
assert_eq!(
|
|
&vrs[1..6],
|
|
b"BEA01",
|
|
"VRS sector 16 should contain BEA01"
|
|
);
|
|
|
|
// FSD at metadata sector should have tag ID = 256
|
|
let fsd = sector(&data, FSD_SECTOR);
|
|
assert_eq!(le_u16(fsd, 0), 256, "FSD tag ID should be 256");
|
|
}
|
|
|
|
#[test]
|
|
fn isowriter_updates_file_size() {
|
|
let buf = Cursor::new(Vec::new());
|
|
let mut w = IsoWriter::new(buf, "SIZE_TEST", "00001.m2ts");
|
|
w.start().unwrap();
|
|
|
|
let test_data = vec![0x42u8; 8192]; // exactly 4 sectors
|
|
let written = w.write_data(&test_data).unwrap();
|
|
assert_eq!(written, 8192);
|
|
|
|
w.finish().unwrap();
|
|
let data = w.writer.into_inner();
|
|
|
|
// Read m2ts ICB at M2TS_ICB_SECTOR and check information length at offset 56
|
|
let icb = sector(&data, M2TS_ICB_SECTOR);
|
|
let file_size = le_u64(icb, 56);
|
|
assert_eq!(
|
|
file_size, 8192,
|
|
"m2ts ICB file size should match bytes written (8192), got {}",
|
|
file_size
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn isowriter_with_names() {
|
|
let buf = Cursor::new(Vec::new());
|
|
let mut w = IsoWriter::new(buf, "MY_DISC", "00042.m2ts");
|
|
w.start().unwrap();
|
|
w.write_data(&[0x00; 2048]).unwrap();
|
|
w.finish().unwrap();
|
|
let data = w.writer.into_inner();
|
|
|
|
// Check PVD (sector 32) volume_id at offset 24 as d-string
|
|
let pvd = sector(&data, VDS_START);
|
|
// d-string: byte 0 = compression ID (8), then ASCII chars
|
|
assert_eq!(pvd[24], 8, "PVD volume_id compression ID should be 8");
|
|
assert_eq!(
|
|
&pvd[25..32],
|
|
b"MY_DISC",
|
|
"PVD should contain volume_id 'MY_DISC'"
|
|
);
|
|
|
|
// Check STREAM directory (sector 266) for m2ts filename in FID
|
|
let stream_dir = sector(&data, STREAM_DIR_SECTOR);
|
|
// The FID for the m2ts file should contain the filename after the parent entry.
|
|
// Search for "00042.m2ts" in the sector data
|
|
let name = b"00042.m2ts";
|
|
let found = stream_dir
|
|
.windows(name.len())
|
|
.any(|w| w == name);
|
|
assert!(
|
|
found,
|
|
"STREAM directory should contain m2ts filename '00042.m2ts'"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn isowriter_empty_content() {
|
|
let buf = Cursor::new(Vec::new());
|
|
let mut w = IsoWriter::new(buf, "EMPTY", "00001.m2ts");
|
|
w.start().unwrap();
|
|
// No data written
|
|
w.finish().unwrap();
|
|
let data = w.writer.into_inner();
|
|
|
|
// Should still have valid UDF structure
|
|
// AVDP at sector 256
|
|
let avdp = sector(&data, AVDP_SECTOR);
|
|
assert_eq!(le_u16(avdp, 0), 2, "AVDP tag should be present even with no data");
|
|
|
|
// VRS
|
|
let vrs = sector(&data, VRS_START);
|
|
assert_eq!(&vrs[1..6], b"BEA01");
|
|
|
|
// FSD
|
|
let fsd = sector(&data, FSD_SECTOR);
|
|
assert_eq!(le_u16(fsd, 0), 256);
|
|
|
|
// m2ts ICB should show 0 file size
|
|
let icb = sector(&data, M2TS_ICB_SECTOR);
|
|
let file_size = le_u64(icb, 56);
|
|
assert_eq!(file_size, 0, "empty content should have 0 file size");
|
|
|
|
// Output should be at least DATA_START sectors (the header structure)
|
|
assert!(
|
|
data.len() >= DATA_START as usize * SECTOR_SIZE as usize,
|
|
"output too small for valid UDF structure"
|
|
);
|
|
}
|
|
}
|