1#![cfg(feature = "std")]
4
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8use crate::resource::{Resource, ResourceError, ResourceKind, ResourceProvider, ResourceRequest};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12enum Fmt {
13 Tex,
15 Tfm,
17 OpenType,
19 Enc,
21 Map,
23}
24
25impl Fmt {
26 fn prefixes(self) -> &'static [&'static str] {
28 match self {
29 Fmt::Tex => &[
31 "tex/xelatex",
32 "tex/latex",
33 "tex/xetex",
34 "tex/generic",
35 "tex",
36 ],
37 Fmt::Tfm => &["fonts/tfm"],
39 Fmt::OpenType => &["fonts/opentype", "fonts/truetype"],
41 Fmt::Enc => &["fonts/enc"],
43 Fmt::Map => &["fonts/map"],
45 }
46 }
47}
48
49#[derive(Debug)]
51pub struct TexmfResources {
52 root: PathBuf,
53 index: HashMap<String, Vec<String>>,
55}
56
57impl TexmfResources {
58 #[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 #[must_use]
90 pub fn root(&self) -> &Path {
91 &self.root
92 }
93
94 #[must_use]
96 pub fn len(&self) -> usize {
97 self.index.len()
98 }
99
100 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.index.is_empty()
104 }
105
106 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 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 _ => {
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 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 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}