1#![forbid(unsafe_code)]
18
19mod cnf;
20mod db;
21mod expand;
22mod format;
23
24use std::collections::HashMap;
25use std::path::{Path, PathBuf};
26
27pub use format::FileFormat;
28
29pub type Shared = std::rc::Rc<std::cell::RefCell<Kpathsea>>;
31
32pub fn shared(k: Kpathsea) -> Shared {
34 std::rc::Rc::new(std::cell::RefCell::new(k))
35}
36
37use cnf::Cnf;
38use db::{DirCache, LsR};
39
40#[derive(Debug, thiserror::Error)]
42pub enum Error {
43 #[error("no texmf.cnf found (searched: {0})")]
45 NoConfig(String),
46 #[error("reading {path}: {source}")]
48 Io {
49 path: PathBuf,
51 source: std::io::Error,
53 },
54}
55
56const DEFAULT_TEXMFCNF: &str = "{$SELFAUTOLOC,$SELFAUTOLOC/share/texmf-local/web2c,\
61$SELFAUTOLOC/share/texmf-dist/web2c,$SELFAUTOLOC/share/texmf/web2c,\
62$SELFAUTOLOC/texmf-local/web2c,$SELFAUTOLOC/texmf-dist/web2c,$SELFAUTOLOC/texmf/web2c,\
63$SELFAUTODIR,$SELFAUTODIR/share/texmf-local/web2c,$SELFAUTODIR/share/texmf-dist/web2c,\
64$SELFAUTODIR/share/texmf/web2c,$SELFAUTODIR/texmf-local/web2c,$SELFAUTODIR/texmf-dist/web2c,\
65$SELFAUTODIR/texmf/web2c,$SELFAUTOGRANDPARENT/texmf-local/web2c,$SELFAUTOPARENT,\
66$SELFAUTOPARENT/share/texmf-local/web2c,$SELFAUTOPARENT/share/texmf-dist/web2c,\
67$SELFAUTOPARENT/share/texmf/web2c,$SELFAUTOPARENT/texmf-local/web2c,\
68$SELFAUTOPARENT/texmf-dist/web2c,$SELFAUTOPARENT/texmf/web2c}:\
69/etc/texmf/web2c:/usr/local/share/texmf/web2c:/usr/share/texlive/texmf-dist/web2c";
70
71pub trait Env {
73 fn var(&self, name: &str) -> Option<String>;
75}
76
77#[derive(Debug, Default, Clone, Copy)]
79pub struct ProcessEnv;
80
81impl Env for ProcessEnv {
82 fn var(&self, name: &str) -> Option<String> {
83 std::env::var(name).ok()
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct Options {
90 pub progname: String,
92 pub engine: String,
94 pub selfautoloc: Option<PathBuf>,
97 pub cache_dir: Option<PathBuf>,
99}
100
101impl Options {
102 pub fn lualatex() -> Self {
104 Options {
105 progname: "lualatex".into(),
106 engine: "luahbtex".into(),
107 selfautoloc: None,
108 cache_dir: None,
109 }
110 }
111}
112
113pub struct Kpathsea {
115 cnf: Cnf,
116 env: Box<dyn Env>,
117 progname: String,
118 engine: String,
119 builtins: HashMap<String, String>,
121 dbs: Vec<LsR>,
122 dirs: DirCache,
123 paths: HashMap<FileFormat, Vec<String>>,
125 cache_dir: Option<PathBuf>,
127}
128
129impl std::fmt::Debug for Kpathsea {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.debug_struct("Kpathsea")
132 .field("progname", &self.progname)
133 .field("engine", &self.engine)
134 .finish_non_exhaustive()
135 }
136}
137
138impl Kpathsea {
139 pub fn new(options: Options) -> Result<Self, Error> {
141 Self::with_env(options, Box::new(ProcessEnv))
142 }
143
144 pub fn with_env(options: Options, env: Box<dyn Env>) -> Result<Self, Error> {
146 let cache_dir = options.cache_dir.clone();
147 let selfautoloc = options
148 .selfautoloc
149 .clone()
150 .or_else(locate_kpsewhich_dir)
151 .unwrap_or_else(|| PathBuf::from("/usr/bin"));
152 let mut builtins = HashMap::new();
153 let loc = selfautoloc.to_string_lossy().into_owned();
154 let dir = parent(&loc);
155 let par = parent(&dir);
156 let grand = parent(&par);
157 builtins.insert("SELFAUTOLOC".to_owned(), loc);
158 builtins.insert("SELFAUTODIR".to_owned(), dir);
159 builtins.insert("SELFAUTOPARENT".to_owned(), par);
160 builtins.insert("SELFAUTOGRANDPARENT".to_owned(), grand);
161 builtins.insert("progname".to_owned(), options.progname.clone());
162 builtins.insert("engine".to_owned(), options.engine.clone());
163
164 let mut kpse = Kpathsea {
165 cnf: Cnf::default(),
166 env,
167 progname: options.progname,
168 engine: options.engine,
169 builtins,
170 dbs: Vec::new(),
171 dirs: DirCache::default(),
172 paths: HashMap::new(),
173 cache_dir,
174 };
175 let t0 = std::time::Instant::now();
176 kpse.load_cnf()?;
177 let t1 = std::time::Instant::now();
178 kpse.load_dbs()?;
179 if std::env::var_os("RETEX_KPSE_TIMING").is_some() {
180 eprintln!("kpse: cnf {:?}, dbs {:?}", t1 - t0, t1.elapsed());
181 }
182 Ok(kpse)
183 }
184
185 pub fn progname(&self) -> &str {
187 &self.progname
188 }
189
190 pub fn engine(&self) -> &str {
192 &self.engine
193 }
194
195 fn load_cnf(&mut self) -> Result<(), Error> {
196 let raw = self
197 .env
198 .var("TEXMFCNF")
199 .map(|v| expand::expand_default(&v, DEFAULT_TEXMFCNF))
200 .unwrap_or_else(|| DEFAULT_TEXMFCNF.to_owned());
201 let expanded = self.expand_vars(&raw);
202 let mut found_any = false;
203 for dir in expand::brace_expand_path(&expanded) {
204 let dir = self.expand_tilde(&dir);
205 let file = Path::new(&dir).join("texmf.cnf");
206 match std::fs::read_to_string(&file) {
207 Ok(text) => {
208 found_any = true;
209 self.cnf.parse(&text);
210 }
211 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
212 Err(e) if e.kind() == std::io::ErrorKind::NotADirectory => {}
213 Err(source) => return Err(Error::Io { path: file, source }),
214 }
215 }
216 if found_any {
217 Ok(())
218 } else {
219 Err(Error::NoConfig(expanded))
220 }
221 }
222
223 fn load_dbs(&mut self) -> Result<(), Error> {
224 let dbs = self.var_value("TEXMFDBS").unwrap_or_default();
225 let mut seen = Vec::new();
226 for elt in expand::brace_expand_path(&dbs) {
227 let elt = elt.trim_start_matches("!!");
228 let elt = self.expand_tilde(elt);
229 let root = elt.trim_end_matches('/').to_owned();
230 if root.is_empty() || seen.contains(&root) {
231 continue;
232 }
233 seen.push(root.clone());
234 for name in ["ls-R", "ls-r"] {
235 let file = Path::new(&root).join(name);
236 let Ok(meta) = std::fs::metadata(&file) else {
237 continue;
238 };
239 let mtime = meta
240 .modified()
241 .ok()
242 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
243 .map_or((0, 0), |d| (d.as_secs() as i64, d.subsec_nanos()));
244 let cache_file = self
245 .cache_dir
246 .as_ref()
247 .map(|d| d.join(format!("lsr-{:016x}.idx", content_key(root.as_bytes()))));
248 if let Some(cf) = &cache_file
249 && let Ok(bytes) = std::fs::read(cf)
250 && let Some(db) = LsR::from_buf(bytes, meta.len(), mtime)
251 {
252 self.dbs.push(db);
253 break;
254 }
255 match std::fs::read(&file) {
256 Ok(bytes) => {
257 let db = LsR::parse(&root, &bytes, meta.len(), mtime);
258 if let Some(cf) = &cache_file {
259 write_atomically(cf, db.bytes());
260 }
261 self.dbs.push(db);
262 break;
263 }
264 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
265 Err(source) => return Err(Error::Io { path: file, source }),
266 }
267 }
268 }
269 Ok(())
270 }
271
272 pub fn var_value(&self, name: &str) -> Option<String> {
275 let raw = self.raw_var(name)?;
276 Some(self.expand_vars(&raw))
277 }
278
279 fn env_var(&self, name: &str) -> Option<String> {
282 let dotted = format!("{name}.{}", self.progname);
283 let underscored = format!("{name}_{}", self.progname);
284 self.env
285 .var(&dotted)
286 .or_else(|| self.env.var(&underscored))
287 .or_else(|| self.env.var(name))
288 }
289
290 fn cnf_var(&self, name: &str) -> Option<String> {
292 let value = self
293 .cnf
294 .get(name, &self.progname)
295 .map(str::to_owned)
296 .or_else(|| self.builtins.get(name).cloned())?;
297 Some(self.expand_tilde(&value))
298 }
299
300 fn raw_var(&self, name: &str) -> Option<String> {
304 if matches!(name, "progname" | "engine") {
306 return self.builtins.get(name).cloned();
307 }
308 let dotted = format!("{name}.{}", self.progname);
309 let underscored = format!("{name}_{}", self.progname);
310 let value = self
311 .env
312 .var(&dotted)
313 .or_else(|| self.env.var(&underscored))
314 .or_else(|| self.env.var(name))
315 .or_else(|| self.cnf.get(name, &self.progname).map(str::to_owned))
316 .or_else(|| self.builtins.get(name).cloned())?;
317 Some(self.expand_tilde(&value))
319 }
320
321 fn expand_vars(&self, s: &str) -> String {
323 expand::expand_vars(s, &|name| self.raw_var(name), 0)
324 }
325
326 fn expand_tilde(&self, s: &str) -> String {
327 if let Some(rest) = s.strip_prefix('~')
328 && (rest.is_empty() || rest.starts_with('/'))
329 {
330 let home = self.env.var("HOME").unwrap_or_default();
331 return format!("{home}{rest}");
332 }
333 s.to_owned()
334 }
335
336 pub fn search_path(&mut self, format: FileFormat) -> &[String] {
339 if !self.paths.contains_key(&format) {
340 let info = format.info();
341 let mut raw = None;
345 for var in info.env_vars {
346 if let Some(env) = self.env_var(var) {
347 let below = self
348 .cnf_var(var)
349 .unwrap_or_else(|| info.default_path.to_owned());
350 raw = Some(expand::expand_default(&self.expand_tilde(&env), &below));
351 break;
352 }
353 if let Some(v) = self.raw_var(var) {
354 raw = Some(v);
355 break;
356 }
357 }
358 let raw = raw.unwrap_or_else(|| info.default_path.to_owned());
359 let expanded = self.expand_vars(&raw);
360 let elts: Vec<String> = expand::brace_expand_path(&expanded)
361 .into_iter()
362 .map(|e| match e.strip_prefix("!!") {
363 Some(rest) => format!("!!{}", self.expand_tilde(rest)),
364 None => self.expand_tilde(&e),
365 })
366 .filter(|e| !e.is_empty())
367 .collect();
368 self.paths.insert(format, elts);
369 }
370 &self.paths[&format]
371 }
372
373 pub fn find_file(&mut self, name: &str, format: FileFormat) -> Option<PathBuf> {
376 let candidates = candidate_names(name, format);
377 if is_absolute_or_explicit_relative(name) {
379 return candidates.iter().map(PathBuf::from).find(|p| p.is_file());
380 }
381 let path = self.search_path(format).to_vec();
382 for elt in &path {
383 if let Some(found) = self.search_element(elt, &candidates) {
384 return Some(found);
385 }
386 }
387 None
388 }
389
390 fn search_element(&mut self, elt: &str, names: &[String]) -> Option<PathBuf> {
393 let (db_only, elt) = match elt.strip_prefix("!!") {
394 Some(rest) => (true, rest),
395 None => (false, elt),
396 };
397 if let Some(db) = self.dbs.iter().find(|db| db.covers(elt)) {
399 for name in names {
400 if let Some(found) = db.lookup(name, elt) {
401 return Some(found);
402 }
403 }
404 if db_only {
405 return None;
406 }
407 } else if db_only {
408 return None;
409 }
410 for dir in self.dirs.expand(elt) {
411 for name in names {
412 let candidate = Path::new(&dir).join(name);
413 if candidate.is_file() {
414 return Some(candidate);
415 }
416 }
417 }
418 None
419 }
420}
421
422fn content_key(b: &[u8]) -> u64 {
424 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
425 for &x in b {
426 h ^= u64::from(x);
427 h = h.wrapping_mul(0x0100_0000_01b3);
428 }
429 h
430}
431
432fn write_atomically(path: &Path, data: &[u8]) {
435 if let Some(dir) = path.parent() {
436 let _ = std::fs::create_dir_all(dir);
437 }
438 let tmp = path.with_extension(format!("tmp{}", std::process::id()));
439 if std::fs::write(&tmp, data).is_ok() && std::fs::rename(&tmp, path).is_err() {
440 let _ = std::fs::remove_file(&tmp);
441 }
442}
443
444fn candidate_names(name: &str, format: FileFormat) -> Vec<String> {
447 let info = format.info();
448 let has_suffix = info.suffixes.iter().any(|s| name.ends_with(s));
449 let mut out = Vec::new();
450 if !has_suffix {
451 for s in info.suffixes {
452 out.push(format!("{name}{s}"));
453 }
454 }
455 out.push(name.to_owned());
456 out
457}
458
459fn is_absolute_or_explicit_relative(name: &str) -> bool {
460 name.starts_with('/') || name.starts_with("./") || name.starts_with("../")
461}
462
463fn parent(p: &str) -> String {
464 let trimmed = p.trim_end_matches('/');
465 if trimmed.is_empty() && p.starts_with('/') {
466 return "/".to_owned();
467 }
468 match trimmed.rfind('/') {
469 Some(0) => "/".to_owned(),
470 Some(i) => trimmed[..i].to_owned(),
471 None => ".".to_owned(),
472 }
473}
474
475fn locate_kpsewhich_dir() -> Option<PathBuf> {
477 let path = std::env::var_os("PATH")?;
478 for dir in std::env::split_paths(&path) {
479 let candidate = dir.join("kpsewhich");
480 if candidate.is_file() {
481 let real = std::fs::canonicalize(&candidate).unwrap_or(candidate);
482 return real.parent().map(Path::to_path_buf);
483 }
484 }
485 None
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn candidates_add_suffix_first() {
494 assert_eq!(
495 candidate_names("article.cls", FileFormat::Tex),
496 vec!["article.cls.tex", "article.cls"]
497 );
498 assert_eq!(candidate_names("foo.tex", FileFormat::Tex), vec!["foo.tex"]);
499 }
500
501 #[test]
502 fn parent_dirs() {
503 assert_eq!(parent("/usr/bin"), "/usr");
504 assert_eq!(parent("/usr"), "/");
505 assert_eq!(parent("/"), "/");
506 }
507}