use crate::Kpathsea;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Access {
Read,
Write,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
pub var: &'static str,
pub value: String,
}
impl Refusal {
pub fn message(&self, name: &str) -> String {
let verb = match self.var {
"openout_any" => "Not writing to",
_ => "Not reading from",
};
format!(
"{verb} {name} ({} = {}; no extended check).",
self.var, self.value
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Level {
Any,
Restricted,
Paranoid,
}
fn level(value: &str) -> Level {
match value.bytes().next() {
Some(b'a' | b'y' | b'1') => Level::Any,
Some(b'r' | b'n' | b'0') => Level::Restricted,
_ => Level::Paranoid,
}
}
fn has_dot_component(name: &str) -> bool {
let b = name.as_bytes();
(0..b.len()).any(|i| {
b[i] == b'.'
&& (i == 0 || b[i - 1] == b'/')
&& !(b[i + 1..].starts_with(b"/") || b[i + 1..].starts_with(b"./"))
})
}
fn has_parent_component(name: &str) -> bool {
name.starts_with("../") || name.contains("/../")
}
fn in_dir(name: &str, dir: &str) -> bool {
!dir.is_empty()
&& name.starts_with(dir)
&& matches!(name.as_bytes().get(dir.len()), None | Some(b'/'))
}
impl Kpathsea {
pub fn set_output_directory(&mut self, dir: Option<String>) {
self.output_directory = Some(dir);
}
fn output_directory(&self) -> Option<String> {
match &self.output_directory {
Some(d) => d.clone(),
None => self.getenv("TEXMF_OUTPUT_DIRECTORY"),
}
}
pub fn name_ok(&self, name: &str, access: Access, extended: bool) -> Result<(), Refusal> {
let (var, default) = match access {
Access::Read => ("openin_any", "a"),
Access::Write => ("openout_any", "p"),
};
let value = self.var_value(var).unwrap_or_else(|| default.to_owned());
let level = level(&value);
let refusal = || Refusal {
var,
value: value.clone(),
};
if self.normal_name_ok(name, level) {
return Ok(());
}
if extended && name.starts_with('/') {
if level == Level::Restricted {
return Ok(());
}
let dirs = [
self.var_value("TEXMFOUTPUT"),
self.output_directory(),
self.var_value("TEXMFVAR"),
self.var_value("TEXMFSYSVAR"),
];
if !has_parent_component(name) && dirs.iter().flatten().any(|d| in_dir(name, d)) {
return Ok(());
}
}
Err(refusal())
}
fn normal_name_ok(&self, name: &str, level: Level) -> bool {
match level {
Level::Any => true,
Level::Restricted => !has_dot_component(name),
Level::Paranoid => {
if has_dot_component(name) || has_parent_component(name) {
return false;
}
!name.starts_with('/')
|| [self.var_value("TEXMFOUTPUT"), self.output_directory()]
.iter()
.flatten()
.any(|d| in_dir(name, d))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dot_components() {
for n in [".x", "a/.x", "..x", "a/..", ".", "..", "/.x/y", ".../x"] {
assert!(has_dot_component(n), "{n}");
}
for n in ["x", "./x", "../x", "a/../x", "a/./x", "./", "x/", "/tmp/x"] {
assert!(!has_dot_component(n), "{n}");
}
}
#[test]
fn directories() {
assert!(in_dir("/tmp/x", "/tmp"));
assert!(in_dir("/tmp", "/tmp"));
assert!(!in_dir("/tmpx/y", "/tmp"));
assert!(!in_dir("/tmp/x", "/tmp/"));
assert!(!in_dir("/tmp/x", ""));
}
}