Skip to main content

lumen/chunks/
head.rs

1//! The `HEAD` chunk: display dimensions, layer count and encoder identity
2//! ([`spec/04-head.md`] section 4.1).
3
4use crate::check::Check;
5use crate::error::{Error, Result};
6use crate::io::{Reader, Writer};
7
8/// Bytes of `HEAD` other than `encoder_name`: 4 before it and 40 after.
9pub const HEAD_FIXED_LEN: usize = 44;
10/// Longest permitted `encoder_name`.
11pub const ENCODER_NAME_MAX: usize = 256;
12
13/// The `HEAD` chunk body.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Head {
16    /// Layout version. `1` for this specification.
17    pub head_version: u32,
18    /// UTF-8 encoder identity, e.g. `DragonFruit 1.0`.
19    pub encoder_name: String,
20    /// Creation timestamp in Unix seconds.
21    pub created_unix_sec: u64,
22    /// Display width in pixels: the layer mask grid.
23    pub display_width_px: u32,
24    /// Display height in pixels: the layer mask grid.
25    pub display_height_px: u32,
26    /// Build plate X dimension in micrometers.
27    pub build_width_um: u32,
28    /// Build plate Y dimension in micrometers.
29    pub build_depth_um: u32,
30    /// Build plate Z dimension in micrometers.
31    pub build_height_um: u32,
32    /// Default layer thickness in micrometers.
33    pub layer_height_um: u32,
34    /// Total layer count, which `LTBL.layer_count` must equal.
35    pub total_layers: u32,
36}
37
38impl Head {
39    /// Parse the chunk payload.
40    pub fn parse(payload: &[u8]) -> Result<Head> {
41        let mut r = Reader::checked(payload, Check::HeadFrame);
42        let head_version = r.u32()?;
43        if head_version != 1 {
44            return Err(Error::new(
45                Check::HeadVersion,
46                format!("unsupported head_version {head_version}"),
47            ));
48        }
49        let encoder_name_len = r.u32()?;
50        if encoder_name_len as usize > ENCODER_NAME_MAX {
51            return Err(Error::new(
52                Check::HeadFrame,
53                format!(
54                    "encoder_name_len {encoder_name_len} exceeds the {ENCODER_NAME_MAX} byte limit"
55                ),
56            ));
57        }
58        let encoder_name = r.utf8(encoder_name_len as usize)?.to_owned();
59        let created_unix_sec = r.u64()?;
60        let display_width_px = r.u32()?;
61        let display_height_px = r.u32()?;
62        let build_width_um = r.u32()?;
63        let build_depth_um = r.u32()?;
64        let build_height_um = r.u32()?;
65        let layer_height_um = r.u32()?;
66        let total_layers = r.u32()?;
67        // Section 11.2: the chunk holds the fixed fields and the name and nothing
68        // else. A longer one is a layout this reader does not know - a `HEAD` written
69        // against another revision of these fields - and parsing its prefix would
70        // report values that are not the ones they are named for.
71        let expected = HEAD_FIXED_LEN + encoder_name_len as usize;
72        if payload.len() != expected {
73            return Err(Error::new(
74                Check::HeadFrame,
75                format!(
76                    "HEAD is {} bytes, not the {expected} its fields and name total",
77                    payload.len()
78                ),
79            ));
80        }
81        Ok(Head {
82            head_version,
83            encoder_name,
84            created_unix_sec,
85            display_width_px,
86            display_height_px,
87            build_width_um,
88            build_depth_um,
89            build_height_um,
90            layer_height_um,
91            total_layers,
92        })
93    }
94
95    /// Serialize the chunk payload.
96    pub fn to_bytes(&self) -> Vec<u8> {
97        let mut w = Writer::with_capacity(HEAD_FIXED_LEN + self.encoder_name.len());
98        w.u32(self.head_version);
99        w.u32(self.encoder_name.len() as u32);
100        w.bytes(self.encoder_name.as_bytes());
101        w.u64(self.created_unix_sec);
102        w.u32(self.display_width_px);
103        w.u32(self.display_height_px);
104        w.u32(self.build_width_um);
105        w.u32(self.build_depth_um);
106        w.u32(self.build_height_um);
107        w.u32(self.layer_height_um);
108        w.u32(self.total_layers);
109        w.into_vec()
110    }
111
112    /// Pixels in one layer mask: `display_width_px * display_height_px`.
113    pub fn total_pixels(&self) -> u32 {
114        self.display_width_px.saturating_mul(self.display_height_px)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn sample(name: &str) -> Head {
123        Head {
124            head_version: 1,
125            encoder_name: name.to_owned(),
126            created_unix_sec: 1_710_000_000,
127            display_width_px: 64,
128            display_height_px: 48,
129            build_width_um: 218_000,
130            build_depth_um: 123_000,
131            build_height_um: 250_000,
132            layer_height_um: 50,
133            total_layers: 4,
134        }
135    }
136
137    #[test]
138    fn hdr_round_trips_with_a_multibyte_name() {
139        let head = sample("DragonFruit 1.0 \u{2713}");
140        let bytes = head.to_bytes();
141        // `encoder_name_len` counts bytes, not characters: the check mark is three.
142        assert_eq!(bytes.len(), HEAD_FIXED_LEN + head.encoder_name.len());
143        assert_eq!(
144            u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize,
145            head.encoder_name.len()
146        );
147        let parsed = Head::parse(&bytes).unwrap();
148        assert_eq!(parsed, head);
149    }
150
151    #[test]
152    fn hdr_rejects_a_length_measured_in_characters() {
153        let head = sample("DragonFruit \u{2713}");
154        let mut bytes = head.to_bytes();
155        // A writer that confused characters for bytes declares too few bytes,
156        // so the name no longer decodes as UTF-8.
157        let chars = head.encoder_name.chars().count() as u32;
158        bytes[4..8].copy_from_slice(&chars.to_le_bytes());
159        let err = Head::parse(&bytes).unwrap_err();
160        assert_eq!(err.check(), Check::HeadFrame);
161    }
162
163    #[test]
164    fn hdr_rejects_an_oversized_or_truncated_frame() {
165        let mut bytes = sample("DragonFruit 1.0").to_bytes();
166        bytes[4..8].copy_from_slice(&(ENCODER_NAME_MAX as u32 + 1).to_le_bytes());
167        assert_eq!(Head::parse(&bytes).unwrap_err().check(), Check::HeadFrame);
168
169        let bytes = sample("DragonFruit 1.0").to_bytes();
170        assert_eq!(
171            Head::parse(&bytes[..bytes.len() - 1]).unwrap_err().check(),
172            Check::HeadFrame
173        );
174        assert_eq!(
175            Head::parse(&bytes[..4]).unwrap_err().check(),
176            Check::HeadFrame
177        );
178
179        // A `HEAD` longer than these fields is a layout this revision does not know,
180        // not padding: a reader that parsed the prefix would report values under names
181        // they do not have, which is how a pre-removal 52-byte `HEAD` turned into a
182        // layer count of 235000 instead of the 800 the file holds.
183        let mut longer = bytes.clone();
184        longer.extend_from_slice(&[0u8; 8]);
185        assert_eq!(Head::parse(&longer).unwrap_err().check(), Check::HeadFrame);
186    }
187
188    #[test]
189    fn hdr_rejects_unknown_versions_and_any_extra_bytes() {
190        let mut bytes = sample("X").to_bytes();
191        bytes[0..4].copy_from_slice(&2u32.to_le_bytes());
192        assert_eq!(Head::parse(&bytes).unwrap_err().check(), Check::HeadVersion);
193
194        // Section 11.2 makes the frame exact, and the rule earns its keep: these fields
195        // were eight bytes longer once, so a reader that ignored what followed them read
196        // the old build height as `total_layers` and reported a file of 800 layers as one
197        // of 235000.
198        let mut bytes = sample("X").to_bytes();
199        bytes.push(0);
200        assert_eq!(Head::parse(&bytes).unwrap_err().check(), Check::HeadFrame);
201    }
202}