Skip to content

Commit 98e7077

Browse files
committed
Auto merge of rust-lang#153025 - joboet:bytestr_precision_display, r=Mark-Simulacrum
core: respect precision in `ByteStr` `Display` Fixes rust-lang#153022. `ByteStr`'s `Display` implementation didn't respect the precision parameter. Just like `Formatter::pad`, this is fixed by counting off the characters in the string and truncating after the requested length – with the added complication that the `ByteStr` needs to be divided into chunks first. By including a fast path that avoids counting the characters when no parameters were specified this should also fix the performance regressions caused by rust-lang#152865.
2 parents eda4fc7 + fa99d5b commit 98e7077

2 files changed

Lines changed: 88 additions & 31 deletions

File tree

library/core/src/bstr/mod.rs

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod traits;
66
pub use traits::{impl_partial_eq, impl_partial_eq_n, impl_partial_eq_ord};
77

88
use crate::borrow::{Borrow, BorrowMut};
9-
use crate::fmt;
9+
use crate::fmt::{self, Alignment};
1010
use crate::ops::{Deref, DerefMut, DerefPure};
1111

1212
/// A wrapper for `&[u8]` representing a human-readable string that's conventionally, but not
@@ -174,43 +174,85 @@ impl fmt::Debug for ByteStr {
174174
#[unstable(feature = "bstr", issue = "134915")]
175175
impl fmt::Display for ByteStr {
176176
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177-
let nchars: usize = self
178-
.utf8_chunks()
179-
.map(|chunk| {
180-
chunk.valid().chars().count() + if chunk.invalid().is_empty() { 0 } else { 1 }
181-
})
182-
.sum();
183-
184-
let padding = f.width().unwrap_or(0).saturating_sub(nchars);
185-
let fill = f.fill();
186-
187-
let (lpad, rpad) = match f.align() {
188-
Some(fmt::Alignment::Right) => (padding, 0),
189-
Some(fmt::Alignment::Center) => {
190-
let half = padding / 2;
191-
(half, half + padding % 2)
177+
fn emit(byte_str: &ByteStr, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178+
for chunk in byte_str.utf8_chunks() {
179+
f.write_str(chunk.valid())?;
180+
if !chunk.invalid().is_empty() {
181+
f.write_str("\u{FFFD}")?;
182+
}
192183
}
193-
// Either alignment is not specified or it's left aligned
194-
// which behaves the same with padding
195-
_ => (0, padding),
196-
};
197184

198-
for _ in 0..lpad {
199-
write!(f, "{fill}")?;
185+
Ok(())
200186
}
201187

202-
for chunk in self.utf8_chunks() {
203-
f.write_str(chunk.valid())?;
204-
if !chunk.invalid().is_empty() {
205-
f.write_str("\u{FFFD}")?;
206-
}
188+
let requested_width = f.width().unwrap_or(0);
189+
if requested_width == 0 && f.precision().is_none() {
190+
// Avoid counting the characters if no truncation or padding was
191+
// requested.
192+
return emit(self, f);
207193
}
208194

209-
for _ in 0..rpad {
210-
write!(f, "{fill}")?;
211-
}
195+
let (truncated, actual_width) = match f.precision() {
196+
// The entire string is truncated away. Weird, but ok.
197+
Some(0) => (ByteStr::new(&[]), 0),
198+
// Advance through string until we run out of space.
199+
Some(precision) => {
200+
let mut remaining_width = precision;
201+
let mut chunks = self.utf8_chunks();
202+
let mut current_width = 0;
203+
let mut offset = 0;
204+
loop {
205+
let Some(chunk) = chunks.next() else {
206+
// We reached the end of the string without running out
207+
// of space, so print the entire string.
208+
break (self, current_width);
209+
};
210+
211+
let mut chars = chunk.valid().char_indices();
212+
let Err(remaining) = chars.advance_by(remaining_width) else {
213+
// We've counted off `precision` characters, so truncate
214+
// the string at the current offset.
215+
break (&self[..offset + chars.offset()], precision);
216+
};
217+
218+
offset += chunk.valid().len();
219+
current_width += remaining_width - remaining.get();
220+
remaining_width = remaining.get();
221+
222+
// `remaining_width` cannot be zero, there is still space
223+
// remaining. So next, count the � character emitted for
224+
// the invalid chunk (if it exists).
225+
if !chunk.invalid().is_empty() {
226+
offset += chunk.invalid().len();
227+
current_width += 1;
228+
remaining_width -= 1;
229+
230+
if remaining_width == 0 {
231+
break (&self[..offset], precision);
232+
}
233+
}
234+
}
235+
}
236+
// The string shouldn't be truncated at all, so just count the number
237+
// of characters to calculate the padding.
238+
None => {
239+
let actual_width = self
240+
.utf8_chunks()
241+
.map(|chunk| {
242+
chunk.valid().chars().count()
243+
+ if chunk.invalid().is_empty() { 0 } else { 1 }
244+
})
245+
.sum();
246+
(self, actual_width)
247+
}
248+
};
212249

213-
Ok(())
250+
// The width is originally stored as a 16-bit number, so this cannot fail.
251+
let padding = u16::try_from(requested_width.saturating_sub(actual_width)).unwrap();
252+
253+
let post_padding = f.padding(padding, Alignment::Left)?;
254+
emit(truncated, f)?;
255+
post_padding.write(f)
214256
}
215257
}
216258

library/coretests/tests/bstr.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,19 @@ fn test_display() {
4949
assert_eq!(&format!("{b2:->3}!"), "�(��!");
5050
assert_eq!(&format!("{b2:-^3}!"), "�(��!");
5151
assert_eq!(&format!("{b2:-^2}!"), "�(��!");
52+
53+
assert_eq!(&format!("{b1:.1}!"), &format!("{:.1}!", "abc"));
54+
assert_eq!(&format!("{b1:.2}!"), &format!("{:.2}!", "abc"));
55+
assert_eq!(&format!("{b1:.3}!"), &format!("{:.3}!", "abc"));
56+
assert_eq!(&format!("{b1:-<5.2}!"), &format!("{:-<5.2}!", "abc"));
57+
assert_eq!(&format!("{b1:-^5.2}!"), &format!("{:-^5.2}!", "abc"));
58+
assert_eq!(&format!("{b1:->5.2}!"), &format!("{:->5.2}!", "abc"));
59+
60+
assert_eq!(&format!("{b2:.1}!"), "�!");
61+
assert_eq!(&format!("{b2:.2}!"), "�(!");
62+
assert_eq!(&format!("{b2:.3}!"), "�(�!");
63+
assert_eq!(&format!("{b2:.4}!"), "�(��!");
64+
assert_eq!(&format!("{b2:-<6.3}!"), "�(�---!");
65+
assert_eq!(&format!("{b2:-^6.3}!"), "-�(�--!");
66+
assert_eq!(&format!("{b2:->6.3}!"), "---�(�!");
5267
}

0 commit comments

Comments
 (0)