From 2dd7cc32d6028526c654a3a5a69db16edf8fcf91 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 24 Oct 2026 10:45:28 +0200 Subject: [PATCH] diff: compare DIR/FILE with FILE when stdin is a directory --- src/diff.rs | 39 +++++++++++++++++++++++++++++++++++++- tests/by-util/test_diff.rs | 27 ++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/diff.rs b/src/diff.rs index b947058..072b9e0 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -19,10 +19,16 @@ use std::process::{exit, ExitCode}; // 1 means some differences were found, // and 2 means trouble. pub fn main(opts: Peekable) -> ExitCode { - let params = parse_params(opts).unwrap_or_else(|error| { + let mut params = parse_params(opts).unwrap_or_else(|error| { eprintln!("{error}"); exit(2); }); + // diff - FILE with a directory on stdin => diff DIRECTORY/FILE FILE + if params.from == "-" && params.to != "-" { + if let Some(from) = stdin_directory_entry(¶ms.to) { + params.from = from; + } + } // if from and to are the same file, no need to perform any comparison let maybe_report_identical_files = || { if params.report_identical_files { @@ -140,3 +146,34 @@ fn print_files_differ(kind: &str, params: &Params) { params.to.to_string_lossy() ); } + +/// When standard input is a directory, returns the path of the entry in it +/// that has the same name as `other`, provided `other` is not a directory. +/// Entries are reached through `/dev/fd/0`, which only names the directory +/// itself where it links to the open file, as with Linux procfs. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn stdin_directory_entry(other: &OsString) -> Option { + use std::os::fd::AsFd; + use std::path::Path; + + let stdin_is_dir = io::stdin() + .as_fd() + .try_clone_to_owned() + .ok() + .and_then(|fd| fs::File::from(fd).metadata().ok()) + .is_some_and(|metadata| metadata.is_dir()); + let other = Path::new(other); + if !stdin_is_dir || other.is_dir() { + return None; + } + Some( + Path::new("/dev/fd/0") + .join(other.file_name()?) + .into_os_string(), + ) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn stdin_directory_entry(_other: &OsString) -> Option { + None +} diff --git a/tests/by-util/test_diff.rs b/tests/by-util/test_diff.rs index 13a8b04..cd375f2 100644 --- a/tests/by-util/test_diff.rs +++ b/tests/by-util/test_diff.rs @@ -5,6 +5,8 @@ use diffutilslib::assert_diff_eq; +#[cfg(any(target_os = "linux", target_os = "android"))] +use assert_cmd::assert::OutputAssertExt; use assert_cmd::cargo::cargo_bin_cmd; use predicates::prelude::*; use std::fs::File; @@ -295,6 +297,31 @@ fn binary_files() -> Result<(), Box> { Ok(()) } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn stdin_is_directory() -> Result<(), Box> { + let tmp_dir = tempdir()?; + let dir = tmp_dir.path().join("dir"); + std::fs::create_dir(&dir)?; + std::fs::write(dir.join("notes"), "alpha\n")?; + let notes = tmp_dir.path().join("notes"); + std::fs::write(¬es, "beta\n")?; + + // The directory itself must be standard input, not its contents. + let output = std::process::Command::new(assert_cmd::cargo::cargo_bin!("diffutils")) + .arg("diff") + .arg("-") + .arg(¬es) + .stdin(File::open(&dir)?) + .output()?; + output + .assert() + .code(predicate::eq(1)) + .failure() + .stdout("1c1\n< alpha\n---\n> beta\n"); + Ok(()) +} + #[test] fn compare_file_to_directory() -> Result<(), Box> { let tmp_dir = tempdir()?;