use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn resolve_bibliography_file(requested: &Path, base_dir: Option<&Path>) -> Option<PathBuf> {
if requested.is_file() {
return Some(requested.to_path_buf());
}
let query = search_query(requested, base_dir);
for variable in ["BIBINPUTS", "TEXBIB"] {
if let Some(value) = std::env::var_os(variable)
&& let Some(path) = resolve_plain_search_path(query, base_dir, &value)
{
return Some(path);
}
}
resolve_with_kpsewhich(query, base_dir)
}
fn search_query<'a>(requested: &'a Path, base_dir: Option<&Path>) -> &'a Path {
base_dir
.and_then(|base| requested.strip_prefix(base).ok())
.unwrap_or(requested)
}
fn resolve_plain_search_path(
query: &Path,
base_dir: Option<&Path>,
value: &OsStr,
) -> Option<PathBuf> {
for entry in std::env::split_paths(value) {
if entry.as_os_str().is_empty() || has_kpathsea_syntax(&entry) {
continue;
}
let root = if entry.is_absolute() {
entry
} else {
base_dir.unwrap_or_else(|| Path::new(".")).join(entry)
};
let candidate = root.join(query);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn has_kpathsea_syntax(path: &Path) -> bool {
let text = path.as_os_str().to_string_lossy();
text.contains("//")
|| text.contains('{')
|| text.contains('}')
|| text.contains('$')
|| text.starts_with('~')
|| text.starts_with("!!")
}
fn resolve_with_kpsewhich(query: &Path, base_dir: Option<&Path>) -> Option<PathBuf> {
let mut command = Command::new("kpsewhich");
command
.arg("--progname=bibtex")
.arg("--format=bib")
.arg("--")
.arg(query);
if let Some(base) = base_dir {
command.current_dir(base);
}
let output = command.output().ok()?;
if !output.status.success() {
return None;
}
let found = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if found.is_empty() {
return None;
}
let path = PathBuf::from(found);
let path = if path.is_absolute() {
path
} else {
base_dir.unwrap_or_else(|| Path::new(".")).join(path)
};
path.is_file().then_some(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_search_path_resolves_relative_to_document_directory() {
let dir = tempfile::tempdir().unwrap();
let bib_dir = dir.path().join("bibliographies");
std::fs::create_dir(&bib_dir).unwrap();
std::fs::write(bib_dir.join("shared.bib"), "@article{key}\n").unwrap();
assert_eq!(
resolve_plain_search_path(
Path::new("shared.bib"),
Some(dir.path()),
OsStr::new("bibliographies"),
),
Some(bib_dir.join("shared.bib"))
);
}
#[test]
fn plain_search_path_allows_nonleading_tilde() {
let dir = tempfile::tempdir().unwrap();
let bib_dir = dir.path().join("RUNNER~1");
std::fs::create_dir(&bib_dir).unwrap();
std::fs::write(bib_dir.join("shared.bib"), "@article{key}\n").unwrap();
assert_eq!(
resolve_plain_search_path(Path::new("shared.bib"), None, bib_dir.as_os_str()),
Some(bib_dir.join("shared.bib"))
);
}
#[test]
fn kpathsea_specific_elements_are_not_guessed() {
assert!(has_kpathsea_syntax(Path::new("/texmf/bibtex//")));
assert!(has_kpathsea_syntax(Path::new("$TEXMF/bibtex/bib")));
assert!(!has_kpathsea_syntax(Path::new("/data/bibliographies")));
}
}