Skip to main content

lumen/chunks/
preview.rs

1//! The `PREV` chunk: a PNG preview, with its role in the descriptor flags
2//! ([`spec/08-print-control.md`] section 4.6).
3
4use crate::check::Check;
5use crate::container::CHUNK_FLAG_ENCRYPTED;
6use crate::error::{Error, Result};
7
8/// The eight-byte PNG signature.
9pub const PNG_SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
10
11/// The descriptor flag bits holding the preview role.
12pub const PREVIEW_ROLE_MASK: u32 = 0x0F;
13
14/// What a preview is for; a reader picks the best fit for its display.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum PreviewRole {
17    /// No stated role.
18    #[default]
19    Unspecified = 0,
20    /// Large preview, recommended 400x300.
21    Large = 1,
22    /// Small preview, recommended 200x125.
23    Small = 2,
24    /// Icon, at most 64x64.
25    Icon = 3,
26}
27
28impl PreviewRole {
29    /// Decode the role from a descriptor's flags.
30    pub fn from_flags(flags: u32) -> Result<PreviewRole> {
31        // Bit 4 is the descriptor's ENCRYPTED flag, not reserved (section 4.6):
32        // a sealed preview still names its role.
33        let reserved = !(PREVIEW_ROLE_MASK | CHUNK_FLAG_ENCRYPTED);
34        if flags & reserved != 0 {
35            return Err(Error::new(
36                Check::PrevFlags,
37                format!("preview flags {flags:#010x} set a reserved bit"),
38            ));
39        }
40        match flags & PREVIEW_ROLE_MASK {
41            0 => Ok(PreviewRole::Unspecified),
42            1 => Ok(PreviewRole::Large),
43            2 => Ok(PreviewRole::Small),
44            3 => Ok(PreviewRole::Icon),
45            role => Err(Error::new(
46                Check::PrevFlags,
47                format!("preview role {role} is reserved"),
48            )),
49        }
50    }
51
52    /// The flag bits a writer sets for this role.
53    pub fn to_flags(self) -> u32 {
54        self as u32
55    }
56}
57
58/// One `PREV` chunk.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Preview {
61    /// The role the descriptor advertises.
62    pub role: PreviewRole,
63    /// The PNG file, verbatim.
64    pub png: Vec<u8>,
65}
66
67/// Whether a payload starts with the PNG signature.
68pub fn is_png(payload: &[u8]) -> bool {
69    payload.len() >= 8 && payload[..8] == PNG_SIGNATURE
70}
71
72/// Check the signature and the `IHDR` chunk, as strict mode requires.
73pub fn check_png(payload: &[u8]) -> Result<()> {
74    // PNG is the one big-endian structure in LUMEN: the first chunk's length
75    // and the IHDR fields are network byte order.
76    if payload.len() < 25 || !is_png(payload) {
77        return Err(Error::new(
78            Check::PrevPngSignature,
79            format!(
80                "a {} byte payload does not begin with the PNG signature",
81                payload.len()
82            ),
83        ));
84    }
85    let declared = u32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
86    if declared != 13 {
87        return Err(Error::new(
88            Check::PrevPngSignature,
89            format!("the first PNG chunk declares {declared} bytes; IHDR is 13"),
90        ));
91    }
92    if &payload[12..16] != b"IHDR" {
93        return Err(Error::new(
94            Check::PrevPngSignature,
95            "the first PNG chunk is not IHDR",
96        ));
97    }
98    Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    /// A well-formed signature, IHDR length, type and body.
106    fn minimal_png() -> Vec<u8> {
107        let mut png = Vec::from(PNG_SIGNATURE);
108        png.extend_from_slice(&13u32.to_be_bytes());
109        png.extend_from_slice(b"IHDR");
110        png.extend_from_slice(&1u32.to_be_bytes()); // width
111        png.extend_from_slice(&1u32.to_be_bytes()); // height
112        png.extend_from_slice(&[8, 6, 0, 0, 0]); // bit depth, color, compression, filter, interlace
113        png
114    }
115
116    #[test]
117    fn check_png_accepts_a_minimal_png_and_rejects_other_bytes() {
118        assert!(check_png(&minimal_png()).is_ok());
119        assert_eq!(
120            check_png(b"this is not a PNG at all, not even close")
121                .unwrap_err()
122                .check(),
123            Check::PrevPngSignature
124        );
125        // A PNG signature followed by a chunk that is not IHDR is rejected.
126        let mut wrong_type = minimal_png();
127        wrong_type[12..16].copy_from_slice(b"IDAT");
128        assert_eq!(
129            check_png(&wrong_type).unwrap_err().check(),
130            Check::PrevPngSignature
131        );
132        // A declared IHDR length other than 13 is rejected.
133        let mut wrong_len = minimal_png();
134        wrong_len[8..12].copy_from_slice(&14u32.to_be_bytes());
135        assert_eq!(
136            check_png(&wrong_len).unwrap_err().check(),
137            Check::PrevPngSignature
138        );
139    }
140
141    #[test]
142    fn preview_roles_reject_reserved_values_but_allow_sealing() {
143        assert_eq!(PreviewRole::from_flags(1).unwrap(), PreviewRole::Large);
144        // Role 4 is reserved even though bit 4 is the (allowed) ENCRYPTED flag.
145        assert_eq!(
146            PreviewRole::from_flags(4).unwrap_err().check(),
147            Check::PrevFlags
148        );
149        assert_eq!(
150            PreviewRole::from_flags(15).unwrap_err().check(),
151            Check::PrevFlags
152        );
153        // Reserved bit 5 is rejected.
154        assert_eq!(
155            PreviewRole::from_flags(1 | (1 << 5)).unwrap_err().check(),
156            Check::PrevFlags
157        );
158        // A sealed preview still decodes its role.
159        let flags = PreviewRole::Icon.to_flags() | CHUNK_FLAG_ENCRYPTED;
160        assert_eq!(PreviewRole::from_flags(flags).unwrap(), PreviewRole::Icon);
161    }
162}