1#![forbid(unsafe_code)]
18
19mod casefold;
20mod cnf;
21mod db;
22mod expand;
23mod format;
24mod safe;
25
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28
29pub use format::FileFormat;
30pub use safe::{Access, Refusal};
31
32pub type Shared = std::rc::Rc<std::cell::RefCell<Kpathsea>>;
34
35pub fn shared(k: Kpathsea) -> Shared {
37 std::rc::Rc::new(std::cell::RefCell::new(k))
38}
39
40use cnf::Cnf;
41use db::{DirCache, LsR};
42
43#[derive(Debug, thiserror::Error)]
45pub enum Error {
46 #[error("no texmf.cnf found (searched: {0})")]
48 NoConfig(String),
49 #[error("reading {path}: {source}")]
51 Io {
52 path: PathBuf,
54 source: std::io::Error,
56 },
57}
58
59const DEFAULT_TEXMFCNF: &str = "{$SELFAUTOLOC,$SELFAUTOLOC/share/texmf-local/web2c,\
64$SELFAUTOLOC/share/texmf-dist/web2c,$SELFAUTOLOC/share/texmf/web2c,\
65$SELFAUTOLOC/texmf-local/web2c,$SELFAUTOLOC/texmf-dist/web2c,$SELFAUTOLOC/texmf/web2c,\
66$SELFAUTODIR,$SELFAUTODIR/share/texmf-local/web2c,$SELFAUTODIR/share/texmf-dist/web2c,\
67$SELFAUTODIR/share/texmf/web2c,$SELFAUTODIR/texmf-local/web2c,$SELFAUTODIR/texmf-dist/web2c,\
68$SELFAUTODIR/texmf/web2c,$SELFAUTOGRANDPARENT/texmf-local/web2c,$SELFAUTOPARENT,\
69$SELFAUTOPARENT/share/texmf-local/web2c,$SELFAUTOPARENT/share/texmf-dist/web2c,\
70$SELFAUTOPARENT/share/texmf/web2c,$SELFAUTOPARENT/texmf-local/web2c,\
71$SELFAUTOPARENT/texmf-dist/web2c,$SELFAUTOPARENT/texmf/web2c}:\
72/etc/texmf/web2c:/usr/local/share/texmf/web2c:/usr/share/texlive/texmf-dist/web2c";
73
74pub trait Env {
76 fn var(&self, name: &str) -> Option<String>;
78}
79
80#[derive(Debug, Default, Clone, Copy)]
82pub struct ProcessEnv;
83
84impl Env for ProcessEnv {
85 fn var(&self, name: &str) -> Option<String> {
86 std::env::var(name).ok()
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct Options {
93 pub progname: String,
95 pub engine: String,
97 pub selfautoloc: Option<PathBuf>,
100 pub cache_dir: Option<PathBuf>,
102 pub cnf_lines: Vec<String>,
105}
106
107impl Options {
108 pub fn lualatex() -> Self {
110 Options {
111 progname: "lualatex".into(),
112 engine: "luahbtex".into(),
113 selfautoloc: None,
114 cache_dir: None,
115 cnf_lines: Vec::new(),
116 }
117 }
118}
119
120pub struct Kpathsea {
122 cnf: Cnf,
123 env: Box<dyn Env>,
124 progname: String,
125 engine: String,
126 builtins: HashMap<String, String>,
128 dbs: Vec<LsR>,
129 dirs: DirCache,
130 paths: HashMap<FileFormat, Vec<String>>,
132 cache_dir: Option<PathBuf>,
134 output_directory: Option<Option<String>>,
136 casefold: bool,
139}
140
141impl std::fmt::Debug for Kpathsea {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 f.debug_struct("Kpathsea")
144 .field("progname", &self.progname)
145 .field("engine", &self.engine)
146 .finish_non_exhaustive()
147 }
148}
149
150impl Kpathsea {
151 pub fn new(options: Options) -> Result<Self, Error> {
153 Self::with_env(options, Box::new(ProcessEnv))
154 }
155
156 pub fn with_env(options: Options, env: Box<dyn Env>) -> Result<Self, Error> {
158 let cache_dir = options.cache_dir.clone();
159 let selfautoloc = options
160 .selfautoloc
161 .clone()
162 .or_else(locate_kpsewhich_dir)
163 .unwrap_or_else(|| PathBuf::from("/usr/bin"));
164 let mut builtins = HashMap::new();
165 let loc = selfautoloc.to_string_lossy().into_owned();
166 let dir = parent(&loc);
167 let par = parent(&dir);
168 let grand = parent(&par);
169 builtins.insert("SELFAUTOLOC".to_owned(), loc);
170 builtins.insert("SELFAUTODIR".to_owned(), dir);
171 builtins.insert("SELFAUTOPARENT".to_owned(), par);
172 builtins.insert("SELFAUTOGRANDPARENT".to_owned(), grand);
173 builtins.insert("progname".to_owned(), options.progname.clone());
174 builtins.insert("engine".to_owned(), options.engine.clone());
175
176 let mut cnf = Cnf::default();
177 for line in &options.cnf_lines {
179 cnf.parse(line);
180 }
181 let mut kpse = Kpathsea {
182 cnf,
183 env,
184 progname: options.progname,
185 engine: options.engine,
186 builtins,
187 dbs: Vec::new(),
188 dirs: DirCache::default(),
189 paths: HashMap::new(),
190 cache_dir,
191 output_directory: None,
192 casefold: false,
193 };
194 let t0 = std::time::Instant::now();
195 kpse.load_cnf()?;
196 kpse.casefold = casefold::enabled(kpse.var_value("texmf_casefold_search").as_deref());
197 let t1 = std::time::Instant::now();
198 kpse.load_dbs()?;
199 if std::env::var_os("RETEX_KPSE_TIMING").is_some() {
200 eprintln!("kpse: cnf {:?}, dbs {:?}", t1 - t0, t1.elapsed());
201 }
202 Ok(kpse)
203 }
204
205 pub fn progname(&self) -> &str {
207 &self.progname
208 }
209
210 pub fn engine(&self) -> &str {
212 &self.engine
213 }
214
215 fn load_cnf(&mut self) -> Result<(), Error> {
216 let raw = self
217 .getenv("TEXMFCNF")
218 .map(|v| expand::expand_default(&v, DEFAULT_TEXMFCNF))
219 .unwrap_or_else(|| DEFAULT_TEXMFCNF.to_owned());
220 let expanded = self.expand_vars(&raw);
221 let mut found_any = false;
222 for dir in expand::brace_expand_path(&expanded) {
223 let dir = self.expand_tilde(&dir);
224 let file = Path::new(&dir).join("texmf.cnf");
225 match std::fs::read_to_string(&file) {
226 Ok(text) => {
227 found_any = true;
228 self.cnf.parse(&text);
229 }
230 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
231 Err(e) if e.kind() == std::io::ErrorKind::NotADirectory => {}
232 Err(source) => return Err(Error::Io { path: file, source }),
233 }
234 }
235 if found_any {
236 Ok(())
237 } else {
238 Err(Error::NoConfig(expanded))
239 }
240 }
241
242 fn load_dbs(&mut self) -> Result<(), Error> {
243 let dbs = self.var_value("TEXMFDBS").unwrap_or_default();
244 let mut seen = Vec::new();
245 for elt in expand::brace_expand_path(&dbs) {
246 let elt = elt.trim_start_matches("!!");
247 let elt = self.expand_tilde(elt);
248 let root = elt.trim_end_matches('/').to_owned();
249 if root.is_empty() || seen.contains(&root) {
250 continue;
251 }
252 seen.push(root.clone());
253 for name in ["ls-R", "ls-r"] {
254 let file = Path::new(&root).join(name);
255 let Ok(meta) = std::fs::metadata(&file) else {
256 continue;
257 };
258 let mtime = meta
259 .modified()
260 .ok()
261 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
262 .map_or((0, 0), |d| (d.as_secs() as i64, d.subsec_nanos()));
263 let cache_file = self
264 .cache_dir
265 .as_ref()
266 .map(|d| d.join(format!("lsr-{:016x}.idx", content_key(root.as_bytes()))));
267 if let Some(cf) = &cache_file
268 && let Ok(bytes) = std::fs::read(cf)
269 && let Some(db) = LsR::from_buf(bytes, meta.len(), mtime)
270 {
271 self.dbs.push(db);
272 break;
273 }
274 match std::fs::read(&file) {
275 Ok(bytes) => {
276 let db = LsR::parse(&root, &bytes, meta.len(), mtime);
277 if let Some(cf) = &cache_file {
278 write_atomically(cf, db.bytes());
279 }
280 self.dbs.push(db);
281 break;
282 }
283 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
284 Err(source) => return Err(Error::Io { path: file, source }),
285 }
286 }
287 }
288 Ok(())
289 }
290
291 pub fn cache_dir(&self) -> Option<&Path> {
293 self.cache_dir.as_deref()
294 }
295
296 pub fn installed_names(&self, suffix: &str) -> Vec<String> {
299 let mut out: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
300 for db in &self.dbs {
301 out.extend(db.names().filter(|n| n.ends_with(suffix)));
302 }
303 out.into_iter().map(str::to_owned).collect()
304 }
305
306 pub fn var_value(&self, name: &str) -> Option<String> {
309 let raw = self.raw_var(name)?;
310 Some(self.expand_vars(&raw))
311 }
312
313 fn getenv(&self, name: &str) -> Option<String> {
317 self.env.var(name).filter(|v| !v.is_empty())
318 }
319
320 fn env_var(&self, name: &str) -> Option<String> {
323 let dotted = format!("{name}.{}", self.progname);
324 let underscored = format!("{name}_{}", self.progname);
325 self.getenv(&dotted)
326 .or_else(|| self.getenv(&underscored))
327 .or_else(|| self.getenv(name))
328 }
329
330 fn cnf_var(&self, name: &str) -> Option<String> {
332 let value = self
333 .cnf
334 .get(name, &self.progname)
335 .map(str::to_owned)
336 .or_else(|| self.builtins.get(name).cloned())?;
337 Some(self.expand_tilde(&value))
338 }
339
340 fn raw_var(&self, name: &str) -> Option<String> {
344 if matches!(
348 name,
349 "progname"
350 | "engine"
351 | "SELFAUTOLOC"
352 | "SELFAUTODIR"
353 | "SELFAUTOPARENT"
354 | "SELFAUTOGRANDPARENT"
355 ) {
356 return self.builtins.get(name).cloned();
357 }
358 let dotted = format!("{name}.{}", self.progname);
359 let underscored = format!("{name}_{}", self.progname);
360 let value = self
361 .getenv(&dotted)
362 .or_else(|| self.getenv(&underscored))
363 .or_else(|| self.getenv(name))
364 .or_else(|| self.cnf.get(name, &self.progname).map(str::to_owned))
365 .or_else(|| self.builtins.get(name).cloned())?;
366 Some(self.expand_tilde(&value))
368 }
369
370 fn expand_vars(&self, s: &str) -> String {
372 expand::expand_vars(s, &|name| self.raw_var(name), 0)
373 }
374
375 fn expand_tilde(&self, s: &str) -> String {
376 if let Some(rest) = s.strip_prefix('~')
377 && (rest.is_empty() || rest.starts_with('/'))
378 {
379 let home = self.getenv("HOME").unwrap_or_default();
380 return format!("{home}{rest}");
381 }
382 s.to_owned()
383 }
384
385 pub fn search_path(&mut self, format: FileFormat) -> &[String] {
388 if !self.paths.contains_key(&format) {
389 let info = format.info();
390 let mut raw = None;
396 let vars = format.variables(&self.progname);
397 if let Some(env) = vars.iter().find_map(|v| self.env_var(v)) {
398 let below = vars
399 .iter()
400 .find_map(|v| self.cnf_var(v))
401 .unwrap_or_else(|| info.default_path.to_owned());
402 raw = Some(expand::expand_default(&self.expand_tilde(&env), &below));
403 }
404 if raw.is_none() {
405 raw = vars.iter().find_map(|v| self.raw_var(v));
406 }
407 let raw = raw.unwrap_or_else(|| info.default_path.to_owned());
408 let expanded = self.expand_vars(&raw);
409 let elts: Vec<String> = expand::brace_expand_path(&expanded)
410 .into_iter()
411 .map(|e| match e.strip_prefix("!!") {
412 Some(rest) => format!("!!{}", self.expand_tilde(rest)),
413 None => self.expand_tilde(&e),
414 })
415 .filter(|e| !e.is_empty())
416 .collect();
417 self.paths.insert(format, elts);
418 }
419 &self.paths[&format]
420 }
421
422 pub fn find_file(&mut self, name: &str, format: FileFormat) -> Option<PathBuf> {
425 let candidates = candidate_names(name, format);
426 if is_absolute_or_explicit_relative(name) {
428 let exact = candidates.iter().map(PathBuf::from).find(|p| p.is_file());
429 if exact.is_some() || !self.casefold {
430 return exact;
431 }
432 return candidates.iter().find_map(|c| {
433 let p = Path::new(c);
434 let dir = p.parent().filter(|d| !d.as_os_str().is_empty())?;
435 casefold::find(dir, p.file_name()?.to_str()?)
436 });
437 }
438 let path = self.search_path(format).to_vec();
439 for elt in &path {
440 if let Some(found) = self.search_element(elt, &candidates) {
441 return Some(found);
442 }
443 }
444 None
445 }
446
447 fn search_element(&mut self, elt: &str, names: &[String]) -> Option<PathBuf> {
450 let (db_only, elt) = match elt.strip_prefix("!!") {
451 Some(rest) => (true, rest),
452 None => (false, elt),
453 };
454 if let Some(db) = self.dbs.iter().find(|db| db.covers(elt)) {
456 for name in names {
457 if let Some(found) = db.lookup(name, elt) {
458 return Some(found);
459 }
460 }
461 if db_only {
462 return None;
463 }
464 } else if db_only {
465 return None;
466 }
467 let dirs = self.dirs.expand(elt);
468 for dir in &dirs {
469 for name in names {
470 let candidate = Path::new(dir).join(name);
471 if candidate.is_file() {
472 return Some(candidate);
473 }
474 }
475 }
476 if self.casefold {
477 for dir in &dirs {
478 for name in names {
479 if let Some(found) = casefold::find(Path::new(dir), name) {
480 return Some(found);
481 }
482 }
483 }
484 }
485 None
486 }
487}
488
489fn content_key(b: &[u8]) -> u64 {
491 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
492 for &x in b {
493 h ^= u64::from(x);
494 h = h.wrapping_mul(0x0100_0000_01b3);
495 }
496 h
497}
498
499fn write_atomically(path: &Path, data: &[u8]) {
502 if let Some(dir) = path.parent() {
503 let _ = std::fs::create_dir_all(dir);
504 }
505 let tmp = path.with_extension(format!("tmp{}", std::process::id()));
506 if std::fs::write(&tmp, data).is_ok() && std::fs::rename(&tmp, path).is_err() {
507 let _ = std::fs::remove_file(&tmp);
508 }
509}
510
511fn candidate_names(name: &str, format: FileFormat) -> Vec<String> {
514 let info = format.info();
515 let has_suffix = info
516 .suffixes
517 .iter()
518 .chain(info.alt_suffixes)
519 .any(|s| name.ends_with(s));
520 let mut out = Vec::new();
521 if !has_suffix {
522 for s in info.suffixes {
523 out.push(format!("{name}{s}"));
524 }
525 }
526 if has_suffix || info.bare {
527 out.push(name.to_owned());
528 }
529 out
530}
531
532fn is_absolute_or_explicit_relative(name: &str) -> bool {
533 name.starts_with('/') || name.starts_with("./") || name.starts_with("../")
534}
535
536fn parent(p: &str) -> String {
537 let trimmed = p.trim_end_matches('/');
538 if trimmed.is_empty() && p.starts_with('/') {
539 return "/".to_owned();
540 }
541 match trimmed.rfind('/') {
542 Some(0) => "/".to_owned(),
543 Some(i) => trimmed[..i].to_owned(),
544 None => ".".to_owned(),
545 }
546}
547
548fn locate_kpsewhich_dir() -> Option<PathBuf> {
550 let path = std::env::var_os("PATH")?;
551 for dir in std::env::split_paths(&path) {
552 let candidate = dir.join("kpsewhich");
553 if candidate.is_file() {
554 let real = std::fs::canonicalize(&candidate).unwrap_or(candidate);
555 return real.parent().map(Path::to_path_buf);
556 }
557 }
558 None
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564
565 #[test]
566 fn candidates_add_suffix_first() {
567 assert_eq!(
569 candidate_names("article.cls", FileFormat::Tex),
570 vec!["article.cls"]
571 );
572 assert_eq!(
573 candidate_names("a.foo", FileFormat::Tex),
574 vec!["a.foo.tex", "a.foo"]
575 );
576 assert_eq!(candidate_names("foo.tex", FileFormat::Tex), vec!["foo.tex"]);
577 }
578
579 #[test]
580 fn parent_dirs() {
581 assert_eq!(parent("/usr/bin"), "/usr");
582 assert_eq!(parent("/usr"), "/");
583 assert_eq!(parent("/"), "/");
584 }
585}