Skip to content

Commit c5a04d2

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 c1cdd77 commit c5a04d2

File tree

6 files changed

+109
-20
lines changed

6 files changed

+109
-20
lines changed

library/core/src/fmt/mod.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1516,8 +1516,11 @@ unsafe fn getcount(args: &[rt::Argument<'_>], cnt: &rt::Count) -> Option<u16> {
15161516
}
15171517

15181518
/// Padding after the end of something. Returned by `Formatter::padding`.
1519+
#[doc(hidden)]
15191520
#[must_use = "don't forget to write the post padding"]
1520-
pub(crate) struct PostPadding {
1521+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1522+
#[derive(Debug)]
1523+
pub struct PostPadding {
15211524
fill: char,
15221525
padding: u16,
15231526
}
@@ -1528,7 +1531,9 @@ impl PostPadding {
15281531
}
15291532

15301533
/// Writes this post padding.
1531-
pub(crate) fn write(self, f: &mut Formatter<'_>) -> Result {
1534+
#[doc(hidden)]
1535+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1536+
pub fn write(self, f: &mut Formatter<'_>) -> Result {
15321537
for _ in 0..self.padding {
15331538
f.buf.write_char(self.fill)?;
15341539
}
@@ -1738,7 +1743,9 @@ impl<'a> Formatter<'a> {
17381743
///
17391744
/// Callers are responsible for ensuring post-padding is written after the
17401745
/// thing that is being padded.
1741-
pub(crate) fn padding(
1746+
#[doc(hidden)]
1747+
#[unstable(feature = "fmt_internals", reason = "internal to standard library", issue = "none")]
1748+
pub fn padding(
17421749
&mut self,
17431750
padding: u16,
17441751
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/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@
298298
#![feature(formatting_options)]
299299
#![feature(if_let_guard)]
300300
#![feature(intra_doc_pointers)]
301+
#![feature(iter_advance_by)]
301302
#![feature(lang_items)]
302303
#![feature(let_chains)]
303304
#![feature(link_cfg)]

library/std/src/sys_common/wtf8.rs

+55-17
Original file line numberDiff line numberDiff line change
@@ -588,23 +588,48 @@ 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 (s, code_point_count) = if let Some(max_code_point_count) = f.options().get_precision()
602+
{
603+
let mut iter = self.code_point_indices();
604+
let remaining = match iter.advance_by(max_code_point_count as usize) {
605+
Ok(()) => 0,
606+
Err(remaining) => remaining.get(),
607+
};
608+
// SAFETY: The offset of `.code_point_indices()` is guaranteed to be
609+
// in-bounds and between code point boundaries.
610+
let truncated = unsafe {
611+
Wtf8::from_bytes_unchecked(self.bytes.get_unchecked(..iter.front_offset))
612+
};
613+
(truncated, max_code_point_count as usize - remaining)
614+
} else {
615+
// Use the optimized code point counting algorithm for the full
616+
// string.
617+
(self, self.code_points().count())
618+
};
619+
620+
// The `width` field is more of a minimum width parameter at this point.
621+
if let Some(width) = f.options().get_width()
622+
&& code_point_count < width as usize
623+
{
624+
// If we're under the minimum width, then fill up the minimum width
625+
// with the specified string + some alignment.
626+
let post_padding = f.padding(width - code_point_count as u16, fmt::Alignment::Left)?;
627+
s.write_lossy(f)?;
628+
post_padding.write(f)
629+
} else {
630+
// If we're over the minimum width or there is no minimum width, we
631+
// can just emit the string.
632+
s.write_lossy(f)
608633
}
609634
}
610635
}
@@ -726,6 +751,19 @@ impl Wtf8 {
726751
}
727752
}
728753

754+
/// Writes the string as lossy UTF-8 like [`Wtf8::to_string_lossy`].
755+
/// It ignores formatter flags.
756+
fn write_lossy(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
757+
let wtf8_bytes = &self.bytes;
758+
let mut pos = 0;
759+
while let Some((surrogate_pos, _)) = self.next_surrogate(pos) {
760+
f.write_str(unsafe { str::from_utf8_unchecked(&wtf8_bytes[pos..surrogate_pos]) })?;
761+
f.write_str(UTF8_REPLACEMENT_CHARACTER)?;
762+
pos = surrogate_pos + 3;
763+
}
764+
f.write_str(unsafe { str::from_utf8_unchecked(&wtf8_bytes[pos..]) })
765+
}
766+
729767
/// Converts the WTF-8 string to potentially ill-formed UTF-16
730768
/// and return an iterator of 16-bit code units.
731769
///

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)