diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2023-09-24 11:21:29 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-24 11:21:29 +0000 |
commit | fa15976c1b4b070ad1bb8cecff23b7d571959852 (patch) | |
tree | 8a6e756e84f4d301d0829db5cd3976d888030215 /src/file_utils.rs | |
parent | Merge pull request #1237 from svenstaro/fix-ci (diff) | |
parent | Fix clippy complaints (diff) | |
download | miniserve-fa15976c1b4b070ad1bb8cecff23b7d571959852.tar.gz miniserve-fa15976c1b4b070ad1bb8cecff23b7d571959852.zip |
Merge pull request #1228 from cyqsimon/upload-refactor
Minor refactor on upload code
Diffstat (limited to 'src/file_utils.rs')
-rw-r--r-- | src/file_utils.rs | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/src/file_utils.rs b/src/file_utils.rs new file mode 100644 index 0000000..114a08f --- /dev/null +++ b/src/file_utils.rs @@ -0,0 +1,84 @@ +use std::{ + io, + path::{Component, Path, PathBuf}, +}; + +/// Guarantee that the path is relative and cannot traverse back to parent directories +/// and optionally prevent traversing hidden directories. +/// +/// See the unit tests tests::test_sanitize_path* for examples +pub fn sanitize_path(path: impl AsRef<Path>, traverse_hidden: bool) -> Option<PathBuf> { + let mut buf = PathBuf::new(); + + for comp in path.as_ref().components() { + match comp { + Component::Normal(name) => buf.push(name), + Component::ParentDir => { + buf.pop(); + } + _ => (), + } + } + + // Double-check that all components are Normal and check for hidden dirs + for comp in buf.components() { + match comp { + Component::Normal(_) if traverse_hidden => (), + Component::Normal(name) if !name.to_str()?.starts_with('.') => (), + _ => return None, + } + } + + Some(buf) +} + +/// Checks if any segment of the path is a symlink. +/// +/// This function fails if [`std::fs::symlink_metadata`] fails, which usually +/// means user has no permission to access the path. +pub fn contains_symlink(path: impl AsRef<Path>) -> io::Result<bool> { + let contains_symlink = path + .as_ref() + .ancestors() + // On Windows, `\\?\` won't exist even though it's the root, but there's no need to check it + // So we filter it out + .filter(|p| p.exists()) + .map(|p| p.symlink_metadata()) + .collect::<Result<Vec<_>, _>>()? + .into_iter() + .any(|p| p.file_type().is_symlink()); + + Ok(contains_symlink) +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + + #[rstest] + #[case("/foo", "foo")] + #[case("////foo", "foo")] + #[case("C:/foo", if cfg!(windows) { "foo" } else { "C:/foo" })] + #[case("../foo", "foo")] + #[case("../foo/../bar/abc", "bar/abc")] + fn test_sanitize_path(#[case] input: &str, #[case] output: &str) { + assert_eq!( + sanitize_path(Path::new(input), true).unwrap(), + Path::new(output) + ); + assert_eq!( + sanitize_path(Path::new(input), false).unwrap(), + Path::new(output) + ); + } + + #[rstest] + #[case(".foo")] + #[case("/.foo")] + #[case("foo/.bar/foo")] + fn test_sanitize_path_no_hidden_files(#[case] input: &str) { + assert_eq!(sanitize_path(Path::new(input), false), None); + } +} |