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
9const MAGIC: &[u8; 6] = b"MTXPKG";
11const VERSION: u8 = 2;
12const STORED: u8 = 0;
13const DEFLATE: u8 = 1;
14const DEFLATE_LEVEL: u8 = 7;
16
17const 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#[derive(Clone)]
22pub struct Format {
23 pub(crate) image: pe::PortableFormatImage,
24 pub(crate) fonts: NativeFontTable,
25}
26
27impl Format {
28 pub fn from_static(bytes: &'static [u8]) -> Result<Format, FormatError> {
30 Self::decode(bytes)
31 }
32
33 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#[derive(Clone, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum FormatError {
60 NotAFormat,
62 Version {
64 found: u8,
66 },
67 Corrupt,
69 Image {
71 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#[derive(Clone, Debug, PartialEq, Eq)]
96#[must_use]
97pub struct FormatBuilder {
98 preamble: String,
99 kernel: Option<String>,
100}
101
102impl FormatBuilder {
103 pub fn new(preamble: impl Into<String>) -> Self {
105 Self {
106 preamble: preamble.into(),
107 kernel: None,
108 }
109 }
110
111 pub fn kernel(mut self, file: impl Into<String>) -> Self {
113 self.kernel = Some(file.into());
114 self
115 }
116
117 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 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
151fn 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#[derive(Clone, Debug, PartialEq, Eq)]
175#[non_exhaustive]
176pub enum BuildError {
177 Initialization,
179 Kernel {
181 name: String,
183 error: ResourceError,
185 },
186 Tex {
188 input: String,
190 message: String,
192 line: i32,
194 },
195 Aborted {
197 input: String,
199 status: Option<i32>,
201 },
202 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
227fn 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
249struct 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
279fn 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}