1use std::{
5 fs::{File, Metadata, OpenOptions},
6 io,
7 path::{Path, PathBuf},
8};
9
10use crate::{walk::PathType, Error, Mistrust, Result, Verifier};
11
12#[derive(Debug, Clone)]
32pub struct CheckedDir {
33 mistrust: Mistrust,
35 location: PathBuf,
37 readable_okay: bool,
39}
40
41impl CheckedDir {
42 pub(crate) fn new(verifier: &Verifier<'_>, path: &Path) -> Result<Self> {
44 let mut mistrust = verifier.mistrust.clone();
45 mistrust.ignore_prefix = crate::canonicalize_opt_prefix(&Some(Some(path.to_path_buf())))?;
53 Ok(CheckedDir {
54 mistrust,
55 location: path.to_path_buf(),
56 readable_okay: verifier.readable_okay,
57 })
58 }
59
60 pub fn make_directory<P: AsRef<Path>>(&self, path: P) -> Result<()> {
66 let path = path.as_ref();
67 self.check_path(path)?;
68 self.verifier().make_directory(self.location.join(path))
69 }
70
71 pub fn make_secure_directory<P: AsRef<Path>>(&self, path: P) -> Result<CheckedDir> {
78 let path = path.as_ref();
79 self.make_directory(path)?;
80 self.verifier().secure_dir(self.location.join(path))
82 }
83
84 pub fn file_access(&self) -> crate::FileAccess<'_> {
86 crate::FileAccess::from_checked_dir(self)
87 }
88
89 pub fn open<P: AsRef<Path>>(&self, path: P, options: &OpenOptions) -> Result<File> {
100 self.file_access().open(path, options)
101 }
102
103 pub fn read_directory<P: AsRef<Path>>(&self, path: P) -> Result<std::fs::ReadDir> {
112 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckPath)?;
113
114 std::fs::read_dir(&path).map_err(|e| Error::io(e, path, "read directory"))
115 }
116
117 pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
125 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
131
132 std::fs::remove_file(&path).map_err(|e| Error::io(e, path, "remove file"))
133 }
134
135 pub fn as_path(&self) -> &Path {
141 self.location.as_path()
142 }
143
144 pub fn join<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
150 let path = path.as_ref();
151 self.check_path(path)?;
152 Ok(self.location.join(path))
153 }
154
155 pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
162 self.file_access().read_to_string(path)
163 }
164
165 pub fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
172 self.file_access().read(path)
173 }
174
175 pub fn write_and_replace<P: AsRef<Path>, C: AsRef<[u8]>>(
192 &self,
193 path: P,
194 contents: C,
195 ) -> Result<()> {
196 self.file_access().write_and_replace(path, contents)
197 }
198
199 pub fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
214 let path = self.verified_full_path(path.as_ref(), FullPathCheck::CheckParent)?;
215
216 let meta = path
217 .symlink_metadata()
218 .map_err(|e| Error::inspecting(e, &path))?;
219
220 if meta.is_symlink() {
221 let err = io::Error::new(
225 io::ErrorKind::Other,
226 format!("Path {:?} is a symlink", path),
227 );
228 return Err(Error::io(err, &path, "metadata"));
229 }
230
231 if let Some(error) = self
232 .verifier()
233 .check_one(path.as_path(), PathType::Content, &meta)
234 .into_iter()
235 .next()
236 {
237 Err(error)
238 } else {
239 Ok(meta)
240 }
241 }
242
243 pub fn verifier(&self) -> Verifier<'_> {
246 let mut v = self.mistrust.verifier();
247 if self.readable_okay {
248 v = v.permit_readable();
249 }
250 v
251 }
252
253 fn check_path(&self, p: &Path) -> Result<()> {
258 use std::path::Component;
259 if p.is_absolute() {
261 return Err(Error::InvalidSubdirectory);
262 }
263
264 for component in p.components() {
265 match component {
266 Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
267 return Err(Error::InvalidSubdirectory)
268 }
269 Component::CurDir | Component::Normal(_) => {}
270 }
271 }
272
273 Ok(())
274 }
275
276 pub(crate) fn verified_full_path(
280 &self,
281 p: &Path,
282 check_type: FullPathCheck,
283 ) -> Result<PathBuf> {
284 self.check_path(p)?;
285 let full_path = self.location.join(p);
286 let to_verify: &Path = match check_type {
287 FullPathCheck::CheckPath => full_path.as_ref(),
288 FullPathCheck::CheckParent => full_path.parent().unwrap_or_else(|| full_path.as_ref()),
289 };
290 self.verifier().check(to_verify)?;
291
292 Ok(full_path)
293 }
294}
295
296#[derive(Clone, Copy, Debug)]
298pub(crate) enum FullPathCheck {
299 CheckPath,
301 CheckParent,
303}
304
305#[cfg(test)]
306mod test {
307 #![allow(clippy::bool_assert_comparison)]
309 #![allow(clippy::clone_on_copy)]
310 #![allow(clippy::dbg_macro)]
311 #![allow(clippy::mixed_attributes_style)]
312 #![allow(clippy::print_stderr)]
313 #![allow(clippy::print_stdout)]
314 #![allow(clippy::single_char_pattern)]
315 #![allow(clippy::unwrap_used)]
316 #![allow(clippy::unchecked_duration_subtraction)]
317 #![allow(clippy::useless_vec)]
318 #![allow(clippy::needless_pass_by_value)]
319 use super::*;
321 use crate::testing::Dir;
322 use std::io::Write;
323
324 #[test]
325 fn easy_case() {
326 let d = Dir::new();
327 d.dir("a/b/c");
328 d.dir("a/b/d");
329 d.file("a/b/c/f1");
330 d.file("a/b/c/f2");
331 d.file("a/b/d/f3");
332
333 d.chmod("a", 0o755);
334 d.chmod("a/b", 0o700);
335 d.chmod("a/b/c", 0o700);
336 d.chmod("a/b/d", 0o777);
337 d.chmod("a/b/c/f1", 0o600);
338 d.chmod("a/b/c/f2", 0o666);
339 d.chmod("a/b/d/f3", 0o600);
340
341 let m = Mistrust::builder()
342 .ignore_prefix(d.canonical_root())
343 .build()
344 .unwrap();
345
346 let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
347
348 sd.make_directory("c/sub1").unwrap();
350 #[cfg(target_family = "unix")]
351 {
352 let e = sd.make_directory("d/sub2").unwrap_err();
353 assert!(matches!(e, Error::BadPermission(..)));
354 }
355
356 let f1 = sd.open("c/f1", OpenOptions::new().read(true)).unwrap();
358 drop(f1);
359 #[cfg(target_family = "unix")]
360 {
361 let e = sd.open("c/f2", OpenOptions::new().read(true)).unwrap_err();
362 assert!(matches!(e, Error::BadPermission(..)));
363 let e = sd.open("d/f3", OpenOptions::new().read(true)).unwrap_err();
364 assert!(matches!(e, Error::BadPermission(..)));
365 }
366
367 let mut f3 = sd
369 .open("c/f-new", OpenOptions::new().write(true).create(true))
370 .unwrap();
371 f3.write_all(b"Hello world").unwrap();
372 drop(f3);
373
374 #[cfg(target_family = "unix")]
375 {
376 let e = sd
377 .open("d/f-new", OpenOptions::new().write(true).create(true))
378 .unwrap_err();
379 assert!(matches!(e, Error::BadPermission(..)));
380 }
381 }
382
383 #[test]
384 fn bad_paths() {
385 let d = Dir::new();
386 d.dir("a");
387 d.chmod("a", 0o700);
388
389 let m = Mistrust::builder()
390 .ignore_prefix(d.canonical_root())
391 .build()
392 .unwrap();
393
394 let sd = m.verifier().secure_dir(d.path("a")).unwrap();
395
396 let e = sd.make_directory("hello/../world").unwrap_err();
397 assert!(matches!(e, Error::InvalidSubdirectory));
398 let e = sd.metadata("hello/../world").unwrap_err();
399 assert!(matches!(e, Error::InvalidSubdirectory));
400
401 let e = sd.make_directory("/hello").unwrap_err();
402 assert!(matches!(e, Error::InvalidSubdirectory));
403 let e = sd.metadata("/hello").unwrap_err();
404 assert!(matches!(e, Error::InvalidSubdirectory));
405
406 sd.make_directory("hello/world").unwrap();
407 }
408
409 #[test]
410 fn read_and_write() {
411 let d = Dir::new();
412 d.dir("a");
413 d.chmod("a", 0o700);
414 let m = Mistrust::builder()
415 .ignore_prefix(d.canonical_root())
416 .build()
417 .unwrap();
418
419 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
420
421 checked
423 .write_and_replace("foo.txt", "this is incredibly silly")
424 .unwrap();
425
426 let s1 = checked.read_to_string("foo.txt").unwrap();
427 let s2 = checked.read("foo.txt").unwrap();
428 assert_eq!(s1, "this is incredibly silly");
429 assert_eq!(s1.as_bytes(), &s2[..]);
430
431 let sub = "sub";
433 let sub_checked = checked.make_secure_directory(sub).unwrap();
434 assert_eq!(sub_checked.as_path(), checked.as_path().join(sub));
435
436 checked
438 .open("bar.tmp", OpenOptions::new().create(true).write(true))
439 .unwrap()
440 .write_all("be the other guy".as_bytes())
441 .unwrap();
442 assert!(checked.join("bar.tmp").unwrap().try_exists().unwrap());
443
444 checked
445 .write_and_replace("bar.txt", "its hard and nobody understands")
446 .unwrap();
447
448 assert!(!checked.join("bar.tmp").unwrap().try_exists().unwrap());
450 let s4 = checked.read_to_string("bar.txt").unwrap();
451 assert_eq!(s4, "its hard and nobody understands");
452 }
453
454 #[test]
455 fn read_directory() {
456 let d = Dir::new();
457 d.dir("a");
458 d.chmod("a", 0o700);
459 d.dir("a/b");
460 d.file("a/b/f");
461 d.file("a/c.d");
462 d.dir("a/x");
463
464 d.chmod("a", 0o700);
465 d.chmod("a/b", 0o700);
466 d.chmod("a/x", 0o777);
467 let m = Mistrust::builder()
468 .ignore_prefix(d.canonical_root())
469 .build()
470 .unwrap();
471
472 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
473
474 assert!(matches!(
475 checked.read_directory("/"),
476 Err(Error::InvalidSubdirectory)
477 ));
478 assert!(matches!(
479 checked.read_directory("b/.."),
480 Err(Error::InvalidSubdirectory)
481 ));
482 let mut members: Vec<String> = checked
483 .read_directory(".")
484 .unwrap()
485 .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
486 .collect();
487 members.sort();
488 assert_eq!(members, vec!["b", "c.d", "x"]);
489
490 let members: Vec<String> = checked
491 .read_directory("b")
492 .unwrap()
493 .map(|ent| ent.unwrap().file_name().to_string_lossy().to_string())
494 .collect();
495 assert_eq!(members, vec!["f"]);
496
497 #[cfg(target_family = "unix")]
498 {
499 assert!(matches!(
500 checked.read_directory("x"),
501 Err(Error::BadPermission(_, _, _))
502 ));
503 }
504 }
505
506 #[test]
507 fn remove_file() {
508 let d = Dir::new();
509 d.dir("a");
510 d.chmod("a", 0o700);
511 d.dir("a/b");
512 d.file("a/b/f");
513 d.dir("a/b/d");
514 d.dir("a/x");
515 d.dir("a/x/y");
516 d.file("a/x/y/z");
517
518 d.chmod("a", 0o700);
519 d.chmod("a/b", 0o700);
520 d.chmod("a/x", 0o777);
521
522 let m = Mistrust::builder()
523 .ignore_prefix(d.canonical_root())
524 .build()
525 .unwrap();
526 let checked = m.verifier().secure_dir(d.path("a")).unwrap();
527
528 assert!(checked.read_to_string("b/f").is_ok());
530 assert!(checked.metadata("b/f").unwrap().is_file());
531 checked.remove_file("b/f").unwrap();
532 assert!(matches!(
533 checked.read_to_string("b/f"),
534 Err(Error::NotFound(_))
535 ));
536 assert!(matches!(checked.metadata("b/f"), Err(Error::NotFound(_))));
537 assert!(matches!(
538 checked.remove_file("b/f"),
539 Err(Error::NotFound(_))
540 ));
541
542 assert!(matches!(
544 checked.remove_file("b/xyzzy/fred"),
545 Err(Error::NotFound(_))
546 ));
547
548 #[cfg(target_family = "unix")]
550 {
551 assert!(matches!(
552 checked.remove_file("x/y/z"),
553 Err(Error::BadPermission(_, _, _))
554 ));
555 assert!(matches!(
556 checked.metadata("x/y/z"),
557 Err(Error::BadPermission(_, _, _))
558 ));
559 }
560 }
561
562 #[test]
563 #[cfg(target_family = "unix")]
564 fn access_symlink() {
565 use crate::testing::LinkType;
566
567 let d = Dir::new();
568 d.dir("a/b");
569 d.file("a/b/f1");
570
571 d.chmod("a/b", 0o700);
572 d.chmod("a/b/f1", 0o600);
573 d.link_rel(LinkType::File, "f1", "a/b/f1-link");
574
575 let m = Mistrust::builder()
576 .ignore_prefix(d.canonical_root())
577 .build()
578 .unwrap();
579
580 let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
581
582 assert!(sd.open("f1", OpenOptions::new().read(true)).is_ok());
583
584 let e = sd.metadata("f1-link").unwrap_err();
586 assert!(
587 matches!(e, Error::Io { ref err, .. } if err.to_string().contains("is a symlink")),
588 "{e:?}"
589 );
590
591 let e = sd
593 .open("f1-link", OpenOptions::new().read(true))
594 .unwrap_err();
595 assert!(
596 matches!(e, Error::Io { ref err, .. } if err.to_string().contains("symbolic")), "{e:?}"
598 );
599 }
600}