Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
14 changes: 13 additions & 1 deletion src/uu/stat/src/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ impl Stater {
j += offset;

// Reject directives like `%<NUMBER>` by checking if width has been parsed.
if j >= bound || chars[j] == '%' {
if j >= bound {

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.

now that the % check moved below, this block does the same as the check_bound on the next line, could be removed, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, removed it: check_bound on the next line reports the same directive when the width ends the format.

let end = j.min(bound - 1);
let invalid_directive: String = chars[old..=end].iter().collect();
return Err(DirectiveError::new(
Expand Down Expand Up @@ -834,6 +834,18 @@ impl Stater {
check_bound(format_str, bound, old, j)?;
}

// A `%` after flags, a width or a precision (`%0%`, `%-%`, `%.3%`) is
// not the `%%` escape; GNU rejects it as an invalid directive.
if chars[j] == '%' {

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.

the width branch above already does the same chars[j] == '%' check, could be dedup, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, the width branch now only handles the end of the format and leaves the % case to the check below.

let invalid_directive: String = chars[old..=j].iter().collect();
return Err(DirectiveError::new(
format_str,
&invalid_directive,
old,
j + 1,
));
}

*i = j;

// Check for multi-character specifiers (e.g., `%Hd`, `%Lr`)
Expand Down
25 changes: 25 additions & 0 deletions tests/by-util/test_stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,31 @@ fn test_printf_invalid_directive() {
.stderr_contains("'%9%': invalid directive");
}

#[test]
fn test_percent_after_flags_is_an_invalid_directive() {
let ts = TestScenario::new(util_name!());
// Only a bare `%%` is the escape. With flags or a precision in between,
// GNU stops at the directive, after printing what came before it.
for (fmt, before, directive) in [
("%0%", "", "%0%"),
("%-%s1212x", "", "%-%"),
("%++-+%1212x", "", "%++-+%"),
("%#%d", "", "%#%"),
("%.3%", "", "%.3%"),
("x%+-%%ss", "x", "%+-%"),
] {
ts.ucmd()
.args(&["-c", fmt, "."])
.fails_with_code(1)
.stdout_is(before)
.stderr_is(format!("stat: '{directive}': invalid directive\n"));
}
ts.ucmd()
.args(&["--printf=100%%\n", "."])
.succeeds()
.stdout_only("100%\n");
}

#[test]
fn test_invalid_directive_after_multibyte_char() {
let ts = TestScenario::new(util_name!());
Expand Down
Loading