diff --git a/Cargo.lock b/Cargo.lock index 93ad0d74f06..f4949053f99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,12 +1004,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "file_diff" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" - [[package]] name = "filedescriptor" version = "0.8.3" @@ -3718,7 +3712,6 @@ name = "uu_install" version = "0.10.0" dependencies = [ "clap", - "file_diff", "fluent", "rustix", "selinux", diff --git a/Cargo.toml b/Cargo.toml index 5e1127efc7a..e2b43ee8770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -432,7 +432,6 @@ data-encoding-macro = "0.1.15" divan = { package = "codspeed-divan-compat", version = "5.0.0" } dns-lookup = { version = "3.0.0" } dunce = "1.0.4" -file_diff = "1.0.0" filetime = "0.2.29" foldhash = "0.2.0" fs_extra = "1.3.0" diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index af6d2f46466..c59b84aff21 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -7,13 +7,13 @@ use std::cmp::Ordering; use std::ffi::OsString; -use std::fs::{File, metadata}; -use std::io::{self, BufRead, BufReader, BufWriter, Read, StdinLock, Write, stderr, stdin}; +use std::fs::File; +use std::io::{self, BufRead, BufReader, BufWriter, StdinLock, Write, stderr, stdin}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; -use uucore::fs::paths_refer_to_same_file; +use uucore::fs::{are_files_identical, paths_refer_to_same_file}; use uucore::line_ending::LineEnding; use uucore::translate; @@ -127,64 +127,6 @@ impl OrderChecker { } } -// Check if two files are identical by comparing their contents -pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result { - // First compare file sizes - let metadata1 = metadata(path1)?; - let metadata2 = metadata(path2)?; - - if metadata1.len() != metadata2.len() { - return Ok(false); - } - - // only proceed if both are regular files - if !metadata1.is_file() || !metadata2.is_file() { - return Ok(false); - } - - let file1 = File::open(path1)?; - let file2 = File::open(path2)?; - - let mut reader1 = BufReader::new(file1); - let mut reader2 = BufReader::new(file2); - - let mut buffer1 = [0; 8192]; - let mut buffer2 = [0; 8192]; - - loop { - // Read from first file with EINTR retry handling - // This loop retries the read operation if it's interrupted by signals (e.g., SIGUSR1) - // instead of failing, which is the POSIX-compliant way to handle interrupted I/O - let bytes1 = loop { - match reader1.read(&mut buffer1) { - Err(e) if e.kind() == io::ErrorKind::Interrupted => {} - result => break result?, - } - }; - - // Read from second file with EINTR retry handling - // Same retry logic as above for the second file to ensure consistent behavior - let bytes2 = loop { - match reader2.read(&mut buffer2) { - Err(e) if e.kind() == io::ErrorKind::Interrupted => {} - result => break result?, - } - }; - - if bytes1 != bytes2 { - return Ok(false); - } - - if bytes1 == 0 { - return Ok(true); - } - - if buffer1[..bytes1] != buffer2[..bytes2] { - return Ok(false); - } - } -} - fn write_line_with_delimiter(writer: &mut W, delim: &[u8], line: &[u8]) -> UResult<()> { writer .write_all(delim) @@ -332,7 +274,7 @@ fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result // some platforms shows different read error // try to override the error message, but failure of it is not serious #[cfg(any(target_os = "wasi", target_os = "windows"))] - if metadata(name).is_ok_and(|m| m.is_dir()) { + if std::fs::metadata(name).is_ok_and(|m| m.is_dir()) { return Err(io::Error::other(translate!("comm-error-is-directory"))); } let f = File::open(name)?; diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index ceda3ec006a..b2f6d11fc86 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -16,7 +16,6 @@ path = "src/install.rs" [dependencies] clap = { workspace = true } -file_diff = { workspace = true } thiserror = { workspace = true } uucore = { workspace = true, default-features = true, features = [ "backup-control", diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 14c731046af..4223675e05c 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -8,7 +8,6 @@ mod mode; use clap::{Arg, ArgAction, ArgMatches, Command}; -use file_diff::diff; #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] use selinux::SecurityContext; use std::ffi::OsString; @@ -24,7 +23,7 @@ use uucore::buf_copy::copy_fast; use uucore::display::Quotable; use uucore::entries::{grp2gid, usr2uid}; use uucore::error::{FromIo, UError, UResult, UUsageError, strip_errno}; -use uucore::fs::dir_strip_dot_for_creation; +use uucore::fs::{are_files_identical, dir_strip_dot_for_creation}; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; #[cfg(unix)] @@ -1344,7 +1343,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { } // Check if the contents of the source and destination files differ. - if !diff(&from.to_string_lossy(), &to.to_string_lossy()) { + if !are_files_identical(from, to).unwrap_or(false) { return true; } diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 8e4e8d770eb..8d0b972d6ca 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -637,6 +637,89 @@ pub fn infos_refer_to_same_file( info1.is_ok() && info1.ok() == info2.ok() } +/// Check if two files are identical by comparing their contents. +/// +/// Returns `Ok(true)` if both files exist, are regular files, and have identical contents. +/// Returns `Ok(false)` if the files differ in size, aren't both regular files, or have different contents. +/// Returns `Err` if an I/O error occurs while opening or reading either file. +/// +/// # Examples +/// +/// ``` +/// use std::io::Write; +/// use tempfile::NamedTempFile; +/// use uucore::fs::are_files_identical; +/// +/// let mut file1 = NamedTempFile::new().unwrap(); +/// let mut file2 = NamedTempFile::new().unwrap(); +/// file1.write_all(b"hello world").unwrap(); +/// file2.write_all(b"hello world").unwrap(); +/// +/// assert!(are_files_identical(file1.path(), file2.path()).unwrap()); +/// ``` +pub fn are_files_identical(path1: impl AsRef, path2: impl AsRef) -> IOResult { + use std::fs::{File, metadata}; + use std::io::{BufReader, ErrorKind, Read}; + + let path1 = path1.as_ref(); + let path2 = path2.as_ref(); + + // First compare file sizes + let metadata1 = metadata(path1)?; + let metadata2 = metadata(path2)?; + + if metadata1.len() != metadata2.len() { + return Ok(false); + } + + // only proceed if both are regular files + if !metadata1.is_file() || !metadata2.is_file() { + return Ok(false); + } + + let file1 = File::open(path1)?; + let file2 = File::open(path2)?; + + let mut reader1 = BufReader::new(file1); + let mut reader2 = BufReader::new(file2); + + let mut buffer1 = [0; 8192]; + let mut buffer2 = [0; 8192]; + + loop { + // Read from first file with EINTR retry handling + // This loop retries the read operation if it's interrupted by signals (e.g., SIGUSR1) + // instead of failing, which is the POSIX-compliant way to handle interrupted I/O + let bytes1 = loop { + match reader1.read(&mut buffer1) { + Err(e) if e.kind() == ErrorKind::Interrupted => {} + result => break result?, + } + }; + + // Read from second file with EINTR retry handling + // Same retry logic as above for the second file to ensure consistent behavior + let bytes2 = loop { + match reader2.read(&mut buffer2) { + Err(e) if e.kind() == ErrorKind::Interrupted => {} + result => break result?, + } + }; + + if bytes1 != bytes2 { + return Ok(false); + } + + if bytes1 == 0 { + return Ok(true); + } + + if buffer1[..bytes1] != buffer2[..bytes2] { + return Ok(false); + } + } +} + /// Converts absolute `path` to be relative to absolute `to` path. pub fn make_path_relative_to, P2: AsRef>(path: P1, to: P2) -> PathBuf { let path = path.as_ref(); @@ -1339,4 +1422,32 @@ mod tests { let attributes = file.as_file().metadata().unwrap().file_attributes(); assert_ne!(attributes & FILE_ATTRIBUTE_SPARSE_FILE, 0); } + + #[test] + fn test_are_files_identical() { + use std::io::Write; + use tempfile::NamedTempFile; + + let mut file1 = NamedTempFile::new().unwrap(); + let mut file2 = NamedTempFile::new().unwrap(); + let mut file3 = NamedTempFile::new().unwrap(); + + file1.write_all(b"hello world").unwrap(); + file2.write_all(b"hello world").unwrap(); + file3.write_all(b"hello rust!").unwrap(); + + // Identical contents + assert!(are_files_identical(file1.path(), file2.path()).unwrap()); + + // Same size, different contents + assert!(!are_files_identical(file1.path(), file3.path()).unwrap()); + + // Different size + let mut file4 = NamedTempFile::new().unwrap(); + file4.write_all(b"hello").unwrap(); + assert!(!are_files_identical(file1.path(), file4.path()).unwrap()); + + // Non-existent file + assert!(are_files_identical(file1.path(), "non_existent_file_path").is_err()); + } }