Skip to content

Commit e9ca273

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 ac91805 commit e9ca273

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

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

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

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

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

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)