j2k-jpeg 0.7.2

JPEG inspect/decode and fallback encode support for j2k
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

use super::{
    parse_dht, parse_dqt, HuffmanTables, HuffmanValues, ProgressiveTableState, QuantTables,
    RawHuffmanTable,
};
use crate::error::JpegError;
use crate::parse::allocation::ParsedMetadataBudget;

fn ones_64() -> [u16; 64] {
    [1; 64]
}

#[test]
fn parses_single_8bit_quant_table() {
    let mut payload = alloc::vec![0u8];
    payload.extend(core::iter::repeat_n(1u8, 64));
    let mut tables = QuantTables::default();
    parse_dqt(&payload, 0, &mut tables, &mut ParsedMetadataBudget::new()).unwrap();
    assert_eq!(tables.entries[0].unwrap(), ones_64());
    assert_eq!(tables.versions.len(), 1);
}

#[test]
fn dqt_redefinition_creates_one_version_and_snapshot_tracks_it() {
    let mut tables = QuantTables::default();
    let mut budget = ParsedMetadataBudget::new();
    for value in [1u8, 2] {
        let mut payload = alloc::vec![0u8];
        payload.extend(core::iter::repeat_n(value, 64));
        parse_dqt(&payload, 0, &mut tables, &mut budget).unwrap();
    }
    let state = ProgressiveTableState::capture(&HuffmanTables::default(), &tables);
    assert_eq!(tables.versions.len(), 2);
    assert_eq!(tables.resolve(&state, 0), Some(&[2; 64]));
}

#[test]
fn parses_single_dc_huffman_table() {
    let mut payload = alloc::vec![0u8, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
    payload.extend_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
    let mut tables = HuffmanTables::default();
    parse_dht(&payload, 0, &mut tables, &mut ParsedMetadataBudget::new()).unwrap();
    let table = tables.dc[0].as_ref().unwrap();
    assert_eq!(
        table.values.as_slice(),
        &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    );
    assert_eq!(tables.versions.len(), 1);
}

#[test]
fn dht_redefinition_creates_one_version_and_snapshot_tracks_it() {
    let mut tables = HuffmanTables::default();
    let mut budget = ParsedMetadataBudget::new();
    for symbol in [0xaau8, 0xbb] {
        let payload = [0u8, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, symbol];
        parse_dht(&payload, 0, &mut tables, &mut budget).unwrap();
    }
    let state = ProgressiveTableState::capture(&tables, &QuantTables::default());
    assert_eq!(tables.versions.len(), 2);
    assert_eq!(
        tables.resolve_dc(&state, 0).unwrap().values.as_slice(),
        &[0xbb]
    );
}

#[test]
fn rejects_truncated_dqt() {
    let payload = alloc::vec![0u8, 1, 2, 3];
    let error = parse_dqt(
        &payload,
        0,
        &mut QuantTables::default(),
        &mut ParsedMetadataBudget::new(),
    )
    .unwrap_err();
    assert!(matches!(error, JpegError::Truncated { .. }));
}

#[test]
fn rejects_invalid_dqt_precision_before_length_interpretation() {
    let mut tables = QuantTables::default();
    let error = parse_dqt(&[0x20], 7, &mut tables, &mut ParsedMetadataBudget::new())
        .expect_err("DQT precision is restricted to 8 or 16 bits");

    assert_eq!(error, JpegError::UnsupportedBitDepth { depth: 2 });
    assert!(tables.versions.is_empty());
}

#[test]
fn rejects_zero_quantizers_without_mutating_table_state() {
    for (precision, zero_offset, payload) in [
        (0_u8, 1_usize, {
            let mut payload = alloc::vec![0_u8];
            payload.extend(core::iter::repeat_n(1_u8, 64));
            payload[1] = 0;
            payload
        }),
        (1_u8, 1_usize, {
            let mut payload = alloc::vec![0x10_u8];
            for _ in 0..64 {
                payload.extend_from_slice(&1_u16.to_be_bytes());
            }
            payload[1] = 0;
            payload[2] = 0;
            payload
        }),
    ] {
        let mut tables = QuantTables::default();
        let error = parse_dqt(&payload, 11, &mut tables, &mut ParsedMetadataBudget::new())
            .expect_err("zero DQT entries are forbidden");
        assert_eq!(
            error,
            JpegError::InvalidQuantizationValue {
                offset: 11 + zero_offset,
                table: 0,
                coefficient: 0,
            },
            "precision {precision}"
        );
        assert!(tables.versions.is_empty());
    }
}

#[test]
fn rejects_huffman_with_more_than_256_values() {
    let mut payload = alloc::vec![0u8];
    payload.extend(core::iter::repeat_n(17u8, 16));
    payload.push(0);
    let error = parse_dht(
        &payload,
        0,
        &mut HuffmanTables::default(),
        &mut ParsedMetadataBudget::new(),
    )
    .unwrap_err();
    assert!(matches!(error, JpegError::InvalidSegmentLength { .. }));
}

#[test]
fn table_state_rejects_an_unvalidated_huffman_class_without_panicking() {
    let mut tables = HuffmanTables::default();
    let error = tables
        .define(
            2,
            0,
            RawHuffmanTable {
                bits: [0; 16],
                values: HuffmanValues::default(),
            },
            &mut ParsedMetadataBudget::new(),
        )
        .expect_err("only DC and AC Huffman classes are valid");

    assert_eq!(
        error,
        JpegError::InternalInvariant {
            reason: "unvalidated DHT class reached table definition",
        }
    );
    assert!(tables.versions.is_empty());

    let error = tables
        .define(
            0,
            4,
            RawHuffmanTable {
                bits: [0; 16],
                values: HuffmanValues::default(),
            },
            &mut ParsedMetadataBudget::new(),
        )
        .expect_err("Huffman slots are limited to four entries");
    assert_eq!(
        error,
        JpegError::InternalInvariant {
            reason: "unvalidated DHT slot reached table definition",
        }
    );
    assert!(tables.versions.is_empty());
}

#[test]
fn table_state_rejects_an_unvalidated_quant_slot_without_panicking() {
    let mut tables = QuantTables::default();
    let error = tables
        .define(4, [1; 64], &mut ParsedMetadataBudget::new())
        .expect_err("quantization slots are limited to four entries");

    assert_eq!(
        error,
        JpegError::InternalInvariant {
            reason: "unvalidated DQT slot reached table definition",
        }
    );
    assert!(tables.versions.is_empty());
}