Repository: uutils/coreutils (Rust)

Issue:
cksum: error handling deviation from GNU
`cksum`'s behavior when encountering directories or non existing files mismatches gnu implementation

```sh
$ cat <<EOF > CHECKSUM
SHA1 (dir) = ffffffffffffffffffffffffffffffffffffffff
SHA1 (not-file) = ffffffffffffffffffffffffffffffffffffffff
EOF
$ mkdir dir

$ ../../../gnu/src/cksum -c CHECKSUM  
../../../gnu/src/cksum: dir: Is a directory
dir: FAILED open or read
../../../gnu/src/cksum: not-file: No such file or directory
not-file: FAILED open or read
../../../gnu/src/cksum: WARNING: 2 listed files could not be read

$ ../../target/debug/cksum -c CHECKSUM
../../target/debug/cksum: dir: Is a directory
../../target/debug/cksum: not-file: No such file or directory
not-file: FAILED open or read
../../target/debug/cksum: WARNING: 1 listed file could not be read

# With ignore missing

$ ../../../gnu/src/cksum -c CHECKSUM --ignore-missing
../../../gnu/src/cksum: dir: Is a directory
dir: FAILED open or read
../../../gnu/src/cksum: WARNING: 1 listed file could not be read
../../../gnu/src/cksum: CHECKSUM: no file was verified

$ ../../target/debug/cksum -c CHECKSUM --ignore-missing
../../target/debug/cksum: dir: Is a directory
../../target/debug/cksum: CHECKSUM: no file was verified
```


Discussion:

Patch:
diff --git a/src/uucore/src/lib/features/checksum.rs b/src/uucore/src/lib/features/checksum.rs
index 27fbc7bf16d..98d57e0bae5 100644
--- a/src/uucore/src/lib/features/checksum.rs
+++ b/src/uucore/src/lib/features/checksum.rs
@@ -154,7 +154,7 @@ pub fn create_sha3(bits: Option<usize>) -> UResult<HashAlgorithm> {
 }
 
 #[allow(clippy::comparison_chain)]
-fn cksum_output(res: &ChecksumResult, ignore_missing: bool, status: bool) {
+fn cksum_output(res: &ChecksumResult, status: bool) {
     if res.bad_format == 1 {
         show_warning_caps!("{} line is improperly formatted", res.bad_format);
     } else if res.bad_format > 1 {
@@ -168,12 +168,10 @@ fn cksum_output(res: &ChecksumResult, ignore_missing: bool, status: bool) {
             show_warning_caps!("{} computed checksums did NOT match", res.failed_cksum);
         }
     }
-    if !ignore_missing {
-        if res.failed_open_file == 1 {
-            show_warning_caps!("{} listed file could not be read", res.failed_open_file);
-        } else if res.failed_open_file > 1 {
-            show_warning_caps!("{} listed files could not be read", res.failed_open_file);
-        }
+    if res.failed_open_file == 1 {
+        show_warning_caps!("{} listed file could not be read", res.failed_open_file);
+    } else if res.failed_open_file > 1 {
+        show_warning_caps!("{} listed files could not be read", res.failed_open_file);
     }
 }
 
@@ -364,10 +362,16 @@ fn get_file_to_check(
     if filename == "-" {
         Some(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file
     } else {
+        let mut failed_open = || {
+            println!("{filename}: FAILED open or read");
+            res.failed_open_file += 1;
+        };
         match File::open(filename) {
             Ok(f) => {
                 if f.metadata().ok()?.is_dir() {
                     show!(USimpleError::new(1, format!("{filename}: Is a directory")));
+                    // also regarded as a failed open
+                    failed_open();
                     None
                 } else {
                     Some(Box::new(f))
@@ -377,9 +381,8 @@ fn get_file_to_check(
                 if !ignore_missing {
                     // yes, we have both stderr and stdout here
                     show!(err.map_err_context(|| filename.to_string()));
-                    println!("{filename}: FAILED open or read");
+                    failed_open();
                 }
-                res.failed_open_file += 1;
                 // we could not open the file but we want to continue
                 None
             }
@@ -612,6 +615,9 @@ where
             return Ok(());
         }
 
+        // if any incorrectly formatted line, show it
+        cksum_output(&res, status);
+
         if ignore_missing && correct_format == 0 {
             // we have only bad format
             // and we had ignore-missing
@@ -633,9 +639,6 @@ where
         if (res.failed_cksum > 0 || res.failed_open_file > 0) && !ignore_missing {
             set_exit_code(1);
         }
-
-        // if any incorrectly formatted line, show it
-        cksum_output(&res, ignore_missing, status);
     }
 
     Ok(())


Test patch:
diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs
index 507c2dffb44..c408ae34d09 100644
--- a/tests/by-util/test_cksum.rs
+++ b/tests/by-util/test_cksum.rs
@@ -1344,3 +1344,42 @@ fn test_check_comment_leading_space() {
         .stdout_contains("foo: OK")
         .stderr_contains("WARNING: 1 line is improperly formatted");
 }
+
+/// This test checks alignment with GNU's error handling.
+/// Windows has a different logic and is guarded by [`test_check_directory_error`].
+#[cfg(not(windows))]
+#[test]
+fn test_check_failed_to_read() {
+    // check `cksum`'s behavior when encountering directories or non existing files
+
+    let scene = TestScenario::new(util_name!());
+    let at = &scene.fixtures;
+
+    at.write(
+        "CHECKSUM",
+        "SHA1 (dir) = ffffffffffffffffffffffffffffffffffffffff\n\
+        SHA1 (not-file) = ffffffffffffffffffffffffffffffffffffffff\n",
+    );
+    at.mkdir("dir");
+
+    scene
+        .ucmd()
+        .arg("--check")
+        .arg("CHECKSUM")
+        .fails()
+        .stdout_is(
+            "dir: FAILED open or read\n\
+            not-file: FAILED open or read\n",
+        )
+        .stderr_contains("cksum: WARNING: 2 listed files could not be read");
+
+    // check with `--ignore-missing`
+    scene
+        .ucmd()
+        .arg("--check")
+        .arg("CHECKSUM")
+        .arg("--ignore-missing")
+        .fails()
+        .stdout_is("dir: FAILED open or read\n")
+        .stderr_contains("cksum: WARNING: 1 listed file could not be read");
+}