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_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 u64::from(geometry.tile_x_origin) + u64::from(geometry.tile_width)
509        <= u64::from(geometry.x_origin)
510        || u64::from(geometry.tile_y_origin) + u64::from(geometry.tile_height)
511            <= u64::from(geometry.y_origin)
512    {
513        return Err(J2kCodestreamHeaderError::InvalidSiz {
514            what: "first tile must overlap image area",
515        });
516    }
517
518    let width = geometry.x_size - geometry.x_origin;
519    let height = geometry.y_size - geometry.y_origin;
520    let tiles_x = (geometry.x_size - geometry.tile_x_origin).div_ceil(geometry.tile_width);
521    let tiles_y = (geometry.y_size - geometry.tile_y_origin).div_ceil(geometry.tile_height);
522    let tile_count = u64::from(tiles_x) * u64::from(tiles_y);
523    if tile_count > MAX_J2K_TILE_COUNT {
524        return Err(J2kCodestreamHeaderError::InvalidSiz {
525            what: "image has too many tiles",
526        });
527    }
528    Ok(((width, height), (tiles_x, tiles_y)))
529}
530
531fn parse_cod(payload: &[u8]) -> Result<ParsedCod, J2kCodestreamHeaderError> {
532    if payload.len() < 10 {
533        return Err(J2kCodestreamHeaderError::InvalidCod {
534            what: "payload shorter than fixed COD header",
535        });
536    }
537    let default_ht_block_coding = payload[8] & 0x40 != 0;
538    let default_mixed_block_coding = payload[8] & 0x80 != 0;
539    let quality_layers = read_u16(payload, 2);
540    if quality_layers == 0 || quality_layers > u16::from(crate::j2c::codestream::MAX_LAYER_COUNT) {
541        return Err(J2kCodestreamHeaderError::InvalidCod {
542            what: "quality-layer count is outside the supported range",
543        });
544    }
545    Ok(ParsedCod {
546        resolution_levels: payload[5].saturating_add(1),
547        quality_layers: u8::try_from(quality_layers).map_err(|_| {
548            J2kCodestreamHeaderError::InvalidCod {
549                what: "quality-layer count does not fit the inspection model",
550            }
551        })?,
552        has_mct: payload[4] != 0,
553        reversible: payload[9] == 1,
554        high_throughput: default_ht_block_coding,
555        default_ht_block_coding,
556        default_mixed_block_coding,
557    })
558}
559
560fn read_u16(bytes: &[u8], offset: usize) -> u16 {
561    u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
562}
563
564fn read_u32(bytes: &[u8], offset: usize) -> u32 {
565    u32::from_be_bytes([
566        bytes[offset],
567        bytes[offset + 1],
568        bytes[offset + 2],
569        bytes[offset + 3],
570    ])
571}
572
573#[cfg(test)]
574mod tests {
575    use super::{inspect_j2k_codestream_header, J2kCodestreamHeaderError};
576    use alloc::{vec, vec::Vec};
577
578    #[test]
579    fn inspect_j2k_codestream_header_accepts_minimal_main_header() {
580        let header = inspect_j2k_codestream_header(&minimal_codestream()).expect("header");
581
582        assert_eq!(header.dimensions, (128, 64));
583        assert_eq!(header.components, 3);
584        assert_eq!(header.bit_depth, 8);
585        assert_eq!(header.tile_size, (64, 64));
586        assert_eq!(header.tile_count, (2, 1));
587        assert_eq!(header.resolution_levels, 6);
588        assert!(header.reversible);
589    }
590
591    #[test]
592    fn inspect_skips_parameterless_reserved_main_header_markers() {
593        let mut bytes = minimal_codestream();
594        let sot = bytes
595            .windows(2)
596            .position(|marker| marker == [0xFF, 0x90])
597            .expect("SOT marker");
598        bytes.splice(sot..sot, [0xFF, 0x30]);
599
600        let header = inspect_j2k_codestream_header(&bytes).expect("header with reserved marker");
601
602        assert_eq!(header.dimensions, (128, 64));
603    }
604
605    #[test]
606    fn inspect_rejects_zero_component_sampling() {
607        let mut bytes = minimal_codestream();
608        rewrite_component_sampling(&mut bytes, 0, 0, 1);
609
610        let err = inspect_j2k_codestream_header(&bytes).expect_err("zero sampling must reject");
611
612        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
613    }
614
615    #[test]
616    fn inspect_accepts_large_dimensions() {
617        let mut bytes = minimal_codestream();
618        rewrite_siz_u32(&mut bytes, 2, 60_001);
619
620        let header = inspect_j2k_codestream_header(&bytes).expect("large width is valid");
621        assert_eq!(header.dimensions, (60_001, 64));
622    }
623
624    #[test]
625    fn inspect_rejects_tile_origin_after_image_origin() {
626        let mut bytes = minimal_codestream();
627        rewrite_siz_u32(&mut bytes, 26, 1);
628
629        let err = inspect_j2k_codestream_header(&bytes).expect_err("bad tile origin must reject");
630
631        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
632    }
633
634    #[test]
635    fn inspect_accepts_tile_extending_beyond_reference_grid() {
636        let mut bytes = minimal_codestream();
637        rewrite_siz_u32(&mut bytes, 2, u32::MAX);
638        rewrite_siz_u32(&mut bytes, 10, u32::MAX - 1);
639        rewrite_siz_u32(&mut bytes, 18, 10);
640        rewrite_siz_u32(&mut bytes, 26, u32::MAX - 2);
641
642        let header = inspect_j2k_codestream_header(&bytes).expect("tile is clipped to image");
643        assert_eq!(header.dimensions, (1, 64));
644        assert_eq!(header.tile_count, (1, 1));
645    }
646
647    #[test]
648    fn inspect_rejects_excessive_tile_count() {
649        let mut bytes = minimal_codestream();
650        rewrite_siz_u32(&mut bytes, 2, 257);
651        rewrite_siz_u32(&mut bytes, 6, 257);
652        rewrite_siz_u32(&mut bytes, 18, 1);
653        rewrite_siz_u32(&mut bytes, 22, 1);
654
655        let err = inspect_j2k_codestream_header(&bytes).expect_err("tile count must reject");
656
657        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
658    }
659
660    #[test]
661    fn inspect_accepts_legal_38_bit_component_metadata() {
662        let mut bytes = minimal_codestream();
663        rewrite_component_descriptor(&mut bytes, 0, 0x25);
664        rewrite_component_descriptor(&mut bytes, 1, 0x80 | 0x25);
665
666        let header = inspect_j2k_codestream_header(&bytes).expect("legal 38-bit SIZ inspect");
667
668        assert_eq!(header.bit_depth, 38);
669        assert_eq!(header.component_info[0].bit_depth, 38);
670        assert!(!header.component_info[0].signed);
671        assert_eq!(header.component_info[1].bit_depth, 38);
672        assert!(header.component_info[1].signed);
673    }
674
675    fn minimal_codestream() -> Vec<u8> {
676        let mut bytes = vec![0xFF, 0x4F];
677        let mut siz = Vec::new();
678        push_u16(&mut siz, 0);
679        push_u32(&mut siz, 128);
680        push_u32(&mut siz, 64);
681        push_u32(&mut siz, 0);
682        push_u32(&mut siz, 0);
683        push_u32(&mut siz, 64);
684        push_u32(&mut siz, 64);
685        push_u32(&mut siz, 0);
686        push_u32(&mut siz, 0);
687        push_u16(&mut siz, 3);
688        for _ in 0..3 {
689            siz.extend_from_slice(&[0x07, 0x01, 0x01]);
690        }
691        bytes.extend_from_slice(&[0xFF, 0x51]);
692        push_u16(
693            &mut bytes,
694            u16::try_from(siz.len() + 2).expect("test SIZ segment length fits u16"),
695        );
696        bytes.extend_from_slice(&siz);
697
698        let cod = [0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x04, 0x04, 0x00, 0x01];
699        bytes.extend_from_slice(&[0xFF, 0x52]);
700        push_u16(
701            &mut bytes,
702            u16::try_from(cod.len() + 2).expect("test COD segment length fits u16"),
703        );
704        bytes.extend_from_slice(&cod);
705        bytes.extend_from_slice(&[0xFF, 0x90, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
706        bytes
707    }
708
709    fn push_u16(out: &mut Vec<u8>, value: u16) {
710        out.extend_from_slice(&value.to_be_bytes());
711    }
712
713    fn push_u32(out: &mut Vec<u8>, value: u32) {
714        out.extend_from_slice(&value.to_be_bytes());
715    }
716
717    fn rewrite_siz_u32(bytes: &mut [u8], payload_offset: usize, value: u32) {
718        let siz = bytes
719            .windows(2)
720            .position(|marker| marker == [0xFF, 0x51])
721            .expect("SIZ marker");
722        let offset = siz + 4 + payload_offset;
723        bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
724    }
725
726    fn rewrite_component_sampling(bytes: &mut [u8], component: usize, x_rsiz: u8, y_rsiz: u8) {
727        let siz = bytes
728            .windows(2)
729            .position(|marker| marker == [0xFF, 0x51])
730            .expect("SIZ marker");
731        let component_offset = siz + 40 + component * 3;
732        bytes[component_offset + 1] = x_rsiz;
733        bytes[component_offset + 2] = y_rsiz;
734    }
735
736    fn rewrite_component_descriptor(bytes: &mut [u8], component: usize, descriptor: u8) {
737        let siz = bytes
738            .windows(2)
739            .position(|marker| marker == [0xFF, 0x51])
740            .expect("SIZ marker");
741        bytes[siz + 40 + component * 3] = descriptor;
742    }
743}