1use alloc::format;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4
5use crate::profile::ProfileId;
6use crate::resource::ResourceProvider;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct FormatImage {
11 pub id: String,
13 pub profile: ProfileId,
15 pub packages: Vec<FormatPackage>,
17 pub state: FormatState,
19}
20
21impl FormatImage {
22 #[must_use]
24 pub fn empty(id: impl Into<String>, profile: ProfileId) -> Self {
25 Self {
26 id: id.into(),
27 profile,
28 packages: Vec::new(),
29 state: FormatState::Empty,
30 }
31 }
32
33 #[must_use]
35 pub fn with_snapshot(
36 id: impl Into<String>,
37 profile: ProfileId,
38 snapshot: FormatSnapshot,
39 ) -> Self {
40 Self {
41 id: id.into(),
42 profile,
43 packages: snapshot.packages.clone(),
44 state: FormatState::Structured(snapshot),
45 }
46 }
47
48 #[must_use]
50 pub fn initializer(id: impl Into<String>, profile: ProfileId) -> FormatInitializer {
51 FormatInitializer::new(id, profile)
52 }
53
54 #[must_use]
56 pub fn plain(profile: ProfileId) -> FormatInitializer {
57 Self::initializer("plain", profile).preload_tex_input("plain.tex")
58 }
59
60 #[must_use]
62 pub fn latex(profile: ProfileId) -> FormatInitializer {
63 Self::initializer("latex", profile).preload_tex_input("latex.ltx")
64 }
65
66 #[must_use]
68 pub fn instantiate_session_state(&self) -> SessionState {
69 match &self.state {
70 FormatState::Empty => SessionState::default(),
71 FormatState::Snapshot(bytes) => SessionState {
72 opaque_engine_state: bytes.clone(),
73 ..SessionState::default()
74 },
75 FormatState::Structured(snapshot) => SessionState {
76 macros: snapshot.macros.clone(),
77 packages: snapshot.packages.clone(),
78 resources: snapshot.resources.clone(),
79 opaque_engine_state: snapshot.opaque_engine_state.clone(),
80 registers: snapshot.registers.clone(),
81 },
82 }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq)]
88pub struct FormatInitializer {
89 id: String,
90 profile: ProfileId,
91 tex_inputs: Vec<String>,
92 packages: Vec<String>,
93 macros: Vec<MacroDefinition>,
94 registers: RegisterSnapshot,
95 opaque_engine_state: Vec<u8>,
96}
97
98impl FormatInitializer {
99 #[must_use]
101 pub fn new(id: impl Into<String>, profile: ProfileId) -> Self {
102 Self {
103 id: id.into(),
104 profile,
105 tex_inputs: Vec::new(),
106 packages: Vec::new(),
107 macros: Vec::new(),
108 registers: RegisterSnapshot::default(),
109 opaque_engine_state: Vec::new(),
110 }
111 }
112
113 #[must_use]
115 pub fn preload_tex_input(mut self, input: impl Into<String>) -> Self {
116 self.tex_inputs.push(input.into());
117 self
118 }
119
120 #[must_use]
122 pub fn preload_package(mut self, package: impl Into<String>) -> Self {
123 self.packages.push(package.into());
124 self
125 }
126
127 #[must_use]
129 pub fn macro_definition(
130 mut self,
131 name: impl Into<String>,
132 replacement: impl Into<Vec<u8>>,
133 ) -> Self {
134 self.macros.push(MacroDefinition {
135 name: name.into(),
136 replacement: replacement.into(),
137 });
138 self
139 }
140
141 #[must_use]
143 pub fn registers(mut self, registers: RegisterSnapshot) -> Self {
144 self.registers = registers;
145 self
146 }
147
148 #[must_use]
150 pub fn opaque_engine_state(mut self, bytes: impl Into<Vec<u8>>) -> Self {
151 self.opaque_engine_state.extend(bytes.into());
152 self
153 }
154
155 pub fn build<R>(self, resources: &R) -> Result<FormatImage, FormatInitError>
157 where
158 R: ResourceProvider,
159 {
160 let mut packages = Vec::with_capacity(self.packages.len());
161 let mut format_resources = Vec::with_capacity(self.tex_inputs.len() + self.packages.len());
162 let mut opaque_engine_state = self.opaque_engine_state;
163
164 for input in self.tex_inputs {
165 let resource =
166 resources
167 .read_tex_input(&input)
168 .map_err(|error| FormatInitError::TexInput {
169 name: input.clone(),
170 message: format!("{error:?}"),
171 })?;
172 opaque_engine_state.extend(&resource.bytes);
173 format_resources.push(FormatResource {
174 name: resource.canonical_name,
175 kind: FormatResourceKind::TexInput,
176 bytes: resource.bytes,
177 });
178 }
179
180 for package in self.packages {
181 let resource_name = package_resource_name(&package);
182 let resource = resources.read_package(&resource_name).map_err(|error| {
183 FormatInitError::Package {
184 name: resource_name.clone(),
185 message: format!("{error:?}"),
186 }
187 })?;
188 let package = FormatPackage {
189 name: package,
190 version: None,
191 };
192 opaque_engine_state.extend(&resource.bytes);
193 format_resources.push(FormatResource {
194 name: resource.canonical_name,
195 kind: FormatResourceKind::Package,
196 bytes: resource.bytes,
197 });
198 packages.push(package);
199 }
200
201 let snapshot = FormatSnapshot {
202 macros: self.macros,
203 packages,
204 resources: format_resources,
205 opaque_engine_state,
206 registers: self.registers,
207 };
208
209 Ok(FormatImage::with_snapshot(self.id, self.profile, snapshot))
210 }
211}
212
213#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct FormatPackage {
216 pub name: String,
218 pub version: Option<String>,
220}
221
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
224pub struct FormatSnapshot {
225 pub macros: Vec<MacroDefinition>,
227 pub packages: Vec<FormatPackage>,
229 pub resources: Vec<FormatResource>,
231 pub opaque_engine_state: Vec<u8>,
233 pub registers: RegisterSnapshot,
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct FormatResource {
240 pub name: String,
242 pub kind: FormatResourceKind,
244 pub bytes: Vec<u8>,
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250#[non_exhaustive]
251pub enum FormatResourceKind {
252 TexInput,
254 Package,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq)]
260pub struct MacroDefinition {
261 pub name: String,
263 pub replacement: Vec<u8>,
265}
266
267#[derive(Clone, Debug, Default, PartialEq, Eq)]
269pub struct RegisterSnapshot {
270 pub counts: Vec<i32>,
272 pub dimensions: Vec<i32>,
274 pub token_lists: Vec<Vec<u8>>,
276}
277
278#[derive(Clone, Debug, Default, PartialEq, Eq)]
280pub struct SessionState {
281 pub macros: Vec<MacroDefinition>,
283 pub packages: Vec<FormatPackage>,
285 pub resources: Vec<FormatResource>,
287 pub opaque_engine_state: Vec<u8>,
289 pub registers: RegisterSnapshot,
291}
292
293impl SessionState {
294 pub fn define_macro(&mut self, name: impl Into<String>, replacement: impl Into<Vec<u8>>) {
296 let name = name.into();
297 let replacement = replacement.into();
298 if let Some(existing) = self
299 .macros
300 .iter_mut()
301 .find(|macro_def| macro_def.name == name)
302 {
303 existing.replacement = replacement;
304 return;
305 }
306
307 self.macros.push(MacroDefinition { name, replacement });
308 }
309
310 #[must_use]
312 pub fn macro_definition(&self, name: &str) -> Option<&MacroDefinition> {
313 self.macros.iter().find(|macro_def| macro_def.name == name)
314 }
315
316 #[must_use]
318 pub fn package(&self, name: &str) -> Option<&FormatPackage> {
319 self.packages.iter().find(|package| package.name == name)
320 }
321
322 #[must_use]
324 pub fn resource(&self, name: &str) -> Option<&FormatResource> {
325 self.resources.iter().find(|resource| resource.name == name)
326 }
327}
328
329#[derive(Clone, Debug, PartialEq, Eq)]
331#[non_exhaustive]
332pub enum FormatInitError {
333 TexInput {
335 name: String,
337 message: String,
339 },
340 Package {
342 name: String,
344 message: String,
346 },
347}
348
349fn package_resource_name(name: &str) -> String {
350 if name.ends_with(".sty") {
351 name.to_string()
352 } else {
353 format!("{name}.sty")
354 }
355}
356
357#[derive(Clone, Debug, Default, PartialEq, Eq)]
359#[non_exhaustive]
360pub enum FormatState {
361 #[default]
363 Empty,
364 Snapshot(Vec<u8>),
366 Structured(FormatSnapshot),
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::profile::ProfileId;
374 use crate::{InMemoryResourceProvider, ResourceKind};
375
376 #[test]
377 fn format_image_instantiates_mutable_session_state() {
378 let snapshot = FormatSnapshot {
379 macros: alloc::vec![MacroDefinition {
380 name: "text".into(),
381 replacement: b"text macro".to_vec(),
382 }],
383 packages: alloc::vec![FormatPackage {
384 name: "latex".into(),
385 version: Some("2026".into()),
386 }],
387 resources: alloc::vec![FormatResource {
388 name: "plain.tex".into(),
389 kind: FormatResourceKind::TexInput,
390 bytes: b"plain".to_vec(),
391 }],
392 opaque_engine_state: b"engine".to_vec(),
393 registers: RegisterSnapshot {
394 counts: alloc::vec![1, 2],
395 dimensions: alloc::vec![3],
396 token_lists: alloc::vec![b"tokens".to_vec()],
397 },
398 };
399 let format = FormatImage::with_snapshot("latex", ProfileId("tex"), snapshot);
400
401 let mut session = format.instantiate_session_state();
402 session.define_macro("text", b"changed".to_vec());
403 session.define_macro("frac", b"fraction".to_vec());
404
405 assert_eq!(
406 session
407 .macro_definition("text")
408 .expect("text macro")
409 .replacement,
410 b"changed".to_vec()
411 );
412 assert_eq!(
413 session
414 .macro_definition("frac")
415 .expect("frac macro")
416 .replacement,
417 b"fraction".to_vec()
418 );
419 assert_eq!(
420 session.resource("plain.tex").expect("plain resource").bytes,
421 b"plain".to_vec()
422 );
423 assert_eq!(session.registers.counts, alloc::vec![1, 2]);
424
425 match &format.state {
426 FormatState::Structured(snapshot) => {
427 assert_eq!(snapshot.macros[0].replacement, b"text macro".to_vec());
428 assert_eq!(snapshot.resources[0].bytes, b"plain".to_vec());
429 assert_eq!(snapshot.registers.counts, alloc::vec![1, 2]);
430 }
431 other => panic!("unexpected format state: {other:?}"),
432 }
433 }
434
435 #[test]
436 fn initializer_preloads_packages_through_resource_provider() {
437 let resources = InMemoryResourceProvider::new()
438 .with_resource("latex.ltx.sty", ResourceKind::Package, b"latex")
439 .with_resource("amsmath.sty", ResourceKind::Package, b"ams");
440
441 let format = FormatImage::initializer("latex+amsmath", ProfileId("tex"))
442 .preload_package("latex.ltx.sty")
443 .preload_package("amsmath")
444 .macro_definition("text", b"text macro")
445 .opaque_engine_state(b"seed".to_vec())
446 .build(&resources)
447 .expect("format should initialize");
448
449 assert_eq!(format.packages.len(), 2);
450 assert_eq!(format.packages[1].name, "amsmath");
451 let session = format.instantiate_session_state();
452 assert!(session.package("amsmath").is_some());
453 assert!(session.resource("amsmath.sty").is_some());
454 assert_eq!(
455 session
456 .macro_definition("text")
457 .expect("text macro")
458 .replacement,
459 b"text macro".to_vec()
460 );
461 assert_eq!(session.opaque_engine_state, b"seedlatexams".to_vec());
462 }
463
464 #[test]
465 fn initializer_reports_missing_preloaded_package() {
466 let resources = InMemoryResourceProvider::new();
467
468 let error = FormatImage::initializer("latex+missing", ProfileId("tex"))
469 .preload_package("missing")
470 .build(&resources)
471 .expect_err("missing package should fail initialization");
472
473 match error {
474 FormatInitError::TexInput { .. } => panic!("unexpected TeX input error"),
475 FormatInitError::Package { name, message } => {
476 assert_eq!(name, "missing.sty");
477 assert!(message.contains("NotFound"));
478 }
479 }
480 }
481
482 #[test]
483 fn initializer_preloads_tex_input_resources() {
484 let resources = InMemoryResourceProvider::new().with_resource(
485 "plain.tex",
486 ResourceKind::TexInput,
487 br"\def\plainchar{A}".to_vec(),
488 );
489
490 let format = FormatImage::initializer("plain", ProfileId("tex"))
491 .preload_tex_input("plain.tex")
492 .build(&resources)
493 .expect("format should preload TeX input bytes");
494
495 let snapshot = match format.state {
496 FormatState::Structured(snapshot) => snapshot,
497 other => panic!("unexpected format state: {other:?}"),
498 };
499 assert_eq!(snapshot.resources.len(), 1);
500 assert_eq!(snapshot.resources[0].name, "plain.tex");
501 assert_eq!(snapshot.resources[0].kind, FormatResourceKind::TexInput);
502 assert_eq!(snapshot.resources[0].bytes, br"\def\plainchar{A}".to_vec());
503 }
504
505 #[test]
506 fn real_format_constructors_preload_engine_entrypoints() {
507 let resources = InMemoryResourceProvider::new()
508 .with_resource("plain.tex", ResourceKind::TexInput, br"\dump".to_vec())
509 .with_resource("latex.ltx", ResourceKind::TexInput, br"\dump".to_vec());
510
511 let plain = FormatImage::plain(ProfileId("tex"))
512 .build(&resources)
513 .expect("plain format entrypoint should resolve");
514 let latex = FormatImage::latex(ProfileId("tex"))
515 .build(&resources)
516 .expect("latex format entrypoint should resolve");
517
518 assert_eq!(plain.id, "plain");
519 assert_eq!(latex.id, "latex");
520
521 let plain_snapshot = match plain.state {
522 FormatState::Structured(snapshot) => snapshot,
523 other => panic!("unexpected plain format state: {other:?}"),
524 };
525 let latex_snapshot = match latex.state {
526 FormatState::Structured(snapshot) => snapshot,
527 other => panic!("unexpected latex format state: {other:?}"),
528 };
529
530 assert_eq!(plain_snapshot.resources[0].name, "plain.tex");
531 assert_eq!(latex_snapshot.resources[0].name, "latex.ltx");
532 }
533}