Skip to content

Commit d901bfa

Browse files
authored
Merge pull request #172 from artichoke/dev/lopopolo-rust-fmt-args-u16
Add tests for dynamic width specifiers exceeding `u16::MAX`
2 parents 7da91cf + 0be636c commit d901bfa

File tree

9 files changed

+319
-117
lines changed

9 files changed

+319
-117
lines changed

.github/workflows/ci.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
- name: Install Rust toolchain
7474
uses: artichoke/setup-rust/[email protected]
7575
with:
76-
toolchain: "1.76.0"
76+
toolchain: "1.84.0"
7777

7878
- name: Compile
7979
run: cargo build --verbose

Cargo.toml

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
[package]
22
name = "strftime-ruby"
33
# remember to set `html_root_url` in `src/lib.rs`.
4-
version = "1.1.0"
4+
version = "1.2.0"
55
authors = ["Ryan Lopopolo <[email protected]>", "x-hgg-x"]
66
license = "MIT"
77
edition = "2021"
8-
rust-version = "1.76.0"
8+
rust-version = "1.84.0"
99
readme = "README.md"
1010
repository = "https://github.com/artichoke/strftime-ruby"
1111
documentation = "https://docs.rs/strftime-ruby"
1212
homepage = "https://github.com/artichoke/strftime-ruby"
1313
description = "Ruby `Time#strftime` parser and formatter"
1414
keywords = ["ruby", "strftime", "time"]
15-
categories = ["date-and-time", "no-std", "no-std::no-alloc", "parser-implementations", "value-formatting"]
15+
categories = [
16+
"date-and-time",
17+
"no-std",
18+
"no-std::no-alloc",
19+
"parser-implementations",
20+
"value-formatting",
21+
]
1622
include = ["/src/**/*", "/tests/**/*", "/LICENSE", "/README.md"]
1723

1824
[lib]

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Add this to your `Cargo.toml`:
3737
3838
```toml
3939
[dependencies]
40-
strftime-ruby = "1.1.0"
40+
strftime-ruby = "1.2.0"
4141
```
4242
4343
## Crate features
@@ -64,7 +64,7 @@ All features are enabled by default.
6464

6565
### Minimum Supported Rust Version
6666

67-
This crate requires at least Rust 1.76.0. This version can be bumped in minor
67+
This crate requires at least Rust 1.84.0. This version can be bumped in minor
6868
releases.
6969

7070
## License

clippy.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
doc-valid-idents = [
2+
"CRuby",
3+
".."
4+
]

src/format/mod.rs

+68-57
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,10 @@ impl Piece {
297297
if self.flags.contains(Flag::LeftPadding) {
298298
write!(f, "{value}")
299299
} else if self.padding == Padding::Spaces {
300-
let width = self.width.unwrap_or(default_width);
300+
let width = self.pad_width(f, b' ', default_width)?;
301301
write!(f, "{value: >width$}")
302302
} else {
303-
let width = self.width.unwrap_or(default_width);
303+
let width = self.pad_width(f, b'0', default_width)?;
304304
write!(f, "{value:0width$}")
305305
}
306306
}
@@ -315,16 +315,24 @@ impl Piece {
315315
if self.flags.contains(Flag::LeftPadding) {
316316
write!(f, "{value}")
317317
} else if self.padding == Padding::Zeros {
318-
let width = self.width.unwrap_or(default_width);
318+
let width = self.pad_width(f, b'0', default_width)?;
319319
write!(f, "{value:0width$}")
320320
} else {
321-
let width = self.width.unwrap_or(default_width);
321+
let width = self.pad_width(f, b' ', default_width)?;
322322
write!(f, "{value: >width$}")
323323
}
324324
}
325325

326+
/// Returns the width to use for the padding.
327+
///
328+
/// Prints any excessive padding directly.
329+
fn pad_width(&self, f: &mut SizeLimiter<'_>, pad: u8, default: usize) -> Result<usize, Error> {
330+
let width = self.width.unwrap_or(default);
331+
f.pad(pad, width.saturating_sub(u16::MAX.into()))?;
332+
Ok(width.min(u16::MAX.into()))
333+
}
334+
326335
/// Format nanoseconds with the specified precision.
327-
#[allow(clippy::uninlined_format_args)] // for readability and symmetry between if branches
328336
fn format_nanoseconds(
329337
&self,
330338
f: &mut SizeLimiter<'_>,
@@ -335,38 +343,37 @@ impl Piece {
335343

336344
if width <= 9 {
337345
let value = nanoseconds / 10u32.pow(9 - width as u32);
338-
write!(f, "{value:0n$}", n = width)
346+
write!(f, "{value:0width$}")
339347
} else {
340-
write!(f, "{nanoseconds:09}{:0n$}", 0, n = width - 9)
348+
write!(f, "{nanoseconds:09}")?;
349+
f.pad(b'0', width - 9)
341350
}
342351
}
343352

344353
/// Format a string value.
345354
fn format_string(&self, f: &mut SizeLimiter<'_>, s: &str) -> Result<(), Error> {
346-
match self.width {
347-
None => write!(f, "{s}"),
348-
Some(width) => {
349-
if self.flags.contains(Flag::LeftPadding) {
350-
write!(f, "{s}")
351-
} else if self.padding == Padding::Zeros {
352-
write!(f, "{s:0>width$}")
353-
} else {
354-
write!(f, "{s: >width$}")
355-
}
356-
}
355+
if !self.flags.contains(Flag::LeftPadding) {
356+
self.write_padding(f, s.len())?;
357357
}
358+
359+
write!(f, "{s}")
358360
}
359361

360362
/// Write padding separately.
361363
fn write_padding(&self, f: &mut SizeLimiter<'_>, min_width: usize) -> Result<(), Error> {
362-
if let Some(width) = self.width {
363-
let n = width.saturating_sub(min_width);
364+
let Some(width) = self.width else {
365+
return Ok(());
366+
};
367+
368+
let n = width.saturating_sub(min_width);
369+
370+
let pad = match self.padding {
371+
Padding::Zeros => b'0',
372+
_ => b' ',
373+
};
374+
375+
f.pad(pad, n)?;
364376

365-
match self.padding {
366-
Padding::Zeros => write!(f, "{:0>n$}", "")?,
367-
_ => write!(f, "{: >n$}", "")?,
368-
};
369-
}
370377
Ok(())
371378
}
372379

@@ -390,14 +397,34 @@ impl Piece {
390397
UtcOffset::new(hour, minute, second)
391398
}
392399

393-
/// Compute hour padding for the `%z` specifier.
394-
fn hour_padding(&self, min_width: usize) -> usize {
395-
const MIN_PADDING: usize = "+hh".len();
400+
/// Write the hour sign.
401+
fn write_hour_sign(f: &mut SizeLimiter<'_>, hour: f64) -> Result<(), Error> {
402+
if hour.is_sign_negative() {
403+
write!(f, "-")?;
404+
} else {
405+
write!(f, "+")?;
406+
}
407+
408+
Ok(())
409+
}
396410

397-
match self.width {
398-
Some(width) => width.saturating_sub(min_width) + MIN_PADDING,
399-
None => MIN_PADDING,
411+
/// Write the hour with padding for the `%z` specifier.
412+
fn write_offset_hour(&self, f: &mut SizeLimiter<'_>, hour: f64, w: usize) -> Result<(), Error> {
413+
let mut pad = self.width.unwrap_or(0).saturating_sub(w);
414+
415+
if hour < 10.0 {
416+
pad += 1;
417+
}
418+
419+
if self.padding == Padding::Spaces {
420+
f.pad(b' ', pad)?;
421+
Self::write_hour_sign(f, hour)?;
422+
} else {
423+
Self::write_hour_sign(f, hour)?;
424+
f.pad(b'0', pad)?;
400425
}
426+
427+
write!(f, "{:.0}", hour.abs())
401428
}
402429

403430
/// Write the time zone UTC offset as `"+hh"`.
@@ -406,13 +433,7 @@ impl Piece {
406433
f: &mut SizeLimiter<'_>,
407434
utc_offset: &UtcOffset,
408435
) -> Result<(), Error> {
409-
let hour = utc_offset.hour;
410-
let n = self.hour_padding("+hh".len());
411-
412-
match self.padding {
413-
Padding::Spaces => write!(f, "{hour: >+n$.0}"),
414-
_ => write!(f, "{hour:+0n$.0}"),
415-
}
436+
self.write_offset_hour(f, utc_offset.hour, "+hh".len())
416437
}
417438

418439
/// Write the time zone UTC offset as `"+hhmm"`.
@@ -421,13 +442,10 @@ impl Piece {
421442
f: &mut SizeLimiter<'_>,
422443
utc_offset: &UtcOffset,
423444
) -> Result<(), Error> {
424-
let UtcOffset { hour, minute, .. } = utc_offset;
425-
let n = self.hour_padding("+hhmm".len());
445+
let UtcOffset { hour, minute, .. } = *utc_offset;
426446

427-
match self.padding {
428-
Padding::Spaces => write!(f, "{hour: >+n$.0}{minute:02}"),
429-
_ => write!(f, "{hour:+0n$.0}{minute:02}"),
430-
}
447+
self.write_offset_hour(f, hour, "+hhmm".len())?;
448+
write!(f, "{minute:02}")
431449
}
432450

433451
/// Write the time zone UTC offset as `"+hh:mm"`.
@@ -436,13 +454,10 @@ impl Piece {
436454
f: &mut SizeLimiter<'_>,
437455
utc_offset: &UtcOffset,
438456
) -> Result<(), Error> {
439-
let UtcOffset { hour, minute, .. } = utc_offset;
440-
let n = self.hour_padding("+hh:mm".len());
457+
let UtcOffset { hour, minute, .. } = *utc_offset;
441458

442-
match self.padding {
443-
Padding::Spaces => write!(f, "{hour: >+n$.0}:{minute:02}"),
444-
_ => write!(f, "{hour:+0n$.0}:{minute:02}"),
445-
}
459+
self.write_offset_hour(f, hour, "+hh:mm".len())?;
460+
write!(f, ":{minute:02}")
446461
}
447462

448463
/// Write the time zone UTC offset as `"+hh:mm:ss"`.
@@ -455,14 +470,10 @@ impl Piece {
455470
hour,
456471
minute,
457472
second,
458-
} = utc_offset;
459-
460-
let n = self.hour_padding("+hh:mm:ss".len());
473+
} = *utc_offset;
461474

462-
match self.padding {
463-
Padding::Spaces => write!(f, "{hour: >+n$.0}:{minute:02}:{second:02}"),
464-
_ => write!(f, "{hour:+0n$.0}:{minute:02}:{second:02}"),
465-
}
475+
self.write_offset_hour(f, hour, "+hh:mm:ss".len())?;
476+
write!(f, ":{minute:02}:{second:02}")
466477
}
467478

468479
/// Format time using the formatting directive.

src/format/utils.rs

+21-1
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,31 @@ impl<'a> SizeLimiter<'a> {
8181
count: 0,
8282
}
8383
}
84+
85+
/// Pad with the specified character.
86+
pub(crate) fn pad(&mut self, c: u8, n: usize) -> Result<(), Error> {
87+
if self.count.saturating_add(n) > self.size_limit {
88+
return Err(Error::FormattedStringTooLarge);
89+
}
90+
91+
let buffer = [c; 1024];
92+
let mut remaining = n;
93+
94+
while remaining > 0 {
95+
let size = remaining.min(1024);
96+
self.inner.write_all(&buffer[..size])?;
97+
remaining -= size;
98+
}
99+
100+
self.count += n;
101+
102+
Ok(())
103+
}
84104
}
85105

86106
impl Write for SizeLimiter<'_> {
87107
fn write(&mut self, buf: &[u8]) -> Result<usize, Error> {
88-
if self.count + buf.len() > self.size_limit {
108+
if self.count.saturating_add(buf.len()) > self.size_limit {
89109
return Err(Error::FormattedStringTooLarge);
90110
}
91111

src/tests/format.rs

+3-53
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,7 @@
1-
#![allow(clippy::should_panic_without_expect)]
2-
31
use crate::format::TimeFormatter;
4-
use crate::{Error, Time};
5-
6-
include!("../mock.rs.in");
7-
8-
fn get_format_err(time: &MockTime<'_>, format: &str) -> Error {
9-
TimeFormatter::new(time, format)
10-
.fmt(&mut &mut [0u8; 100][..])
11-
.unwrap_err()
12-
}
13-
14-
fn check_format(time: &MockTime<'_>, format: &str, expected: &str) {
15-
const SIZE: usize = 100;
16-
let mut buf = [0u8; SIZE];
17-
let mut cursor = &mut buf[..];
18-
19-
TimeFormatter::new(time, format).fmt(&mut cursor).unwrap();
20-
let written = SIZE - cursor.len();
21-
let data = core::str::from_utf8(&buf[..written]).unwrap();
22-
23-
assert_eq!(data, expected);
24-
}
25-
26-
fn check_all(times: &[MockTime<'_>], format: &str, all_expected: &[&str]) {
27-
assert_eq!(times.len(), all_expected.len());
28-
for (time, expected) in times.iter().zip(all_expected) {
29-
check_format(time, format, expected);
30-
}
31-
}
32-
33-
#[test]
34-
#[should_panic]
35-
#[rustfmt::skip]
36-
fn test_check_format_panics_on_error() {
37-
let time = MockTime { year: 1111, ..Default::default() };
38-
39-
check_format(&time, "'%Y'", "'1112'");
40-
}
2+
use crate::Error;
413

42-
#[test]
43-
#[should_panic]
44-
#[rustfmt::skip]
45-
fn test_check_all_panics_on_error() {
46-
let times = [
47-
MockTime { year: -1111, ..Default::default() },
48-
MockTime { year: -11, ..Default::default() },
49-
MockTime { year: 1, ..Default::default() },
50-
MockTime { year: 1111, ..Default::default() },
51-
];
52-
53-
check_all(&times, "'%Y'", &["'-1111'", "'-0011'", "'0001'", "'1112'"]);
54-
}
4+
use super::{check_all, check_format, get_format_err, MockTime};
555

566
#[test]
577
#[rustfmt::skip]
@@ -875,7 +825,7 @@ fn test_format_large_width() {
875825
check_format(&time, "%-100000000m", "1");
876826
check_format(&time, "%2147483648m", "%2147483648m");
877827

878-
let err = get_format_err(&time, "%2147483647m");
828+
let err = get_format_err(&time, "%1000m");
879829
assert!(matches!(err, Error::WriteZero));
880830
}
881831

0 commit comments

Comments
 (0)