Skip to content

Commit cc9bb15

Browse files
committed
Auto merge of #83342 - Count-Count:win-console-incomplete-utf8, r=m-ou-se
Allow writing of incomplete UTF-8 sequences to the Windows console via stdout/stderr # Problem Writes of just an incomplete UTF-8 byte sequence (e.g. `b"\xC3"` or `b"\xF0\x9F"`) to stdout/stderr with a Windows console attached error with `io::ErrorKind::InvalidData, "Windows stdio in console mode does not support writing non-UTF-8 byte sequences"` even though further writes could complete the codepoint. This is currently a rare occurence since the [linewritershim](https://github.com/rust-lang/rust/blob/2c56ea38b045624dc8b42ec948fc169eaff1206a/library/std/src/io/buffered/linewritershim.rs) implementation flushes complete lines immediately and buffers up to 1024 bytes for incomplete lines. It can still happen as described in #83258. The problem will become more pronounced once the developer can switch stdout/stderr from line-buffered to block-buffered or immediate when the changes in the "Switchable buffering for Stdout" pull request (#78515) get merged. # Patch description If there is at least one valid UTF-8 codepoint all valid UTF-8 is passed through to the extracted `write_valid_utf8_to_console()` fn. The new code only comes into play if `write()` is being passed a short byte slice comprising an incomplete UTF-8 codepoint. In this case up to three bytes are buffered in the `IncompleteUtf8` struct associated with `Stdout` / `Stderr`. The bytes are accepted one at a time. As soon as an error can be detected `io::ErrorKind::InvalidData, "Windows stdio in console mode does not support writing non-UTF-8 byte sequences"` is returned. Once a complete UTF-8 codepoint is received it is passed to the `write_valid_utf8_to_console()` and the buffer length is set to zero. Calling `flush()` will neither error nor write anything if an incomplete codepoint is present in the buffer. # Tests Currently there are no Windows-specific tests for console writing code at all. Writing (regression) tests for this problem is a bit challenging since unit tests and UI tests don't run in a console and suddenly popping up another console window might be surprising to developers running the testsuite and it might not work at all in CI builds. To just test the new functionality in unit tests the code would need to be refactored. Some guidance on how to proceed would be appreciated. # Public API changes * `std::str::verifications::utf8_char_width()` would be exposed as `std::str::utf8_char_width()` behind the "str_internals" feature gate. # Related issues * Fixes #83258. * PR #78515 will exacerbate the problem. # Open questions * Add tests? * Squash into one commit with better commit message?
2 parents e3c71f1 + fbfde7e commit cc9bb15

File tree

2 files changed

+91
-15
lines changed

2 files changed

+91
-15
lines changed

library/core/src/str/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub use iter::SplitAsciiWhitespace;
6969
pub use iter::SplitInclusive;
7070

7171
#[unstable(feature = "str_internals", issue = "none")]
72-
pub use validations::next_code_point;
72+
pub use validations::{next_code_point, utf8_char_width};
7373

7474
use iter::MatchIndicesInternal;
7575
use iter::SplitInternal;

library/std/src/sys/windows/stdio.rs

+90-14
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,25 @@ use crate::str;
99
use crate::sys::c;
1010
use crate::sys::cvt;
1111
use crate::sys::handle::Handle;
12+
use core::str::utf8_char_width;
1213

1314
// Don't cache handles but get them fresh for every read/write. This allows us to track changes to
1415
// the value over time (such as if a process calls `SetStdHandle` while it's running). See #40490.
1516
pub struct Stdin {
1617
surrogate: u16,
1718
}
18-
pub struct Stdout;
19-
pub struct Stderr;
19+
pub struct Stdout {
20+
incomplete_utf8: IncompleteUtf8,
21+
}
22+
23+
pub struct Stderr {
24+
incomplete_utf8: IncompleteUtf8,
25+
}
26+
27+
struct IncompleteUtf8 {
28+
bytes: [u8; 4],
29+
len: u8,
30+
}
2031

2132
// Apparently Windows doesn't handle large reads on stdin or writes to stdout/stderr well (see
2233
// #13304 for details).
@@ -51,7 +62,15 @@ fn is_console(handle: c::HANDLE) -> bool {
5162
unsafe { c::GetConsoleMode(handle, &mut mode) != 0 }
5263
}
5364

54-
fn write(handle_id: c::DWORD, data: &[u8]) -> io::Result<usize> {
65+
fn write(
66+
handle_id: c::DWORD,
67+
data: &[u8],
68+
incomplete_utf8: &mut IncompleteUtf8,
69+
) -> io::Result<usize> {
70+
if data.is_empty() {
71+
return Ok(0);
72+
}
73+
5574
let handle = get_handle(handle_id)?;
5675
if !is_console(handle) {
5776
unsafe {
@@ -62,22 +81,73 @@ fn write(handle_id: c::DWORD, data: &[u8]) -> io::Result<usize> {
6281
}
6382
}
6483

65-
// As the console is meant for presenting text, we assume bytes of `data` come from a string
66-
// and are encoded as UTF-8, which needs to be encoded as UTF-16.
84+
if incomplete_utf8.len > 0 {
85+
assert!(
86+
incomplete_utf8.len < 4,
87+
"Unexpected number of bytes for incomplete UTF-8 codepoint."
88+
);
89+
if data[0] >> 6 != 0b10 {
90+
// not a continuation byte - reject
91+
incomplete_utf8.len = 0;
92+
return Err(io::Error::new_const(
93+
io::ErrorKind::InvalidData,
94+
&"Windows stdio in console mode does not support writing non-UTF-8 byte sequences",
95+
));
96+
}
97+
incomplete_utf8.bytes[incomplete_utf8.len as usize] = data[0];
98+
incomplete_utf8.len += 1;
99+
let char_width = utf8_char_width(incomplete_utf8.bytes[0]);
100+
if (incomplete_utf8.len as usize) < char_width {
101+
// more bytes needed
102+
return Ok(1);
103+
}
104+
let s = str::from_utf8(&incomplete_utf8.bytes[0..incomplete_utf8.len as usize]);
105+
incomplete_utf8.len = 0;
106+
match s {
107+
Ok(s) => {
108+
assert_eq!(char_width, s.len());
109+
let written = write_valid_utf8_to_console(handle, s)?;
110+
assert_eq!(written, s.len()); // guaranteed by write_valid_utf8_to_console() for single codepoint writes
111+
return Ok(1);
112+
}
113+
Err(_) => {
114+
return Err(io::Error::new_const(
115+
io::ErrorKind::InvalidData,
116+
&"Windows stdio in console mode does not support writing non-UTF-8 byte sequences",
117+
));
118+
}
119+
}
120+
}
121+
122+
// As the console is meant for presenting text, we assume bytes of `data` are encoded as UTF-8,
123+
// which needs to be encoded as UTF-16.
67124
//
68125
// If the data is not valid UTF-8 we write out as many bytes as are valid.
69-
// Only when there are no valid bytes (which will happen on the next call), return an error.
126+
// If the first byte is invalid it is either first byte of a multi-byte sequence but the
127+
// provided byte slice is too short or it is the first byte of an invalide multi-byte sequence.
70128
let len = cmp::min(data.len(), MAX_BUFFER_SIZE / 2);
71129
let utf8 = match str::from_utf8(&data[..len]) {
72130
Ok(s) => s,
73131
Err(ref e) if e.valid_up_to() == 0 => {
74-
return Err(io::Error::new_const(
75-
io::ErrorKind::InvalidData,
76-
&"Windows stdio in console mode does not support writing non-UTF-8 byte sequences",
77-
));
132+
let first_byte_char_width = utf8_char_width(data[0]);
133+
if first_byte_char_width > 1 && data.len() < first_byte_char_width {
134+
incomplete_utf8.bytes[0] = data[0];
135+
incomplete_utf8.len = 1;
136+
return Ok(1);
137+
} else {
138+
return Err(io::Error::new_const(
139+
io::ErrorKind::InvalidData,
140+
&"Windows stdio in console mode does not support writing non-UTF-8 byte sequences",
141+
));
142+
}
78143
}
79144
Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(),
80145
};
146+
147+
write_valid_utf8_to_console(handle, utf8)
148+
}
149+
150+
fn write_valid_utf8_to_console(handle: c::HANDLE, utf8: &str) -> io::Result<usize> {
81151
let mut utf16 = [0u16; MAX_BUFFER_SIZE / 2];
82152
let mut len_utf16 = 0;
83153
for (chr, dest) in utf8.encode_utf16().zip(utf16.iter_mut()) {
@@ -259,15 +329,21 @@ fn utf16_to_utf8(utf16: &[u16], utf8: &mut [u8]) -> io::Result<usize> {
259329
Ok(written)
260330
}
261331

332+
impl IncompleteUtf8 {
333+
pub const fn new() -> IncompleteUtf8 {
334+
IncompleteUtf8 { bytes: [0; 4], len: 0 }
335+
}
336+
}
337+
262338
impl Stdout {
263339
pub const fn new() -> Stdout {
264-
Stdout
340+
Stdout { incomplete_utf8: IncompleteUtf8::new() }
265341
}
266342
}
267343

268344
impl io::Write for Stdout {
269345
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
270-
write(c::STD_OUTPUT_HANDLE, buf)
346+
write(c::STD_OUTPUT_HANDLE, buf, &mut self.incomplete_utf8)
271347
}
272348

273349
fn flush(&mut self) -> io::Result<()> {
@@ -277,13 +353,13 @@ impl io::Write for Stdout {
277353

278354
impl Stderr {
279355
pub const fn new() -> Stderr {
280-
Stderr
356+
Stderr { incomplete_utf8: IncompleteUtf8::new() }
281357
}
282358
}
283359

284360
impl io::Write for Stderr {
285361
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
286-
write(c::STD_ERROR_HANDLE, buf)
362+
write(c::STD_ERROR_HANDLE, buf, &mut self.incomplete_utf8)
287363
}
288364

289365
fn flush(&mut self) -> io::Result<()> {

0 commit comments

Comments
 (0)