Skip to main content

j2k_native/
inspect.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Lightweight JPEG 2000 codestream header inspection.
4
5extern crate alloc;
6
7use alloc::vec::Vec;
8use core::fmt;
9
10use crate::j2c::capabilities::{CapabilityMarkerError, CapabilityMarkerState, Htj2kCapabilities};
11use crate::{MAX_J2K_IMAGE_DIMENSION, MAX_J2K_SPEC_COMPONENTS, MAX_J2K_TILE_COUNT};
12
13const MARKER_SOC: u8 = 0x4F;
14const MARKER_CAP: u8 = 0x50;
15const MARKER_SIZ: u8 = 0x51;
16const MARKER_COD: u8 = 0x52;
17const MARKER_CPF: u8 = 0x59;
18const MARKER_SOT: u8 = 0x90;
19const MARKER_SOD: u8 = 0x93;
20const MARKER_EOC: u8 = 0xD9;
21
22/// Parsed JPEG 2000 codestream metadata from the main header.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct J2kCodestreamHeaderMetadata {
25    /// Reference-grid image dimensions derived from SIZ.
26    pub dimensions: (u32, u32),
27    /// Number of codestream components.
28    pub components: u16,
29    /// Maximum component precision in bits.
30    pub bit_depth: u8,
31    /// Reference tile width and height.
32    pub tile_size: (u32, u32),
33    /// Number of reference tiles horizontally and vertically.
34    pub tile_count: (u32, u32),
35    /// Per-component SIZ precision and sampling metadata.
36    pub component_info: Vec<J2kCodestreamComponentHeader>,
37    /// Number of resolution levels from COD.
38    pub resolution_levels: u8,
39    /// Whether COD enables a multi-component transform.
40    pub has_mct: bool,
41    /// Whether COD selects the reversible 5/3 transform.
42    pub reversible: bool,
43    /// Whether the codestream advertises high-throughput block coding.
44    pub high_throughput: bool,
45}
46
47/// Parsed SIZ component metadata.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct J2kCodestreamComponentHeader {
50    /// Component precision in bits.
51    pub bit_depth: u8,
52    /// Whether component samples are signed.
53    pub signed: bool,
54    /// Horizontal SIZ sampling factor (`XRsiz`).
55    pub x_rsiz: u8,
56    /// Vertical SIZ sampling factor (`YRsiz`).
57    pub y_rsiz: u8,
58}
59
60/// Error returned by [`inspect_j2k_codestream_header`].
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum J2kCodestreamHeaderError {
64    /// Input was shorter than the required prefix.
65    TooShort {
66        /// Required byte count.
67        need: usize,
68        /// Available byte count.
69        have: usize,
70    },
71    /// Input ended while reading a marker or marker segment.
72    TruncatedAt {
73        /// Byte offset where the truncated segment begins.
74        offset: usize,
75        /// Segment being read.
76        segment: &'static str,
77    },
78    /// A codestream marker did not start with `0xFF`.
79    InvalidMarker {
80        /// Byte offset of the invalid marker.
81        offset: usize,
82        /// Byte found where the marker code was expected.
83        marker: u8,
84    },
85    /// A required codestream marker was absent.
86    MissingRequiredMarker {
87        /// Missing marker name.
88        marker: &'static str,
89    },
90    /// A generic marker segment was malformed.
91    InvalidSegment {
92        /// Byte offset of the segment length.
93        offset: usize,
94        /// Description of the invalid segment.
95        what: &'static str,
96    },
97    /// The SIZ marker segment was malformed or unsupported.
98    InvalidSiz {
99        /// Description of the invalid SIZ segment.
100        what: &'static str,
101    },
102    /// The COD marker segment was malformed or unsupported.
103    InvalidCod {
104        /// Description of the invalid COD segment.
105        what: &'static str,
106    },
107    /// The CAP marker segment was malformed or internally inconsistent.
108    InvalidCap {
109        /// Description of the invalid CAP segment.
110        what: &'static str,
111    },
112    /// The CPF marker segment was malformed.
113    InvalidCpf {
114        /// Description of the invalid CPF segment.
115        what: &'static str,
116    },
117    /// The header is valid, but outside the public inspection contract.
118    Unsupported {
119        /// Description of the unsupported feature.
120        what: &'static str,
121    },
122    /// The host allocator rejected bounded per-component header metadata.
123    HostAllocationFailed {
124        /// Requested metadata byte count.
125        bytes: usize,
126    },
127}
128
129impl fmt::Display for J2kCodestreamHeaderError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            Self::TooShort { need, have } => {
133                write!(f, "input too short: need {need} bytes, have {have}")
134            }
135            Self::TruncatedAt { offset, segment } => {
136                write!(f, "truncated {segment} at offset {offset}")
137            }
138            Self::InvalidMarker { offset, marker } => {
139                write!(
140                    f,
141                    "invalid codestream marker FF{marker:02X} at offset {offset}"
142                )
143            }
144            Self::MissingRequiredMarker { marker } => {
145                write!(f, "missing required codestream marker {marker}")
146            }
147            Self::InvalidSegment { what, .. } => write!(f, "invalid marker segment: {what}"),
148            Self::InvalidSiz { what } => write!(f, "invalid SIZ segment: {what}"),
149            Self::InvalidCod { what } => write!(f, "invalid COD segment: {what}"),
150            Self::InvalidCap { what } => write!(f, "invalid CAP segment: {what}"),
151            Self::InvalidCpf { what } => write!(f, "invalid CPF segment: {what}"),
152            Self::Unsupported { what } => write!(f, "unsupported codestream header: {what}"),
153            Self::HostAllocationFailed { bytes } => {
154                write!(f, "codestream header allocation failed for {bytes} bytes")
155            }
156        }
157    }
158}
159
160impl core::error::Error for J2kCodestreamHeaderError {}
161
162/// Inspect a raw JPEG 2000 codestream main header without decoding tile data.
163///
164/// This helper reads SIZ/COD metadata and stops at SOT/SOD/EOC. It intentionally
165/// does not require full decode headers such as QCD, so callers can inspect the
166/// same lightweight codestreams that later decode construction may reject.
167///
168/// # Errors
169///
170/// Returns an error when required markers are missing, truncated, or inconsistent.
171pub fn inspect_j2k_codestream_header(
172    input: &[u8],
173) -> Result<J2kCodestreamHeaderMetadata, J2kCodestreamHeaderError> {
174    inspect_main_header(input).map(|inspected| inspected.metadata)
175}
176
177/// Inspect Part 15 CAP/CPF and main COD style facts without decoding tile data.
178///
179/// Returns `Ok(None)` for a valid codestream that does not advertise `Pcap15`.
180/// The existing [`inspect_j2k_codestream_header`] metadata layout remains
181/// unchanged; this expert API carries the additional Part 15 facts.
182///
183/// # Errors
184///
185/// Returns an error when required main-header markers or CAP/CPF values are
186/// missing, truncated, duplicated, reserved, or internally inconsistent.
187pub fn inspect_htj2k_capabilities(
188    input: &[u8],
189) -> Result<Option<Htj2kCapabilities>, J2kCodestreamHeaderError> {
190    inspect_main_header(input).map(|inspected| inspected.htj2k)
191}
192
193struct InspectedMainHeader {
194    metadata: J2kCodestreamHeaderMetadata,
195    htj2k: Option<Htj2kCapabilities>,
196}
197
198fn inspect_main_header(input: &[u8]) -> Result<InspectedMainHeader, J2kCodestreamHeaderError> {
199    if input.len() < 2 {
200        return Err(J2kCodestreamHeaderError::TooShort {
201            need: 2,
202            have: input.len(),
203        });
204    }
205    if !looks_like_j2k_codestream(input) {
206        return Err(J2kCodestreamHeaderError::InvalidMarker {
207            offset: 0,
208            marker: input[1],
209        });
210    }
211
212    let mut offset = 2usize;
213    let mut siz = None;
214    let mut cod = None;
215    let mut capabilities = CapabilityMarkerState::default();
216    let mut terminated = false;
217
218    while offset < input.len() {
219        let marker = read_marker(input, &mut offset)?;
220        match marker {
221            MARKER_SOT | MARKER_SOD | MARKER_EOC => {
222                terminated = true;
223                break;
224            }
225            MARKER_SIZ => {
226                let payload = read_segment_payload(input, &mut offset, "SIZ")?;
227                siz = Some(parse_siz(payload)?);
228            }
229            MARKER_COD => {
230                let payload = read_segment_payload(input, &mut offset, "COD")?;
231                cod = Some(parse_cod(payload)?);
232            }
233            MARKER_CAP => {
234                let payload = read_segment_payload(input, &mut offset, "CAP")?;
235                capabilities
236                    .record_cap(payload)
237                    .map_err(map_capability_error)?;
238            }
239            MARKER_CPF => {
240                let payload = read_segment_payload(input, &mut offset, "CPF")?;
241                capabilities
242                    .record_cpf(payload)
243                    .map_err(map_capability_error)?;
244            }
245            0x30..=0x3F => {}
246            _ => {
247                let _ = read_segment_payload(input, &mut offset, "segment")?;
248            }
249        }
250    }
251
252    if !terminated {
253        return Err(J2kCodestreamHeaderError::TruncatedAt {
254            offset,
255            segment: "main header terminator",
256        });
257    }
258
259    let siz = siz.ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "SIZ" })?;
260    capabilities
261        .validate_rsiz(siz.rsiz)
262        .map_err(map_capability_error)?;
263    let high_throughput_cap = capabilities.high_throughput();
264    let cod = cod
265        .ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "COD" })?
266        .with_high_throughput_cap(high_throughput_cap);
267    let htj2k = capabilities
268        .to_public(
269            cod.quality_layers,
270            cod.default_ht_block_coding,
271            cod.default_mixed_block_coding,
272        )
273        .map_err(map_capability_error)?;
274
275    Ok(InspectedMainHeader {
276        metadata: J2kCodestreamHeaderMetadata {
277            dimensions: siz.dimensions,
278            components: siz.components,
279            bit_depth: siz.bit_depth,
280            tile_size: siz.tile_size,
281            tile_count: siz.tile_count,
282            component_info: siz.component_info,
283            resolution_levels: cod.resolution_levels,
284            has_mct: cod.has_mct,
285            reversible: cod.reversible,
286            high_throughput: cod.high_throughput,
287        },
288        htj2k,
289    })
290}
291
292fn map_capability_error(error: CapabilityMarkerError) -> J2kCodestreamHeaderError {
293    match error {
294        CapabilityMarkerError::Cap(what) => J2kCodestreamHeaderError::InvalidCap { what },
295        CapabilityMarkerError::Cpf(what) => J2kCodestreamHeaderError::InvalidCpf { what },
296        CapabilityMarkerError::Allocation { bytes } => {
297            J2kCodestreamHeaderError::HostAllocationFailed { bytes }
298        }
299    }
300}
301
302/// Return whether bytes start with the raw JPEG 2000 SOC marker.
303#[must_use]
304pub fn looks_like_j2k_codestream(input: &[u8]) -> bool {
305    input.len() >= 2 && input[0] == 0xFF && input[1] == MARKER_SOC
306}
307
308#[derive(Debug, Clone)]
309struct ParsedSiz {
310    rsiz: u16,
311    dimensions: (u32, u32),
312    components: u16,
313    bit_depth: u8,
314    tile_size: (u32, u32),
315    tile_count: (u32, u32),
316    component_info: Vec<J2kCodestreamComponentHeader>,
317}
318
319#[derive(Debug, Clone, Copy)]
320#[expect(
321    clippy::struct_excessive_bools,
322    reason = "the flags preserve independent COD and CAP facts for header inspection"
323)]
324struct ParsedCod {
325    resolution_levels: u8,
326    quality_layers: u8,
327    has_mct: bool,
328    reversible: bool,
329    high_throughput: bool,
330    default_ht_block_coding: bool,
331    default_mixed_block_coding: bool,
332}
333
334impl ParsedCod {
335    const fn with_high_throughput_cap(mut self, high_throughput_cap: bool) -> Self {
336        self.high_throughput |= high_throughput_cap;
337        self
338    }
339}
340
341fn read_marker(input: &[u8], offset: &mut usize) -> Result<u8, J2kCodestreamHeaderError> {
342    if *offset + 2 > input.len() {
343        return Err(J2kCodestreamHeaderError::TruncatedAt {
344            offset: *offset,
345            segment: "marker",
346        });
347    }
348    if input[*offset] != 0xFF {
349        return Err(J2kCodestreamHeaderError::InvalidMarker {
350            offset: *offset,
351            marker: input[*offset],
352        });
353    }
354    let marker = input[*offset + 1];
355    *offset += 2;
356    Ok(marker)
357}
358
359fn read_segment_payload<'a>(
360    input: &'a [u8],
361    offset: &mut usize,
362    segment: &'static str,
363) -> Result<&'a [u8], J2kCodestreamHeaderError> {
364    if *offset + 2 > input.len() {
365        return Err(J2kCodestreamHeaderError::TruncatedAt {
366            offset: *offset,
367            segment,
368        });
369    }
370    let length = u16::from_be_bytes([input[*offset], input[*offset + 1]]) as usize;
371    if length < 2 {
372        return Err(J2kCodestreamHeaderError::InvalidSegment {
373            offset: *offset,
374            what: "segment length smaller than header",
375        });
376    }
377    let start = *offset + 2;
378    let end = *offset + length;
379    if end > input.len() {
380        return Err(J2kCodestreamHeaderError::TruncatedAt {
381            offset: *offset,
382            segment,
383        });
384    }
385    *offset = end;
386    Ok(&input[start..end])
387}
388
389struct SizGeometry {
390    x_size: u32,
391    y_size: u32,
392    x_origin: u32,
393    y_origin: u32,
394    tile_width: u32,
395    tile_height: u32,
396    tile_x_origin: u32,
397    tile_y_origin: u32,
398}
399
400type SizDimensionsAndTileCount = ((u32, u32), (u32, u32));
401
402fn parse_siz(payload: &[u8]) -> Result<ParsedSiz, J2kCodestreamHeaderError> {
403    if payload.len() < 36 {
404        return Err(J2kCodestreamHeaderError::InvalidSiz {
405            what: "payload shorter than fixed SIZ header",
406        });
407    }
408    let geometry = SizGeometry {
409        x_size: read_u32(payload, 2),
410        y_size: read_u32(payload, 6),
411        x_origin: read_u32(payload, 10),
412        y_origin: read_u32(payload, 14),
413        tile_width: read_u32(payload, 18),
414        tile_height: read_u32(payload, 22),
415        tile_x_origin: read_u32(payload, 26),
416        tile_y_origin: read_u32(payload, 30),
417    };
418    let component_count = read_u16(payload, 34);
419
420    let required_len = usize::from(component_count)
421        .checked_mul(3)
422        .and_then(|component_bytes| 36usize.checked_add(component_bytes))
423        .ok_or(J2kCodestreamHeaderError::InvalidSiz {
424            what: "component descriptor length overflows",
425        })?;
426    if payload.len() < required_len {
427        return Err(J2kCodestreamHeaderError::InvalidSiz {
428            what: "component descriptors truncated",
429        });
430    }
431    let ((width, height), (tiles_x, tiles_y)) = validate_siz_geometry(&geometry, component_count)?;
432
433    let mut bit_depth = 0u8;
434    let component_len = usize::from(component_count);
435    let component_bytes = component_len
436        .checked_mul(core::mem::size_of::<J2kCodestreamComponentHeader>())
437        .ok_or(J2kCodestreamHeaderError::HostAllocationFailed { bytes: usize::MAX })?;
438    let mut component_info = Vec::new();
439    component_info
440        .try_reserve_exact(component_len)
441        .map_err(|_| J2kCodestreamHeaderError::HostAllocationFailed {
442            bytes: component_bytes,
443        })?;
444    for idx in 0..component_len {
445        let ssiz = payload[36 + idx * 3];
446        let precision = (ssiz & 0x7F) + 1;
447        let x_rsiz = payload[36 + idx * 3 + 1];
448        let y_rsiz = payload[36 + idx * 3 + 2];
449        if x_rsiz == 0 || y_rsiz == 0 {
450            return Err(J2kCodestreamHeaderError::InvalidSiz {
451                what: "component sampling factors must be non-zero",
452            });
453        }
454        bit_depth = bit_depth.max(precision);
455        component_info.push(J2kCodestreamComponentHeader {
456            bit_depth: precision,
457            signed: ssiz & 0x80 != 0,
458            x_rsiz,
459            y_rsiz,
460        });
461    }
462
463    Ok(ParsedSiz {
464        rsiz: read_u16(payload, 0),
465        dimensions: (width, height),
466        components: component_count,
467        bit_depth,
468        tile_size: (geometry.tile_width, geometry.tile_height),
469        tile_count: (tiles_x, tiles_y),
470        component_info,
471    })
472}
473
474fn validate_siz_geometry(
475    geometry: &SizGeometry,
476    component_count: u16,
477) -> Result<SizDimensionsAndTileCount, J2kCodestreamHeaderError> {
478    if component_count == 0 {
479        return Err(J2kCodestreamHeaderError::InvalidSiz {
480            what: "component count must be non-zero",
481        });
482    }
483    if component_count > MAX_J2K_SPEC_COMPONENTS {
484        return Err(J2kCodestreamHeaderError::InvalidSiz {
485            what: "component count exceeds JPEG 2000 limit",
486        });
487    }
488    if geometry.x_size <= geometry.x_origin || geometry.y_size <= geometry.y_origin {
489        return Err(J2kCodestreamHeaderError::InvalidSiz {
490            what: "image origin must be smaller than image size",
491        });
492    }
493    if geometry.tile_width == 0 || geometry.tile_height == 0 {
494        return Err(J2kCodestreamHeaderError::InvalidSiz {
495            what: "tile size must be non-zero",
496        });
497    }
498    if geometry.tile_x_origin >= geometry.x_size || geometry.tile_y_origin >= geometry.y_size {
499        return Err(J2kCodestreamHeaderError::InvalidSiz {
500            what: "tile origin must be within image bounds",
501        });
502    }
503    if geometry.tile_x_origin > geometry.x_origin || geometry.tile_y_origin > geometry.y_origin {
504        return Err(J2kCodestreamHeaderError::InvalidSiz {
505            what: "tile origin must not exceed image origin",
506        });
507    }
508    if geometry
509        .tile_x_origin
510        .checked_add(geometry.tile_width)
511        .ok_or(J2kCodestreamHeaderError::InvalidSiz {
512            what: "tile extent overflows",
513        })?
514        <= geometry.x_origin
515        || geometry
516            .tile_y_origin
517            .checked_add(geometry.tile_height)
518            .ok_or(J2kCodestreamHeaderError::InvalidSiz {
519                what: "tile extent overflows",
520            })?
521            <= geometry.y_origin
522    {
523        return Err(J2kCodestreamHeaderError::InvalidSiz {
524            what: "first tile must overlap image area",
525        });
526    }
527
528    let width = geometry.x_size - geometry.x_origin;
529    let height = geometry.y_size - geometry.y_origin;
530    if width > MAX_J2K_IMAGE_DIMENSION || height > MAX_J2K_IMAGE_DIMENSION {
531        return Err(J2kCodestreamHeaderError::InvalidSiz {
532            what: "image dimensions exceed JPEG 2000 inspect limit",
533        });
534    }
535    let tiles_x = (geometry.x_size - geometry.tile_x_origin).div_ceil(geometry.tile_width);
536    let tiles_y = (geometry.y_size - geometry.tile_y_origin).div_ceil(geometry.tile_height);
537    let tile_count = u64::from(tiles_x) * u64::from(tiles_y);
538    if tile_count > MAX_J2K_TILE_COUNT {
539        return Err(J2kCodestreamHeaderError::InvalidSiz {
540            what: "image has too many tiles",
541        });
542    }
543    Ok(((width, height), (tiles_x, tiles_y)))
544}
545
546fn parse_cod(payload: &[u8]) -> Result<ParsedCod, J2kCodestreamHeaderError> {
547    if payload.len() < 10 {
548        return Err(J2kCodestreamHeaderError::InvalidCod {
549            what: "payload shorter than fixed COD header",
550        });
551    }
552    let default_ht_block_coding = payload[8] & 0x40 != 0;
553    let default_mixed_block_coding = payload[8] & 0x80 != 0;
554    let quality_layers = read_u16(payload, 2);
555    if quality_layers == 0 || quality_layers > u16::from(crate::j2c::codestream::MAX_LAYER_COUNT) {
556        return Err(J2kCodestreamHeaderError::InvalidCod {
557            what: "quality-layer count is outside the supported range",
558        });
559    }
560    Ok(ParsedCod {
561        resolution_levels: payload[5].saturating_add(1),
562        quality_layers: u8::try_from(quality_layers).map_err(|_| {
563            J2kCodestreamHeaderError::InvalidCod {
564                what: "quality-layer count does not fit the inspection model",
565            }
566        })?,
567        has_mct: payload[4] != 0,
568        reversible: payload[9] == 1,
569        high_throughput: default_ht_block_coding,
570        default_ht_block_coding,
571        default_mixed_block_coding,
572    })
573}
574
575fn read_u16(bytes: &[u8], offset: usize) -> u16 {
576    u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
577}
578
579fn read_u32(bytes: &[u8], offset: usize) -> u32 {
580    u32::from_be_bytes([
581        bytes[offset],
582        bytes[offset + 1],
583        bytes[offset + 2],
584        bytes[offset + 3],
585    ])
586}
587
588#[cfg(test)]
589mod tests {
590    use super::{inspect_j2k_codestream_header, J2kCodestreamHeaderError};
591    use alloc::{vec, vec::Vec};
592
593    #[test]
594    fn inspect_j2k_codestream_header_accepts_minimal_main_header() {
595        let header = inspect_j2k_codestream_header(&minimal_codestream()).expect("header");
596
597        assert_eq!(header.dimensions, (128, 64));
598        assert_eq!(header.components, 3);
599        assert_eq!(header.bit_depth, 8);
600        assert_eq!(header.tile_size, (64, 64));
601        assert_eq!(header.tile_count, (2, 1));
602        assert_eq!(header.resolution_levels, 6);
603        assert!(header.reversible);
604    }
605
606    #[test]
607    fn inspect_skips_parameterless_reserved_main_header_markers() {
608        let mut bytes = minimal_codestream();
609        let sot = bytes
610            .windows(2)
611            .position(|marker| marker == [0xFF, 0x90])
612            .expect("SOT marker");
613        bytes.splice(sot..sot, [0xFF, 0x30]);
614
615        let header = inspect_j2k_codestream_header(&bytes).expect("header with reserved marker");
616
617        assert_eq!(header.dimensions, (128, 64));
618    }
619
620    #[test]
621    fn inspect_rejects_zero_component_sampling() {
622        let mut bytes = minimal_codestream();
623        rewrite_component_sampling(&mut bytes, 0, 0, 1);
624
625        let err = inspect_j2k_codestream_header(&bytes).expect_err("zero sampling must reject");
626
627        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
628    }
629
630    #[test]
631    fn inspect_rejects_oversized_dimensions() {
632        let mut bytes = minimal_codestream();
633        rewrite_siz_u32(&mut bytes, 2, 60_001);
634
635        let err = inspect_j2k_codestream_header(&bytes).expect_err("oversized width must reject");
636
637        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
638    }
639
640    #[test]
641    fn inspect_rejects_tile_origin_after_image_origin() {
642        let mut bytes = minimal_codestream();
643        rewrite_siz_u32(&mut bytes, 26, 1);
644
645        let err = inspect_j2k_codestream_header(&bytes).expect_err("bad tile origin must reject");
646
647        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
648    }
649
650    #[test]
651    fn inspect_rejects_tile_extent_overflow() {
652        let mut bytes = minimal_codestream();
653        rewrite_siz_u32(&mut bytes, 2, u32::MAX);
654        rewrite_siz_u32(&mut bytes, 10, u32::MAX - 1);
655        rewrite_siz_u32(&mut bytes, 18, 10);
656        rewrite_siz_u32(&mut bytes, 26, u32::MAX - 2);
657
658        let err = inspect_j2k_codestream_header(&bytes).expect_err("overflow must reject");
659
660        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
661    }
662
663    #[test]
664    fn inspect_rejects_excessive_tile_count() {
665        let mut bytes = minimal_codestream();
666        rewrite_siz_u32(&mut bytes, 2, 257);
667        rewrite_siz_u32(&mut bytes, 6, 257);
668        rewrite_siz_u32(&mut bytes, 18, 1);
669        rewrite_siz_u32(&mut bytes, 22, 1);
670
671        let err = inspect_j2k_codestream_header(&bytes).expect_err("tile count must reject");
672
673        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
674    }
675
676    #[test]
677    fn inspect_accepts_legal_38_bit_component_metadata() {
678        let mut bytes = minimal_codestream();
679        rewrite_component_descriptor(&mut bytes, 0, 0x25);
680        rewrite_component_descriptor(&mut bytes, 1, 0x80 | 0x25);
681
682        let header = inspect_j2k_codestream_header(&bytes).expect("legal 38-bit SIZ inspect");
683
684        assert_eq!(header.bit_depth, 38);
685        assert_eq!(header.component_info[0].bit_depth, 38);
686        assert!(!header.component_info[0].signed);
687        assert_eq!(header.component_info[1].bit_depth, 38);
688        assert!(header.component_info[1].signed);
689    }
690
691    fn minimal_codestream() -> Vec<u8> {
692        let mut bytes = vec![0xFF, 0x4F];
693        let mut siz = Vec::new();
694        push_u16(&mut siz, 0);
695        push_u32(&mut siz, 128);
696        push_u32(&mut siz, 64);
697        push_u32(&mut siz, 0);
698        push_u32(&mut siz, 0);
699        push_u32(&mut siz, 64);
700        push_u32(&mut siz, 64);
701        push_u32(&mut siz, 0);
702        push_u32(&mut siz, 0);
703        push_u16(&mut siz, 3);
704        for _ in 0..3 {
705            siz.extend_from_slice(&[0x07, 0x01, 0x01]);
706        }
707        bytes.extend_from_slice(&[0xFF, 0x51]);
708        push_u16(
709            &mut bytes,
710            u16::try_from(siz.len() + 2).expect("test SIZ segment length fits u16"),
711        );
712        bytes.extend_from_slice(&siz);
713
714        let cod = [0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x04, 0x04, 0x00, 0x01];
715        bytes.extend_from_slice(&[0xFF, 0x52]);
716        push_u16(
717            &mut bytes,
718            u16::try_from(cod.len() + 2).expect("test COD segment length fits u16"),
719        );
720        bytes.extend_from_slice(&cod);
721        bytes.extend_from_slice(&[0xFF, 0x90, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
722        bytes
723    }
724
725    fn push_u16(out: &mut Vec<u8>, value: u16) {
726        out.extend_from_slice(&value.to_be_bytes());
727    }
728
729    fn push_u32(out: &mut Vec<u8>, value: u32) {
730        out.extend_from_slice(&value.to_be_bytes());
731    }
732
733    fn rewrite_siz_u32(bytes: &mut [u8], payload_offset: usize, value: u32) {
734        let siz = bytes
735            .windows(2)
736            .position(|marker| marker == [0xFF, 0x51])
737            .expect("SIZ marker");
738        let offset = siz + 4 + payload_offset;
739        bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
740    }
741
742    fn rewrite_component_sampling(bytes: &mut [u8], component: usize, x_rsiz: u8, y_rsiz: u8) {
743        let siz = bytes
744            .windows(2)
745            .position(|marker| marker == [0xFF, 0x51])
746            .expect("SIZ marker");
747        let component_offset = siz + 40 + component * 3;
748        bytes[component_offset + 1] = x_rsiz;
749        bytes[component_offset + 2] = y_rsiz;
750    }
751
752    fn rewrite_component_descriptor(bytes: &mut [u8], component: usize, descriptor: u8) {
753        let siz = bytes
754            .windows(2)
755            .position(|marker| marker == [0xFF, 0x51])
756            .expect("SIZ marker");
757        bytes[siz + 40 + component * 3] = descriptor;
758    }
759}