diff --git a/Cargo.lock b/Cargo.lock index 69365cc..aa40301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,7 +562,7 @@ dependencies = [ [[package]] name = "softpath" -version = "0.2.2" +version = "0.3.0" dependencies = [ "criterion", "dirs", diff --git a/Cargo.toml b/Cargo.toml index d0566f7..a13567b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "softpath" -version = "0.2.2" +version = "0.3.0" edition = "2021" -authors = ["ALIBI Ghazu "] +authors = ["ALIBI Ghazi "] description = "A human-friendly file and directory path manipulation library for Rust." license = "MIT" repository = "https://github.com/GhaziAlibi/softpath" diff --git a/README.md b/README.md index c138059..3f39d32 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ fn main() -> Result<(), softpath::SoftPathError> { // Copy it somewhere else let backup = "~/config/backup/app.json".into_path()?; + if backup.exists()? { + backup.remove()?; + } config_file.copy_to(&backup)?; // Create directories as needed diff --git a/src/lib.rs b/src/lib.rs index 613c2b2..d39db73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,9 @@ //! //! // Copy to backup location //! let backup = backup_path.join("app.json").into_path()?; +//! if backup.exists()? { +//! backup.remove()?; +//! } //! config_file.copy_to(&backup)?; //! # Ok(()) //! # } diff --git a/src/ops/implementations/path_impl.rs b/src/ops/implementations/path_impl.rs index 916adc0..1a545d9 100644 --- a/src/ops/implementations/path_impl.rs +++ b/src/ops/implementations/path_impl.rs @@ -39,6 +39,8 @@ impl PathExt for &Path { } fn read_to_string(&self) -> Result { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; fs::read_to_string(self).map_err(SoftPathError::from) } @@ -47,17 +49,33 @@ impl PathExt for &Path { } fn copy_to>(&self, dest: P) -> Result<(), SoftPathError> { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; let dest_path = dest.as_ref(); crate::utils::check_path_traversal(dest_path)?; crate::utils::check_symlink_cycles(dest_path)?; + if dest_path.exists() { + return Err(SoftPathError::Io(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Destination already exists: {}", dest_path.display()), + ))); + } fs::copy(self, dest)?; Ok(()) } fn move_to>(&self, dest: P) -> Result<(), SoftPathError> { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; let dest_path = dest.as_ref(); crate::utils::check_path_traversal(dest_path)?; crate::utils::check_symlink_cycles(dest_path)?; + if dest_path.exists() { + return Err(SoftPathError::Io(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Destination already exists: {}", dest_path.display()), + ))); + } fs::rename(self, dest)?; Ok(()) } diff --git a/src/ops/implementations/pathbuf_impl.rs b/src/ops/implementations/pathbuf_impl.rs index 0b6456c..bcca1e4 100644 --- a/src/ops/implementations/pathbuf_impl.rs +++ b/src/ops/implementations/pathbuf_impl.rs @@ -45,6 +45,8 @@ impl PathExt for PathBuf { } fn read_to_string(&self) -> Result { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; fs::read_to_string(self).map_err(SoftPathError::from) } @@ -53,17 +55,33 @@ impl PathExt for PathBuf { } fn copy_to>(&self, dest: P) -> Result<(), SoftPathError> { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; let dest_path = dest.as_ref(); crate::utils::check_path_traversal(dest_path)?; crate::utils::check_symlink_cycles(dest_path)?; + if dest_path.exists() { + return Err(SoftPathError::Io(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Destination already exists: {}", dest_path.display()), + ))); + } fs::copy(self, dest)?; Ok(()) } fn move_to>(&self, dest: P) -> Result<(), SoftPathError> { + crate::utils::check_path_traversal(self)?; + crate::utils::check_symlink_cycles(self)?; let dest_path = dest.as_ref(); crate::utils::check_path_traversal(dest_path)?; crate::utils::check_symlink_cycles(dest_path)?; + if dest_path.exists() { + return Err(SoftPathError::Io(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!("Destination already exists: {}", dest_path.display()), + ))); + } fs::rename(self, dest)?; Ok(()) } diff --git a/src/ops/implementations/str_impl.rs b/src/ops/implementations/str_impl.rs index b1ac37b..5c35320 100644 --- a/src/ops/implementations/str_impl.rs +++ b/src/ops/implementations/str_impl.rs @@ -68,7 +68,7 @@ impl PathExt for &str { fn read_to_string(&self) -> Result { let path = self.into_path()?; - fs::read_to_string(path).map_err(SoftPathError::from) + path.read_to_string() } fn write_string(&self, contents: &str) -> Result<(), SoftPathError> { @@ -78,20 +78,12 @@ impl PathExt for &str { fn copy_to>(&self, dest: P) -> Result<(), SoftPathError> { let from = self.into_path()?; - let dest_path = dest.as_ref(); - crate::utils::check_path_traversal(dest_path)?; - crate::utils::check_symlink_cycles(dest_path)?; - fs::copy(&from, dest)?; - Ok(()) + from.copy_to(dest) } fn move_to>(&self, dest: P) -> Result<(), SoftPathError> { let from = self.into_path()?; - let dest_path = dest.as_ref(); - crate::utils::check_path_traversal(dest_path)?; - crate::utils::check_symlink_cycles(dest_path)?; - fs::rename(&from, dest)?; - Ok(()) + from.move_to(dest) } fn is_empty(&self) -> Result { diff --git a/src/ops/path_ext.rs b/src/ops/path_ext.rs index ce40e78..42ecf71 100644 --- a/src/ops/path_ext.rs +++ b/src/ops/path_ext.rs @@ -111,6 +111,7 @@ pub trait PathExt { /// - The source path does not exist /// - The destination path already exists /// - The user lacks permissions + /// - Any parent directories of the destination are missing fn move_to>(&self, dest: P) -> Result<(), SoftPathError>; /// Returns true if the path points to an empty file or directory. diff --git a/tests/file_operations.rs b/tests/file_operations.rs index 34c8739..faa9a64 100644 --- a/tests/file_operations.rs +++ b/tests/file_operations.rs @@ -59,6 +59,34 @@ fn test_copy_and_move() { cleanup_test_dir(&test_dir); } +#[test] +fn test_copy_and_move_reject_existing_destination() -> Result<(), SoftPathError> { + let test_dir = setup_test_dir(); + + let source = test_dir.join("source.txt"); + source.write_string("source content")?; + + let existing_copy_dest = test_dir.join("existing_copy.txt"); + existing_copy_dest.write_string("existing content")?; + let copy_err = source.copy_to(&existing_copy_dest).unwrap_err(); + assert!(matches!( + copy_err, + SoftPathError::Io(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists + )); + + let existing_move_dest = test_dir.join("existing_move.txt"); + existing_move_dest.write_string("existing content")?; + let move_err = source.move_to(&existing_move_dest).unwrap_err(); + assert!(matches!( + move_err, + SoftPathError::Io(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists + )); + assert!(source.exists()?); + + cleanup_test_dir(&test_dir); + Ok(()) +} + #[test] fn test_is_empty() -> Result<(), SoftPathError> { let test_dir = setup_test_dir(); diff --git a/tests/test_softpath_errors.rs b/tests/test_softpath_errors.rs index 9264a85..64db296 100644 --- a/tests/test_softpath_errors.rs +++ b/tests/test_softpath_errors.rs @@ -125,6 +125,15 @@ fn test_io_errors() { } } +#[test] +fn test_read_to_string_path_traversal_error() { + let malicious_path = "../../../../../../../../../etc/passwd"; + assert!(matches!( + malicious_path.read_to_string(), + Err(SoftPathError::PathTraversal(_)) + )); +} + #[test] fn test_error_matching_patterns() { let malicious_path = "../../../../../../../../../etc/passwd";