Skip to main content

mathtex_engine/
format.rs

1use std::fmt;
2
3use mathtex_font::FontLoader;
4use mathtex_portable_engine_generated as pe;
5
6use crate::adapter::{FontTable, NativeFontTable, ProviderFiles};
7use crate::resource::{ResourceError, ResourceKind, ResourceProvider};
8
9/// Magic of the packaged format container, followed by a version and a compression byte.
10const MAGIC: &[u8; 6] = b"MTXPKG";
11const VERSION: u8 = 2;
12const STORED: u8 = 0;
13const DEFLATE: u8 = 1;
14/// DEFLATE level for packaging, only the builder pays for it, not the reader.
15const DEFLATE_LEVEL: u8 = 7;
16
17/// Defines `\hostbox` when the preamble left it undefined, with its own catcodes so any regime works.
18const HOST_BOX_DEFINITION: &str = r"\ifdefined\hostbox\else\begingroup\catcode123=1 \catcode125=2 \catcode35=6 \gdef\hostbox#1{\Uhostbox#1\relax}\endgroup\fi";
19
20/// A validated packaged XeTeX format and its native fonts, the same bytes load on every little endian target.
21#[derive(Clone)]
22pub struct Format {
23    pub(crate) image: pe::PortableFormatImage,
24    pub(crate) fonts: NativeFontTable,
25}
26
27impl Format {
28    /// Loads a format [`FormatBuilder::build`] wrote, such as one included with `include_bytes!`.
29    pub fn from_static(bytes: &'static [u8]) -> Result<Format, FormatError> {
30        Self::decode(bytes)
31    }
32
33    /// Loads a format [`FormatBuilder::build`] wrote.
34    pub fn from_owned(bytes: Vec<u8>) -> Result<Format, FormatError> {
35        Self::decode(&bytes)
36    }
37
38    fn decode(bytes: &[u8]) -> Result<Format, FormatError> {
39        let (image, fonts) = unpack(bytes)?;
40        let image =
41            pe::PortableFormatImage::from_bytes(&image).map_err(|error| FormatError::Image {
42                message: error.to_string(),
43            })?;
44        Ok(Self { image, fonts })
45    }
46}
47
48impl fmt::Debug for Format {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.debug_struct("Format")
51            .field("native_fonts", &self.fonts.len())
52            .finish_non_exhaustive()
53    }
54}
55
56/// Why bytes are not a usable format.
57#[derive(Clone, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum FormatError {
60    /// The bytes do not start with the packaged format header.
61    NotAFormat,
62    /// The container was written by a version of this crate with another layout.
63    Version {
64        /// Version byte found.
65        found: u8,
66    },
67    /// The payload does not inflate or its parts do not add up.
68    Corrupt,
69    /// The format image was dumped by another engine build or on a big endian target, or fails validation.
70    Image {
71        /// Why the image was refused.
72        message: String,
73    },
74}
75
76impl fmt::Display for FormatError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::NotAFormat => f.write_str("not a packaged mathtex format"),
80            Self::Version { found } => {
81                write!(
82                    f,
83                    "packaged format version {found}, this build reads {VERSION}"
84                )
85            }
86            Self::Corrupt => f.write_str("packaged format is corrupt"),
87            Self::Image { message } => write!(f, "{message}, bake the format again"),
88        }
89    }
90}
91
92impl std::error::Error for FormatError {}
93
94/// Bakes a packaged format: INITEX runs an optional kernel file, then the preamble, then dumps.
95#[derive(Clone, Debug, PartialEq, Eq)]
96#[must_use]
97pub struct FormatBuilder {
98    preamble: String,
99    kernel: Option<String>,
100}
101
102impl FormatBuilder {
103    /// A format whose INITEX run reads `preamble`, which should not `\dump` itself.
104    pub fn new(preamble: impl Into<String>) -> Self {
105        Self {
106            preamble: preamble.into(),
107            kernel: None,
108        }
109    }
110
111    /// Runs `file`, such as `latex.ltx`, through INITEX to its own `\dump` before the preamble.
112    pub fn kernel(mut self, file: impl Into<String>) -> Self {
113        self.kernel = Some(file.into());
114        self
115    }
116
117    /// Runs the kernel and preamble, defines `\hostbox` if still undefined, and returns the packaged format.
118    pub fn build(
119        self,
120        resources: &impl ResourceProvider,
121        fonts: &impl FontLoader,
122    ) -> Result<Vec<u8>, BuildError> {
123        let mut table = FontTable::default();
124        let empty = pe::PortableFormatImage::empty();
125        let mut engine = pe::PortableTexEngine::from_format(&empty, ProviderFiles(resources))
126            .with_font_platform(table.platform(fonts));
127        if !engine.initialize_format_state() {
128            return Err(BuildError::Initialization);
129        }
130        if let Some(kernel) = &self.kernel {
131            let bytes = resources
132                .read(kernel, ResourceKind::TexInput)
133                .map_err(|error| BuildError::Kernel {
134                    name: kernel.clone(),
135                    error,
136                })?
137                .bytes;
138            run_input(&mut engine, kernel, bytes)?;
139        }
140        // The newline keeps a trailing comment in the preamble from swallowing the definitions after it.
141        let preamble = format!("{}\n{HOST_BOX_DEFINITION}\\dump", self.preamble);
142        run_input(&mut engine, "preamble", preamble.into_bytes())?;
143        if !engine.finalize_trie() {
144            return Err(BuildError::Trie);
145        }
146        let image = engine.into_format().to_bytes();
147        Ok(pack(&image, &table.snapshot()))
148    }
149}
150
151/// Runs one INITEX input to its `\dump` or `\end`.
152fn run_input(
153    engine: &mut pe::PortableTexEngine<'_>,
154    input: &str,
155    bytes: Vec<u8>,
156) -> Result<(), BuildError> {
157    if engine.begin_primary_input(input, bytes) && engine.run_format_initialization() {
158        return Ok(());
159    }
160    Err(match engine.last_error() {
161        Some(error) => BuildError::Tex {
162            input: input.into(),
163            message: error.message.clone(),
164            line: error.line,
165        },
166        None => BuildError::Aborted {
167            input: input.into(),
168            status: engine.last_abort_status(),
169        },
170    })
171}
172
173/// Why a format could not be baked.
174#[derive(Clone, Debug, PartialEq, Eq)]
175#[non_exhaustive]
176pub enum BuildError {
177    /// INITEX could not initialize its tables.
178    Initialization,
179    /// The kernel file could not be read.
180    Kernel {
181        /// The kernel's name.
182        name: String,
183        /// Why it could not be read.
184        error: ResourceError,
185    },
186    /// TeX reported an error, which ends a format build.
187    Tex {
188        /// The kernel's name or `preamble`.
189        input: String,
190        /// TeX's error message.
191        message: String,
192        /// Line of the innermost file TeX was reading.
193        line: i32,
194    },
195    /// The run stopped without an error message.
196    Aborted {
197        /// The kernel's name or `preamble`.
198        input: String,
199        /// The engine's exit status, when it set one.
200        status: Option<i32>,
201    },
202    /// The hyphenation patterns overflow the trie.
203    Trie,
204}
205
206impl fmt::Display for BuildError {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        match self {
209            Self::Initialization => f.write_str("INITEX initialization failed"),
210            Self::Kernel { name, error } => write!(f, "cannot read the kernel {name}: {error}"),
211            Self::Tex {
212                input,
213                message,
214                line,
215            } => write!(f, "{input}, line {line}: {message}"),
216            Self::Aborted { input, status } => match status {
217                Some(status) => write!(f, "{input}: TeX stopped with status {status}"),
218                None => write!(f, "{input}: TeX stopped"),
219            },
220            Self::Trie => f.write_str("the hyphenation patterns overflow the trie"),
221        }
222    }
223}
224
225impl std::error::Error for BuildError {}
226
227/// The container: magic, version, compression, payload length, then the payload, deflated.
228fn pack(image: &[u8], fonts: &[(pe::PortableFontHandle, String, i32)]) -> Vec<u8> {
229    let mut payload = Vec::with_capacity(image.len() + 64);
230    payload.extend_from_slice(&(image.len() as u64).to_le_bytes());
231    payload.extend_from_slice(image);
232    payload.extend_from_slice(&(fonts.len() as u32).to_le_bytes());
233    for (handle, spec, size) in fonts {
234        payload.extend_from_slice(&(*handle as u64).to_le_bytes());
235        payload.extend_from_slice(&size.to_le_bytes());
236        payload.extend_from_slice(&(spec.len() as u32).to_le_bytes());
237        payload.extend_from_slice(spec.as_bytes());
238    }
239    let compressed = miniz_oxide::deflate::compress_to_vec(&payload, DEFLATE_LEVEL);
240    let mut out = Vec::with_capacity(compressed.len() + 16);
241    out.extend_from_slice(MAGIC);
242    out.push(VERSION);
243    out.push(DEFLATE);
244    out.extend_from_slice(&(payload.len() as u64).to_le_bytes());
245    out.extend_from_slice(&compressed);
246    out
247}
248
249/// Cursor over container bytes whose reads fail instead of running past the end.
250struct Reader<'a> {
251    bytes: &'a [u8],
252    at: usize,
253}
254
255impl<'a> Reader<'a> {
256    fn take(&mut self, len: usize) -> Result<&'a [u8], FormatError> {
257        let end = self.at.checked_add(len).ok_or(FormatError::Corrupt)?;
258        let slice = self.bytes.get(self.at..end).ok_or(FormatError::Corrupt)?;
259        self.at = end;
260        Ok(slice)
261    }
262
263    fn u32(&mut self) -> Result<u32, FormatError> {
264        let bytes = self.take(4)?;
265        Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
266    }
267
268    fn u64(&mut self) -> Result<u64, FormatError> {
269        let mut word = [0; 8];
270        word.copy_from_slice(self.take(8)?);
271        Ok(u64::from_le_bytes(word))
272    }
273
274    fn len(&mut self) -> Result<usize, FormatError> {
275        usize::try_from(self.u64()?).map_err(|_| FormatError::Corrupt)
276    }
277}
278
279/// Splits a container into its format image bytes and native font table.
280fn unpack(bytes: &[u8]) -> Result<(Vec<u8>, NativeFontTable), FormatError> {
281    if bytes.get(..MAGIC.len()) != Some(&MAGIC[..]) {
282        return Err(FormatError::NotAFormat);
283    }
284    let mut header = Reader {
285        bytes,
286        at: MAGIC.len(),
287    };
288    let version = header.take(1)?[0];
289    if version != VERSION {
290        return Err(FormatError::Version { found: version });
291    }
292    let compression = header.take(1)?[0];
293    let payload_len = header.len()?;
294    let body = &bytes[header.at..];
295    let payload = match compression {
296        DEFLATE => miniz_oxide::inflate::decompress_to_vec_with_limit(body, payload_len)
297            .map_err(|_| FormatError::Corrupt)?,
298        STORED => body.to_vec(),
299        _ => return Err(FormatError::Corrupt),
300    };
301    if payload.len() != payload_len {
302        return Err(FormatError::Corrupt);
303    }
304    let mut payload = Reader {
305        bytes: &payload,
306        at: 0,
307    };
308    let image_len = payload.len()?;
309    let image = payload.take(image_len)?.to_vec();
310    let count = payload.u32()? as usize;
311    let mut fonts = Vec::with_capacity(count.min(1024));
312    for _ in 0..count {
313        let handle = usize::try_from(payload.u64()?).map_err(|_| FormatError::Corrupt)?;
314        let size = payload.u32()? as i32;
315        let spec_len = payload.u32()? as usize;
316        let spec = std::str::from_utf8(payload.take(spec_len)?)
317            .map_err(|_| FormatError::Corrupt)?
318            .to_string();
319        fonts.push((handle, spec, size));
320    }
321    if payload.at != payload.bytes.len() {
322        return Err(FormatError::Corrupt);
323    }
324    Ok((image, fonts))
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use crate::resource::InMemoryResourceProvider;
331    use mathtex_font::InMemoryFontLoader;
332
333    fn plain_format() -> Vec<u8> {
334        FormatBuilder::new(r"\catcode`\{=1 \catcode`\}=2 \catcode`\$=3 \def\hello{hi}")
335            .build(&InMemoryResourceProvider::new(), &InMemoryFontLoader::new())
336            .expect("a catcode preamble bakes")
337    }
338
339    #[test]
340    fn containers_round_trip_their_image_and_font_table() {
341        let fonts = vec![(3, "[latinmodern-math.otf]:script=math".to_string(), 655_360)];
342        let packed = pack(b"image", &fonts);
343        assert_eq!(unpack(&packed), Ok((b"image".to_vec(), fonts)));
344    }
345
346    #[test]
347    fn foreign_and_damaged_bytes_are_refused() {
348        let packed = plain_format();
349        assert!(Format::from_owned(packed.clone()).is_ok());
350        assert_eq!(
351            Format::from_owned(vec![1, 2, 3]).err(),
352            Some(FormatError::NotAFormat)
353        );
354        let mut other_version = packed.clone();
355        other_version[MAGIC.len()] = VERSION + 1;
356        assert_eq!(
357            Format::from_owned(other_version).err(),
358            Some(FormatError::Version { found: VERSION + 1 })
359        );
360        let truncated = packed[..packed.len() / 2].to_vec();
361        assert_eq!(
362            Format::from_owned(truncated).err(),
363            Some(FormatError::Corrupt)
364        );
365        let stale = pack(b"not an image", &[]);
366        assert!(matches!(
367            Format::from_owned(stale),
368            Err(FormatError::Image { .. })
369        ));
370    }
371
372    #[test]
373    fn a_preamble_error_names_its_input_and_line() {
374        let error = FormatBuilder::new("\\relax\n\\undefinedmacro")
375            .build(&InMemoryResourceProvider::new(), &InMemoryFontLoader::new())
376            .expect_err("an undefined control sequence fails the build");
377        assert!(
378            matches!(&error, BuildError::Tex { input, line: 2, .. } if input == "preamble"),
379            "{error:?}"
380        );
381        let error = FormatBuilder::new("")
382            .kernel("missing.ltx")
383            .build(&InMemoryResourceProvider::new(), &InMemoryFontLoader::new())
384            .expect_err("a missing kernel fails the build");
385        assert!(matches!(error, BuildError::Kernel { .. }), "{error:?}");
386    }
387}