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