aboutsummaryrefslogtreecommitdiffstats
path: root/src/file_utils.rs
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2023-09-24 11:21:29 +0000
committerGitHub <noreply@github.com>2023-09-24 11:21:29 +0000
commitfa15976c1b4b070ad1bb8cecff23b7d571959852 (patch)
tree8a6e756e84f4d301d0829db5cd3976d888030215 /src/file_utils.rs
parentMerge pull request #1237 from svenstaro/fix-ci (diff)
parentFix clippy complaints (diff)
downloadminiserve-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.rs84
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);
+ }
+}