1use alloc::collections::BTreeMap;
2use alloc::string::String;
3use alloc::vec::Vec;
4#[cfg(feature = "std")]
5use std::path::{Component, Path, PathBuf};
6
7use mathtex_ir::ByteSpan;
8
9pub trait ResourceProvider {
11 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError>;
13
14 fn read(&self, name: &str, kind: ResourceKind) -> Result<Resource, ResourceError> {
16 self.read_request(&ResourceRequest::new(name, kind))
17 }
18
19 fn read_tex_input(&self, name: &str) -> Result<Resource, ResourceError> {
21 self.read(name, ResourceKind::TexInput)
22 }
23
24 fn read_package(&self, name: &str) -> Result<Resource, ResourceError> {
26 self.read(name, ResourceKind::Package)
27 }
28
29 fn read_class(&self, name: &str) -> Result<Resource, ResourceError> {
31 self.read(name, ResourceKind::Class)
32 }
33
34 fn read_font_definition(&self, name: &str) -> Result<Resource, ResourceError> {
36 self.read(name, ResourceKind::FontDefinition)
37 }
38
39 fn read_package_support(&self, name: &str) -> Result<Resource, ResourceError> {
41 self.read(name, ResourceKind::PackageSupport)
42 }
43
44 fn read_font(&self, name: &str) -> Result<Resource, ResourceError> {
46 self.read(name, ResourceKind::Font)
47 }
48
49 fn read_encoding(&self, name: &str) -> Result<Resource, ResourceError> {
51 self.read(name, ResourceKind::Encoding)
52 }
53
54 fn read_map(&self, name: &str) -> Result<Resource, ResourceError> {
56 self.read(name, ResourceKind::Map)
57 }
58
59 fn read_config(&self, name: &str) -> Result<Resource, ResourceError> {
61 self.read(name, ResourceKind::Config)
62 }
63
64 fn read_format_image(&self, name: &str) -> Result<Resource, ResourceError> {
66 self.read(name, ResourceKind::FormatImage)
67 }
68
69 fn read_asset(&self, package: &str, name: &str) -> Result<Resource, ResourceError> {
71 self.read_request(&ResourceRequest::asset(package, name))
72 }
73}
74
75impl<T> ResourceProvider for &T
76where
77 T: ResourceProvider,
78{
79 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
80 (*self).read_request(request)
81 }
82}
83
84#[derive(Clone, Debug)]
86pub struct ResolverResourceProvider<F> {
87 resolver: F,
88}
89
90impl<F> ResolverResourceProvider<F> {
91 #[must_use]
93 pub fn new(resolver: F) -> Self {
94 Self { resolver }
95 }
96
97 #[must_use]
99 pub fn resolver(&self) -> &F {
100 &self.resolver
101 }
102}
103
104impl<F> ResourceProvider for ResolverResourceProvider<F>
105where
106 F: Fn(&ResourceRequest) -> Result<Resource, ResourceError>,
107{
108 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
109 (self.resolver)(request)
110 }
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
115pub struct InMemoryResourceProvider {
116 resources: BTreeMap<ResourceKey, Resource>,
117}
118
119impl InMemoryResourceProvider {
120 #[must_use]
122 pub fn new() -> Self {
123 Self::default()
124 }
125
126 #[must_use]
128 pub fn with_resource(
129 mut self,
130 name: impl Into<String>,
131 kind: ResourceKind,
132 bytes: impl Into<Vec<u8>>,
133 ) -> Self {
134 self.insert(name, kind, bytes);
135 self
136 }
137
138 pub fn insert(
140 &mut self,
141 name: impl Into<String>,
142 kind: ResourceKind,
143 bytes: impl Into<Vec<u8>>,
144 ) {
145 self.insert_request(ResourceRequest::new(name, kind), bytes);
146 }
147
148 pub fn insert_request(&mut self, request: ResourceRequest, bytes: impl Into<Vec<u8>>) {
150 let key = ResourceKey::from_request(&request);
151 let resource = Resource {
152 canonical_name: request.canonical_name(),
153 kind: request.kind,
154 bytes: bytes.into(),
155 };
156 self.resources.insert(key, resource);
157 }
158
159 #[must_use]
161 pub fn len(&self) -> usize {
162 self.resources.len()
163 }
164
165 #[must_use]
167 pub fn is_empty(&self) -> bool {
168 self.resources.is_empty()
169 }
170}
171
172impl ResourceProvider for InMemoryResourceProvider {
173 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
174 let key = ResourceKey::from_request(request);
175
176 self.resources
177 .get(&key)
178 .cloned()
179 .ok_or_else(|| ResourceError::NotFound {
180 name: request.canonical_name(),
181 kind: request.kind,
182 })
183 }
184}
185
186#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct OverlayResourceProvider<Overrides, Base> {
189 overrides: Overrides,
190 base: Base,
191}
192
193impl<Overrides, Base> OverlayResourceProvider<Overrides, Base> {
194 #[must_use]
196 pub fn new(overrides: Overrides, base: Base) -> Self {
197 Self { overrides, base }
198 }
199
200 #[must_use]
202 pub fn overrides(&self) -> &Overrides {
203 &self.overrides
204 }
205
206 #[must_use]
208 pub fn base(&self) -> &Base {
209 &self.base
210 }
211}
212
213impl<Overrides, Base> ResourceProvider for OverlayResourceProvider<Overrides, Base>
214where
215 Overrides: ResourceProvider,
216 Base: ResourceProvider,
217{
218 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
219 match self.overrides.read_request(request) {
220 Ok(resource) => Ok(resource),
221 Err(ResourceError::NotFound { .. }) => self.base.read_request(request),
222 Err(error) => Err(error),
223 }
224 }
225}
226
227#[derive(Clone, Debug, PartialEq, Eq)]
229pub struct ResourceBundle {
230 id: String,
231 resources: InMemoryResourceProvider,
232}
233
234impl ResourceBundle {
235 #[must_use]
237 pub fn new(id: impl Into<String>) -> Self {
238 Self {
239 id: id.into(),
240 resources: InMemoryResourceProvider::new(),
241 }
242 }
243
244 #[must_use]
246 pub fn id(&self) -> &str {
247 &self.id
248 }
249
250 #[must_use]
252 pub fn with_resource(
253 mut self,
254 name: impl Into<String>,
255 kind: ResourceKind,
256 bytes: impl Into<Vec<u8>>,
257 ) -> Self {
258 self.resources.insert(name, kind, bytes);
259 self
260 }
261
262 #[must_use]
264 pub fn with_request(mut self, request: ResourceRequest, bytes: impl Into<Vec<u8>>) -> Self {
265 self.resources.insert_request(request, bytes);
266 self
267 }
268}
269
270impl ResourceProvider for ResourceBundle {
271 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
272 self.resources.read_request(request)
273 }
274}
275
276#[cfg(feature = "std")]
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct FileSystemResourceProvider {
280 root: PathBuf,
281}
282
283#[cfg(feature = "std")]
284impl FileSystemResourceProvider {
285 #[must_use]
287 pub fn new(root: impl Into<PathBuf>) -> Self {
288 Self { root: root.into() }
289 }
290
291 #[must_use]
293 pub fn root(&self) -> &Path {
294 &self.root
295 }
296
297 fn validate_name(name: &str, kind: ResourceKind) -> Result<&Path, ResourceError> {
298 let path = Path::new(name);
299 let invalid_component = path.components().any(|component| {
300 matches!(
301 component,
302 Component::ParentDir | Component::RootDir | Component::Prefix(_)
303 )
304 });
305
306 if path.is_absolute() || invalid_component {
307 return Err(ResourceError::Denied {
308 name: name.to_string(),
309 message: "resource path must be relative to the provider root".to_string(),
310 });
311 }
312
313 if name.is_empty() {
314 return Err(ResourceError::Invalid {
315 name: name.to_string(),
316 message: "resource name cannot be empty".to_string(),
317 });
318 }
319
320 let _ = kind;
321 Ok(path)
322 }
323}
324
325#[cfg(feature = "std")]
326impl ResourceProvider for FileSystemResourceProvider {
327 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
328 let name = request.canonical_name();
329 let relative_path = Self::validate_name(&name, request.kind)?;
330 let path = self.root.join(relative_path);
331 let bytes = std::fs::read(&path).map_err(|error| ResourceError::NotFound {
332 name: format!("{} ({})", name, error),
333 kind: request.kind,
334 })?;
335
336 Ok(Resource {
337 canonical_name: path.to_string_lossy().into_owned(),
338 kind: request.kind,
339 bytes,
340 })
341 }
342}
343
344#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
345struct ResourceKey {
346 name: String,
347 kind: ResourceKind,
348}
349
350impl ResourceKey {
351 fn from_request(request: &ResourceRequest) -> Self {
352 Self {
353 name: request.canonical_name(),
354 kind: request.kind,
355 }
356 }
357}
358
359#[derive(Clone, Debug, PartialEq, Eq)]
361pub struct ResourceRequest {
362 pub name: String,
364 pub kind: ResourceKind,
366 pub package: Option<String>,
368 pub source: Option<ResourceRequestSource>,
370}
371
372#[derive(Clone, Debug, PartialEq, Eq)]
374pub struct ResourceRequestSource {
375 pub name: String,
377 pub span: ByteSpan,
379}
380
381impl ResourceRequest {
382 #[must_use]
384 pub fn new(name: impl Into<String>, kind: ResourceKind) -> Self {
385 Self {
386 name: name.into(),
387 kind,
388 package: None,
389 source: None,
390 }
391 }
392
393 #[must_use]
395 pub fn asset(package: impl Into<String>, name: impl Into<String>) -> Self {
396 Self {
397 name: name.into(),
398 kind: ResourceKind::Asset,
399 package: Some(package.into()),
400 source: None,
401 }
402 }
403
404 #[must_use]
406 pub fn with_source(mut self, name: impl Into<String>, span: ByteSpan) -> Self {
407 self.source = Some(ResourceRequestSource {
408 name: name.into(),
409 span,
410 });
411 self
412 }
413
414 #[must_use]
416 pub fn canonical_name(&self) -> String {
417 match (&self.package, self.kind) {
418 (Some(package), ResourceKind::Asset) => {
419 let mut name = String::with_capacity(package.len() + 1 + self.name.len());
420 name.push_str(package);
421 name.push('/');
422 name.push_str(&self.name);
423 name
424 }
425 _ => self.name.clone(),
426 }
427 }
428}
429
430#[derive(Clone, Debug, PartialEq, Eq)]
432pub struct Resource {
433 pub canonical_name: String,
435 pub kind: ResourceKind,
437 pub bytes: Vec<u8>,
439}
440
441impl Resource {
442 #[must_use]
444 pub fn new(
445 canonical_name: impl Into<String>,
446 kind: ResourceKind,
447 bytes: impl Into<Vec<u8>>,
448 ) -> Self {
449 Self {
450 canonical_name: canonical_name.into(),
451 kind,
452 bytes: bytes.into(),
453 }
454 }
455
456 #[must_use]
458 pub fn from_request(request: &ResourceRequest, bytes: impl Into<Vec<u8>>) -> Self {
459 Self::new(request.canonical_name(), request.kind, bytes)
460 }
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
465#[non_exhaustive]
466pub enum ResourceKind {
467 TexInput,
469 Package,
471 Class,
473 FontDefinition,
475 PackageSupport,
477 Font,
479 Encoding,
481 Map,
483 Config,
485 FormatImage,
487 Asset,
489}
490
491#[derive(Clone, Debug, PartialEq, Eq)]
493#[non_exhaustive]
494pub enum ResourceError {
495 NotFound {
497 name: String,
499 kind: ResourceKind,
501 },
502 Invalid {
504 name: String,
506 message: String,
508 },
509 Denied {
511 name: String,
513 message: String,
515 },
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn in_memory_provider_loads_package_without_filesystem() {
524 let provider = InMemoryResourceProvider::new().with_resource(
525 "amsmath.sty",
526 ResourceKind::Package,
527 br"\ProvidesPackage{amsmath}".to_vec(),
528 );
529
530 let resource = provider
531 .read("amsmath.sty", ResourceKind::Package)
532 .expect("package should resolve");
533
534 assert_eq!(resource.canonical_name, "amsmath.sty");
535 assert_eq!(resource.kind, ResourceKind::Package);
536 assert_eq!(resource.bytes, br"\ProvidesPackage{amsmath}".to_vec());
537 }
538
539 #[test]
540 fn in_memory_provider_keeps_resource_kinds_separate() {
541 let provider =
542 InMemoryResourceProvider::new().with_resource("cmr10", ResourceKind::Font, b"font");
543
544 let error = provider
545 .read("cmr10", ResourceKind::Package)
546 .expect_err("font must not satisfy package lookup");
547
548 assert_eq!(
549 error,
550 ResourceError::NotFound {
551 name: "cmr10".to_string(),
552 kind: ResourceKind::Package,
553 }
554 );
555 }
556
557 #[test]
558 fn provider_convenience_methods_cover_tex_resource_kinds() {
559 let provider = InMemoryResourceProvider::new()
560 .with_resource("plain.tex", ResourceKind::TexInput, b"tex")
561 .with_resource("amsmath.sty", ResourceKind::Package, b"package")
562 .with_resource("article.cls", ResourceKind::Class, b"class")
563 .with_resource("ot1cmr.fd", ResourceKind::FontDefinition, b"fd")
564 .with_resource("size10.clo", ResourceKind::PackageSupport, b"support")
565 .with_resource("latinmodern-math.otf", ResourceKind::Font, b"font")
566 .with_resource("t1.enc", ResourceKind::Encoding, b"encoding")
567 .with_resource("pdftex.map", ResourceKind::Map, b"map")
568 .with_resource("texmf.cnf", ResourceKind::Config, b"config")
569 .with_resource("latex.fmt", ResourceKind::FormatImage, b"format");
570
571 assert_eq!(
572 provider.read_tex_input("plain.tex").expect("tex").bytes,
573 b"tex"
574 );
575 assert_eq!(
576 provider.read_package("amsmath.sty").expect("package").bytes,
577 b"package"
578 );
579 assert_eq!(
580 provider.read_class("article.cls").expect("class").bytes,
581 b"class"
582 );
583 assert_eq!(
584 provider
585 .read_font_definition("ot1cmr.fd")
586 .expect("font definition")
587 .bytes,
588 b"fd"
589 );
590 assert_eq!(
591 provider
592 .read_package_support("size10.clo")
593 .expect("package support")
594 .bytes,
595 b"support"
596 );
597 assert_eq!(
598 provider
599 .read_font("latinmodern-math.otf")
600 .expect("font")
601 .bytes,
602 b"font"
603 );
604 assert_eq!(
605 provider.read_encoding("t1.enc").expect("encoding").bytes,
606 b"encoding"
607 );
608 assert_eq!(provider.read_map("pdftex.map").expect("map").bytes, b"map");
609 assert_eq!(
610 provider.read_config("texmf.cnf").expect("config").bytes,
611 b"config"
612 );
613 assert_eq!(
614 provider
615 .read_format_image("latex.fmt")
616 .expect("format")
617 .bytes,
618 b"format"
619 );
620 }
621
622 #[test]
623 fn typed_asset_requests_include_package_owner() {
624 let mut provider = InMemoryResourceProvider::new();
625 provider.insert_request(ResourceRequest::asset("mhchem", "arrows.dat"), b"asset");
626
627 let resource = provider
628 .read_asset("mhchem", "arrows.dat")
629 .expect("asset should resolve");
630
631 assert_eq!(resource.kind, ResourceKind::Asset);
632 assert_eq!(resource.canonical_name, "mhchem/arrows.dat");
633 assert_eq!(resource.bytes, b"asset");
634 }
635
636 #[test]
637 fn overlay_provider_prefers_overrides_before_base_bundle() {
638 let base = ResourceBundle::new("latex-base")
639 .with_resource("article.cls", ResourceKind::TexInput, b"base")
640 .with_resource("amsmath.sty", ResourceKind::Package, b"base-ams");
641 let overrides = InMemoryResourceProvider::new().with_resource(
642 "amsmath.sty",
643 ResourceKind::Package,
644 b"override-ams",
645 );
646 let provider = OverlayResourceProvider::new(overrides, base);
647
648 let package = provider
649 .read_package("amsmath.sty")
650 .expect("override package should resolve");
651 let class = provider
652 .read_tex_input("article.cls")
653 .expect("base input should resolve");
654
655 assert_eq!(package.bytes, b"override-ams");
656 assert_eq!(class.bytes, b"base");
657 }
658
659 #[test]
660 fn overlay_provider_preserves_denied_errors_from_overrides() {
661 #[derive(Clone, Debug, PartialEq, Eq)]
662 struct DenyProvider;
663
664 impl ResourceProvider for DenyProvider {
665 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
666 Err(ResourceError::Denied {
667 name: request.canonical_name(),
668 message: "denied by policy".to_string(),
669 })
670 }
671 }
672
673 let base = InMemoryResourceProvider::new().with_resource(
674 "plain.tex",
675 ResourceKind::TexInput,
676 b"base",
677 );
678 let provider = OverlayResourceProvider::new(DenyProvider, base);
679
680 let error = provider
681 .read_tex_input("plain.tex")
682 .expect_err("override denial must not fall through");
683
684 assert_eq!(
685 error,
686 ResourceError::Denied {
687 name: "plain.tex".to_string(),
688 message: "denied by policy".to_string(),
689 }
690 );
691 }
692
693 #[test]
694 fn resource_bundle_resolves_package_owned_assets() {
695 let bundle = ResourceBundle::new("chemistry")
696 .with_request(ResourceRequest::asset("mhchem", "arrows.dat"), b"asset");
697
698 let asset = bundle
699 .read_asset("mhchem", "arrows.dat")
700 .expect("bundle asset should resolve");
701
702 assert_eq!(bundle.id(), "chemistry");
703 assert_eq!(asset.canonical_name, "mhchem/arrows.dat");
704 assert_eq!(asset.bytes, b"asset");
705 }
706
707 #[test]
708 fn resolver_provider_delegates_typed_requests_to_host_resolver() {
709 let provider = ResolverResourceProvider::new(|request: &ResourceRequest| {
710 if request.kind == ResourceKind::Package && request.name == "amsmath.sty" {
711 Ok(Resource::from_request(request, b"package"))
712 } else if request.kind == ResourceKind::Asset
713 && request.package.as_deref() == Some("mhchem")
714 && request.name == "arrows.dat"
715 {
716 Ok(Resource::from_request(request, b"asset"))
717 } else {
718 Err(ResourceError::NotFound {
719 name: request.canonical_name(),
720 kind: request.kind,
721 })
722 }
723 });
724
725 let package = provider
726 .read_package("amsmath.sty")
727 .expect("package should resolve through resolver");
728 let asset = provider
729 .read_asset("mhchem", "arrows.dat")
730 .expect("asset should resolve through resolver");
731 let error = provider
732 .read_tex_input("missing.tex")
733 .expect_err("missing input should propagate resolver error");
734
735 assert_eq!(package.canonical_name, "amsmath.sty");
736 assert_eq!(package.bytes, b"package");
737 assert_eq!(asset.canonical_name, "mhchem/arrows.dat");
738 assert_eq!(asset.bytes, b"asset");
739 assert_eq!(
740 error,
741 ResourceError::NotFound {
742 name: "missing.tex".to_string(),
743 kind: ResourceKind::TexInput,
744 }
745 );
746 }
747
748 #[cfg(feature = "std")]
749 #[test]
750 fn filesystem_provider_loads_relative_resource() {
751 let root =
752 std::env::temp_dir().join(format!("mathtex-resource-test-{}", std::process::id()));
753 std::fs::create_dir_all(&root).expect("create test root");
754 let path = root.join("plain.tex");
755 std::fs::write(&path, b"\\relax").expect("write resource");
756
757 let provider = FileSystemResourceProvider::new(&root);
758 let resource = provider
759 .read("plain.tex", ResourceKind::TexInput)
760 .expect("relative resource should load");
761
762 assert_eq!(resource.kind, ResourceKind::TexInput);
763 assert_eq!(resource.bytes, b"\\relax");
764
765 std::fs::remove_file(path).expect("remove resource");
766 std::fs::remove_dir(root).expect("remove test root");
767 }
768
769 #[cfg(feature = "std")]
770 #[test]
771 fn filesystem_provider_rejects_parent_directory_escape() {
772 let provider = FileSystemResourceProvider::new(std::env::temp_dir());
773 let error = provider
774 .read("../plain.tex", ResourceKind::TexInput)
775 .expect_err("parent path must be denied");
776
777 assert_eq!(
778 error,
779 ResourceError::Denied {
780 name: "../plain.tex".to_string(),
781 message: "resource path must be relative to the provider root".to_string(),
782 }
783 );
784 }
785}