diff --git a/tools/cp-utility/src/main.rs b/tools/cp-utility/src/main.rs index 6e140611b..83b69d2bb 100644 --- a/tools/cp-utility/src/main.rs +++ b/tools/cp-utility/src/main.rs @@ -28,6 +28,8 @@ enum CopyType { SingleFile, /// equivalent to cp -a Archive, + /// equivalent to cp -r + Recursive, } /// Encapsulate a copy operation @@ -42,10 +44,10 @@ struct CopyOperation { /// Parse command line arguments and transform into `CopyOperation` fn parse_args(args: Vec<&str>) -> io::Result { - if !(args.len() == 3 || args.len() == 4 && args[1].eq("-a")) { + if !(args.len() == 3 || (args.len() == 4 && (args[1] == "-a" || args[1] == "-r"))) { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "Invalid parameters. Expected cp [-a] ", + "Invalid parameters. Expected cp [-a | -r] ", )); } @@ -53,7 +55,11 @@ fn parse_args(args: Vec<&str>) -> io::Result { return Ok(CopyOperation { source: PathBuf::from(args[2]), destination: PathBuf::from(args[3]), - copy_type: CopyType::Archive, + copy_type: match args[1] { + "-a" => CopyType::Archive, + "-r" => CopyType::Recursive, + _ => panic!("Invalid option. Expected -a or -r"), + }, }); } @@ -69,10 +75,40 @@ fn do_copy(operation: CopyOperation) -> io::Result<()> { match operation.copy_type { CopyType::Archive => copy_archive(&operation.source, &operation.destination)?, CopyType::SingleFile => fs::copy(&operation.source, &operation.destination).map(|_| ())?, + CopyType::Recursive => copy_recursive(&operation.source, &operation.destination)?, }; Ok(()) } +fn copy_recursive(source: &Path, dest: &Path) -> io::Result<()> { + let mut stack = VecDeque::new(); + stack.push_back((source.to_path_buf(), dest.to_path_buf())); + while let Some((current_source, current_dest)) = stack.pop_back() { + if current_source.is_dir() { + if !current_dest.exists() { + fs::create_dir(¤t_dest)?; + } + for entry in fs::read_dir(current_source)? { + let next_source = entry?.path(); + let next_dest = + current_dest + .clone() + .join(next_source.file_name().ok_or(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid source file", + ))?); + stack.push_back((next_source, next_dest)); + } + } else if current_source.is_symlink() { + // Follow symbolic links as regular files + fs::copy(current_source, current_dest)?; + } else if current_source.is_file() { + fs::copy(current_source, current_dest)?; + } + } + Ok(()) +} + // Execute the recursive type of copy operation fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> { let mut stack = VecDeque::new(); @@ -100,7 +136,6 @@ fn copy_archive(source: &Path, dest: &Path) -> io::Result<()> { fs::copy(current_source, current_dest)?; } } - Ok(()) } @@ -163,7 +198,7 @@ mod tests { fn parser_failure() { // prepare let inputs = vec![ - vec!["cp", "-r", "foo.txt", "bar.txt"], + vec!["cp", "-r", "foo.txt", "bar.txt", "foo1.txt"], vec!["cp", "-a", "param1", "param2", "param3"], vec!["cp", "param1", "param2", "param3"], ]; @@ -177,6 +212,24 @@ mod tests { } } + #[test] + fn parser_correct() { + // prepare + let inputs = vec![ + vec!["cp", "-r", "foo.txt", "bar.txt"], + vec!["cp", "-a", "param1", "param2"], + vec!["cp", "param1", "param2"], + ]; + + for input in inputs.into_iter() { + // act + let result = parse_args(input.clone()); + + // assert + assert!(result.is_ok(), "input should fail {:?}", input); + } + } + #[test] fn test_copy_single() { // prepare @@ -220,6 +273,51 @@ mod tests { assert!(result.is_err()); } + #[test] + fn test_copy_recursive() { + // prepare + let tempdir = tempfile::tempdir().unwrap(); + let test_base = tempdir.path().to_path_buf(); + ["foo", "foo/foo0", "foo/foo1", "foo/bar"] + .iter() + .for_each(|x| create_dir(&test_base, x)); + let files = [ + "foo/file1.txt", + "foo/file2.txt", + "foo/foo1/file3.txt", + "foo/bar/file4.txt", + ]; + files.iter().for_each(|x| create_file(&test_base, x)); + [("foo/symlink1.txt", "./file1.txt")] + .iter() + .for_each(|(x, y)| create_symlink(&test_base, x, y)); + + // act + let recursive_copy = CopyOperation { + copy_type: CopyType::Recursive, + source: test_base.join("foo"), + destination: test_base.join("bar"), + }; + do_copy(recursive_copy).unwrap(); + + // assert + files.iter().for_each(|x| { + assert_same_file( + &test_base.join(x), + &test_base.join(x.replace("foo/", "bar/")), + ) + }); + assert_same_file( + &test_base.join("foo/symlink1.txt"), + &test_base.join("bar/symlink1.txt"), + ); + // recursive copy will treat symlink as a file + assert_recursive_same_link( + &test_base.join("foo/symlink1.txt"), + &test_base.join("bar/symlink1.txt"), + ) + } + #[test] fn test_copy_archive() { // prepare @@ -342,4 +440,16 @@ mod tests { assert_eq!(fs::read_link(source).unwrap(), fs::read_link(dest).unwrap()); } + + fn assert_recursive_same_link(source: &Path, dest: &Path) { + assert!(source.exists()); + assert!(dest.exists()); + assert!(source.is_symlink()); + assert!(dest.is_file()); + + assert_eq!( + fs::read_to_string(source).unwrap(), + fs::read_to_string(dest).unwrap() + ); + } }