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 => &["tex/xelatex", "tex/latex", "tex/xetex", "tex/generic", "tex"],
31 Fmt::Tfm => &["fonts/tfm"],
33 Fmt::OpenType => &["fonts/opentype", "fonts/truetype"],
35 Fmt::Enc => &["fonts/enc"],
37 Fmt::Map => &["fonts/map"],
39 }
40 }
41}
42
43#[derive(Debug)]
45pub struct TexmfResources {
46 root: PathBuf,
47 index: HashMap<String, Vec<String>>,
49}
50
51impl TexmfResources {
52 #[must_use]
54 pub fn from_root(root: impl Into<PathBuf>) -> Option<Self> {
55 let root = root.into();
56 let bytes = std::fs::read(root.join("ls-R")).ok()?;
57 let text = String::from_utf8_lossy(&bytes);
58
59 let mut index: HashMap<String, Vec<String>> = HashMap::new();
60 let mut cur_dir = String::new();
61 for line in text.lines() {
62 let line = line.trim_end();
63 if line.is_empty() || line.starts_with('%') {
64 continue;
65 }
66 if let Some(dir) = line.strip_suffix(':') {
67 cur_dir = dir.strip_prefix("./").unwrap_or(dir).to_string();
68 continue;
69 }
70 index
71 .entry(line.to_string())
72 .or_default()
73 .push(cur_dir.clone());
74 }
75
76 if index.is_empty() {
77 return None;
78 }
79 Some(Self { root, index })
80 }
81
82 #[must_use]
84 pub fn root(&self) -> &Path {
85 &self.root
86 }
87
88 #[must_use]
90 pub fn len(&self) -> usize {
91 self.index.len()
92 }
93
94 #[must_use]
96 pub fn is_empty(&self) -> bool {
97 self.index.is_empty()
98 }
99
100 fn normalize(name: &str) -> String {
102 let mut n = name.trim();
103 loop {
104 if let Some(s) = n.strip_prefix("./") {
105 n = s;
106 } else if let Some(s) = n.strip_prefix("[]") {
107 n = s;
108 } else if let Some(s) = n.strip_prefix(':') {
109 n = s;
110 } else {
111 break;
112 }
113 }
114 let n = n.trim_matches(|c| c == '[' || c == ']' || c == '"' || c == '\'');
115 n.rsplit(['/', '\\']).next().unwrap_or(n).to_string()
116 }
117
118 fn format_for(kind: ResourceKind, filename: &str) -> Fmt {
120 let lower = filename.to_ascii_lowercase();
121 match kind {
122 ResourceKind::Encoding => Fmt::Enc,
123 ResourceKind::Map => Fmt::Map,
124 ResourceKind::Font => {
125 if lower.ends_with(".otf") || lower.ends_with(".ttf") || lower.ends_with(".otc") {
126 Fmt::OpenType
127 } else {
128 Fmt::Tfm
129 }
130 }
131 _ => {
133 if lower.ends_with(".enc") {
134 Fmt::Enc
135 } else if lower.ends_with(".map") {
136 Fmt::Map
137 } else if lower.ends_with(".tfm") {
138 Fmt::Tfm
139 } else if lower.ends_with(".otf") || lower.ends_with(".ttf") {
140 Fmt::OpenType
141 } else {
142 Fmt::Tex
143 }
144 }
145 }
146 }
147
148 fn resolve(&self, filename: &str, fmt: Fmt) -> Option<PathBuf> {
150 let dirs = self.index.get(filename)?;
151 for prefix in fmt.prefixes() {
152 for dir in dirs {
153 if dir == prefix || dir.strip_prefix(prefix).is_some_and(|r| r.starts_with('/')) {
154 return Some(self.root.join(dir).join(filename));
155 }
156 }
157 }
158 None
159 }
160
161 fn candidates(request: &ResourceRequest) -> Vec<String> {
163 let base = Self::normalize(&request.canonical_name());
164 let mut out = vec![base.clone()];
165 if Path::new(&base).extension().is_none() {
166 let exts: &[&str] = match request.kind {
167 ResourceKind::Package => &[".sty", ".tex", ".def", ".ltx"],
168 ResourceKind::Class => &[".cls"],
169 ResourceKind::FontDefinition => &[".fd"],
170 ResourceKind::PackageSupport => &[".def", ".cfg", ".ldf", ".clo", ".sty", ".tex"],
171 ResourceKind::Config => &[".cfg", ".cnf", ".tex"],
172 ResourceKind::Encoding => &[".enc"],
173 ResourceKind::Map => &[".map"],
174 ResourceKind::Font => &[".tfm", ".otf", ".ttf"],
175 ResourceKind::TexInput => &[".tex", ".ltx", ".def", ".sty", ".cfg", ".fd"],
176 _ => &[".tex", ".sty", ".def", ".cfg", ".ltx", ".fd", ".cls", ".enc"],
177 };
178 for e in exts {
179 out.push(format!("{base}{e}"));
180 }
181 }
182 out
183 }
184}
185
186impl ResourceProvider for TexmfResources {
187 fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
188 for cand in Self::candidates(request) {
189 let fmt = Self::format_for(request.kind, &cand);
190 if let Some(path) = self.resolve(&cand, fmt) {
191 if let Ok(bytes) = std::fs::read(&path) {
192 return Ok(Resource::from_request(request, bytes));
193 }
194 }
195 }
196 Err(ResourceError::NotFound {
197 name: request.canonical_name(),
198 kind: request.kind,
199 })
200 }
201}