#![deny(missing_docs)]
#[cfg(kpathsea_linked)]
use kpathsea_sys::*;
#[cfg(kpathsea_linked)]
use std::ffi::{CStr, CString};
use std::path::PathBuf;
mod subprocess;
use subprocess::SubprocessKpse;
pub type Result<T> = std::result::Result<T, &'static str>;
#[cfg(not(kpathsea_linked))]
const NO_BACKEND: &str = "kpathsea: no libkpathsea is linked and no `kpsewhich` executable is \
available — TeX file lookups cannot resolve. Install a TeX distribution, \
or point KPSEWHICH at its kpsewhich.";
pub type Format = u32;
pub mod formats {
use super::Format;
pub const TEX: Format = 26;
pub const BIB: Format = 6;
pub const BST: Format = 7;
pub const CNF: Format = 8;
pub const FONTMAP: Format = 11;
pub const TYPE1: Format = 32;
pub const TRUETYPE: Format = 36;
}
#[cfg(all(test, kpathsea_linked, not(windows)))]
mod format_drift {
#[test]
fn formats_match_libkpathsea() {
use kpathsea_sys::*;
assert_eq!(crate::formats::TEX, kpse_file_format_type_kpse_tex_format);
assert_eq!(crate::formats::BIB, kpse_file_format_type_kpse_bib_format);
assert_eq!(crate::formats::BST, kpse_file_format_type_kpse_bst_format);
assert_eq!(crate::formats::CNF, kpse_file_format_type_kpse_cnf_format);
assert_eq!(
crate::formats::FONTMAP,
kpse_file_format_type_kpse_fontmap_format
);
assert_eq!(
crate::formats::TYPE1,
kpse_file_format_type_kpse_type1_format
);
assert_eq!(
crate::formats::TRUETYPE,
kpse_file_format_type_kpse_truetype_format
);
}
}
fn kpsewhich_format_name(format: Format) -> Option<&'static str> {
match format {
formats::TEX => Some("tex"),
formats::BIB => Some("bib"),
formats::BST => Some("bst"),
formats::CNF => Some("cnf"),
formats::FONTMAP => Some("map"),
formats::TYPE1 => Some("type1 fonts"),
formats::TRUETYPE => Some("truetype fonts"),
_ => None,
}
}
enum Backend {
#[cfg(kpathsea_linked)]
InProcess(kpathsea),
Subprocess(SubprocessKpse),
}
pub struct Kpaths(Backend);
unsafe impl Send for Kpaths {}
fn kpsewhich_executable() -> Result<PathBuf> {
let name = std::env::var("KPSEWHICH").unwrap_or_else(|_| "kpsewhich".to_string());
which::which(&name).map_err(|_| "Error finding kpsewhich executable")
}
#[cfg(kpathsea_linked)]
fn path_to_cstring(path: PathBuf) -> Result<CString> {
CString::new(path.to_string_lossy().into_owned()).map_err(|_| "path contains a NUL byte")
}
#[cfg(kpathsea_linked)]
fn get_kpsewhich_path() -> Result<CString> {
path_to_cstring(kpsewhich_executable()?)
}
#[cfg(kpathsea_linked)]
fn current_exe_program_name() -> Result<CString> {
path_to_cstring(std::env::current_exe().map_err(|_| "current executable path is unavailable")?)
}
#[cfg(kpathsea_linked)]
fn program_name_anchor(
kpsewhich: Result<CString>,
current_exe: impl FnOnce() -> Result<CString>,
) -> CString {
kpsewhich
.or_else(|_| current_exe())
.unwrap_or_else(|_| CString::from(c"kpsewhich"))
}
#[cfg(all(test, kpathsea_linked))]
mod anchor_tiers {
use super::*;
#[test]
fn prefers_kpsewhich_and_leaves_current_exe_unevaluated() {
let mut consulted = false;
let got = program_name_anchor(Ok(CString::new("/usr/bin/kpsewhich").unwrap()), || {
consulted = true;
Ok(CString::new("/proc/self/exe").unwrap())
});
assert_eq!(got.to_str().unwrap(), "/usr/bin/kpsewhich");
assert!(
!consulted,
"current_exe must not be consulted when kpsewhich resolves"
);
}
#[test]
fn degrades_to_current_exe_when_kpsewhich_is_unresolvable() {
let got = program_name_anchor(Err("no kpsewhich"), || {
Ok(CString::new("/proc/self/exe").unwrap())
});
assert_eq!(got.to_str().unwrap(), "/proc/self/exe");
}
#[test]
fn degrades_to_a_literal_when_every_source_fails() {
let got = program_name_anchor(Err("no kpsewhich"), || Err("no current_exe"));
assert_eq!(got.to_str().unwrap(), "kpsewhich");
}
}
#[cfg(kpathsea_linked)]
static KPSE_GLOBAL_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(all(kpathsea_linked, not(windows)))]
unsafe fn filename_has_suffix_in(filename: &str, mut list: *mut const_string) -> bool {
while !list.is_null() && !unsafe { *list }.is_null() {
let suffix = unsafe { CStr::from_ptr(*list) }.to_str().unwrap();
if filename.len() > suffix.len() && filename.ends_with(suffix) {
return true;
}
list = unsafe { list.offset(1) };
}
false
}
#[cfg(all(kpathsea_linked, not(windows)))]
fn guess_format_from_filename(kpse: kpathsea, filename: &str) -> Format {
if !filename.contains('.') {
return formats::TEX;
}
for format_type in 0..kpse_file_format_type_kpse_last_format {
let format_info: &mut kpse_format_info_type =
unsafe { &mut (*kpse).format_info[format_type as usize] };
if format_info.type_.is_null() {
unsafe {
kpathsea_init_format(kpse, format_type as kpse_file_format_type);
}
}
if unsafe { filename_has_suffix_in(filename, format_info.suffix) }
|| unsafe { filename_has_suffix_in(filename, format_info.alt_suffix) }
{
return format_type as Format;
}
}
formats::TEX
}
#[cfg(all(kpathsea_linked, any(windows, test)))]
#[rustfmt::skip]
const FORMAT_SUFFIXES: &[(Format, &str)] = &[
(0, "gf"), (1, "pk"),
(3, ".tfm"), (4, ".afm"), (5, ".base"), (6, ".bib"), (7, ".bst"),
(8, ".cnf"), (9, "ls-R"), (9, "ls-r"), (10, ".fmt"), (11, ".map"),
(12, ".mem"), (13, ".mf"), (14, ".pool"), (15, ".mft"), (16, ".mp"),
(17, ".pool"), (19, ".ocp"), (20, ".ofm"), (20, ".tfm"), (21, ".opl"),
(21, ".pl"), (22, ".otp"), (23, ".ovf"), (23, ".vf"), (24, ".ovp"),
(24, ".vpl"), (25, ".eps"), (25, ".epsi"),
(26, ".tex"), (26, ".sty"), (26, ".cls"), (26, ".fd"), (26, ".aux"),
(26, ".bbl"), (26, ".def"), (26, ".clo"), (26, ".ldf"),
(28, ".pool"), (29, ".dtx"), (29, ".ins"), (30, ".pro"),
(32, ".pfa"), (32, ".pfb"), (33, ".vf"), (35, ".ist"),
(36, ".ttf"), (36, ".ttc"), (36, ".TTF"), (36, ".TTC"), (36, ".dfont"),
(37, ".t42"), (37, ".T42"), (42, ".web"), (42, ".ch"),
(43, ".w"), (43, ".web"), (43, ".ch"), (44, ".enc"), (46, ".sfd"),
(47, ".otf"), (47, ".OTF"), (49, ".lig"),
(51, ".lua"), (51, ".luatex"), (51, ".luc"), (51, ".luctex"),
(51, ".texlua"), (51, ".texluc"), (51, ".tlu"),
(52, ".fea"), (53, ".cid"), (53, ".cidmap"),
(54, ".mlbib"), (54, ".bib"), (55, ".mlbst"), (55, ".bst"),
(56, ".dll"), (56, ".so"), (57, ".ris"), (58, ".bltxml"),
];
#[cfg(all(kpathsea_linked, windows))]
fn guess_format_from_filename(_kpse: kpathsea, filename: &str) -> Format {
if !filename.contains('.') {
return formats::TEX;
}
for &(format, suffix) in FORMAT_SUFFIXES {
if filename.len() > suffix.len() && filename.ends_with(suffix) {
return format;
}
}
formats::TEX
}
#[cfg(all(test, kpathsea_linked, not(windows)))]
mod suffix_table_drift {
use super::*;
#[test]
fn format_suffixes_match_libkpathsea() {
let kpaths = Kpaths::new().expect("needs a TeX toolchain with libkpathsea");
let kpse = match &kpaths.0 {
Backend::InProcess(kpse) => *kpse,
Backend::Subprocess(_) => panic!("linked build should construct the in-process backend"),
};
let mut live: Vec<(Format, String)> = Vec::new();
for format_type in 0..kpse_file_format_type_kpse_last_format {
unsafe { kpathsea_init_format(kpse, format_type) };
let info = unsafe { &(*kpse).format_info[format_type as usize] };
for &list in &[info.suffix, info.alt_suffix] {
let mut entry = list;
while !entry.is_null() && !unsafe { *entry }.is_null() {
let suffix = unsafe { CStr::from_ptr(*entry) }
.to_str()
.unwrap()
.to_string();
live.push((format_type as Format, suffix));
entry = unsafe { entry.offset(1) };
}
}
}
let table: Vec<(Format, String)> = FORMAT_SUFFIXES
.iter()
.map(|&(format, suffix)| (format, suffix.to_string()))
.collect();
assert_eq!(
table, live,
"FORMAT_SUFFIXES drifted from the linked libkpathsea — regenerate it from this walk"
);
}
}
impl Kpaths {
pub fn new() -> Result<Self> {
#[cfg(kpathsea_linked)]
{
let program_name = program_name_anchor(get_kpsewhich_path(), current_exe_program_name);
let _guard = KPSE_GLOBAL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let kpse = unsafe { kpathsea_new() };
unsafe { kpathsea_set_program_name(kpse, program_name.as_ptr(), std::ptr::null()) }
Ok(Kpaths(Backend::InProcess(kpse)))
}
#[cfg(not(kpathsea_linked))]
{
Self::new_subprocess().map_err(|_| {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| eprintln!("{NO_BACKEND}"));
NO_BACKEND
})
}
}
pub fn new_subprocess() -> Result<Self> {
Ok(Kpaths(Backend::Subprocess(SubprocessKpse::new()?)))
}
pub fn with_kpsewhich<P: Into<PathBuf>>(path: P) -> Self {
Kpaths(Backend::Subprocess(SubprocessKpse::with_kpsewhich(
path.into(),
)))
}
pub fn is_in_process(&self) -> bool {
match &self.0 {
#[cfg(kpathsea_linked)]
Backend::InProcess(_) => true,
Backend::Subprocess(_) => false,
}
}
pub fn find_file(&self, name: &str) -> Option<String> {
match &self.0 {
#[cfg(kpathsea_linked)]
Backend::InProcess(kpse) => {
let file_format_type = guess_format_from_filename(*kpse, name);
self.find_file_with_format(name, file_format_type)
}
Backend::Subprocess(sub) => sub.find_first(&[name]),
}
}
pub fn find_first(&self, candidates: &[&str]) -> Option<String> {
match &self.0 {
#[cfg(kpathsea_linked)]
Backend::InProcess(_) => candidates.iter().find_map(|c| self.find_file(c)),
Backend::Subprocess(sub) => sub.find_first(candidates),
}
}
pub fn find_file_with_format(&self, name: &str, format: Format) -> Option<String> {
match &self.0 {
#[cfg(kpathsea_linked)]
Backend::InProcess(kpse) => {
let c_name = CString::new(name).ok()?;
let c_filename_buf = unsafe { kpathsea_find_file(*kpse, c_name.as_ptr(), format, 0) };
if !c_filename_buf.is_null() {
let c_filepath: &CStr = unsafe { CStr::from_ptr(c_filename_buf) };
let filepath = c_filepath.to_str().unwrap().to_owned();
if filepath.is_empty() {
None
} else {
Some(filepath)
}
} else {
None
}
}
Backend::Subprocess(sub) => sub.find_with_format_name(name, kpsewhich_format_name(format)),
}
}
}
impl Drop for Kpaths {
fn drop(&mut self) {
match &self.0 {
#[cfg(kpathsea_linked)]
Backend::InProcess(kpse) => {
let _guard = KPSE_GLOBAL_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
unsafe { kpathsea_finish(*kpse) }
}
Backend::Subprocess(_) => {}
}
}
}