Skip to content

Commit 8ae7686

Browse files
committed
fix(backend): prevent terminal character corruption by buffering incomplete UTF-8 bytes at read boundaries
1 parent c27f0d6 commit 8ae7686

1 file changed

Lines changed: 81 additions & 2 deletions

File tree

anycode-backend/src/terminal.rs

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,19 +79,52 @@ impl Terminal {
7979
tokio::task::spawn_blocking(move || {
8080
tracing::info!("PTY reader started");
8181
let mut buf = [0u8; 1024];
82+
let mut unprocessed = Vec::new();
8283
loop {
8384
match reader.read(&mut buf) {
8485
Ok(0) => break,
8586
Ok(n) => {
86-
let s = String::from_utf8_lossy(&buf[..n]).to_string();
87-
let _ = pty_output_tx.blocking_send(s);
87+
unprocessed.extend_from_slice(&buf[..n]);
88+
let mut check_idx = 0;
89+
while check_idx < unprocessed.len() {
90+
match std::str::from_utf8(&unprocessed[check_idx..]) {
91+
Ok(s) => {
92+
let _ = pty_output_tx.blocking_send(s.to_string());
93+
unprocessed.clear();
94+
break;
95+
}
96+
Err(e) => {
97+
let valid_up_to = e.valid_up_to();
98+
if valid_up_to > 0 {
99+
if let Ok(s) = std::str::from_utf8(&unprocessed[check_idx..check_idx + valid_up_to]) {
100+
let _ = pty_output_tx.blocking_send(s.to_string());
101+
}
102+
check_idx += valid_up_to;
103+
}
104+
match e.error_len() {
105+
Some(err_len) => {
106+
let _ = pty_output_tx.blocking_send(String::from("\u{FFFD}"));
107+
check_idx += err_len;
108+
}
109+
None => {
110+
unprocessed.drain(..check_idx);
111+
break;
112+
}
113+
}
114+
}
115+
}
116+
}
88117
}
89118
Err(e) => {
90119
tracing::warn!("PTY read error: {:?}", e);
91120
break;
92121
}
93122
}
94123
}
124+
if !unprocessed.is_empty() {
125+
let s = String::from_utf8_lossy(&unprocessed).to_string();
126+
let _ = pty_output_tx.blocking_send(s);
127+
}
95128
tracing::info!("PTY reader stopped");
96129
});
97130
}
@@ -209,4 +242,50 @@ mod tests {
209242

210243
Ok(())
211244
}
245+
246+
#[tokio::test]
247+
async fn test_terminal_unicode() -> Result<()> {
248+
let (tx, mut rx) = mpsc::channel::<String>(10);
249+
250+
let shell = if cfg!(target_os = "windows") {
251+
"cmd.exe".to_string()
252+
} else {
253+
"cat".to_string()
254+
};
255+
256+
let terminal = Terminal::new(
257+
"test_unicode".to_string(),
258+
"session2".to_string(),
259+
30,
260+
80,
261+
Some(shell),
262+
None,
263+
tx,
264+
)
265+
.await?;
266+
267+
let unicode_str = "привет, как дела? 🚀";
268+
terminal.send_input(format!("{}\n", unicode_str)).await?;
269+
270+
let mut output = String::new();
271+
let _ = timeout(Duration::from_secs(2), async {
272+
while let Some(chunk) = rx.recv().await {
273+
output.push_str(&chunk);
274+
if output.contains(unicode_str) {
275+
break;
276+
}
277+
}
278+
})
279+
.await;
280+
281+
println!("Output: {}", output);
282+
283+
assert!(
284+
output.contains(unicode_str),
285+
"terminal did not echo expected unicode output, got: {}",
286+
output
287+
);
288+
289+
Ok(())
290+
}
212291
}

0 commit comments

Comments
 (0)