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