Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
66 changes: 4 additions & 62 deletions src/uu/comm/src/comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<bool> {
// 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<W: Write>(writer: &mut W, delim: &[u8], line: &[u8]) -> UResult<()> {
writer
.write_all(delim)
Expand Down Expand Up @@ -332,7 +274,7 @@ fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result<LineReader>
// 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)?;
Expand Down
1 change: 0 additions & 1 deletion src/uu/install/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 2 additions & 3 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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;
}

Expand Down
111 changes: 111 additions & 0 deletions src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path>, path2: impl AsRef<Path>) -> IOResult<bool> {
Comment thread
sylvestre marked this conversation as resolved.
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<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, to: P2) -> PathBuf {
let path = path.as_ref();
Expand Down Expand Up @@ -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());
}
}
Loading