mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
fix(run): decode Windows programs' piped output in any system language (#3060)
Commands run on pipes (Run configurations without terminal emulation, and agent commands) showed mojibake for Windows' own programs: ping, dir, cmd's messages and PowerShell's native output write a pipe in the console (OEM) code page, wsl.exe and cmd /u write UTF-16LE, while a UTF-8 console decoded every byte as UTF-8. On Windows, a UTF-8 console now decodes piped output line by line: a UTF-8 line stays UTF-8, any other line is decoded in the console code page (encoding_rs for GBK, Big5, Shift_JIS, EUC-KR, 866, 874 and the 125x pages; Windows for the single-byte OEM pages it lacks), and a read with NUL bytes is UTF-16LE. ASCII passes through at once; the rest of a line is held until it shows its encoding, and a held prompt is shown after a batch interval without more output. An explicit console encoding and terminal emulation are unchanged; other platforms keep the plain UTF-8 decoder. Co-authored-by: guoyucode <29471644+guoyucode@users.noreply.github.com>
This commit is contained in:
@@ -76,7 +76,8 @@ fn decode_tool_output_with_code_page(bytes: &[u8], code_page: u32) -> String {
|
||||
text
|
||||
}
|
||||
|
||||
fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> {
|
||||
/// The `encoding_rs` encoding of a Windows code page, when it has one.
|
||||
pub fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> {
|
||||
Some(match cp {
|
||||
65001 => encoding_rs::UTF_8,
|
||||
936 => encoding_rs::GBK,
|
||||
@@ -101,6 +102,56 @@ fn encoding_for_code_page(cp: u32) -> Option<&'static encoding_rs::Encoding> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The code page Windows console programs write to a pipe in: the OEM code page (GBK on a
|
||||
/// Chinese system, Shift_JIS on a Japanese one, EUC-KR on a Korean one, 437 / 850 / 866…
|
||||
/// elsewhere). `None` when it is UTF-8 (Windows' "Use Unicode UTF-8" option) or off Windows.
|
||||
pub fn console_code_page() -> Option<u32> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// SAFETY: GetOEMCP is an argument-less query with no preconditions.
|
||||
let cp = unsafe { windows_sys::Win32::Globalization::GetOEMCP() };
|
||||
(cp != 65001).then_some(cp)
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether every character of code page `cp` is one byte, so text in it can be decoded
|
||||
/// piece by piece.
|
||||
#[cfg(windows)]
|
||||
pub fn is_single_byte_code_page(cp: u32) -> bool {
|
||||
use windows_sys::Win32::Globalization::{CPINFO, GetCPInfo};
|
||||
// SAFETY: CPINFO is plain data, which GetCPInfo fills in.
|
||||
unsafe {
|
||||
let mut info: CPINFO = std::mem::zeroed();
|
||||
GetCPInfo(cp, &mut info) != 0 && info.MaxCharSize == 1
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` in code page `cp`, decoded by Windows (for the code pages `encoding_rs` lacks,
|
||||
/// like the OEM pages 437, 850 or 852).
|
||||
#[cfg(windows)]
|
||||
pub fn decode_code_page(bytes: &[u8], cp: u32) -> String {
|
||||
use windows_sys::Win32::Globalization::MultiByteToWideChar;
|
||||
let Ok(len) = i32::try_from(bytes.len()) else {
|
||||
return String::from_utf8_lossy(bytes).into_owned();
|
||||
};
|
||||
if len == 0 {
|
||||
return String::new();
|
||||
}
|
||||
// SAFETY: `bytes` holds `len` bytes; no output buffer asks for the length needed.
|
||||
let needed = unsafe { MultiByteToWideChar(cp, 0, bytes.as_ptr(), len, std::ptr::null_mut(), 0) };
|
||||
if needed <= 0 {
|
||||
return String::from_utf8_lossy(bytes).into_owned();
|
||||
}
|
||||
let mut wide = vec![0u16; needed as usize];
|
||||
// SAFETY: `wide` holds `needed` units.
|
||||
let written = unsafe { MultiByteToWideChar(cp, 0, bytes.as_ptr(), len, wide.as_mut_ptr(), needed) };
|
||||
String::from_utf16_lossy(&wide[..written.max(0) as usize])
|
||||
}
|
||||
|
||||
const STD_PROPERTIES: &[&str] = &["stdout.encoding", "stderr.encoding", "sun.stdout.encoding", "sun.stderr.encoding"];
|
||||
const FILE_ENCODING: &str = "file.encoding";
|
||||
const TOOL_OPTIONS: &str = "JAVA_TOOL_OPTIONS";
|
||||
|
||||
@@ -498,7 +498,8 @@ impl ProcessManager {
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
spawn_pump(rx, on_output.clone(), entry.clone(), sink, options.encoding.clone(), notice)?;
|
||||
let decoder = || OutputDecoder::for_pipes(options.encoding.as_deref());
|
||||
spawn_pump(rx, on_output.clone(), entry.clone(), sink, [decoder(), decoder()], notice)?;
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(e) = result {
|
||||
@@ -629,7 +630,10 @@ impl ProcessManager {
|
||||
#[cfg(unix)]
|
||||
Some(Box::new(wait_readable)),
|
||||
)
|
||||
.and_then(|_| spawn_pump(rx, on_output, entry.clone(), sink, options.encoding.clone(), None));
|
||||
.and_then(|_| {
|
||||
let decoder = || OutputDecoder::for_label(options.encoding.as_deref());
|
||||
spawn_pump(rx, on_output, entry.clone(), sink, [decoder(), decoder()], None)
|
||||
});
|
||||
if let Err(e) = result {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
@@ -1120,18 +1124,35 @@ fn chunk(stream: Stream, text: String) -> OutputChunk {
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends what `decode` writes to `pending`, through the notice filter when there is one.
|
||||
fn emit(
|
||||
decode: impl FnOnce(&mut String),
|
||||
notice: &mut Option<ide_core::output_encoding::NoticeFilter>,
|
||||
decoded: &mut String,
|
||||
pending: &mut String,
|
||||
) {
|
||||
match notice {
|
||||
Some(filter) => {
|
||||
decode(decoded);
|
||||
filter.filter(decoded, pending);
|
||||
decoded.clear();
|
||||
}
|
||||
None => decode(pending),
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges both streams in arrival order, batching consecutive output of the same stream.
|
||||
/// `decoders` decode stdout and stderr.
|
||||
fn spawn_pump(
|
||||
rx: mpsc::Receiver<Msg>,
|
||||
on_output: OutputFn,
|
||||
entry: Arc<Entry>,
|
||||
sink: SharedSink,
|
||||
encoding: Option<String>,
|
||||
decoders: [OutputDecoder; 2],
|
||||
notice: Option<String>,
|
||||
) -> std::io::Result<()> {
|
||||
std::thread::Builder::new().name(format!("openidea-run-pump-{}", entry.id.0)).spawn(move || {
|
||||
let mut out_dec = OutputDecoder::for_label(encoding.as_deref());
|
||||
let mut err_dec = OutputDecoder::for_label(encoding.as_deref());
|
||||
let [mut out_dec, mut err_dec] = decoders;
|
||||
// "Picked up JAVA_TOOL_OPTIONS: …" from every JVM given the console encoding, on
|
||||
// either stream (`spring-boot:run` copies its fork's stderr to Maven's stdout).
|
||||
let filter = || notice.clone().map(ide_core::output_encoding::NoticeFilter::new);
|
||||
@@ -1140,6 +1161,8 @@ fn spawn_pump(
|
||||
let mut pending = String::new();
|
||||
let mut pending_stream = Stream::Stdout;
|
||||
let mut batch_started: Option<Instant> = None;
|
||||
// Since when a decoder holds part of a line back to tell its encoding.
|
||||
let mut held_since: Option<Instant> = None;
|
||||
let mut status: Option<Exit> = None;
|
||||
let mut drain_deadline: Option<Instant> = None;
|
||||
|
||||
@@ -1154,6 +1177,7 @@ fn spawn_pump(
|
||||
let now = Instant::now();
|
||||
let wait = [
|
||||
batch_started.map(|t| (t + BATCH_INTERVAL).saturating_duration_since(now)),
|
||||
held_since.map(|t| (t + BATCH_INTERVAL).saturating_duration_since(now)),
|
||||
drain_deadline.map(|t| t.saturating_duration_since(now)),
|
||||
]
|
||||
.into_iter()
|
||||
@@ -1170,14 +1194,8 @@ fn spawn_pump(
|
||||
Stream::Stdout => (&mut out_dec, &mut out_notice),
|
||||
Stream::Stderr => (&mut err_dec, &mut err_notice),
|
||||
};
|
||||
match notice {
|
||||
Some(filter) => {
|
||||
dec.decode_into(&bytes, &mut decoded);
|
||||
filter.filter(&decoded, &mut pending);
|
||||
decoded.clear();
|
||||
}
|
||||
None => dec.decode_into(&bytes, &mut pending),
|
||||
}
|
||||
emit(|out| dec.decode_into(&bytes, out), notice, &mut decoded, &mut pending);
|
||||
held_since = if out_dec.holds() || err_dec.holds() { held_since.or(Some(now)) } else { None };
|
||||
if !pending.is_empty() && batch_started.is_none() {
|
||||
batch_started = Some(Instant::now());
|
||||
}
|
||||
@@ -1193,6 +1211,25 @@ fn spawn_pump(
|
||||
if drain_deadline.is_some_and(|d| Instant::now() >= d) {
|
||||
break;
|
||||
}
|
||||
// Nothing more came: show a held prompt.
|
||||
if held_since.is_some_and(|t| t.elapsed() >= BATCH_INTERVAL) {
|
||||
held_since = None;
|
||||
for (stream, dec, notice) in [
|
||||
(Stream::Stdout, &mut out_dec, &mut out_notice),
|
||||
(Stream::Stderr, &mut err_dec, &mut err_notice),
|
||||
] {
|
||||
if dec.holds() {
|
||||
if stream != pending_stream {
|
||||
flush(&mut pending, pending_stream, &mut batch_started);
|
||||
pending_stream = stream;
|
||||
}
|
||||
emit(|out| dec.release(out), notice, &mut decoded, &mut pending);
|
||||
}
|
||||
}
|
||||
if !pending.is_empty() && batch_started.is_none() {
|
||||
batch_started = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
if batch_started.is_some_and(|t| t.elapsed() >= BATCH_INTERVAL) {
|
||||
flush(&mut pending, pending_stream, &mut batch_started);
|
||||
}
|
||||
|
||||
+390
-1
@@ -136,6 +136,7 @@ mod tests {
|
||||
pub enum OutputDecoder {
|
||||
Utf8(Utf8Decoder),
|
||||
Other(Box<encoding_rs::Decoder>),
|
||||
Mixed(Box<MixedDecoder>),
|
||||
}
|
||||
|
||||
impl OutputDecoder {
|
||||
@@ -147,10 +148,36 @@ impl OutputDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::for_label`] for output read from pipes. On Windows a UTF-8 console also decodes
|
||||
/// what Windows' own programs write: the lines that are not UTF-8 in the system's console
|
||||
/// code page (`ping`, `dir`, `cmd`'s messages), whatever the system's language, and UTF-16
|
||||
/// (`wsl.exe`, `cmd /u`).
|
||||
pub fn for_pipes(label: Option<&str>) -> Self {
|
||||
match Self::for_label(label) {
|
||||
Self::Utf8(_) if cfg!(windows) => Self::Mixed(Box::new(MixedDecoder::new(
|
||||
ide_core::output_encoding::console_code_page().and_then(NativeDecoder::for_code_page),
|
||||
))),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_into(&mut self, bytes: &[u8], out: &mut String) {
|
||||
match self {
|
||||
Self::Utf8(d) => d.decode_into(bytes, out),
|
||||
Self::Other(d) => decode_with(d, bytes, out, false),
|
||||
Self::Mixed(d) => d.decode_into(bytes, out),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether part of a line is held back until it shows its encoding.
|
||||
pub fn holds(&self) -> bool {
|
||||
matches!(self, Self::Mixed(d) if d.holds())
|
||||
}
|
||||
|
||||
/// Decodes the held part of a line now (no more output came for a while).
|
||||
pub fn release(&mut self, out: &mut String) {
|
||||
if let Self::Mixed(d) = self {
|
||||
d.release(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,10 +189,218 @@ impl OutputDecoder {
|
||||
decode_with(d, &[], &mut out, true);
|
||||
out
|
||||
}
|
||||
Self::Mixed(d) => d.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoder for a Windows code page: `encoding_rs`'s, or Windows' own for the single-byte
|
||||
/// pages `encoding_rs` lacks (the OEM pages 437, 850, 852…).
|
||||
pub enum NativeDecoder {
|
||||
Encoding(Box<encoding_rs::Decoder>),
|
||||
#[cfg(windows)]
|
||||
SingleByte(u32),
|
||||
}
|
||||
|
||||
impl NativeDecoder {
|
||||
/// `None` for UTF-8 and for code pages neither can decode piece by piece.
|
||||
pub fn for_code_page(cp: u32) -> Option<Self> {
|
||||
if let Some(enc) = ide_core::output_encoding::encoding_for_code_page(cp) {
|
||||
return (enc != encoding_rs::UTF_8)
|
||||
.then(|| Self::Encoding(Box::new(enc.new_decoder_without_bom_handling())));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if ide_core::output_encoding::is_single_byte_code_page(cp) {
|
||||
return Some(Self::SingleByte(cp));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn decode_into(&mut self, bytes: &[u8], out: &mut String, last: bool) {
|
||||
match self {
|
||||
Self::Encoding(d) => decode_with(d, bytes, out, last),
|
||||
#[cfg(windows)]
|
||||
Self::SingleByte(cp) => out.push_str(&ide_core::output_encoding::decode_code_page(bytes, *cp)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Line {
|
||||
/// Only ASCII so far, which reads the same in both.
|
||||
Open,
|
||||
Utf8,
|
||||
Native,
|
||||
}
|
||||
|
||||
/// UTF-8, the system code page for the lines that are not UTF-8, and UTF-16: on pipes, cargo,
|
||||
/// git or node write UTF-8, Windows' own programs the console code page (GBK, Shift_JIS,
|
||||
/// EUC-KR, 866…) and a few of them (`wsl.exe`, `cmd /u`) UTF-16LE, even within one command.
|
||||
///
|
||||
/// Each line is decoded as UTF-8 or the code page (UTF-8 with replacement characters without
|
||||
/// one). The part of a line after its first non-ASCII byte is held until the line ends or
|
||||
/// turns out not to be UTF-8; [`Self::release`] decodes a held prompt when no more output comes.
|
||||
///
|
||||
/// A read with NUL bytes, which text in the others never has, is UTF-16LE (ASCII characters
|
||||
/// and line ends in it have them); UTF-16 goes on until one of its lines has ended and a read
|
||||
/// without NUL bytes comes.
|
||||
pub struct MixedDecoder {
|
||||
utf8: Utf8Decoder,
|
||||
native: Option<NativeDecoder>,
|
||||
line: Line,
|
||||
held: Vec<u8>,
|
||||
utf16: Option<Utf16>,
|
||||
}
|
||||
|
||||
impl MixedDecoder {
|
||||
pub fn new(native: Option<NativeDecoder>) -> Self {
|
||||
Self { utf8: Utf8Decoder::new(), native, line: Line::Open, held: Vec::new(), utf16: None }
|
||||
}
|
||||
|
||||
pub fn decode_into(&mut self, bytes: &[u8], out: &mut String) {
|
||||
if bytes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let nul = bytes.contains(&0);
|
||||
match self.utf16.as_ref().map(|u| u.open || nul) {
|
||||
Some(true) => {
|
||||
if let Some(u) = &mut self.utf16 {
|
||||
u.decode(bytes, out);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Some(false) => self.utf16 = None,
|
||||
None if nul => {
|
||||
self.end_line(out);
|
||||
let mut u = Utf16::new();
|
||||
u.decode(utf16_aligned(bytes), out);
|
||||
self.utf16 = Some(u);
|
||||
return;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
for piece in bytes.split_inclusive(|b| *b == b'\n') {
|
||||
let ends = piece.last() == Some(&b'\n');
|
||||
match self.line {
|
||||
Line::Utf8 => self.utf8.decode_into(piece, out),
|
||||
Line::Native => self.decode_native(piece, out),
|
||||
Line::Open => {
|
||||
let mut rest = piece;
|
||||
if self.held.is_empty() {
|
||||
let ascii = piece.iter().take_while(|b| b.is_ascii()).count();
|
||||
out.push_str(std::str::from_utf8(&piece[..ascii]).unwrap_or_default());
|
||||
rest = &piece[ascii..];
|
||||
}
|
||||
self.held.extend_from_slice(rest);
|
||||
let decided = match std::str::from_utf8(&self.held) {
|
||||
Err(e) if e.error_len().is_some() => Some(Line::Native),
|
||||
Ok(_) if ends => Some(Line::Utf8),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(line) = decided {
|
||||
self.line = line;
|
||||
self.flush_held(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ends {
|
||||
self.line = Line::Open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_held(&mut self, out: &mut String) {
|
||||
let held = std::mem::take(&mut self.held);
|
||||
match self.line {
|
||||
Line::Native => self.decode_native(&held, out),
|
||||
Line::Open | Line::Utf8 => self.utf8.decode_into(&held, out),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_native(&mut self, bytes: &[u8], out: &mut String) {
|
||||
match &mut self.native {
|
||||
Some(native) => native.decode_into(bytes, out, false),
|
||||
None => self.utf8.decode_into(bytes, out),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes the rest of the current line as it is.
|
||||
fn end_line(&mut self, out: &mut String) {
|
||||
if self.holds() {
|
||||
self.line = if std::str::from_utf8(&self.held).is_ok() { Line::Utf8 } else { Line::Native };
|
||||
self.flush_held(out);
|
||||
}
|
||||
out.push_str(&self.utf8.finish());
|
||||
self.line = Line::Open;
|
||||
}
|
||||
|
||||
pub fn holds(&self) -> bool {
|
||||
!self.held.is_empty()
|
||||
}
|
||||
|
||||
/// Decodes the held part of the line as UTF-8: held text has been UTF-8 so far.
|
||||
pub fn release(&mut self, out: &mut String) {
|
||||
if self.holds() {
|
||||
self.line = Line::Utf8;
|
||||
self.flush_held(out);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(&mut self) -> String {
|
||||
let mut out = String::new();
|
||||
if let Some(mut u) = self.utf16.take() {
|
||||
decode_with(&mut u.decoder, &[], &mut out, true);
|
||||
}
|
||||
self.end_line(&mut out);
|
||||
if let Some(native) = &mut self.native {
|
||||
native.decode_into(&[], &mut out, true);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
struct Utf16 {
|
||||
decoder: encoding_rs::Decoder,
|
||||
/// Inside a line or a code unit: the next read goes on in UTF-16 whatever it holds.
|
||||
open: bool,
|
||||
/// An odd number of bytes so far.
|
||||
odd: bool,
|
||||
}
|
||||
|
||||
impl Utf16 {
|
||||
fn new() -> Self {
|
||||
Self { decoder: encoding_rs::UTF_16LE.new_decoder_with_bom_removal(), open: false, odd: false }
|
||||
}
|
||||
|
||||
fn decode(&mut self, bytes: &[u8], out: &mut String) {
|
||||
let start = out.len();
|
||||
decode_with(&mut self.decoder, bytes, out, false);
|
||||
self.odd ^= bytes.len() % 2 == 1;
|
||||
if self.odd {
|
||||
self.open = true;
|
||||
} else if out.len() > start {
|
||||
self.open = !out.ends_with('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `bytes` from the first byte of a UTF-16LE code unit. NUL bytes are mostly the high bytes of
|
||||
/// ASCII characters, at odd offsets from it; at even ones, the first byte is the high byte of a
|
||||
/// character whose low byte came before (and was shown as ASCII).
|
||||
fn utf16_aligned(bytes: &[u8]) -> &[u8] {
|
||||
let (mut even, mut odd) = (0usize, 0usize);
|
||||
for (i, b) in bytes.iter().enumerate() {
|
||||
if *b == 0 {
|
||||
if i % 2 == 0 {
|
||||
even += 1;
|
||||
} else {
|
||||
odd += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if even > odd { &bytes[1..] } else { bytes }
|
||||
}
|
||||
|
||||
fn decode_with(d: &mut encoding_rs::Decoder, bytes: &[u8], out: &mut String, last: bool) {
|
||||
let mut src = bytes;
|
||||
loop {
|
||||
@@ -180,7 +415,8 @@ fn decode_with(d: &mut encoding_rs::Decoder, bytes: &[u8], out: &mut String, las
|
||||
|
||||
#[cfg(test)]
|
||||
mod encoding_tests {
|
||||
use super::OutputDecoder;
|
||||
use super::{MixedDecoder, NativeDecoder, OutputDecoder};
|
||||
use ide_core::output_encoding::encoding_for_code_page;
|
||||
|
||||
#[test]
|
||||
fn gbk_split_across_chunks() {
|
||||
@@ -202,4 +438,157 @@ mod encoding_tests {
|
||||
assert_eq!(out, "héllo");
|
||||
assert!(matches!(d, OutputDecoder::Utf8(_)));
|
||||
}
|
||||
|
||||
fn mixed(cp: u32) -> MixedDecoder {
|
||||
MixedDecoder::new(Some(NativeDecoder::for_code_page(cp).expect("code page")))
|
||||
}
|
||||
|
||||
fn encode(cp: u32, text: &str) -> Vec<u8> {
|
||||
let (bytes, _, unmappable) = encoding_for_code_page(cp).expect("code page").encode(text);
|
||||
assert!(!unmappable, "{text} in {cp}");
|
||||
bytes.into_owned()
|
||||
}
|
||||
|
||||
/// Simplified and Traditional Chinese, Korean, Japanese, Cyrillic (OEM and ANSI), Western.
|
||||
const SAMPLES: &[(u32, &str)] = &[
|
||||
(936, "正在 Ping 127.0.0.1 具有 32 字节的数据:"),
|
||||
(950, "系統找不到指定的路徑。"),
|
||||
(949, "지정된 경로를 찾을 수 없습니다."),
|
||||
(932, "指定されたパスが見つかりません。"),
|
||||
(866, "Системе не удается найти указанный путь."),
|
||||
(1251, "Системе не удается найти указанный путь."),
|
||||
(1252, "Le chemin d'accès spécifié est introuvable."),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn utf8_and_code_page_lines_mix_whatever_the_chunks() {
|
||||
for &(cp, native) in SAMPLES {
|
||||
let mut bytes = " Compiling 编译 ü\n".as_bytes().to_vec();
|
||||
bytes.extend(encode(cp, native));
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
bytes.extend_from_slice("ok ✓\n".as_bytes());
|
||||
bytes.extend(encode(cp, native));
|
||||
let expected = format!(" Compiling 编译 ü\n{native}\r\nok ✓\n{native}");
|
||||
for split in 0..=bytes.len() {
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
d.decode_into(&bytes[..split], &mut out);
|
||||
d.decode_into(&bytes[split..], &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, expected, "code page {cp}, split at {split}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn byte_by_byte_code_page_output() {
|
||||
for &(cp, native) in SAMPLES {
|
||||
let bytes = [encode(cp, native), b"\n".to_vec()].concat();
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
for b in &bytes {
|
||||
d.decode_into(std::slice::from_ref(b), &mut out);
|
||||
}
|
||||
assert!(!d.holds());
|
||||
assert_eq!(out, format!("{native}\n"), "code page {cp}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_and_code_page_prompts_show_at_once_and_utf8_ones_on_release() {
|
||||
let mut d = mixed(936);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"Password: ", &mut out);
|
||||
assert_eq!(out, "Password: ");
|
||||
assert!(!d.holds());
|
||||
|
||||
out.clear();
|
||||
d.decode_into(&encode(936, "请按任意键继续. . ."), &mut out);
|
||||
assert_eq!(out, "请按任意键继续. . .");
|
||||
assert!(!d.holds());
|
||||
|
||||
let mut d = mixed(936);
|
||||
out.clear();
|
||||
d.decode_into("Continue? 继续 ".as_bytes(), &mut out);
|
||||
assert_eq!(out, "Continue? ");
|
||||
assert!(d.holds());
|
||||
d.release(&mut out);
|
||||
assert_eq!(out, "Continue? 继续 ");
|
||||
d.decode_into("[y/N]\n".as_bytes(), &mut out);
|
||||
assert_eq!(out, "Continue? 继续 [y/N]\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_released_line_split_inside_a_character_completes() {
|
||||
let mut d = mixed(949);
|
||||
let mut out = String::new();
|
||||
let bytes = "é中".as_bytes();
|
||||
d.decode_into(&bytes[..3], &mut out);
|
||||
d.release(&mut out);
|
||||
d.decode_into(&bytes[3..], &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "é中");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_code_page_has_no_mixed_decoder() {
|
||||
assert!(NativeDecoder::for_code_page(65001).is_none());
|
||||
}
|
||||
|
||||
fn utf16(text: &str) -> Vec<u8> {
|
||||
text.encode_utf16().flat_map(u16::to_le_bytes).collect()
|
||||
}
|
||||
|
||||
/// What `wsl.exe -l -v` writes, on a Chinese system.
|
||||
const WSL: &str = " NAME STATE VERSION\r\n* Ubuntu-22.04 Running 2\r\n\
|
||||
适用于 Linux 的 Windows 子系统: 默认\r\n";
|
||||
|
||||
#[test]
|
||||
fn utf16_output_between_utf8_and_code_page_lines_whatever_the_chunks() {
|
||||
let before = " Compiling 编译\n".as_bytes();
|
||||
let wsl = utf16(WSL);
|
||||
let after = [encode(936, "系统找不到指定的路径。"), "\r\ndone ✓\n".as_bytes().to_vec()].concat();
|
||||
let expected = format!(" Compiling 编译\n{WSL}系统找不到指定的路径。\r\ndone ✓\n");
|
||||
for split in 0..=wsl.len() {
|
||||
let mut d = mixed(936);
|
||||
let mut out = String::new();
|
||||
for chunk in [before, &wsl[..split], &wsl[split..], &after] {
|
||||
d.decode_into(chunk, &mut out);
|
||||
}
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, expected, "split at {split}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_with_a_byte_order_mark_or_starting_with_cjk() {
|
||||
let mut d = mixed(949);
|
||||
let mut out = String::new();
|
||||
d.decode_into(&[&[0xFF, 0xFE][..], &utf16("한국어 OK\r\n")].concat(), &mut out);
|
||||
d.decode_into(&utf16("中文 OK\r\n"), &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "한국어 OK\r\n中文 OK\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_code_page_other_lines_are_lossy_utf8_and_utf16_still_reads() {
|
||||
let mut d = MixedDecoder::new(None);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"caf\xe9\n", &mut out);
|
||||
d.decode_into(&utf16("wsl 子系统\r\n"), &mut out);
|
||||
d.decode_into("ok ✓\n".as_bytes(), &mut out);
|
||||
out.push_str(&d.finish());
|
||||
assert_eq!(out, "caf\u{FFFD}\nwsl 子系统\r\nok ✓\n");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn oem_code_pages_without_an_encoding_rs_table_decode_through_windows() {
|
||||
for cp in [437, 850, 852] {
|
||||
let mut d = mixed(cp);
|
||||
let mut out = String::new();
|
||||
d.decode_into(b"caf\x82\r\n", &mut out);
|
||||
assert_eq!(out, "caf\u{e9}\r\n", "code page {cp}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#![cfg(windows)]
|
||||
|
||||
use ide_core::{IdeEvent, SharedSink};
|
||||
use ide_run::{OutputChunk, ProcessManager, RunConfig, StartOptions};
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Stdout and stderr of a shell command line run on pipes in a UTF-8 console.
|
||||
fn run(command: &str) -> String {
|
||||
let (tx, events) = mpsc::channel();
|
||||
let tx = Mutex::new(tx);
|
||||
let sink: SharedSink = Arc::new(move |e: IdeEvent| {
|
||||
let _ = tx.lock().send(e);
|
||||
});
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let config = RunConfig {
|
||||
id: "t:out".into(),
|
||||
name: "t:out".into(),
|
||||
kind: "shell".into(),
|
||||
command: command.into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: BTreeMap::new(),
|
||||
detected: false,
|
||||
is_test: false,
|
||||
debug_adapter: None,
|
||||
allow_parallel: false,
|
||||
extras: Default::default(),
|
||||
};
|
||||
let chunks: Arc<Mutex<Vec<OutputChunk>>> = Arc::default();
|
||||
let out = chunks.clone();
|
||||
let pm = ProcessManager::new();
|
||||
let info = pm
|
||||
.start(root.path(), &config, &StartOptions::default(), sink, Box::new(move |c| out.lock().push(c)))
|
||||
.expect("start");
|
||||
loop {
|
||||
match events.recv_timeout(Duration::from_secs(20)).expect("RunExited event") {
|
||||
IdeEvent::RunExited { run, .. } if run == info.id => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
chunks
|
||||
.lock()
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
OutputChunk::Stdout { text } | OutputChunk::Stderr { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `cmd`'s own messages reach the pipe in the console code page (GBK, Shift_JIS, EUC-KR,
|
||||
/// 437…, by the system's language), and still read right in a UTF-8 console.
|
||||
#[test]
|
||||
fn native_messages_in_the_console_code_page_are_decoded() {
|
||||
let text = run(r"dir C:\openidea-no-such-dir");
|
||||
assert!(text.trim().lines().count() >= 2, "{text:?}");
|
||||
assert!(!text.contains('\u{FFFD}'), "{text:?}");
|
||||
}
|
||||
|
||||
/// `cmd /u` writes UTF-16LE, as `wsl.exe` does; it reads right whatever the system's language.
|
||||
#[test]
|
||||
fn utf16_output_is_decoded() {
|
||||
let text = run("cmd /u /c echo 中文 한국어 OK");
|
||||
assert_eq!(text, "中文 한국어 OK\r\n");
|
||||
}
|
||||
Reference in New Issue
Block a user