1use crate::{
2 FileId,
3 engine::{StateWorkingSet, VirtualPath},
4};
5use std::{
6 ffi::OsStr,
7 path::{Path, PathBuf},
8};
9
10pub const MAX_RUN_SCRIPT_BYTES: u64 = 1_048_576;
20
21const TEXT_PROBE_BYTES: usize = 8192;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ScriptLoadError {
27 TooLarge { size: u64, max_size: u64 },
29 NotText,
31 Unreadable,
33}
34
35pub fn looks_like_text(bytes: &[u8]) -> bool {
44 let sample = if bytes.len() > TEXT_PROBE_BYTES {
45 &bytes[..TEXT_PROBE_BYTES]
46 } else {
47 bytes
48 };
49
50 if sample.is_empty() {
51 return true;
52 }
53
54 if sample.contains(&0) {
56 return false;
57 }
58
59 if std::str::from_utf8(sample).is_err() {
61 return false;
62 }
63
64 let suspicious_controls = sample
67 .iter()
68 .filter(|&&b| b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r'))
69 .count();
70 let mostly_controls = suspicious_controls.saturating_mul(10) > sample.len().saturating_mul(3);
72 !mostly_controls
73}
74
75pub fn read_run_script_file(path: &Path, max_bytes: u64) -> Result<Vec<u8>, ScriptLoadError> {
79 let size = std::fs::metadata(path)
80 .map(|m| m.len())
81 .map_err(|_| ScriptLoadError::Unreadable)?;
82 if size > max_bytes {
83 return Err(ScriptLoadError::TooLarge {
84 size,
85 max_size: max_bytes,
86 });
87 }
88 let contents = std::fs::read(path).map_err(|_| ScriptLoadError::Unreadable)?;
89 if !looks_like_text(&contents) {
90 return Err(ScriptLoadError::NotText);
91 }
92 Ok(contents)
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
101pub enum ParserPath {
102 RealPath(PathBuf),
103 VirtualFile(PathBuf, usize),
104 VirtualDir(PathBuf, Vec<ParserPath>),
105}
106
107impl ParserPath {
108 pub fn is_dir(&self) -> bool {
109 match self {
110 ParserPath::RealPath(p) => p.is_dir(),
111 ParserPath::VirtualFile(..) => false,
112 ParserPath::VirtualDir(..) => true,
113 }
114 }
115
116 pub fn is_file(&self) -> bool {
117 match self {
118 ParserPath::RealPath(p) => p.is_file(),
119 ParserPath::VirtualFile(..) => true,
120 ParserPath::VirtualDir(..) => false,
121 }
122 }
123
124 pub fn exists(&self) -> bool {
125 match self {
126 ParserPath::RealPath(p) => p.exists(),
127 ParserPath::VirtualFile(..) => true,
128 ParserPath::VirtualDir(..) => true,
129 }
130 }
131
132 pub fn path(&self) -> &Path {
133 match self {
134 ParserPath::RealPath(p) => p,
135 ParserPath::VirtualFile(p, _) => p,
136 ParserPath::VirtualDir(p, _) => p,
137 }
138 }
139
140 pub fn path_buf(self) -> PathBuf {
141 match self {
142 ParserPath::RealPath(p) => p,
143 ParserPath::VirtualFile(p, _) => p,
144 ParserPath::VirtualDir(p, _) => p,
145 }
146 }
147
148 pub fn parent(&self) -> Option<&Path> {
149 match self {
150 ParserPath::RealPath(p) => p.parent(),
151 ParserPath::VirtualFile(p, _) => p.parent(),
152 ParserPath::VirtualDir(p, _) => p.parent(),
153 }
154 }
155
156 pub fn read_dir(&self) -> Option<Vec<ParserPath>> {
157 match self {
158 ParserPath::RealPath(p) => p.read_dir().ok().map(|read_dir| {
159 read_dir
160 .flatten()
161 .map(|dir_entry| ParserPath::RealPath(dir_entry.path()))
162 .collect()
163 }),
164 ParserPath::VirtualFile(..) => None,
165 ParserPath::VirtualDir(_, files) => Some(files.clone()),
166 }
167 }
168
169 pub fn file_stem(&self) -> Option<&OsStr> {
170 self.path().file_stem()
171 }
172
173 pub fn extension(&self) -> Option<&OsStr> {
174 self.path().extension()
175 }
176
177 pub fn join(self, path: impl AsRef<Path>) -> ParserPath {
178 match self {
179 ParserPath::RealPath(p) => ParserPath::RealPath(p.join(path)),
180 ParserPath::VirtualFile(p, file_id) => ParserPath::VirtualFile(p.join(path), file_id),
181 ParserPath::VirtualDir(p, entries) => {
182 let new_p = p.join(path);
183 let mut pp = ParserPath::RealPath(new_p.clone());
184 for entry in entries {
185 if new_p == entry.path() {
186 pp = entry.clone();
187 }
188 }
189 pp
190 }
191 }
192 }
193
194 pub fn open<'a>(
195 &'a self,
196 working_set: &'a StateWorkingSet,
197 ) -> std::io::Result<Box<dyn std::io::Read + 'a>> {
198 match self {
199 ParserPath::RealPath(p) => {
200 std::fs::File::open(p).map(|f| Box::new(f) as Box<dyn std::io::Read>)
201 }
202 ParserPath::VirtualFile(_, file_id) => working_set
203 .get_contents_of_file(FileId::new(*file_id))
204 .map(|bytes| Box::new(bytes) as Box<dyn std::io::Read>)
205 .ok_or(std::io::ErrorKind::NotFound.into()),
206
207 ParserPath::VirtualDir(..) => Err(std::io::ErrorKind::NotFound.into()),
208 }
209 }
210
211 pub fn read<'a>(&'a self, working_set: &'a StateWorkingSet) -> Option<Vec<u8>> {
212 self.open(working_set)
213 .and_then(|mut reader| {
214 let mut vec = vec![];
215 reader.read_to_end(&mut vec)?;
216 Ok(vec)
217 })
218 .ok()
219 }
220
221 pub fn len(&self, working_set: &StateWorkingSet) -> Option<u64> {
223 match self {
224 ParserPath::RealPath(p) => std::fs::metadata(p).ok().map(|m| m.len()),
225 ParserPath::VirtualFile(_, file_id) => working_set
226 .get_contents_of_file(FileId::new(*file_id))
227 .map(|bytes| bytes.len() as u64),
228 ParserPath::VirtualDir(..) => None,
229 }
230 }
231
232 pub fn read_run_script(
237 &self,
238 working_set: &StateWorkingSet,
239 max_bytes: u64,
240 ) -> Result<Vec<u8>, ScriptLoadError> {
241 match self {
242 ParserPath::RealPath(p) => read_run_script_file(p, max_bytes),
243 ParserPath::VirtualFile(_, file_id) => {
244 let contents = working_set
245 .get_contents_of_file(FileId::new(*file_id))
246 .ok_or(ScriptLoadError::Unreadable)?;
247 let size = contents.len() as u64;
248 if size > max_bytes {
249 return Err(ScriptLoadError::TooLarge {
250 size,
251 max_size: max_bytes,
252 });
253 }
254 if !looks_like_text(contents) {
255 return Err(ScriptLoadError::NotText);
256 }
257 Ok(contents.to_vec())
258 }
259 ParserPath::VirtualDir(..) => Err(ScriptLoadError::Unreadable),
260 }
261 }
262
263 pub fn from_virtual_path(
264 working_set: &StateWorkingSet,
265 name: &str,
266 virtual_path: &VirtualPath,
267 ) -> Self {
268 match virtual_path {
269 VirtualPath::File(file_id) => {
270 ParserPath::VirtualFile(PathBuf::from(name), file_id.get())
271 }
272 VirtualPath::Dir(entries) => ParserPath::VirtualDir(
273 PathBuf::from(name),
274 entries
275 .iter()
276 .map(|virtual_path_id| {
277 let (virt_name, virt_path) = working_set.get_virtual_path(*virtual_path_id);
278 ParserPath::from_virtual_path(working_set, virt_name, virt_path)
279 })
280 .collect(),
281 ),
282 }
283 }
284
285 fn normalize_native(path: &str) -> PathBuf {
287 Path::new(&path)
288 .components()
289 .fold(PathBuf::new(), |mut acc, comp| {
290 acc.push(comp);
291 acc
292 })
293 }
294
295 fn normalize_forward(path: impl AsRef<Path>) -> PathBuf {
297 PathBuf::from(
298 path.as_ref()
299 .to_string_lossy()
300 .replace(std::path::MAIN_SEPARATOR, "/"),
301 )
302 }
303
304 pub fn normalize_slashes_forward(self) -> Self {
305 match self {
306 ParserPath::RealPath(p) => ParserPath::RealPath(Self::normalize_forward(p)),
307 ParserPath::VirtualFile(p, file_id) => {
308 ParserPath::VirtualFile(Self::normalize_forward(p), file_id)
309 }
310 ParserPath::VirtualDir(p, entries) => {
311 ParserPath::VirtualDir(Self::normalize_forward(p), entries)
312 }
313 }
314 }
315
316 pub fn normalize_slashes_native(self) -> Self {
317 match self {
318 ParserPath::RealPath(p) => {
319 ParserPath::RealPath(Self::normalize_native(p.to_string_lossy().as_ref()))
320 }
321 ParserPath::VirtualFile(p, file_id) => ParserPath::VirtualFile(
322 Self::normalize_native(p.to_string_lossy().as_ref()),
323 file_id,
324 ),
325 ParserPath::VirtualDir(p, entries) => ParserPath::VirtualDir(
326 Self::normalize_native(p.to_string_lossy().as_ref()),
327 entries,
328 ),
329 }
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn looks_like_text_accepts_empty_and_normal_scripts() {
339 assert!(looks_like_text(b""));
340 assert!(looks_like_text(b"def main [] { 'hi' }\n"));
341 assert!(looks_like_text(b"let x = 1\t# tab and comment\r\n"));
342 assert!(looks_like_text("print '你好'\n".as_bytes()));
344 }
345
346 #[test]
347 fn looks_like_text_rejects_nul() {
348 assert!(!looks_like_text(b"abc\0def"));
349 }
350
351 #[test]
352 fn looks_like_text_rejects_invalid_utf8() {
353 assert!(!looks_like_text(&[0x80, 0x81, 0xFF]));
354 }
355
356 #[test]
357 fn looks_like_text_rejects_dense_controls() {
358 let dense = vec![0x01u8; 50];
359 assert!(!looks_like_text(&dense));
360 }
361
362 #[test]
363 fn looks_like_text_allows_sparse_controls() {
364 let mut bytes = b"print 'hello'\n".to_vec();
366 bytes.push(0x07);
367 assert!(looks_like_text(&bytes));
368 }
369}