1use crate::Kpathsea;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Access {
30 Read,
32 Write,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Refusal {
39 pub var: &'static str,
41 pub value: String,
43}
44
45impl Refusal {
46 pub fn message(&self, name: &str) -> String {
49 let verb = match self.var {
50 "openout_any" => "Not writing to",
51 _ => "Not reading from",
52 };
53 format!(
54 "{verb} {name} ({} = {}; no extended check).",
55 self.var, self.value
56 )
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum Level {
62 Any,
63 Restricted,
64 Paranoid,
65}
66
67fn level(value: &str) -> Level {
68 match value.bytes().next() {
69 Some(b'a' | b'y' | b'1') => Level::Any,
70 Some(b'r' | b'n' | b'0') => Level::Restricted,
71 _ => Level::Paranoid,
72 }
73}
74
75fn has_dot_component(name: &str) -> bool {
78 let b = name.as_bytes();
79 (0..b.len()).any(|i| {
80 b[i] == b'.'
81 && (i == 0 || b[i - 1] == b'/')
82 && !(b[i + 1..].starts_with(b"/") || b[i + 1..].starts_with(b"./"))
83 })
84}
85
86fn has_parent_component(name: &str) -> bool {
88 name.starts_with("../") || name.contains("/../")
89}
90
91fn in_dir(name: &str, dir: &str) -> bool {
93 !dir.is_empty()
94 && name.starts_with(dir)
95 && matches!(name.as_bytes().get(dir.len()), None | Some(b'/'))
96}
97
98impl Kpathsea {
99 pub fn set_output_directory(&mut self, dir: Option<String>) {
103 self.output_directory = Some(dir);
104 }
105
106 fn output_directory(&self) -> Option<String> {
107 match &self.output_directory {
108 Some(d) => d.clone(),
109 None => self.getenv("TEXMF_OUTPUT_DIRECTORY"),
110 }
111 }
112
113 pub fn name_ok(&self, name: &str, access: Access, extended: bool) -> Result<(), Refusal> {
117 let (var, default) = match access {
118 Access::Read => ("openin_any", "a"),
119 Access::Write => ("openout_any", "p"),
120 };
121 let value = self.var_value(var).unwrap_or_else(|| default.to_owned());
122 let level = level(&value);
123 let refusal = || Refusal {
124 var,
125 value: value.clone(),
126 };
127 if self.normal_name_ok(name, level) {
128 return Ok(());
129 }
130 if extended && name.starts_with('/') {
131 if level == Level::Restricted {
132 return Ok(());
133 }
134 let dirs = [
135 self.var_value("TEXMFOUTPUT"),
136 self.output_directory(),
137 self.var_value("TEXMFVAR"),
138 self.var_value("TEXMFSYSVAR"),
139 ];
140 if !has_parent_component(name) && dirs.iter().flatten().any(|d| in_dir(name, d)) {
141 return Ok(());
142 }
143 }
144 Err(refusal())
145 }
146
147 fn normal_name_ok(&self, name: &str, level: Level) -> bool {
148 match level {
149 Level::Any => true,
150 Level::Restricted => !has_dot_component(name),
151 Level::Paranoid => {
152 if has_dot_component(name) || has_parent_component(name) {
153 return false;
154 }
155 !name.starts_with('/')
156 || [self.var_value("TEXMFOUTPUT"), self.output_directory()]
157 .iter()
158 .flatten()
159 .any(|d| in_dir(name, d))
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn dot_components() {
171 for n in [".x", "a/.x", "..x", "a/..", ".", "..", "/.x/y", ".../x"] {
172 assert!(has_dot_component(n), "{n}");
173 }
174 for n in ["x", "./x", "../x", "a/../x", "a/./x", "./", "x/", "/tmp/x"] {
175 assert!(!has_dot_component(n), "{n}");
176 }
177 }
178
179 #[test]
180 fn directories() {
181 assert!(in_dir("/tmp/x", "/tmp"));
182 assert!(in_dir("/tmp", "/tmp"));
183 assert!(!in_dir("/tmpx/y", "/tmp"));
184 assert!(!in_dir("/tmp/x", "/tmp/"));
185 assert!(!in_dir("/tmp/x", ""));
186 }
187}