mirx 0.50.0

MIRX binary image format — flat & chunk-based container for ahead-of-time UI assets
Documentation
use alloc::vec;
use alloc::vec::Vec;

use super::*;
use crate::header::{CHUNK_TABLE_ENTRY_LEN, chunk_type};
use crate::{crc32, encode_chunks};

fn set_file_size(bytes: &mut [u8], file_size: u32) {
    bytes[16..20].copy_from_slice(&file_size.to_le_bytes());
    refresh_crc(bytes);
}

fn set_table_offset(bytes: &mut [u8], table_offset: u32) {
    bytes[12..16].copy_from_slice(&table_offset.to_le_bytes());
    refresh_crc(bytes);
}

fn set_payload_range(bytes: &mut [u8], index: usize, offset: u32, size: u32) {
    let entry = CHUNK_FILE_HEADER_LEN + index * CHUNK_TABLE_ENTRY_LEN;
    bytes[entry + 4..entry + 8].copy_from_slice(&offset.to_le_bytes());
    bytes[entry + 8..entry + 12].copy_from_slice(&size.to_le_bytes());
}

fn refresh_crc(bytes: &mut [u8]) {
    let checksum = crc32(&bytes[..40]);
    bytes[40..44].copy_from_slice(&checksum.to_le_bytes());
}

fn preserve() -> ReadOptions {
    ReadOptions::new().with_trailing_bytes(TrailingBytesPolicy::Preserve)
}

#[test]
fn table_must_fit_inside_the_declared_file() {
    let mut bytes = encode_chunks(&[(chunk_type::META, 0, b"")]);
    bytes.extend_from_slice(&[0; CHUNK_TABLE_ENTRY_LEN]);
    set_file_size(&mut bytes, CHUNK_FILE_HEADER_LEN as u32);

    let expected = Err(ReadError::ChunkTableOutOfBounds {
        offset: CHUNK_FILE_HEADER_LEN as u32,
        count: 1,
        file_size: CHUNK_FILE_HEADER_LEN as u32,
    });
    assert_eq!(Reader::open(&bytes), expected);
    assert_eq!(Reader::open_with(&bytes, &preserve()), expected);

    let mut empty = encode_chunks(&[]);
    empty.push(0);
    set_table_offset(&mut empty, CHUNK_FILE_HEADER_LEN as u32 + 1);
    assert!(matches!(
        Reader::open_with(&empty, &preserve()),
        Err(ReadError::ChunkTableOutOfBounds { count: 0, .. })
    ));
}

#[test]
fn table_and_empty_payload_may_end_at_logical_eof() {
    let bytes = encode_chunks(&[(chunk_type::META, 0, b"")]);
    let reader = Reader::open(&bytes).unwrap();
    let chunk = reader.chunks().next().unwrap();
    assert_eq!(chunk.payload(), b"");
    assert_eq!(chunk.payload_offset() as usize, reader.logical_len());

    let mut empty = encode_chunks(&[]);
    let empty_len = empty.len() as u32;
    set_table_offset(&mut empty, empty_len);
    assert_eq!(Reader::open(&empty).unwrap().chunks().len(), 0);
}

#[test]
fn payload_cannot_borrow_preserved_trailing_bytes() {
    for (start_delta, size) in [(0, 4), (-2, 4), (1, 0)] {
        let mut bytes = encode_chunks(&[(chunk_type::META, 0, b"body")]);
        let logical_len = bytes.len();
        bytes.extend_from_slice(b"tail");
        let offset = (logical_len as i64 + start_delta) as u32;
        set_payload_range(&mut bytes, 0, offset, size);

        assert_eq!(
            Reader::open_with(&bytes, &preserve()),
            Err(ReadError::ChunkPayloadOutOfBounds {
                index: 0,
                offset,
                size,
            })
        );
    }
}

#[test]
fn payload_ranges_cannot_point_into_the_header() {
    for (offset, size) in [(0, 1), (43, 1), (43, 2), (43, 0)] {
        let mut bytes = encode_chunks(&[(chunk_type::META, 0, b"body")]);
        set_payload_range(&mut bytes, 0, offset, size);
        assert_eq!(
            Reader::open(&bytes),
            Err(ReadError::ChunkPayloadOverlapsHeader {
                index: 0,
                offset,
                size,
            })
        );
    }
}

#[test]
fn payload_ranges_cannot_point_into_the_table() {
    let table_start = CHUNK_FILE_HEADER_LEN as u32;
    let table_end = table_start + CHUNK_TABLE_ENTRY_LEN as u32;
    for (offset, size) in [
        (table_start, 1),
        (table_start + 1, CHUNK_TABLE_ENTRY_LEN as u32),
        (table_start, 0),
    ] {
        let mut bytes = encode_chunks(&[(chunk_type::META, 0, b"body")]);
        set_payload_range(&mut bytes, 0, offset, size);
        assert_eq!(
            Reader::open(&bytes),
            Err(ReadError::ChunkPayloadOverlapsTable {
                index: 0,
                offset,
                size,
            })
        );
    }

    let mut boundary = encode_chunks(&[(chunk_type::META, 0, b"body")]);
    set_payload_range(&mut boundary, 0, table_end, 4);
    assert_eq!(
        Reader::open(&boundary)
            .unwrap()
            .chunks()
            .next()
            .unwrap()
            .payload(),
        b"body"
    );
}

#[test]
fn payload_may_use_the_gap_before_a_relocated_table() {
    let base = encode_chunks(&[(chunk_type::META, 0, b"body")]);
    let table_offset = 64usize;
    let table_end = table_offset + CHUNK_TABLE_ENTRY_LEN;
    let mut bytes = vec![0; table_end];
    bytes[..CHUNK_FILE_HEADER_LEN].copy_from_slice(&base[..CHUNK_FILE_HEADER_LEN]);
    bytes[44..48].copy_from_slice(b"body");
    bytes[table_offset..table_end].copy_from_slice(
        &base[CHUNK_FILE_HEADER_LEN..CHUNK_FILE_HEADER_LEN + CHUNK_TABLE_ENTRY_LEN],
    );
    bytes[table_offset + 4..table_offset + 8].copy_from_slice(&44u32.to_le_bytes());
    set_table_offset(&mut bytes, table_offset as u32);
    set_file_size(&mut bytes, table_end as u32);

    assert_eq!(
        Reader::open(&bytes)
            .unwrap()
            .chunks()
            .next()
            .unwrap()
            .payload(),
        b"body"
    );

    bytes[table_offset + 4..table_offset + 8].copy_from_slice(&60u32.to_le_bytes());
    bytes[table_offset + 8..table_offset + 12].copy_from_slice(&8u32.to_le_bytes());
    assert_eq!(
        Reader::open(&bytes),
        Err(ReadError::ChunkPayloadOverlapsTable {
            index: 0,
            offset: 60,
            size: 8,
        })
    );
}

#[test]
fn overlapping_payloads_remain_legal() {
    let mut bytes = encode_chunks(&[
        (chunk_type::META, 0, b"abcdef"),
        (chunk_type::FONT, 0, b"uvwxyz"),
    ]);
    let first_offset = u32::from_le_bytes(
        bytes[CHUNK_FILE_HEADER_LEN + 4..CHUNK_FILE_HEADER_LEN + 8]
            .try_into()
            .unwrap(),
    );
    set_payload_range(&mut bytes, 1, first_offset + 2, 4);

    let chunks = Reader::open(&bytes)
        .unwrap()
        .chunks()
        .map(|chunk| chunk.payload())
        .collect::<Vec<_>>();
    assert_eq!(chunks, [b"abcdef".as_slice(), b"cdef".as_slice()]);
}