Skip to main content

mathtex_engine/
texmf.rs

1//! Native filesystem [`ResourceProvider`] over a TeXLive tree using `ls-R` and texmf.cnf priority.
2
3#![cfg(feature = "std")]
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use crate::resource::{Resource, ResourceError, ResourceKind, ResourceProvider, ResourceRequest};
9
10/// search format, mapping each resource kind to its ordered `texmf.cnf` prefixes.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12enum Fmt {
13    /// TeX inputs: `.tex`/`.sty`/`.cls`/`.def`/`.cfg`/`.fd`/`.ltx`/`.clo`.
14    Tex,
15    /// TeX font metrics: `.tfm`.
16    Tfm,
17    /// OpenType/TrueType font programs.
18    OpenType,
19    /// Font encoding files: `.enc`.
20    Enc,
21    /// Font map files: `.map`.
22    Map,
23}
24
25impl Fmt {
26    /// Ordered relative directory prefixes, highest priority first, matching each prefix subtree.
27    fn prefixes(self) -> &'static [&'static str] {
28        match self {
29            // Search entries for TeX input files.
30            Fmt::Tex => &[
31                "tex/xelatex",
32                "tex/latex",
33                "tex/xetex",
34                "tex/generic",
35                "tex",
36            ],
37            // Search entries for TeX font metrics.
38            Fmt::Tfm => &["fonts/tfm"],
39            // Search entries for font programs.
40            Fmt::OpenType => &["fonts/opentype", "fonts/truetype"],
41            // Search entries for font encodings.
42            Fmt::Enc => &["fonts/enc"],
43            // Search entries for font maps.
44            Fmt::Map => &["fonts/map"],
45        }
46    }
47}
48
49/// Resource provider backed by a TeXMF tree root and its `ls-R` filename index.
50#[derive(Debug)]
51pub struct TexmfResources {
52    root: PathBuf,
53    /// Basename to relative dirs containing it, in `ls-R` order.
54    index: HashMap<String, Vec<String>>,
55}
56
57impl TexmfResources {
58    /// Returns `None` when `ls-R` is absent or empty, run `mktexlsr` to generate it.
59    #[must_use]
60    pub fn from_root(root: impl Into<PathBuf>) -> Option<Self> {
61        let root = root.into();
62        let bytes = std::fs::read(root.join("ls-R")).ok()?;
63        let text = String::from_utf8_lossy(&bytes);
64
65        let mut index: HashMap<String, Vec<String>> = HashMap::new();
66        let mut cur_dir = String::new();
67        for line in text.lines() {
68            let line = line.trim_end();
69            if line.is_empty() || line.starts_with('%') {
70                continue;
71            }
72            if let Some(dir) = line.strip_suffix(':') {
73                cur_dir = dir.strip_prefix("./").unwrap_or(dir).to_string();
74                continue;
75            }
76            index
77                .entry(line.to_string())
78                .or_default()
79                .push(cur_dir.clone());
80        }
81
82        if index.is_empty() {
83            return None;
84        }
85        Some(Self { root, index })
86    }
87
88    /// Returns the filesystem root of the TeXMF tree.
89    #[must_use]
90    pub fn root(&self) -> &Path {
91        &self.root
92    }
93
94    /// Returns the number of basename entries in the `ls-R` index.
95    #[must_use]
96    pub fn len(&self) -> usize {
97        self.index.len()
98    }
99
100    /// Returns true when the `ls-R` index contains no entries.
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.index.is_empty()
104    }
105
106    /// Normalizes an engine resource name to the basename used as a lookup key.
107    fn normalize(name: &str) -> String {
108        let mut n = name.trim();
109        loop {
110            if let Some(s) = n.strip_prefix("./") {
111                n = s;
112            } else if let Some(s) = n.strip_prefix("[]") {
113                n = s;
114            } else if let Some(s) = n.strip_prefix(':') {
115                n = s;
116            } else {
117                break;
118            }
119        }
120        let n = n.trim_matches(|c| c == '[' || c == ']' || c == '"' || c == '\'');
121        n.rsplit(['/', '\\']).next().unwrap_or(n).to_string()
122    }
123
124    /// Maps a resource kind and filename extension to the texmf.cnf search format.
125    fn format_for(kind: ResourceKind, filename: &str) -> Fmt {
126        let lower = filename.to_ascii_lowercase();
127        match kind {
128            ResourceKind::Encoding => Fmt::Enc,
129            ResourceKind::Map => Fmt::Map,
130            ResourceKind::Font => {
131                if lower.ends_with(".otf") || lower.ends_with(".ttf") || lower.ends_with(".otc") {
132                    Fmt::OpenType
133                } else {
134                    Fmt::Tfm
135                }
136            }
137            // Use the tex tree by default, refining by extension for stray font assets.
138            _ => {
139                if lower.ends_with(".enc") {
140                    Fmt::Enc
141                } else if lower.ends_with(".map") {
142                    Fmt::Map
143                } else if lower.ends_with(".tfm") {
144                    Fmt::Tfm
145                } else if lower.ends_with(".otf") || lower.ends_with(".ttf") {
146                    Fmt::OpenType
147                } else {
148                    Fmt::Tex
149                }
150            }
151        }
152    }
153
154    /// Returns the path for the first search prefix subtree containing the filename.
155    fn resolve(&self, filename: &str, fmt: Fmt) -> Option<PathBuf> {
156        let dirs = self.index.get(filename)?;
157        for prefix in fmt.prefixes() {
158            for dir in dirs {
159                if dir == prefix || dir.strip_prefix(prefix).is_some_and(|r| r.starts_with('/')) {
160                    return Some(self.root.join(dir).join(filename));
161                }
162            }
163        }
164        None
165    }
166
167    /// Candidate filenames for a request, including kind suffixes when no extension is present.
168    fn candidates(request: &ResourceRequest) -> Vec<String> {
169        let base = Self::normalize(&request.canonical_name());
170        let mut out = vec![base.clone()];
171        if Path::new(&base).extension().is_none() {
172            let exts: &[&str] = match request.kind {
173                ResourceKind::Package => &[".sty", ".tex", ".def", ".ltx"],
174                ResourceKind::Class => &[".cls"],
175                ResourceKind::FontDefinition => &[".fd"],
176                ResourceKind::PackageSupport => &[".def", ".cfg", ".ldf", ".clo", ".sty", ".tex"],
177                ResourceKind::Config => &[".cfg", ".cnf", ".tex"],
178                ResourceKind::Encoding => &[".enc"],
179                ResourceKind::Map => &[".map"],
180                ResourceKind::Font => &[".tfm", ".otf", ".ttf"],
181                ResourceKind::TexInput => &[".tex", ".ltx", ".def", ".sty", ".cfg", ".fd"],
182                _ => &[
183                    ".tex", ".sty", ".def", ".cfg", ".ltx", ".fd", ".cls", ".enc",
184                ],
185            };
186            for e in exts {
187                out.push(format!("{base}{e}"));
188            }
189        }
190        out
191    }
192}
193
194impl ResourceProvider for TexmfResources {
195    fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
196        for cand in Self::candidates(request) {
197            let fmt = Self::format_for(request.kind, &cand);
198            if let Some(path) = self.resolve(&cand, fmt) {
199                if let Ok(bytes) = std::fs::read(&path) {
200                    return Ok(Resource::from_request(request, bytes));
201                }
202            }
203        }
204        Err(ResourceError::NotFound {
205            name: request.canonical_name(),
206            kind: request.kind,
207        })
208    }
209}