From 313ee3003b5bd527f10df66e849073c9b843a9f9 Mon Sep 17 00:00:00 2001 From: David Gilbertson Date: Tue, 27 Jan 2026 10:11:27 +1100 Subject: [PATCH] fix: handle utf-8 in windows sandbox logs (#8647) Currently `apply_patch` will fail on Windows if the file contents happen to have a multi-byte character at the point where the `preview` function truncates. I've used the existing `take_bytes_at_char_boundary` helper and added a regression test (that fails without the fix). This is related to #4013 but doesn't fix it. --- codex-rs/Cargo.lock | 1 + codex-rs/windows-sandbox-rs/Cargo.toml | 1 + codex-rs/windows-sandbox-rs/src/logging.rs | 20 +++++++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d0b3a0ec3..836854b0e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2006,6 +2006,7 @@ dependencies = [ "chrono", "codex-protocol", "codex-utils-absolute-path", + "codex-utils-string", "dirs-next", "dunce", "pretty_assertions", diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index aa872035b..f92c389e4 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -25,6 +25,7 @@ chrono = { version = "0.4.42", default-features = false, features = [ "std", ] } codex-utils-absolute-path = { workspace = true } +codex-utils-string = { workspace = true } dunce = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/codex-rs/windows-sandbox-rs/src/logging.rs b/codex-rs/windows-sandbox-rs/src/logging.rs index 2e4de1d29..f575ce3b8 100644 --- a/codex-rs/windows-sandbox-rs/src/logging.rs +++ b/codex-rs/windows-sandbox-rs/src/logging.rs @@ -4,6 +4,8 @@ use std::path::Path; use std::path::PathBuf; use std::sync::OnceLock; +use codex_utils_string::take_bytes_at_char_boundary; + const LOG_COMMAND_PREVIEW_LIMIT: usize = 200; pub const LOG_FILE_NAME: &str = "sandbox.log"; @@ -22,7 +24,7 @@ fn preview(command: &[String]) -> String { if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT { joined } else { - joined[..LOG_COMMAND_PREVIEW_LIMIT].to_string() + take_bytes_at_char_boundary(&joined, LOG_COMMAND_PREVIEW_LIMIT).to_string() } } @@ -72,3 +74,19 @@ pub fn log_note(msg: &str, base_dir: Option<&Path>) { let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); append_line(&format!("[{ts} {}] {}", exe_label(), msg), base_dir); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preview_does_not_panic_on_utf8_boundary() { + // Place a 4-byte emoji such that naive (byte-based) truncation would split it. + let prefix = "x".repeat(LOG_COMMAND_PREVIEW_LIMIT - 1); + let command = vec![format!("{prefix}😀")]; + let result = std::panic::catch_unwind(|| preview(&command)); + assert!(result.is_ok()); + let previewed = result.unwrap(); + assert!(previewed.len() <= LOG_COMMAND_PREVIEW_LIMIT); + } +}