diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d8b99165e97..47edce9ac15 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1936,6 +1936,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 aa872035bc0..f92c389e4bc 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 2e4de1d29a6..f575ce3b8d8 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); + } +}