Skip to content

Commit 56ce0e2

Browse files
authored
cksum: Fix file quoting on stderr (#8272)
* cksum: handle escaping in stderr for file errors * uutests: Minor fix and stdX_contains_bytes feature * test(cksum): add test for stderr escaping
1 parent c64adee commit 56ce0e2

File tree

5 files changed

+91
-14
lines changed

5 files changed

+91
-14
lines changed

src/uucore/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ default = []
9393
# * non-default features
9494
backup-control = []
9595
colors = []
96-
checksum = ["data-encoding", "sum"]
96+
checksum = ["data-encoding", "quoting-style", "sum"]
9797
encoding = ["data-encoding", "data-encoding-macro", "z85"]
9898
entries = ["libc"]
9999
extendedbigdecimal = ["bigdecimal", "num-traits"]

src/uucore/src/lib/features/checksum.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use std::{
1818

1919
use crate::{
2020
error::{FromIo, UError, UResult, USimpleError},
21-
os_str_as_bytes, os_str_from_bytes, read_os_string_lines, show, show_error, show_warning_caps,
21+
os_str_as_bytes, os_str_from_bytes,
22+
quoting_style::{QuotingStyle, locale_aware_escape_name},
23+
read_os_string_lines, show, show_error, show_warning_caps,
2224
sum::{
2325
Blake2b, Blake3, Bsd, CRC32B, Crc, Digest, DigestWriter, Md5, Sha1, Sha3_224, Sha3_256,
2426
Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Shake128, Shake256, Sm3, SysV,
@@ -734,7 +736,7 @@ fn get_file_to_check(
734736
opts: ChecksumOptions,
735737
) -> Result<Box<dyn Read>, LineCheckError> {
736738
let filename_bytes = os_str_as_bytes(filename).expect("UTF-8 error");
737-
let filename_lossy = String::from_utf8_lossy(filename_bytes);
739+
738740
if filename == "-" {
739741
Ok(Box::new(stdin())) // Use stdin if "-" is specified in the checksum file
740742
} else {
@@ -747,15 +749,23 @@ fn get_file_to_check(
747749
opts.verbose,
748750
);
749751
};
752+
let print_error = |err: io::Error| {
753+
show!(err.map_err_context(|| {
754+
locale_aware_escape_name(filename, QuotingStyle::SHELL_ESCAPE)
755+
// This is non destructive thanks to the escaping
756+
.to_string_lossy()
757+
.to_string()
758+
}));
759+
};
750760
match File::open(filename) {
751761
Ok(f) => {
752762
if f.metadata()
753763
.map_err(|_| LineCheckError::CantOpenFile)?
754764
.is_dir()
755765
{
756-
show!(USimpleError::new(
757-
1,
758-
format!("{filename_lossy}: Is a directory")
766+
print_error(io::Error::new(
767+
io::ErrorKind::IsADirectory,
768+
"Is a directory",
759769
));
760770
// also regarded as a failed open
761771
failed_open();
@@ -767,7 +777,7 @@ fn get_file_to_check(
767777
Err(err) => {
768778
if !opts.ignore_missing {
769779
// yes, we have both stderr and stdout here
770-
show!(err.map_err_context(|| filename_lossy.to_string()));
780+
print_error(err);
771781
failed_open();
772782
}
773783
// we could not open the file but we want to continue

tests/by-util/test_cksum.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,7 +1453,7 @@ fn test_check_trailing_space_fails() {
14531453
/// in checksum files.
14541454
/// These tests are excluded from Windows because it does not provide any safe
14551455
/// conversion between `OsString` and byte sequences for non-utf-8 strings.
1456-
mod check_utf8 {
1456+
mod check_encoding {
14571457

14581458
// This test should pass on linux and macos.
14591459
#[cfg(not(windows))]
@@ -1467,15 +1467,12 @@ mod check_utf8 {
14671467
BLAKE2b (empty) = eGoC90IBWQPGxv2FJVLScpEvR0DhWEdhiobiF/cfVBnSXhAxr+5YUxOJZESTTrBLkDpoWxRIt1XVb3Aa/pvizg==\n"
14681468
;
14691469

1470-
let scene = TestScenario::new(util_name!());
1471-
let at = &scene.fixtures;
1470+
let (at, mut cmd) = at_and_ucmd!();
14721471

14731472
at.touch("empty");
14741473
at.write_bytes("check", hashes);
14751474

1476-
scene
1477-
.ucmd()
1478-
.arg("--check")
1475+
cmd.arg("--check")
14791476
.arg(at.subdir.join("check"))
14801477
.succeeds()
14811478
.stdout_is("empty: OK\nempty: OK\nempty: OK\n")
@@ -1528,6 +1525,29 @@ mod check_utf8 {
15281525
.stdout_is_bytes(b"flakey\xffname: FAILED open or read\n")
15291526
.stderr_contains("1 listed file could not be read");
15301527
}
1528+
1529+
#[cfg(target_os = "linux")]
1530+
#[test]
1531+
fn test_quoting_in_stderr() {
1532+
use super::*;
1533+
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
1534+
1535+
let (at, mut cmd) = at_and_ucmd!();
1536+
1537+
at.mkdir(<OsStr as OsStrExt>::from_bytes(b"FFF\xffDIR"));
1538+
at.write_bytes(
1539+
"check",
1540+
b"SHA256 (FFF\xffFFF) = 29953405eaa3dcc41c37d1621d55b6a47eee93e05613e439e73295029740b10c\nSHA256 (FFF\xffDIR) = 29953405eaa3dcc41c37d1621d55b6a47eee93e05613e439e73295029740b10c\n",
1541+
);
1542+
1543+
cmd.arg("-c")
1544+
.arg("check")
1545+
.fails_with_code(1)
1546+
.stdout_contains_bytes(b"FFF\xffFFF: FAILED open or read")
1547+
.stdout_contains_bytes(b"FFF\xffDIR: FAILED open or read")
1548+
.stderr_contains("'FFF'$'\\377''FFF': No such file or directory")
1549+
.stderr_contains("'FFF'$'\\377''DIR': Is a directory");
1550+
}
15311551
}
15321552

15331553
#[test]

tests/uutests/src/lib/macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ macro_rules! new_ucmd {
7171
#[macro_export]
7272
macro_rules! at_and_ucmd {
7373
() => {{
74-
let ts = TestScenario::new(util_name!());
74+
let ts = ::uutests::util::TestScenario::new(::uutests::util_name!());
7575
(ts.fixtures.clone(), ts.ucmd())
7676
}};
7777
}

tests/uutests/src/lib/util.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
clippy::missing_errors_doc
1313
)]
1414

15+
use core::str;
1516
#[cfg(unix)]
1617
use libc::mode_t;
1718
#[cfg(unix)]
@@ -758,6 +759,29 @@ impl CmdResult {
758759
self
759760
}
760761

762+
/// Verify if stdout contains a byte sequence
763+
///
764+
/// # Examples
765+
///
766+
/// ```rust,ignore
767+
/// new_ucmd!()
768+
/// .arg("--help")
769+
/// .succeeds()
770+
/// .stdout_contains_bytes(b"hello \xff");
771+
/// ```
772+
#[track_caller]
773+
pub fn stdout_contains_bytes<T: AsRef<[u8]>>(&self, cmp: T) -> &Self {
774+
assert!(
775+
self.stdout()
776+
.windows(cmp.as_ref().len())
777+
.any(|sub| sub == cmp.as_ref()),
778+
"'{:?}'\ndoes not contain\n'{:?}'",
779+
self.stdout(),
780+
cmp.as_ref()
781+
);
782+
self
783+
}
784+
761785
/// Verify if stderr contains a specific string
762786
///
763787
/// # Examples
@@ -780,6 +804,29 @@ impl CmdResult {
780804
self
781805
}
782806

807+
/// Verify if stderr contains a byte sequence
808+
///
809+
/// # Examples
810+
///
811+
/// ```rust,ignore
812+
/// new_ucmd!()
813+
/// .arg("--help")
814+
/// .succeeds()
815+
/// .stdout_contains_bytes(b"hello \xff");
816+
/// ```
817+
#[track_caller]
818+
pub fn stderr_contains_bytes<T: AsRef<[u8]>>(&self, cmp: T) -> &Self {
819+
assert!(
820+
self.stderr()
821+
.windows(cmp.as_ref().len())
822+
.any(|sub| sub == cmp.as_ref()),
823+
"'{:?}'\ndoes not contain\n'{:?}'",
824+
self.stderr(),
825+
cmp.as_ref()
826+
);
827+
self
828+
}
829+
783830
/// Verify if stdout does not contain a specific string
784831
///
785832
/// # Examples

0 commit comments

Comments
 (0)