-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
printf: Consistently handle negative widths/precision #7620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -316,7 +316,7 @@ impl Spec { | |
match self { | ||
Self::Char { width, align_left } => { | ||
let (width, neg_width) = | ||
resolve_asterisk_maybe_negative(*width, &mut args).unwrap_or_default(); | ||
resolve_asterisk_width(*width, &mut args).unwrap_or_default(); | ||
write_padded(writer, &[args.get_char()], width, *align_left || neg_width) | ||
} | ||
Self::String { | ||
|
@@ -325,15 +325,15 @@ impl Spec { | |
precision, | ||
} => { | ||
let (width, neg_width) = | ||
resolve_asterisk_maybe_negative(*width, &mut args).unwrap_or_default(); | ||
resolve_asterisk_width(*width, &mut args).unwrap_or_default(); | ||
|
||
// GNU does do this truncation on a byte level, see for instance: | ||
// printf "%.1s" 🙃 | ||
// > � | ||
// For now, we let printf panic when we truncate within a code point. | ||
// TODO: We need to not use Rust's formatting for aligning the output, | ||
// so that we can just write bytes to stdout without panicking. | ||
let precision = resolve_asterisk(*precision, &mut args); | ||
let precision = resolve_asterisk_precision(*precision, &mut args); | ||
let s = args.get_str(); | ||
let truncated = match precision { | ||
Some(p) if p < s.len() => &s[..p], | ||
|
@@ -349,7 +349,7 @@ impl Spec { | |
Self::EscapedString => { | ||
let s = args.get_str(); | ||
let mut parsed = Vec::new(); | ||
for c in parse_escape_only(s.as_bytes(), OctalParsing::default()) { | ||
for c in parse_escape_only(s.as_bytes(), OctalParsing::ThreeDigits) { | ||
match c.write(&mut parsed)? { | ||
ControlFlow::Continue(()) => {} | ||
ControlFlow::Break(()) => { | ||
|
@@ -382,8 +382,10 @@ impl Spec { | |
positive_sign, | ||
alignment, | ||
} => { | ||
let width = resolve_asterisk(*width, &mut args).unwrap_or(0); | ||
let precision = resolve_asterisk(*precision, &mut args).unwrap_or(0); | ||
let (width, neg_width) = | ||
resolve_asterisk_width(*width, &mut args).unwrap_or((0, false)); | ||
let precision = | ||
resolve_asterisk_precision(*precision, &mut args).unwrap_or_default(); | ||
let i = args.get_i64(); | ||
|
||
if precision as u64 > i32::MAX as u64 { | ||
|
@@ -394,7 +396,11 @@ impl Spec { | |
width, | ||
precision, | ||
positive_sign: *positive_sign, | ||
alignment: *alignment, | ||
alignment: if neg_width { | ||
NumberAlignment::Left | ||
} else { | ||
*alignment | ||
}, | ||
} | ||
.fmt(writer, i) | ||
.map_err(FormatError::IoError) | ||
|
@@ -405,8 +411,10 @@ impl Spec { | |
precision, | ||
alignment, | ||
} => { | ||
let width = resolve_asterisk(*width, &mut args).unwrap_or(0); | ||
let precision = resolve_asterisk(*precision, &mut args).unwrap_or(0); | ||
let (width, neg_width) = | ||
resolve_asterisk_width(*width, &mut args).unwrap_or((0, false)); | ||
let precision = | ||
resolve_asterisk_precision(*precision, &mut args).unwrap_or_default(); | ||
let i = args.get_u64(); | ||
|
||
if precision as u64 > i32::MAX as u64 { | ||
|
@@ -417,7 +425,11 @@ impl Spec { | |
variant: *variant, | ||
precision, | ||
width, | ||
alignment: *alignment, | ||
alignment: if neg_width { | ||
NumberAlignment::Left | ||
} else { | ||
*alignment | ||
}, | ||
} | ||
.fmt(writer, i) | ||
.map_err(FormatError::IoError) | ||
|
@@ -431,8 +443,9 @@ impl Spec { | |
alignment, | ||
precision, | ||
} => { | ||
let width = resolve_asterisk(*width, &mut args).unwrap_or(0); | ||
let precision = resolve_asterisk(*precision, &mut args); | ||
let (width, neg_width) = | ||
resolve_asterisk_width(*width, &mut args).unwrap_or((0, false)); | ||
let precision = resolve_asterisk_precision(*precision, &mut args); | ||
let f: ExtendedBigDecimal = args.get_extended_big_decimal(); | ||
|
||
if precision.is_some_and(|p| p as u64 > i32::MAX as u64) { | ||
|
@@ -448,7 +461,11 @@ impl Spec { | |
case: *case, | ||
force_decimal: *force_decimal, | ||
positive_sign: *positive_sign, | ||
alignment: *alignment, | ||
alignment: if neg_width { | ||
NumberAlignment::Left | ||
} else { | ||
*alignment | ||
}, | ||
} | ||
.fmt(writer, &f) | ||
.map_err(FormatError::IoError) | ||
|
@@ -457,18 +474,9 @@ impl Spec { | |
} | ||
} | ||
|
||
fn resolve_asterisk<'a>( | ||
option: Option<CanAsterisk<usize>>, | ||
mut args: impl ArgumentIter<'a>, | ||
) -> Option<usize> { | ||
match option { | ||
None => None, | ||
Some(CanAsterisk::Asterisk) => Some(usize::try_from(args.get_u64()).ok().unwrap_or(0)), | ||
Some(CanAsterisk::Fixed(w)) => Some(w), | ||
} | ||
} | ||
|
||
fn resolve_asterisk_maybe_negative<'a>( | ||
/// Determine the width, potentially getting a value from args | ||
/// Returns the non-negative width and whether the value should be left-aligned. | ||
fn resolve_asterisk_width<'a>( | ||
option: Option<CanAsterisk<usize>>, | ||
mut args: impl ArgumentIter<'a>, | ||
) -> Option<(usize, bool)> { | ||
|
@@ -486,6 +494,23 @@ fn resolve_asterisk_maybe_negative<'a>( | |
} | ||
} | ||
|
||
/// Determines the precision, which should (if defined) | ||
/// be a non-negative number. | ||
fn resolve_asterisk_precision<'a>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would be nice to add a comment explaining what this function is doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure thing, added doc and unit tests. Thanks for the review! |
||
option: Option<CanAsterisk<usize>>, | ||
mut args: impl ArgumentIter<'a>, | ||
) -> Option<usize> { | ||
match option { | ||
None => None, | ||
Some(CanAsterisk::Asterisk) => match args.get_i64() { | ||
v if v >= 0 => usize::try_from(v).ok(), | ||
v if v < 0 => Some(0usize), | ||
_ => None, | ||
}, | ||
Some(CanAsterisk::Fixed(w)) => Some(w), | ||
} | ||
} | ||
|
||
fn write_padded( | ||
mut writer: impl Write, | ||
text: &[u8], | ||
|
@@ -527,3 +552,110 @@ fn eat_number(rest: &mut &[u8], index: &mut usize) -> Option<usize> { | |
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
mod resolve_asterisk_width { | ||
use super::*; | ||
use crate::format::FormatArgument; | ||
|
||
#[test] | ||
fn no_width() { | ||
assert_eq!(None, resolve_asterisk_width(None, Vec::new().iter())); | ||
} | ||
|
||
#[test] | ||
fn fixed_width() { | ||
assert_eq!( | ||
Some((42, false)), | ||
resolve_asterisk_width(Some(CanAsterisk::Fixed(42)), Vec::new().iter()) | ||
); | ||
} | ||
|
||
#[test] | ||
fn asterisks_with_numbers() { | ||
assert_eq!( | ||
Some((42, false)), | ||
resolve_asterisk_width( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::SignedInt(42)].iter() | ||
) | ||
); | ||
assert_eq!( | ||
Some((42, false)), | ||
resolve_asterisk_width( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::Unparsed("42".to_string())].iter() | ||
) | ||
); | ||
|
||
assert_eq!( | ||
Some((42, true)), | ||
resolve_asterisk_width( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::SignedInt(-42)].iter() | ||
) | ||
); | ||
assert_eq!( | ||
Some((42, true)), | ||
resolve_asterisk_width( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::Unparsed("-42".to_string())].iter() | ||
) | ||
); | ||
} | ||
} | ||
|
||
mod resolve_asterisk_precision { | ||
use super::*; | ||
use crate::format::FormatArgument; | ||
|
||
#[test] | ||
fn no_width() { | ||
assert_eq!(None, resolve_asterisk_precision(None, Vec::new().iter())); | ||
} | ||
|
||
#[test] | ||
fn fixed_width() { | ||
assert_eq!( | ||
Some(42), | ||
resolve_asterisk_precision(Some(CanAsterisk::Fixed(42)), Vec::new().iter()) | ||
); | ||
} | ||
|
||
#[test] | ||
fn asterisks_with_numbers() { | ||
assert_eq!( | ||
Some(42), | ||
resolve_asterisk_precision( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::SignedInt(42)].iter() | ||
) | ||
); | ||
assert_eq!( | ||
Some(42), | ||
resolve_asterisk_precision( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::Unparsed("42".to_string())].iter() | ||
) | ||
); | ||
|
||
assert_eq!( | ||
Some(0), | ||
resolve_asterisk_precision( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::SignedInt(-42)].iter() | ||
) | ||
); | ||
assert_eq!( | ||
Some(0), | ||
resolve_asterisk_precision( | ||
Some(CanAsterisk::Asterisk), | ||
vec![FormatArgument::Unparsed("-42".to_string())].iter() | ||
) | ||
); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make sure, this doesn't break stuff like this right?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't break it (this is with GNU coreutils 9.6 in /bin):
I added some tests to show that octal characters are still accepted at different lengths (1-3 numbers normally and 1-4 with leading zero for %b)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please avoid screenshot.
They are terrible for accessibility and search
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the reminder, I replaced the screenshot