Skip to main content

j2k_native/
inspect.rs

1// SPDX-License-Identifier: 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}
111
112impl fmt::Display for J2kCodestreamHeaderError {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::TooShort { need, have } => {
116                write!(f, "input too short: need {need} bytes, have {have}")
117            }
118            Self::TruncatedAt { offset, segment } => {
119                write!(f, "truncated {segment} at offset {offset}")
120            }
121            Self::InvalidMarker { offset, marker } => {
122                write!(
123                    f,
124                    "invalid codestream marker FF{marker:02X} at offset {offset}"
125                )
126            }
127            Self::MissingRequiredMarker { marker } => {
128                write!(f, "missing required codestream marker {marker}")
129            }
130            Self::InvalidSegment { what, .. } => write!(f, "invalid marker segment: {what}"),
131            Self::InvalidSiz { what } => write!(f, "invalid SIZ segment: {what}"),
132            Self::InvalidCod { what } => write!(f, "invalid COD segment: {what}"),
133            Self::Unsupported { what } => write!(f, "unsupported codestream header: {what}"),
134        }
135    }
136}
137
138/// Inspect a raw JPEG 2000 codestream main header without decoding tile data.
139///
140/// This helper reads SIZ/COD metadata and stops at SOT/SOD/EOC. It intentionally
141/// does not require full decode headers such as QCD, so callers can inspect the
142/// same lightweight codestreams that later decode construction may reject.
143pub fn inspect_j2k_codestream_header(
144    input: &[u8],
145) -> Result<J2kCodestreamHeaderMetadata, J2kCodestreamHeaderError> {
146    if input.len() < 2 {
147        return Err(J2kCodestreamHeaderError::TooShort {
148            need: 2,
149            have: input.len(),
150        });
151    }
152    if !looks_like_j2k_codestream(input) {
153        return Err(J2kCodestreamHeaderError::InvalidMarker {
154            offset: 0,
155            marker: input[1],
156        });
157    }
158
159    let mut offset = 2usize;
160    let mut siz = None;
161    let mut cod = None;
162    let mut high_throughput_cap = false;
163    let mut terminated = false;
164
165    while offset < input.len() {
166        let marker = read_marker(input, &mut offset)?;
167        match marker {
168            MARKER_SOT | MARKER_SOD | MARKER_EOC => {
169                terminated = true;
170                break;
171            }
172            MARKER_SIZ => {
173                let payload = read_segment_payload(input, &mut offset, "SIZ")?;
174                siz = Some(parse_siz(payload)?);
175            }
176            MARKER_COD => {
177                let payload = read_segment_payload(input, &mut offset, "COD")?;
178                cod = Some(parse_cod(payload)?);
179            }
180            MARKER_CAP => {
181                let _ = read_segment_payload(input, &mut offset, "CAP")?;
182                high_throughput_cap = true;
183            }
184            _ => {
185                let _ = read_segment_payload(input, &mut offset, "segment")?;
186            }
187        }
188    }
189
190    if !terminated {
191        return Err(J2kCodestreamHeaderError::TruncatedAt {
192            offset,
193            segment: "main header terminator",
194        });
195    }
196
197    let siz = siz.ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "SIZ" })?;
198    let cod = cod
199        .ok_or(J2kCodestreamHeaderError::MissingRequiredMarker { marker: "COD" })?
200        .with_high_throughput_cap(high_throughput_cap);
201
202    Ok(J2kCodestreamHeaderMetadata {
203        dimensions: siz.dimensions,
204        components: siz.components,
205        bit_depth: siz.bit_depth,
206        tile_size: siz.tile_size,
207        tile_count: siz.tile_count,
208        component_info: siz.component_info,
209        resolution_levels: cod.resolution_levels,
210        has_mct: cod.has_mct,
211        reversible: cod.reversible,
212        high_throughput: cod.high_throughput,
213    })
214}
215
216/// Return whether bytes start with the raw JPEG 2000 SOC marker.
217#[must_use]
218pub fn looks_like_j2k_codestream(input: &[u8]) -> bool {
219    input.len() >= 2 && input[0] == 0xFF && input[1] == MARKER_SOC
220}
221
222#[derive(Debug, Clone)]
223struct ParsedSiz {
224    dimensions: (u32, u32),
225    components: u16,
226    bit_depth: u8,
227    tile_size: (u32, u32),
228    tile_count: (u32, u32),
229    component_info: Vec<J2kCodestreamComponentHeader>,
230}
231
232#[derive(Debug, Clone, Copy)]
233struct ParsedCod {
234    resolution_levels: u8,
235    has_mct: bool,
236    reversible: bool,
237    high_throughput: bool,
238}
239
240impl ParsedCod {
241    const fn with_high_throughput_cap(mut self, high_throughput_cap: bool) -> Self {
242        self.high_throughput |= high_throughput_cap;
243        self
244    }
245}
246
247fn read_marker(input: &[u8], offset: &mut usize) -> Result<u8, J2kCodestreamHeaderError> {
248    if *offset + 2 > input.len() {
249        return Err(J2kCodestreamHeaderError::TruncatedAt {
250            offset: *offset,
251            segment: "marker",
252        });
253    }
254    if input[*offset] != 0xFF {
255        return Err(J2kCodestreamHeaderError::InvalidMarker {
256            offset: *offset,
257            marker: input[*offset],
258        });
259    }
260    let marker = input[*offset + 1];
261    *offset += 2;
262    Ok(marker)
263}
264
265fn read_segment_payload<'a>(
266    input: &'a [u8],
267    offset: &mut usize,
268    segment: &'static str,
269) -> Result<&'a [u8], J2kCodestreamHeaderError> {
270    if *offset + 2 > input.len() {
271        return Err(J2kCodestreamHeaderError::TruncatedAt {
272            offset: *offset,
273            segment,
274        });
275    }
276    let length = u16::from_be_bytes([input[*offset], input[*offset + 1]]) as usize;
277    if length < 2 {
278        return Err(J2kCodestreamHeaderError::InvalidSegment {
279            offset: *offset,
280            what: "segment length smaller than header",
281        });
282    }
283    let start = *offset + 2;
284    let end = *offset + length;
285    if end > input.len() {
286        return Err(J2kCodestreamHeaderError::TruncatedAt {
287            offset: *offset,
288            segment,
289        });
290    }
291    *offset = end;
292    Ok(&input[start..end])
293}
294
295#[allow(clippy::similar_names)]
296fn parse_siz(payload: &[u8]) -> Result<ParsedSiz, J2kCodestreamHeaderError> {
297    if payload.len() < 36 {
298        return Err(J2kCodestreamHeaderError::InvalidSiz {
299            what: "payload shorter than fixed SIZ header",
300        });
301    }
302    let x_size = read_u32(payload, 2);
303    let y_size = read_u32(payload, 6);
304    let x_origin = read_u32(payload, 10);
305    let y_origin = read_u32(payload, 14);
306    let tile_width = read_u32(payload, 18);
307    let tile_height = read_u32(payload, 22);
308    let tile_x_origin = read_u32(payload, 26);
309    let tile_y_origin = read_u32(payload, 30);
310    let component_count = read_u16(payload, 34);
311
312    let component_bytes = usize::from(component_count) * 3;
313    if payload.len() < 36 + component_bytes {
314        return Err(J2kCodestreamHeaderError::InvalidSiz {
315            what: "component descriptors truncated",
316        });
317    }
318    if component_count == 0 {
319        return Err(J2kCodestreamHeaderError::InvalidSiz {
320            what: "component count must be non-zero",
321        });
322    }
323    if component_count > MAX_J2K_SPEC_COMPONENTS {
324        return Err(J2kCodestreamHeaderError::InvalidSiz {
325            what: "component count exceeds JPEG 2000 limit",
326        });
327    }
328    if x_size <= x_origin || y_size <= y_origin {
329        return Err(J2kCodestreamHeaderError::InvalidSiz {
330            what: "image origin must be smaller than image size",
331        });
332    }
333    if tile_width == 0 || tile_height == 0 {
334        return Err(J2kCodestreamHeaderError::InvalidSiz {
335            what: "tile size must be non-zero",
336        });
337    }
338    if tile_x_origin >= x_size || tile_y_origin >= y_size {
339        return Err(J2kCodestreamHeaderError::InvalidSiz {
340            what: "tile origin must be within image bounds",
341        });
342    }
343    if tile_x_origin > x_origin || tile_y_origin > y_origin {
344        return Err(J2kCodestreamHeaderError::InvalidSiz {
345            what: "tile origin must not exceed image origin",
346        });
347    }
348    if tile_x_origin
349        .checked_add(tile_width)
350        .ok_or(J2kCodestreamHeaderError::InvalidSiz {
351            what: "tile extent overflows",
352        })?
353        <= x_origin
354        || tile_y_origin
355            .checked_add(tile_height)
356            .ok_or(J2kCodestreamHeaderError::InvalidSiz {
357                what: "tile extent overflows",
358            })?
359            <= y_origin
360    {
361        return Err(J2kCodestreamHeaderError::InvalidSiz {
362            what: "first tile must overlap image area",
363        });
364    }
365
366    let width = x_size - x_origin;
367    let height = y_size - y_origin;
368    if width > MAX_J2K_IMAGE_DIMENSION || height > MAX_J2K_IMAGE_DIMENSION {
369        return Err(J2kCodestreamHeaderError::InvalidSiz {
370            what: "image dimensions exceed JPEG 2000 inspect limit",
371        });
372    }
373    let tiles_x = (x_size - tile_x_origin).div_ceil(tile_width);
374    let tiles_y = (y_size - tile_y_origin).div_ceil(tile_height);
375    let tile_count = u64::from(tiles_x) * u64::from(tiles_y);
376    if tile_count > MAX_J2K_TILE_COUNT {
377        return Err(J2kCodestreamHeaderError::InvalidSiz {
378            what: "image has too many tiles",
379        });
380    }
381    let mut bit_depth = 0u8;
382    let mut component_info = Vec::with_capacity(usize::from(component_count));
383    for idx in 0..usize::from(component_count) {
384        let ssiz = payload[36 + idx * 3];
385        let precision = (ssiz & 0x7F) + 1;
386        let x_rsiz = payload[36 + idx * 3 + 1];
387        let y_rsiz = payload[36 + idx * 3 + 2];
388        if x_rsiz == 0 || y_rsiz == 0 {
389            return Err(J2kCodestreamHeaderError::InvalidSiz {
390                what: "component sampling factors must be non-zero",
391            });
392        }
393        bit_depth = bit_depth.max(precision);
394        component_info.push(J2kCodestreamComponentHeader {
395            bit_depth: precision,
396            signed: ssiz & 0x80 != 0,
397            x_rsiz,
398            y_rsiz,
399        });
400    }
401
402    Ok(ParsedSiz {
403        dimensions: (width, height),
404        components: component_count,
405        bit_depth,
406        tile_size: (tile_width, tile_height),
407        tile_count: (tiles_x, tiles_y),
408        component_info,
409    })
410}
411
412fn parse_cod(payload: &[u8]) -> Result<ParsedCod, J2kCodestreamHeaderError> {
413    if payload.len() < 10 {
414        return Err(J2kCodestreamHeaderError::InvalidCod {
415            what: "payload shorter than fixed COD header",
416        });
417    }
418    Ok(ParsedCod {
419        resolution_levels: payload[5].saturating_add(1),
420        has_mct: payload[4] != 0,
421        reversible: payload[9] == 1,
422        high_throughput: payload[8] & 0x40 != 0,
423    })
424}
425
426fn read_u16(bytes: &[u8], offset: usize) -> u16 {
427    u16::from_be_bytes([bytes[offset], bytes[offset + 1]])
428}
429
430fn read_u32(bytes: &[u8], offset: usize) -> u32 {
431    u32::from_be_bytes([
432        bytes[offset],
433        bytes[offset + 1],
434        bytes[offset + 2],
435        bytes[offset + 3],
436    ])
437}
438
439#[cfg(test)]
440mod tests {
441    use super::{inspect_j2k_codestream_header, J2kCodestreamHeaderError};
442    use alloc::{vec, vec::Vec};
443
444    #[test]
445    fn inspect_j2k_codestream_header_accepts_minimal_main_header() {
446        let header = inspect_j2k_codestream_header(&minimal_codestream()).expect("header");
447
448        assert_eq!(header.dimensions, (128, 64));
449        assert_eq!(header.components, 3);
450        assert_eq!(header.bit_depth, 8);
451        assert_eq!(header.tile_size, (64, 64));
452        assert_eq!(header.tile_count, (2, 1));
453        assert_eq!(header.resolution_levels, 6);
454        assert!(header.reversible);
455    }
456
457    #[test]
458    fn inspect_rejects_zero_component_sampling() {
459        let mut bytes = minimal_codestream();
460        rewrite_component_sampling(&mut bytes, 0, 0, 1);
461
462        let err = inspect_j2k_codestream_header(&bytes).expect_err("zero sampling must reject");
463
464        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
465    }
466
467    #[test]
468    fn inspect_rejects_oversized_dimensions() {
469        let mut bytes = minimal_codestream();
470        rewrite_siz_u32(&mut bytes, 2, 60_001);
471
472        let err = inspect_j2k_codestream_header(&bytes).expect_err("oversized width must reject");
473
474        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
475    }
476
477    #[test]
478    fn inspect_rejects_tile_origin_after_image_origin() {
479        let mut bytes = minimal_codestream();
480        rewrite_siz_u32(&mut bytes, 26, 1);
481
482        let err = inspect_j2k_codestream_header(&bytes).expect_err("bad tile origin must reject");
483
484        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
485    }
486
487    #[test]
488    fn inspect_rejects_tile_extent_overflow() {
489        let mut bytes = minimal_codestream();
490        rewrite_siz_u32(&mut bytes, 2, u32::MAX);
491        rewrite_siz_u32(&mut bytes, 10, u32::MAX - 1);
492        rewrite_siz_u32(&mut bytes, 18, 10);
493        rewrite_siz_u32(&mut bytes, 26, u32::MAX - 2);
494
495        let err = inspect_j2k_codestream_header(&bytes).expect_err("overflow must reject");
496
497        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
498    }
499
500    #[test]
501    fn inspect_rejects_excessive_tile_count() {
502        let mut bytes = minimal_codestream();
503        rewrite_siz_u32(&mut bytes, 2, 257);
504        rewrite_siz_u32(&mut bytes, 6, 257);
505        rewrite_siz_u32(&mut bytes, 18, 1);
506        rewrite_siz_u32(&mut bytes, 22, 1);
507
508        let err = inspect_j2k_codestream_header(&bytes).expect_err("tile count must reject");
509
510        assert!(matches!(err, J2kCodestreamHeaderError::InvalidSiz { .. }));
511    }
512
513    fn minimal_codestream() -> Vec<u8> {
514        let mut bytes = vec![0xFF, 0x4F];
515        let mut siz = Vec::new();
516        push_u16(&mut siz, 0);
517        push_u32(&mut siz, 128);
518        push_u32(&mut siz, 64);
519        push_u32(&mut siz, 0);
520        push_u32(&mut siz, 0);
521        push_u32(&mut siz, 64);
522        push_u32(&mut siz, 64);
523        push_u32(&mut siz, 0);
524        push_u32(&mut siz, 0);
525        push_u16(&mut siz, 3);
526        for _ in 0..3 {
527            siz.extend_from_slice(&[0x07, 0x01, 0x01]);
528        }
529        bytes.extend_from_slice(&[0xFF, 0x51]);
530        push_u16(&mut bytes, (siz.len() + 2) as u16);
531        bytes.extend_from_slice(&siz);
532
533        let cod = [0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x04, 0x04, 0x00, 0x01];
534        bytes.extend_from_slice(&[0xFF, 0x52]);
535        push_u16(&mut bytes, (cod.len() + 2) as u16);
536        bytes.extend_from_slice(&cod);
537        bytes.extend_from_slice(&[0xFF, 0x90, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
538        bytes
539    }
540
541    fn push_u16(out: &mut Vec<u8>, value: u16) {
542        out.extend_from_slice(&value.to_be_bytes());
543    }
544
545    fn push_u32(out: &mut Vec<u8>, value: u32) {
546        out.extend_from_slice(&value.to_be_bytes());
547    }
548
549    fn rewrite_siz_u32(bytes: &mut [u8], payload_offset: usize, value: u32) {
550        let siz = bytes
551            .windows(2)
552            .position(|marker| marker == [0xFF, 0x51])
553            .expect("SIZ marker");
554        let offset = siz + 4 + payload_offset;
555        bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
556    }
557
558    fn rewrite_component_sampling(bytes: &mut [u8], component: usize, x_rsiz: u8, y_rsiz: u8) {
559        let siz = bytes
560            .windows(2)
561            .position(|marker| marker == [0xFF, 0x51])
562            .expect("SIZ marker");
563        let component_offset = siz + 40 + component * 3;
564        bytes[component_offset + 1] = x_rsiz;
565        bytes[component_offset + 2] = y_rsiz;
566    }
567}