hadris-iso 2.0.0

Pure Rust ISO 9660 library with allocation-free reading, Joliet, Rock Ridge, and El Torito
Documentation
#![cfg(all(feature = "std", feature = "sync", feature = "read"))]

use core::cell::Cell;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::Mutex;

use bytemuck::Zeroable;
use hadris_iso::directory::{DirectoryRecord, DirectoryRecordHeader, DirectoryRef, FileFlags};
use hadris_iso::io::LogicalSector;
use hadris_iso::joliet::JolietLevel;
use hadris_iso::read::{IsoNamespace, IsoReader};
use hadris_iso::types::{U16LsbMsb, U32LsbMsb};
use hadris_iso::volume::{
    PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor, VolumeDescriptorHeader,
    VolumeDescriptorSetTerminator, VolumeDescriptorType,
};

struct CountingAllocator;

static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
static TEST_LOCK: Mutex<()> = Mutex::new(());

thread_local! {
    static COUNTING: Cell<bool> = const { Cell::new(false) };
}

fn is_counting() -> bool {
    COUNTING
        .try_with(|counting| counting.get())
        .unwrap_or(false)
}

unsafe impl GlobalAlloc for CountingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if is_counting() {
            ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
        }
        unsafe { System.alloc(layout) }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        unsafe { System.dealloc(ptr, layout) }
    }

    unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
        if is_counting() {
            ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
        }
        unsafe { System.realloc(ptr, layout, size) }
    }
}

#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;

const BLOCK: usize = 2048;

fn root_header(extent: u32) -> DirectoryRecordHeader {
    DirectoryRecordHeader {
        len: 34,
        extended_attr_record: 0,
        extent: U32LsbMsb::new(extent),
        data_len: U32LsbMsb::new(BLOCK as u32),
        flags: FileFlags::DIRECTORY.bits(),
        volume_sequence_number: U16LsbMsb::new(1),
        file_identifier_len: 1,
        ..DirectoryRecordHeader::default()
    }
}

fn put(image: &mut [u8], sector: usize, offset: usize, bytes: &[u8]) {
    let start = sector * BLOCK + offset;
    image[start..start + bytes.len()].copy_from_slice(bytes);
}

fn fixture() -> Vec<u8> {
    let mut image = vec![0_u8; 32 * BLOCK];

    let mut pvd = PrimaryVolumeDescriptor::new("NOALLOC", 32);
    pvd.dir_record.header = root_header(20);
    put(&mut image, 16, 0, bytemuck::bytes_of(&pvd));

    let mut svd = SupplementaryVolumeDescriptor::zeroed();
    svd.header = VolumeDescriptorHeader::new(VolumeDescriptorType::SupplementaryVolumeDescriptor);
    svd.escape_sequences = JolietLevel::Level3.escape_sequence();
    svd.logical_block_size = U16LsbMsb::new(BLOCK as u16);
    svd.volume_sequence_number = U16LsbMsb::new(1);
    svd.dir_record.header = root_header(23);
    svd.file_structure_version = 1;
    put(&mut image, 17, 0, bytemuck::bytes_of(&svd));
    put(
        &mut image,
        18,
        0,
        VolumeDescriptorSetTerminator::new().to_bytes(),
    );

    let mut first = DirectoryRecord::new(
        b"PAYLOAD.BIN;1",
        &[],
        DirectoryRef {
            extent: LogicalSector(21),
            size: 5,
        },
        FileFlags::NOT_FINAL,
    );
    first.header_mut().volume_sequence_number = U16LsbMsb::new(1);
    let mut second = DirectoryRecord::new(
        b"PAYLOAD.BIN;1",
        &[],
        DirectoryRef {
            extent: LogicalSector(22),
            size: 6,
        },
        FileFlags::empty(),
    );
    second.header_mut().volume_sequence_number = U16LsbMsb::new(1);
    put(&mut image, 20, 0, first.to_bytes());
    put(&mut image, 20, first.size(), second.to_bytes());
    put(&mut image, 21, 0, b"hello");
    put(&mut image, 22, 0, b" world");

    let joliet_name = [
        0x65, 0xe5, 0x67, 0x2c, 0x00, b'.', 0x00, b'T', 0x00, b'X', 0x00, b'T', 0x00, b';', 0x00,
        b'1',
    ];
    let mut joliet = DirectoryRecord::new(
        &joliet_name,
        &[],
        DirectoryRef {
            extent: LogicalSector(24),
            size: 7,
        },
        FileFlags::empty(),
    );
    joliet.header_mut().volume_sequence_number = U16LsbMsb::new(1);
    put(&mut image, 23, 0, joliet.to_bytes());
    put(&mut image, 24, 0, b"unicode");
    image
}

fn exercise_navigation_and_streaming(
    image: &[u8],
    primary_output: &mut [u8; 11],
    joliet_output: &mut [u8; 7],
) {
    let mut reader = IsoReader::open(hadris_io::Cursor::new(image)).unwrap();
    assert_eq!(
        reader.preferred_root().namespace(),
        IsoNamespace::Joliet(JolietLevel::Level3)
    );

    let primary = reader.primary_root();
    let entry = reader
        .find_path_in(primary, "payload.bin")
        .unwrap()
        .unwrap();
    assert!(entry.is_multi_extent());
    assert_eq!(entry.total_size(), 11);
    let mut file = reader.open_file(&entry).unwrap();
    for byte in &mut *primary_output {
        assert_eq!(file.read_chunk(core::slice::from_mut(byte)).unwrap(), 1);
    }
    assert_eq!(file.read_chunk(&mut primary_output[..1]).unwrap(), 0);

    let entry = reader.find_path("日本.TXT").unwrap().unwrap();
    let mut file = reader.open_file(&entry).unwrap();
    assert_eq!(file.read_chunk(joliet_output).unwrap(), 7);
}

#[test]
fn navigation_and_streaming_allocate_nothing() {
    let _guard = TEST_LOCK.lock().unwrap();
    let image = fixture();
    let mut warm_primary_output = [0_u8; 11];
    let mut warm_joliet_output = [0_u8; 7];
    exercise_navigation_and_streaming(&image, &mut warm_primary_output, &mut warm_joliet_output);

    let mut primary_output = [0_u8; 11];
    let mut joliet_output = [0_u8; 7];
    ALLOCATIONS.store(0, Ordering::Relaxed);
    COUNTING.set(true);
    exercise_navigation_and_streaming(&image, &mut primary_output, &mut joliet_output);

    COUNTING.set(false);
    assert_eq!(ALLOCATIONS.load(Ordering::Relaxed), 0);
    assert_eq!(&primary_output, b"hello world");
    assert_eq!(&joliet_output, b"unicode");
}

#[test]
fn malformed_record_length_is_rejected() {
    let _guard = TEST_LOCK.lock().unwrap();
    let mut image = fixture();
    image[20 * BLOCK] = 2;
    let mut reader = IsoReader::open(hadris_io::Cursor::new(image.as_slice())).unwrap();
    let error = reader
        .find_path_in(reader.primary_root(), "PAYLOAD.BIN")
        .unwrap_err();
    assert_eq!(error.kind(), hadris_iso::ErrorKind::InvalidData);
}