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