Skip to content

Commit 96762a4

Browse files
committed
Handle formatter flags in WTF-8 OsStr Display
The Display implementation for `OsStr` and `Path` on Windows (the WTF-8 version) only handles formatter flags when the entire string is valid UTF-8. As most paths are valid UTF-8, the common case is formatted like `str`; however, flags are ignored when they contain an unpaired surrogate. Implement its Display with the same logic as that of `str`. Fixes #136617 for Windows.
1 parent e2ab773 commit 96762a4

File tree

5 files changed

+110
-20
lines changed

5 files changed

+110
-20
lines changed

library/core/src/fmt/mod.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1513,8 +1513,11 @@ unsafe fn getcount(args: &[rt::Argument<'_>], cnt: &rt::Count) -> Option<usize>
15131513
}
15141514

15151515
/// Padding after the end of something. Returned by `Formatter::padding`.
1516+
#[doc(hidden)]
15161517
#[must_use = "don't forget to write the post padding"]
1517-
pub(crate) struct PostPadding {
1518+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1519+
#[derive(Debug)]
1520+
pub struct PostPadding {
15181521
fill: char,
15191522
padding: usize,
15201523
}
@@ -1525,7 +1528,9 @@ impl PostPadding {
15251528
}
15261529

15271530
/// Writes this post padding.
1528-
pub(crate) fn write(self, f: &mut Formatter<'_>) -> Result {
1531+
#[doc(hidden)]
1532+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1533+
pub fn write(self, f: &mut Formatter<'_>) -> Result {
15291534
for _ in 0..self.padding {
15301535
f.buf.write_char(self.fill)?;
15311536
}
@@ -1743,7 +1748,9 @@ impl<'a> Formatter<'a> {
17431748
///
17441749
/// Callers are responsible for ensuring post-padding is written after the
17451750
/// thing that is being padded.
1746-
pub(crate) fn padding(
1751+
#[doc(hidden)]
1752+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1753+
pub fn padding(
17471754
&mut self,
17481755
padding: usize,
17491756
default: Alignment,

library/std/src/ffi/os_str/tests.rs

+16
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ fn test_os_string_join() {
105105
assert_eq!("a b c", strings_abc.join(OsStr::new(" ")));
106106
}
107107

108+
#[test]
109+
fn display() {
110+
let os_string = OsString::from("bcd");
111+
assert_eq!(format!("a{:^10}e", os_string.display()), "a bcd e");
112+
}
113+
114+
#[cfg(windows)]
115+
#[test]
116+
fn display_invalid_wtf8_windows() {
117+
use crate::os::windows::ffi::OsStringExt;
118+
119+
let os_string = OsString::from_wide(&[b'b' as _, 0xD800, b'd' as _]);
120+
assert_eq!(format!("a{:^10}e", os_string.display()), "a b�d e");
121+
assert_eq!(format!("a{:^10}e", os_string.as_os_str().display()), "a b�d e");
122+
}
123+
108124
#[test]
109125
fn test_os_string_default() {
110126
let os_string: OsString = Default::default();

library/std/src/sys_common/wtf8.rs

+57-17
Original file line numberDiff line numberDiff line change
@@ -587,23 +587,40 @@ impl fmt::Debug for Wtf8 {
587587
/// Formats the string with unpaired surrogates substituted with the replacement
588588
/// character, U+FFFD.
589589
impl fmt::Display for Wtf8 {
590-
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
591-
let wtf8_bytes = &self.bytes;
592-
let mut pos = 0;
593-
loop {
594-
match self.next_surrogate(pos) {
595-
Some((surrogate_pos, _)) => {
596-
formatter.write_str(unsafe {
597-
str::from_utf8_unchecked(&wtf8_bytes[pos..surrogate_pos])
598-
})?;
599-
formatter.write_str(UTF8_REPLACEMENT_CHARACTER)?;
600-
pos = surrogate_pos + 3;
601-
}
602-
None => {
603-
let s = unsafe { str::from_utf8_unchecked(&wtf8_bytes[pos..]) };
604-
if pos == 0 { return s.fmt(formatter) } else { return formatter.write_str(s) }
605-
}
606-
}
590+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591+
// Corresponds to `Formatter::pad`, but for `Wtf8` instead of `str`.
592+
593+
// Make sure there's a fast path up front.
594+
if f.options().get_width().is_none() && f.options().get_precision().is_none() {
595+
return self.write_lossy(f);
596+
}
597+
598+
// The `precision` field can be interpreted as a maximum width for the
599+
// string being formatted.
600+
let max_code_point_count = f.options().get_precision().unwrap_or(usize::MAX);
601+
let mut iter = self.code_points();
602+
let code_point_count = iter.by_ref().take(max_code_point_count).count();
603+
604+
// If our string is longer than the maximum width, truncate it and
605+
// handle other flags in terms of the truncated string.
606+
let byte_len = self.len() - iter.as_slice().len();
607+
// SAFETY: The index is derived from the offset of `.code_points()`,
608+
// which is guaranteed to be in-bounds and between character boundaries.
609+
let s = unsafe { Wtf8::from_bytes_unchecked(self.bytes.get_unchecked(..byte_len)) };
610+
611+
// The `width` field is more of a minimum width parameter at this point.
612+
if let Some(width) = f.options().get_width()
613+
&& code_point_count < width
614+
{
615+
// If we're under the minimum width, then fill up the minimum width
616+
// with the specified string + some alignment.
617+
let post_padding = f.padding(width - code_point_count, fmt::Alignment::Left)?;
618+
s.write_lossy(f)?;
619+
post_padding.write(f)
620+
} else {
621+
// If we're over the minimum width or there is no minimum width, we
622+
// can just emit the string.
623+
s.write_lossy(f)
607624
}
608625
}
609626
}
@@ -719,6 +736,19 @@ impl Wtf8 {
719736
}
720737
}
721738

739+
/// Writes the string as lossy UTF-8 like [`Wtf8::to_string_lossy`].
740+
/// It ignores formatter flags.
741+
fn write_lossy(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
742+
let wtf8_bytes = &self.bytes;
743+
let mut pos = 0;
744+
while let Some((surrogate_pos, _)) = self.next_surrogate(pos) {
745+
f.write_str(unsafe { str::from_utf8_unchecked(&wtf8_bytes[pos..surrogate_pos]) })?;
746+
f.write_str(UTF8_REPLACEMENT_CHARACTER)?;
747+
pos = surrogate_pos + 3;
748+
}
749+
f.write_str(unsafe { str::from_utf8_unchecked(&wtf8_bytes[pos..]) })
750+
}
751+
722752
/// Converts the WTF-8 string to potentially ill-formed UTF-16
723753
/// and return an iterator of 16-bit code units.
724754
///
@@ -1003,6 +1033,16 @@ impl Iterator for Wtf8CodePoints<'_> {
10031033
}
10041034
}
10051035

1036+
impl<'a> Wtf8CodePoints<'a> {
1037+
/// Views the underlying data as a subslice of the original data.
1038+
#[inline]
1039+
pub fn as_slice(&self) -> &Wtf8 {
1040+
// SAFETY: `Wtf8CodePoints` is only made from a `Wtf8Str`, which
1041+
// guarantees the iter is valid WTF-8.
1042+
unsafe { Wtf8::from_bytes_unchecked(self.bytes.as_slice()) }
1043+
}
1044+
}
1045+
10061046
/// Generates a wide character sequence for potentially ill-formed UTF-16.
10071047
#[stable(feature = "rust1", since = "1.0.0")]
10081048
#[derive(Clone)]

library/std/src/sys_common/wtf8/tests.rs

+15
Original file line numberDiff line numberDiff line change
@@ -748,3 +748,18 @@ fn unwobbly_wtf8_plus_utf8_is_utf8() {
748748
string.push_str("some utf-8");
749749
assert!(string.is_known_utf8);
750750
}
751+
752+
#[test]
753+
fn display_wtf8() {
754+
let string = Wtf8Buf::from_wide(&[b'b' as _, 0xD800, b'd' as _]);
755+
assert!(!string.is_known_utf8);
756+
assert_eq!(format!("a{:^10}e", string), "a b�d e");
757+
assert_eq!(format!("a{:^10}e", string.as_slice()), "a b�d e");
758+
759+
let mut string = Wtf8Buf::from_str("bcd");
760+
assert!(string.is_known_utf8);
761+
assert_eq!(format!("a{:^10}e", string), "a bcd e");
762+
assert_eq!(format!("a{:^10}e", string.as_slice()), "a bcd e");
763+
string.is_known_utf8 = false;
764+
assert_eq!(format!("a{:^10}e", string), "a bcd e");
765+
}

library/std/tests/path.rs

+12
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,18 @@ fn test_clone_into() {
18191819
fn display_format_flags() {
18201820
assert_eq!(format!("a{:#<5}b", Path::new("").display()), "a#####b");
18211821
assert_eq!(format!("a{:#<5}b", Path::new("a").display()), "aa####b");
1822+
assert_eq!(format!("a{:^10}e", Path::new("bcd").display()), "a bcd e");
1823+
}
1824+
1825+
#[cfg(windows)]
1826+
#[test]
1827+
fn display_invalid_wtf8_windows() {
1828+
use std::ffi::OsString;
1829+
use std::os::windows::ffi::OsStringExt;
1830+
1831+
let path_buf = PathBuf::from(OsString::from_wide(&[b'b' as _, 0xD800, b'd' as _]));
1832+
assert_eq!(format!("a{:^10}e", path_buf.display()), "a b�d e");
1833+
assert_eq!(format!("a{:^10}e", path_buf.as_path().display()), "a b�d e");
18221834
}
18231835

18241836
#[test]

0 commit comments

Comments
 (0)