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
2 changes: 2 additions & 0 deletions src/uu/date/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ date-error-format-modifier-width-too-large = format modifier width '{$width}' is
date-error-format-missing-plus = the argument {$arg} lacks a leading '+';
when using an option to specify date(s), any non-option
argument must be a format string beginning with '+'

date-error-read = {$path}: read error: {$error}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check the exact GNU wording/exit code with LANG=C?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. With LANG=C, GNU coreutils 9.12 exits 1, writes no stdout, and reports gnudate: /proc/self/mem: read error: Input/output error on this glibc host. The uutils result matches after normalizing the executable name. The test keeps the OS error suffix flexible because musl spells it I/O error.

96 changes: 79 additions & 17 deletions src/uu/date/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,21 @@ enum DateError {
CannotSetDate { path: String, error: String },
#[error("{}", translate!("date-error-invalid-format", "format" => .format, "error" => .error))]
InvalidFormat { format: String, error: String },
#[error("{}", translate!("date-error-read", "path" => .path, "error" => .error))]
Read { path: String, error: String },
#[cfg(target_os = "redox")]
#[error("{}", translate!("date-error-setting-date-not-supported-redox"))]
SettingDateNotSupportedRedox,
}

impl UError for DateError {}

#[derive(Debug)]
enum DateInputError {
InvalidDate(String),
Read { path: String, error: std::io::Error },
}

/// Settings for this program, parsed from the command line
struct Settings {
utc: bool,
Expand Down Expand Up @@ -573,11 +581,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
parse(input, true)
};

let iter = std::iter::once(date);
let iter =
std::iter::once(date.map_err(|(input, _)| DateInputError::InvalidDate(input)));
Box::new(iter)
}
DateSource::Stdin => parse_dates_from_reader(
std::io::stdin(),
"-".to_string(),
&now,
DebugOptions::new(settings.debug, true),
allow_extended,
Expand All @@ -592,6 +602,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
File::open(path).map_err_context(|| path.as_os_str().maybe_quote().to_string())?;
parse_dates_from_reader(
file,
path.as_os_str().maybe_quote().to_string(),
&now,
DebugOptions::new(settings.debug, true),
allow_extended,
Expand Down Expand Up @@ -672,12 +683,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}
}
Err((input, _err)) => {
Err(DateInputError::InvalidDate(input)) => {
let _ = stdout.flush();

show!(DateError::InvalidDate {
date: input.clone()
});
show!(DateError::InvalidDate { date: input });
}
Err(DateInputError::Read { path, error }) => {
let _ = stdout.flush();
return Err(Box::new(DateError::Read {
path,
error: strip_errno(&error),
}));
}
}
}
Expand Down Expand Up @@ -1242,26 +1258,39 @@ fn try_parse_with_abbreviation<S: AsRef<str>>(date_str: S, now: &Zoned) -> Optio
/// Returns a boxed iterator over the parse results.
fn parse_dates_from_reader<R: Read + 'static>(
reader: R,
path: String,
now: &Zoned,
dbg_opts: DebugOptions,
allow_extended: bool,
) -> Box<
dyn Iterator<Item = Result<ParsedDateTime, (String, parse_datetime::ParseDateTimeError)>> + '_,
> {
let lines = BufReader::new(reader).split(b'\n');
Box::new(lines.map_while(Result::ok).map(move |mut bytes| {
) -> Box<dyn Iterator<Item = Result<ParsedDateTime, DateInputError>> + '_> {
let lines = BufReader::new(reader).split(b'\n').map(move |line| {
let mut bytes = match line {
Ok(bytes) => bytes,
Err(error) => {
return Err(DateInputError::Read {
path: path.clone(),
error,
});
}
};
// Strip a trailing '\r' (CRLF input; GNU's lexer ignores it too)
if bytes.last() == Some(&b'\r') {
bytes.pop();
}
match String::from_utf8(bytes) {
Ok(s) => parse_date(s, now, dbg_opts, allow_extended),
// Report lines with invalid UTF-8 (with non-printable bytes
// octal-escaped like GNU) instead of silently stopping the input
Err(e) => Err((
escape_invalid_bytes(e.as_bytes()),
parse_datetime::ParseDateTimeError::InvalidInput,
)),
Ok(s) => parse_date(s, now, dbg_opts, allow_extended)
.map_err(|(input, _)| DateInputError::InvalidDate(input)),
Err(e) => Err(DateInputError::InvalidDate(escape_invalid_bytes(
e.as_bytes(),
))),
}
});
Box::new(lines.scan(false, |failed, result| {
if *failed {
None
} else {
*failed = matches!(&result, Err(DateInputError::Read { .. }));
Some(result)
}
}))
}
Expand Down Expand Up @@ -1457,6 +1486,39 @@ fn set_system_datetime(date: Zoned) -> UResult<()> {
mod tests {
use super::*;

#[test]
fn test_reader_reports_error_after_complete_lines() {
struct FailingReader;
impl Read for FailingReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("read failed"))
}
}

let reader = std::io::Cursor::new(b"@0\r\ninvalid\n@1\n").chain(FailingReader);
let now = Timestamp::UNIX_EPOCH.to_zoned(TimeZone::UTC);
let results: Vec<_> = parse_dates_from_reader(
reader,
"test input".to_string(),
&now,
DebugOptions::new(false, false),
false,
)
.take(5)
.collect();
assert_eq!(results.len(), 4);
assert!(results[0].is_ok());
assert!(
matches!(&results[1], Err(DateInputError::InvalidDate(input)) if input == "invalid")
);
assert!(results[2].is_ok());
assert!(matches!(
&results[3],
Err(DateInputError::Read { path, error })
if path == "test input" && error.to_string() == "read failed"
));
}

#[test]
fn test_parse_military_timezone_with_offset() {
// Valid cases: letter only, letter + digit, uppercase
Expand Down
11 changes: 11 additions & 0 deletions tests/by-util/test_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3446,3 +3446,14 @@ fn test_non_utf8_operands_are_octal_escaped() {
.stderr_contains(expected);
}
}

#[test]
#[cfg(target_os = "linux")]
fn test_date_file_read_error() {
new_ucmd!()
.env("LC_ALL", "C")
.args(&["-f", "/proc/self/mem"])
.fails_with_code(1)
.no_stdout()
.stderr_contains("/proc/self/mem: read error:");
Comment on lines +3453 to +3458
}
Loading