Skip to content

Commit 4e0a6c8

Browse files
committed
diff: add --no-dereference
Should make test no-dereference pass.
1 parent fa6dc56 commit 4e0a6c8

5 files changed

Lines changed: 233 additions & 22 deletions

File tree

‎src/diff.rs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
5353
params.from = from;
5454
}
5555
}
56-
let both_directories = params.from != "-"
57-
&& params.to != "-"
58-
&& Path::new(&params.from).is_dir()
59-
&& Path::new(&params.to).is_dir();
60-
let outcome = if both_directories {
61-
dir_diff::compare_directories(&params, &option_args)
56+
let both_directories = Path::new(&params.from).is_dir() && Path::new(&params.to).is_dir();
57+
// Directories, and files whose symbolic links are not followed, are
58+
// compared by type before their contents.
59+
let compare_types =
60+
params.from != "-" && params.to != "-" && (both_directories || params.no_dereference);
61+
let outcome = if compare_types {
62+
dir_diff::compare(&params, &option_args)
6263
} else {
6364
compare_files(&params, None)
6465
};

‎src/dir_diff.rs‎

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,31 @@ use std::borrow::Cow;
1010
use std::cmp::Ordering;
1111
use std::ffi::{OsStr, OsString};
1212
use std::fs::{self, Metadata};
13-
use std::io;
13+
use std::io::{self, Write};
1414
use std::path::{Path, PathBuf};
1515

1616
use crate::diff::{compare_files, Outcome};
1717
use crate::params::{MissingFile, Params};
1818
use crate::utils::{
1919
fold_file_name_case, quote_file_name, quote_shell_argument, report_failure_to_read_input_file,
20+
single_quote,
2021
};
2122

22-
/// Compares the directories `params.from` and `params.to`. `option_args` are
23-
/// the command line options, repeated in the header printed before the
24-
/// differences of each pair of files.
25-
pub fn compare_directories(params: &Params, option_args: &[OsString]) -> Outcome {
23+
/// Compares `params.from` and `params.to` according to their types: the
24+
/// entries of directories, the targets of symbolic links that are not
25+
/// followed, or the contents of files. `option_args` are the command line
26+
/// options, repeated in the header printed before the differences of each
27+
/// pair of files found in directories.
28+
pub fn compare(params: &Params, option_args: &[OsString]) -> Outcome {
2629
let mut walker = DirectoryWalker {
2730
params,
2831
option_args,
2932
ancestors: Vec::new(),
3033
};
31-
walker.compare_directories(&params.from, &params.to, Level::Top)
34+
walker.compare_entries(&params.from, &params.to, Level::Top)
3235
}
3336

34-
/// Whether directories are the operands of diff, or nested in them.
37+
/// Whether files are the operands of diff, or found in directories.
3538
#[derive(Clone, Copy, Eq, PartialEq)]
3639
enum Level {
3740
Top,
@@ -83,17 +86,17 @@ impl DirectoryWalker<'_> {
8386
for entry in entries {
8487
let entry_outcome = match entry {
8588
Entry::Both(from, to) => {
86-
self.compare_entries(&join(from_dir, from), &join(to_dir, to))
89+
self.compare_entries(&join(from_dir, from), &join(to_dir, to), Level::Nested)
8790
}
8891
Entry::FromOnly(name) if self.params.missing_file == MissingFile::Empty => {
89-
self.compare_entries(&join(from_dir, name), &join(to_dir, name))
92+
self.compare_entries(&join(from_dir, name), &join(to_dir, name), Level::Nested)
9093
}
9194
Entry::FromOnly(name) => report_only_in(from_dir, name),
9295
Entry::ToOnly(name) if self.params.missing_file == MissingFile::Error => {
9396
report_only_in(to_dir, name)
9497
}
9598
Entry::ToOnly(name) => {
96-
self.compare_entries(&join(from_dir, name), &join(to_dir, name))
99+
self.compare_entries(&join(from_dir, name), &join(to_dir, name), Level::Nested)
97100
}
98101
};
99102
outcome = outcome.max(entry_outcome);
@@ -148,7 +151,7 @@ impl DirectoryWalker<'_> {
148151

149152
/// Compares two entries of the compared directories, one of which may be
150153
/// missing when missing files are treated as empty.
151-
fn compare_entries(&mut self, from: &OsStr, to: &OsStr) -> Outcome {
154+
fn compare_entries(&mut self, from: &OsStr, to: &OsStr, level: Level) -> Outcome {
152155
let (Ok(from_metadata), Ok(to_metadata)) = (self.metadata(from), self.metadata(to)) else {
153156
return Outcome::Trouble;
154157
};
@@ -158,8 +161,8 @@ impl DirectoryWalker<'_> {
158161
{
159162
if same_file::is_same_file(from, to).unwrap_or(false) {
160163
Outcome::Identical
161-
} else if self.params.recursive {
162-
self.compare_directories(from, to, Level::Nested)
164+
} else if level == Level::Top || self.params.recursive {
165+
self.compare_directories(from, to, level)
163166
} else {
164167
println!(
165168
"Common subdirectories: {} and {}",
@@ -169,6 +172,11 @@ impl DirectoryWalker<'_> {
169172
Outcome::Identical
170173
}
171174
}
175+
(Some(from_metadata), Some(to_metadata))
176+
if from_metadata.is_symlink() && to_metadata.is_symlink() =>
177+
{
178+
self.compare_symbolic_links(from, to)
179+
}
172180
(Some(from_metadata), Some(to_metadata))
173181
if !(from_metadata.is_file() && to_metadata.is_file()) =>
174182
{
@@ -194,10 +202,53 @@ impl DirectoryWalker<'_> {
194202
Outcome::Identical
195203
}
196204
}
205+
// The operands themselves are compared without a header, as when
206+
// they are not directories.
207+
_ if level == Level::Top => compare_files(self.params, None),
197208
_ => self.compare_files(from, to),
198209
}
199210
}
200211

212+
/// Compares the targets of two symbolic links.
213+
fn compare_symbolic_links(&self, from: &OsStr, to: &OsStr) -> Outcome {
214+
let (from_target, to_target) = match (fs::read_link(from), fs::read_link(to)) {
215+
(Ok(from_target), Ok(to_target)) => (from_target, to_target),
216+
(Err(error), _) => {
217+
self.report_error(from, &error);
218+
return Outcome::Trouble;
219+
}
220+
(_, Err(error)) => {
221+
self.report_error(to, &error);
222+
return Outcome::Trouble;
223+
}
224+
};
225+
if from_target == to_target {
226+
if self.params.report_identical_files {
227+
println!(
228+
"Files {} and {} are identical",
229+
from.to_string_lossy(),
230+
to.to_string_lossy()
231+
);
232+
}
233+
return Outcome::Identical;
234+
}
235+
let mut message = b"Symbolic links ".to_vec();
236+
for (link, target, separator) in [
237+
(from, from_target.as_os_str(), &b" and "[..]),
238+
(to, to_target.as_os_str(), b" differ\n"),
239+
] {
240+
message.extend(single_quote(link.as_encoded_bytes()));
241+
message.extend_from_slice(b" -> ");
242+
message.extend(single_quote(target.as_encoded_bytes()));
243+
message.extend_from_slice(separator);
244+
}
245+
if io::stdout().write_all(&message).is_err() {
246+
return Outcome::Trouble;
247+
}
248+
Outcome::Different
249+
}
250+
251+
/// Compares two files found in the compared directories.
201252
fn compare_files(&self, from: &OsStr, to: &OsStr) -> Outcome {
202253
let params = Params {
203254
from: from.to_os_string(),
@@ -220,7 +271,12 @@ impl DirectoryWalker<'_> {
220271
/// Returns the metadata of `path`, `None` if it is missing on purpose
221272
/// (-N), or an error after reporting it.
222273
fn metadata(&self, path: &OsStr) -> Result<Option<Metadata>, ()> {
223-
match fs::metadata(path) {
274+
let metadata = if self.params.no_dereference {
275+
fs::symlink_metadata(path)
276+
} else {
277+
fs::metadata(path)
278+
};
279+
match metadata {
224280
Ok(metadata) => Ok(Some(metadata)),
225281
Err(error)
226282
if error.kind() == io::ErrorKind::NotFound
@@ -305,6 +361,10 @@ fn join(dir: &OsStr, name: &OsStr) -> OsString {
305361
/// Describes the type of a file, as in "File X is a fifo while ...".
306362
fn file_kind(metadata: &Metadata) -> &'static str {
307363
let file_type = metadata.file_type();
364+
// Only seen with --no-dereference, as symbolic links are followed otherwise.
365+
if file_type.is_symlink() {
366+
return "symbolic link";
367+
}
308368
if file_type.is_dir() {
309369
return "directory";
310370
}

‎src/params.rs‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub struct Params {
5858
/// (`-S`).
5959
pub starting_file: Option<OsString>,
6060
pub ignore_file_name_case: bool,
61+
pub no_dereference: bool,
6162
}
6263

6364
/// The pattern of `-p`, matching the first line of C functions.
@@ -85,6 +86,7 @@ impl Default for Params {
8586
recursive: false,
8687
starting_file: None,
8788
ignore_file_name_case: false,
89+
no_dereference: false,
8890
}
8991
}
9092
}
@@ -193,6 +195,10 @@ pub fn parse_params<I: Iterator<Item = OsString>>(
193195
push_function_line_pattern(&mut params, &pattern.to_string_lossy())?;
194196
continue;
195197
}
198+
if param == "--no-dereference" {
199+
params.no_dereference = true;
200+
continue;
201+
}
196202
if param == "--ignore-file-name-case" {
197203
params.ignore_file_name_case = true;
198204
continue;
@@ -363,11 +369,20 @@ pub fn parse_params<I: Iterator<Item = OsString>>(
363369
let mut from_path: PathBuf = PathBuf::from(&params.from);
364370
let mut to_path: PathBuf = PathBuf::from(&params.to);
365371

366-
if from_path.is_dir() && to_path.is_file() {
372+
// Symbolic links that are not followed are compared like files.
373+
let is_file = |path: &Path| {
374+
if params.no_dereference {
375+
path.symlink_metadata()
376+
.is_ok_and(|metadata| !metadata.is_dir())
377+
} else {
378+
path.is_file()
379+
}
380+
};
381+
if from_path.is_dir() && is_file(&to_path) {
367382
let name = directory_entry(&from_path, to_path.file_name().unwrap(), &params);
368383
from_path.push(name);
369384
params.from = from_path.into_os_string();
370-
} else if from_path.is_file() && to_path.is_dir() {
385+
} else if is_file(&from_path) && to_path.is_dir() {
371386
let name = directory_entry(&to_path, from_path.file_name().unwrap(), &params);
372387
to_path.push(name);
373388
params.to = to_path.into_os_string();

‎src/utils.rs‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ pub fn write_file_header(output: &mut Vec<u8>, marker: &str, name: &OsStr, times
114114
output.push(b'\n');
115115
}
116116

117+
/// Puts `name` between single quotes, writing a quote in it as `'\''`.
118+
#[must_use]
119+
pub fn single_quote(name: &[u8]) -> Vec<u8> {
120+
let mut quoted = Vec::with_capacity(name.len() + 2);
121+
quoted.push(b'\'');
122+
for &byte in name {
123+
if byte == b'\'' {
124+
quoted.extend_from_slice(br"'\''");
125+
} else {
126+
quoted.push(byte);
127+
}
128+
}
129+
quoted.push(b'\'');
130+
quoted
131+
}
132+
117133
/// Quotes a command line argument the way a shell would need it, for the
118134
/// headers that repeat the options of a directory comparison.
119135
#[must_use]
@@ -457,6 +473,13 @@ mod tests {
457473
assert_eq!(quoted_arg("don't"), "\"don't\"");
458474
assert_eq!(quoted_arg("don't $pay"), "'don'\\''t $pay'");
459475
}
476+
477+
#[test]
478+
fn single_quotes() {
479+
assert_eq!(single_quote(b"plain"), b"'plain'");
480+
assert_eq!(single_quote(b"it's"), br"'it'\''s'");
481+
assert_eq!(single_quote(b""), b"''");
482+
}
460483
}
461484

462485
mod write_line {

‎tests/by-util/test_diff.rs‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,118 @@ fn directory_name_matching() -> Result<(), Box<dyn std::error::Error>> {
655655
Ok(())
656656
}
657657

658+
#[cfg(unix)]
659+
#[test]
660+
fn no_dereference() -> Result<(), Box<dyn std::error::Error>> {
661+
use std::os::unix::fs::symlink;
662+
663+
let tmp_dir = tempdir()?;
664+
let dir = tmp_dir.path();
665+
std::fs::write(dir.join("plain"), "text\n")?;
666+
std::fs::write(dir.join("other"), "text\n")?;
667+
symlink("plain", dir.join("to-plain"))?;
668+
symlink("plain", dir.join("also-to-plain"))?;
669+
symlink("other", dir.join("to-other"))?;
670+
let path = |name: &str| dir.join(name).to_str().unwrap().to_string();
671+
672+
let mut cmd = cargo_bin_cmd!("diffutils");
673+
cmd.arg("diff").arg(path("plain")).arg(path("to-other"));
674+
cmd.assert().code(predicate::eq(0)).stdout("");
675+
676+
let mut cmd = cargo_bin_cmd!("diffutils");
677+
cmd.arg("diff")
678+
.arg("--no-dereference")
679+
.arg(path("plain"))
680+
.arg(path("to-other"));
681+
cmd.assert().code(predicate::eq(1)).stdout(format!(
682+
"File {} is a regular file while file {} is a symbolic link\n",
683+
path("plain"),
684+
path("to-other")
685+
));
686+
687+
let mut cmd = cargo_bin_cmd!("diffutils");
688+
cmd.arg("diff")
689+
.arg("--no-dereference")
690+
.arg(path("to-plain"))
691+
.arg(path("also-to-plain"));
692+
cmd.assert().code(predicate::eq(0)).stdout("");
693+
694+
let mut cmd = cargo_bin_cmd!("diffutils");
695+
cmd.arg("diff")
696+
.arg("--no-dereference")
697+
.arg(path("to-plain"))
698+
.arg(path("to-other"));
699+
cmd.assert().code(predicate::eq(1)).stdout(format!(
700+
"Symbolic links '{}' -> 'plain' and '{}' -> 'other' differ\n",
701+
path("to-plain"),
702+
path("to-other")
703+
));
704+
705+
// In directories, dangling links are entries like any other.
706+
std::fs::create_dir(dir.join("left"))?;
707+
std::fs::create_dir(dir.join("right"))?;
708+
symlink("missing", dir.join("left/dangling"))?;
709+
symlink("../plain", dir.join("right/dangling"))?;
710+
let mut cmd = cargo_bin_cmd!("diffutils");
711+
cmd.arg("diff")
712+
.arg("-r")
713+
.arg("--no-dereference")
714+
.arg(path("left"))
715+
.arg(path("right"));
716+
cmd.assert().code(predicate::eq(1)).stdout(format!(
717+
"Symbolic links '{0}/dangling' -> 'missing' and '{1}/dangling' -> '../plain' differ\n",
718+
path("left"),
719+
path("right")
720+
));
721+
722+
let mut cmd = cargo_bin_cmd!("diffutils");
723+
cmd.arg("diff")
724+
.arg("-r")
725+
.arg(path("left"))
726+
.arg(path("right"));
727+
cmd.assert()
728+
.code(predicate::eq(2))
729+
.stderr(predicate::str::contains(
730+
"dangling: No such file or directory",
731+
));
732+
733+
// Links with the same target are identical files.
734+
let mut cmd = cargo_bin_cmd!("diffutils");
735+
cmd.arg("diff")
736+
.arg("-s")
737+
.arg("--no-dereference")
738+
.arg(path("to-plain"))
739+
.arg(path("also-to-plain"));
740+
cmd.assert().code(predicate::eq(0)).stdout(format!(
741+
"Files {} and {} are identical\n",
742+
path("to-plain"),
743+
path("also-to-plain")
744+
));
745+
746+
// A file compared with a directory is looked up in it, and that entry is
747+
// not followed either.
748+
std::fs::write(dir.join("left/plain"), "text\n")?;
749+
symlink("../other", dir.join("left/to-other"))?;
750+
let mut cmd = cargo_bin_cmd!("diffutils");
751+
cmd.arg("diff")
752+
.arg("--no-dereference")
753+
.arg(path("plain"))
754+
.arg(path("left"));
755+
cmd.assert().code(predicate::eq(0)).stdout("");
756+
757+
let mut cmd = cargo_bin_cmd!("diffutils");
758+
cmd.arg("diff")
759+
.arg("--no-dereference")
760+
.arg(path("left"))
761+
.arg(path("to-other"));
762+
cmd.assert().code(predicate::eq(1)).stdout(format!(
763+
"Symbolic links '{}/to-other' -> '../other' and '{}' -> 'other' differ\n",
764+
path("left"),
765+
path("to-other")
766+
));
767+
Ok(())
768+
}
769+
658770
#[test]
659771
fn compare_file_to_directory() -> Result<(), Box<dyn std::error::Error>> {
660772
let tmp_dir = tempdir()?;

0 commit comments

Comments
 (0)