Skip to content
Open
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
75 changes: 58 additions & 17 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,51 +87,56 @@ pub fn compare_files(params: &Params, header: Option<&[u8]>) -> Outcome {
return Outcome::Identical;
}

// read files
fn read_file_contents(filepath: &OsString) -> io::Result<Vec<u8>> {
// read files, along with how many leading bytes decide if they are binary
fn read_file_contents(filepath: &OsString) -> io::Result<(Vec<u8>, usize)> {
let mut content = Vec::new();
if filepath == "-" {
let mut content = Vec::new();
io::stdin().read_to_end(&mut content).and(Ok(content))
io::stdin().read_to_end(&mut content)?;
Ok((content, stdin_binary_probe_len()))
} else {
fs::read(filepath)
let mut file = fs::File::open(filepath)?;
file.read_to_end(&mut content)?;
Ok((content, binary_probe_len(&file)))
}
}
let mut io_error = false;
let mut from_missing = false;
let mut to_missing = false;
let from_content = match read_file_contents(&params.from) {
let (from_content, from_probe_len) = match read_file_contents(&params.from) {
Ok(from_content) => from_content,
Err(e)
if e.kind() == io::ErrorKind::NotFound && params.missing_file != MissingFile::Error =>
{
from_missing = true;
vec![]
(vec![], 0)
}
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.from, &e);
io_error = true;
vec![]
(vec![], 0)
}
};
let to_content = match read_file_contents(&params.to) {
let (to_content, to_probe_len) = match read_file_contents(&params.to) {
Ok(to_content) => to_content,
Err(e)
if e.kind() == io::ErrorKind::NotFound && params.missing_file == MissingFile::Empty =>
{
to_missing = true;
vec![]
(vec![], 0)
}
Err(e) => {
report_failure_to_read_input_file(&params.executable, &params.to, &e);
io_error = true;
vec![]
(vec![], 0)
}
};
if io_error {
return Outcome::Trouble;
}

if !params.text && (is_binary(&from_content) || is_binary(&to_content)) {
if !params.text
&& (is_binary(&from_content, from_probe_len) || is_binary(&to_content, to_probe_len))
{
if from_content == to_content {
maybe_report_identical_files();
return Outcome::Identical;
Expand Down Expand Up @@ -248,12 +253,48 @@ fn output_palette(params: &Params) -> Option<Palette> {
}
}

/// Number of leading bytes inspected when deciding whether an input is binary.
const BINARY_PROBE_LEN: usize = 16 * 1024;
/// Number of leading bytes inspected when deciding whether an input is binary,
/// where the file system does not tell its preferred I/O size.
const DEFAULT_BINARY_PROBE_LEN: usize = 16 * 1024;

/// An input is treated as binary when a NUL byte occurs in its first
/// `probe_len` bytes.
fn is_binary(content: &[u8], probe_len: usize) -> bool {
content[..content.len().min(probe_len)].contains(&0)
}

/// Like GNU diff, only the first buffer read from a file decides whether it
/// is binary, and that buffer is as large as the preferred I/O size of the
/// file (`st_blksize`).
#[cfg(unix)]
fn binary_probe_len(file: &fs::File) -> usize {
use std::os::unix::fs::MetadataExt;
file.metadata()
.ok()
.and_then(|metadata| usize::try_from(metadata.blksize()).ok())
.filter(|&len| len > 0)
.unwrap_or(DEFAULT_BINARY_PROBE_LEN)
}

#[cfg(not(unix))]
fn binary_probe_len(_file: &fs::File) -> usize {
DEFAULT_BINARY_PROBE_LEN
}

#[cfg(unix)]
fn stdin_binary_probe_len() -> usize {
use std::os::fd::AsFd;
io::stdin()
.as_fd()
.try_clone_to_owned()
.map_or(DEFAULT_BINARY_PROBE_LEN, |fd| {
binary_probe_len(&fs::File::from(fd))
})
}

/// An input is treated as binary when a NUL byte occurs near its start.
fn is_binary(content: &[u8]) -> bool {
content[..content.len().min(BINARY_PROBE_LEN)].contains(&0)
#[cfg(not(unix))]
fn stdin_binary_probe_len() -> usize {
DEFAULT_BINARY_PROBE_LEN
}

/// Print the one-line summary used instead of the differences, `kind` naming
Expand Down
31 changes: 31 additions & 0 deletions tests/by-util/test_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,37 @@ fn missing_file_as_empty_keeps_stdin_timestamp(
.stdout_does_not_contain("1970");
}

// Only the first block of a file, as large as its preferred I/O size, is
// looked at to decide whether it is binary.
#[cfg(unix)]
#[test]
fn binary_detection_reads_first_block() {
use std::os::unix::fs::MetadataExt;

let (at, ts) = at_and_ts!();
let mut data = b"line\n".repeat(2000);
at.write_bytes("text", &data);
let block_size = usize::try_from(at.metadata("text").blksize()).unwrap();
if !(1..data.len()).contains(&block_size) {
return;
}
data[block_size] = 0;
at.write_bytes("late_nul", &data);
data[block_size - 1] = 0;
at.write_bytes("early_nul", &data);

// Each line is 5 bytes long
let line = block_size / 5 + 1;
for operands in [["late_nul", "text"], ["text", "late_nul"]] {
let result = ts.ucmd().args(&operands).fails_with_code(1);
assert!(result.stdout_str().starts_with(&format!("{line}c{line}\n")));
}
ts.ucmd()
.args(&["early_nul", "text"])
.fails_with_code(1)
.stdout_only("Binary files early_nul and text differ\n");
}

#[test]
fn binary_files() {
let (at, ts) = at_and_ts!();
Expand Down
Loading