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
52 changes: 22 additions & 30 deletions src/uu/sort/src/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3179,45 +3179,37 @@ fn get_rand_string() -> [u8; SALT_LEN] {
}

const SALT_LEN: usize = 16; // 128-bit salt
const MAX_BYTES: usize = 1024 * 1024; // Read cap: 1 MiB
const BUF_LEN: usize = 8192; // 8 KiB read buffer
const U64_LEN: usize = 8;
const RANDOM_SOURCE_TAG: &[u8] = b"uutils-sort-random-source"; // Domain separation tag

/// Create a 128-bit salt by hashing up to 1 MiB from the given file.
/// Create a 128-bit salt by hashing the first [`SALT_LEN`] bytes from the file.
Comment thread
tasodoufu marked this conversation as resolved.
Comment thread
tasodoufu marked this conversation as resolved.
///
/// The file has to hold at least [`SALT_LEN`] bytes. GNU asks for the same 128
/// bits and reports `end of file` when the source cannot supply them, rather
/// than shuffling with whatever it managed to read.
/// bits and reports `end of file` when the source cannot supply them.
fn salt_from_random_source(path: &Path) -> UResult<[u8; SALT_LEN]> {
// GNU reads exactly SALT_LEN bytes from the source and keeps the file open
// (a named pipe keeps its writer blocked until the reader closes it), so an
// over-read here would hang writers like
// `printf %016d > fifo & sort -R --random-source=fifo`. Match GNU and stop
// after the salt bytes are collected.
let mut reader = open_with_open_failed_error(path)?;
let mut buf = [0u8; BUF_LEN];
let mut total = 0usize;
// freeze seed for --random-source
let mut hasher = FoldHasher::with_seed(1, SharedSeed::global_fixed());

while let n @ 1.. = reader
.read(&mut buf)
.map_err(|error| SortError::ReadFailed {
path: path.to_owned(),
error,
})?
&& let remaining @ 1.. = MAX_BYTES.saturating_sub(total)
{
let take = n.min(remaining);
hasher.write(&buf[..take]);
total = total.saturating_add(take);
if take < n {
break;
let mut salt = [0u8; SALT_LEN];
reader.read_exact(&mut salt).map_err(|error| {
if error.kind() == std::io::ErrorKind::UnexpectedEof {
SortError::RandomSourceEndOfFile {
path: path.to_owned(),
}
} else {
SortError::ReadFailed {
path: path.to_owned(),
error,
}
}
}
})?;

if total < SALT_LEN {
return Err(SortError::RandomSourceEndOfFile {
path: path.to_owned(),
}
.into());
}
// freeze seed for --random-source
let mut hasher = FoldHasher::with_seed(1, SharedSeed::global_fixed());
hasher.write(&salt);

let first = hasher.finish();
// freeze seed for --random-source
Expand Down
17 changes: 12 additions & 5 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -822,11 +822,18 @@ fn parse_date(ref_zoned: Zoned, s: &str) -> Result<FileTime, TouchError> {
// Also support other formats found in the GNU tests like
// in tests/misc/stat-nanoseconds.sh
// or tests/touch/no-rights.sh
for fmt in [
format::YYYYMMDDHHMMS,
format::YYYYMMDDHHMMSS,
format::YYYYMMDDHHMM_OFFSET,
] {
// A date and time without an explicit timezone is interpreted in the
Comment thread
tasodoufu marked this conversation as resolved.
// local zone (GNU behavior), not as UTC (issue #15291). Formats that
// carry a trailing %z offset are still resolved via that offset, so they
// keep an explicit UTC branch.
if let Ok(filetime) = strtime::parse(format::YYYYMMDDHHMMS, s)
Comment thread
tasodoufu marked this conversation as resolved.
.and_then(|tm| tm.to_datetime())
.and_then(|dt| TimeZone::system().to_ambiguous_zoned(dt).unambiguous())
.map(|zdt| timestamp_to_filetime(zdt.timestamp()))
{
return Ok(filetime);
}
for fmt in [format::YYYYMMDDHHMMSS, format::YYYYMMDDHHMM_OFFSET] {
if let Ok(parsed) = strtime::parse(fmt, s)
.and_then(|tm| tm.to_datetime())
.and_then(|dt| TimeZone::UTC.to_zoned(dt))
Expand Down
64 changes: 64 additions & 0 deletions tests/by-util/test_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,70 @@ fn test_random_source_is_not_read_without_random_sort() {
.stdout_is("a\nb\nc\n");
}

#[test]
fn test_random_source_larger_than_the_salt() {
let ts = TestScenario::new(uutests::util_name!());
let at = &ts.fixtures;
at.write("input", "b\na\nc\n");
let mut source_a = vec![b'a'; 32];
let mut source_b = source_a.clone();
source_a[16] = b'b';
source_b[16] = b'c';
at.write_bytes("source-a", &source_a);
at.write_bytes("source-b", &source_b);

let first = ts
.ucmd()
.args(&["-R", "--random-source=source-a", "input"])
.succeeds()
.stdout_move_str();
let second = ts
.ucmd()
.args(&["-R", "--random-source=source-b", "input"])
.succeeds()
.stdout_move_str();

assert_eq!(first, second);
}

#[test]
#[cfg(unix)]
fn test_random_source_named_pipe_does_not_overread() {
// GNU reads exactly the 128-bit salt from the source and keeps the file
// open while shuffling. An over-read on a named pipe would block forever
// on a writer that only supplied the salt.
use std::io::Write;

let ts = TestScenario::new(uutests::util_name!());
ts.fixtures.write("input", "b\na\nc\n");
ts.fixtures.mkdir("d");
ts.fixtures.mkfifo("d/source");

let fifo_w = ts.fixtures.plus("d/source");
let (done_tx, done_rx) = std::sync::mpsc::channel();
let writer = std::thread::spawn(move || {
let mut file = std::fs::OpenOptions::new()
.write(true)
.open(&fifo_w)
.unwrap();
file.write_all(&[b'x'; SALT_TEST_LEN]).unwrap();
// Keep the descriptor open until sort has finished. An over-read
// would block here waiting for bytes beyond the salt.
done_rx.recv().unwrap();
drop(file);
});

ts.ucmd()
.args(&["-R", "--random-source=d/source", "input"])
.succeeds()
.stdout_matches(&regex::Regex::new("[abc]\n[abc]\n[abc]\n").unwrap());
done_tx.send(()).unwrap();
writer.join().unwrap();
}

#[cfg(unix)]
const SALT_TEST_LEN: usize = 16;

#[test]
fn test_random_ignore_case() {
let input = "ABC\nABc\nAbC\nAbc\naBC\naBc\nabC\nabc\n";
Expand Down
28 changes: 28 additions & 0 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1390,3 +1390,31 @@ fn test_touch_set_time_on_unreadable_unwritable_file() {
assert_eq!(atime, expected);
assert_eq!(mtime, expected);
}

// -d with a date and time lacking an explicit timezone must be interpreted in
// the local zone (GNU compatibility), not as UTC. Regression test for
// https://github.com/uutils/coreutils/issues/15291.
#[test]
fn test_touch_date_without_offset_is_local_time() {
// Force TZ=Asia/Tokyo: the harness default TZ=UTC would make the buggy
// "interpret as UTC" behavior indistinguishable from the correct one.
let (at, mut ucmd) = at_and_ucmd!();
let file = "touch_local_no_offset";
at.touch(file);
ucmd.env("TZ", "Asia/Tokyo")
.args(&["-d", "2026-08-01 12:00:00", file])
Comment thread
tasodoufu marked this conversation as resolved.
.succeeds()
.no_output();
let (atime, mtime) = get_file_times(&at, file);
// 1_785_585_600 = 2026-08-01 12:00 UTC; Asia/Tokyo is UTC+9, so civil
// 12:00 local is 03:00 UTC = 1_785_553_200.
let expected = FileTime::from_unix_time(1_785_585_600 - 9 * 3600, 0);
assert_eq!(
mtime, expected,
"mtime should be 2026-08-01 12:00 Asia/Tokyo, not UTC"
);
assert_eq!(
atime, expected,
"atime should be 2026-08-01 12:00 Asia/Tokyo, not UTC"
);
}
Loading