#![cfg(feature = "std")]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::resource::{Resource, ResourceError, ResourceKind, ResourceProvider, ResourceRequest};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Fmt {
Tex,
Tfm,
OpenType,
Enc,
Map,
}
impl Fmt {
fn prefixes(self) -> &'static [&'static str] {
match self {
Fmt::Tex => &["tex/xelatex", "tex/latex", "tex/xetex", "tex/generic", "tex"],
Fmt::Tfm => &["fonts/tfm"],
Fmt::OpenType => &["fonts/opentype", "fonts/truetype"],
Fmt::Enc => &["fonts/enc"],
Fmt::Map => &["fonts/map"],
}
}
}
#[derive(Debug)]
pub struct TexmfResources {
root: PathBuf,
index: HashMap<String, Vec<String>>,
}
impl TexmfResources {
#[must_use]
pub fn from_root(root: impl Into<PathBuf>) -> Option<Self> {
let root = root.into();
let bytes = std::fs::read(root.join("ls-R")).ok()?;
let text = String::from_utf8_lossy(&bytes);
let mut index: HashMap<String, Vec<String>> = HashMap::new();
let mut cur_dir = String::new();
for line in text.lines() {
let line = line.trim_end();
if line.is_empty() || line.starts_with('%') {
continue;
}
if let Some(dir) = line.strip_suffix(':') {
cur_dir = dir.strip_prefix("./").unwrap_or(dir).to_string();
continue;
}
index
.entry(line.to_string())
.or_default()
.push(cur_dir.clone());
}
if index.is_empty() {
return None;
}
Some(Self { root, index })
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn len(&self) -> usize {
self.index.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
fn normalize(name: &str) -> String {
let mut n = name.trim();
loop {
if let Some(s) = n.strip_prefix("./") {
n = s;
} else if let Some(s) = n.strip_prefix("[]") {
n = s;
} else if let Some(s) = n.strip_prefix(':') {
n = s;
} else {
break;
}
}
let n = n.trim_matches(|c| c == '[' || c == ']' || c == '"' || c == '\'');
n.rsplit(['/', '\\']).next().unwrap_or(n).to_string()
}
fn format_for(kind: ResourceKind, filename: &str) -> Fmt {
let lower = filename.to_ascii_lowercase();
match kind {
ResourceKind::Encoding => Fmt::Enc,
ResourceKind::Map => Fmt::Map,
ResourceKind::Font => {
if lower.ends_with(".otf") || lower.ends_with(".ttf") || lower.ends_with(".otc") {
Fmt::OpenType
} else {
Fmt::Tfm
}
}
_ => {
if lower.ends_with(".enc") {
Fmt::Enc
} else if lower.ends_with(".map") {
Fmt::Map
} else if lower.ends_with(".tfm") {
Fmt::Tfm
} else if lower.ends_with(".otf") || lower.ends_with(".ttf") {
Fmt::OpenType
} else {
Fmt::Tex
}
}
}
}
fn resolve(&self, filename: &str, fmt: Fmt) -> Option<PathBuf> {
let dirs = self.index.get(filename)?;
for prefix in fmt.prefixes() {
for dir in dirs {
if dir == prefix || dir.strip_prefix(prefix).is_some_and(|r| r.starts_with('/')) {
return Some(self.root.join(dir).join(filename));
}
}
}
None
}
fn candidates(request: &ResourceRequest) -> Vec<String> {
let base = Self::normalize(&request.canonical_name());
let mut out = vec![base.clone()];
if Path::new(&base).extension().is_none() {
let exts: &[&str] = match request.kind {
ResourceKind::Package => &[".sty", ".tex", ".def", ".ltx"],
ResourceKind::Class => &[".cls"],
ResourceKind::FontDefinition => &[".fd"],
ResourceKind::PackageSupport => &[".def", ".cfg", ".ldf", ".clo", ".sty", ".tex"],
ResourceKind::Config => &[".cfg", ".cnf", ".tex"],
ResourceKind::Encoding => &[".enc"],
ResourceKind::Map => &[".map"],
ResourceKind::Font => &[".tfm", ".otf", ".ttf"],
ResourceKind::TexInput => &[".tex", ".ltx", ".def", ".sty", ".cfg", ".fd"],
_ => &[".tex", ".sty", ".def", ".cfg", ".ltx", ".fd", ".cls", ".enc"],
};
for e in exts {
out.push(format!("{base}{e}"));
}
}
out
}
}
impl ResourceProvider for TexmfResources {
fn read_request(&self, request: &ResourceRequest) -> Result<Resource, ResourceError> {
for cand in Self::candidates(request) {
let fmt = Self::format_for(request.kind, &cand);
if let Some(path) = self.resolve(&cand, fmt) {
if let Ok(bytes) = std::fs::read(&path) {
return Ok(Resource::from_request(request, bytes));
}
}
}
Err(ResourceError::NotFound {
name: request.canonical_name(),
kind: request.kind,
})
}
}