From 67ee7282bb78cec2fc9f97ae97a49a9a3b3df7cc Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 20 Sep 2026 05:33:28 -0700 Subject: [PATCH 1/2] fix(vmm): truncate a one-shot compose preview by character, not by byte --- dstack/vmm/src/one_shot.rs | 49 ++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/dstack/vmm/src/one_shot.rs b/dstack/vmm/src/one_shot.rs index 62c4bd78b..8d40fe505 100644 --- a/dstack/vmm/src/one_shot.rs +++ b/dstack/vmm/src/one_shot.rs @@ -11,6 +11,21 @@ use crate::main_service; use anyhow::{Context, Result}; use fs_err as fs; +/// The head of a compose file, for an error message about it. +/// +/// Truncation counts characters, not bytes: a compose file is operator input +/// and may hold any UTF-8, and slicing one at byte 200 aborts the process +/// whenever a character straddles that byte. +fn compose_preview(compose_file: &str) -> String { + const PREVIEW_CHARS: usize = 200; + let preview: String = compose_file.chars().take(PREVIEW_CHARS).collect(); + if preview.len() < compose_file.len() { + format!("{preview}...") + } else { + preview + } +} + pub async fn run_one_shot( vm_config_path: &str, config: Config, @@ -182,11 +197,7 @@ Example of correct compose_file structure: Debug: Compose file content (first 200 chars): {}", error_msg, - if vm_config.compose_file.len() > 200 { - format!("{}...", &vm_config.compose_file[..200]) - } else { - vm_config.compose_file.clone() - } + compose_preview(&vm_config.compose_file) ); } @@ -197,11 +208,7 @@ Debug: Compose file content (first 200 chars): Compose file content (first 200 chars): {}", error_msg, - if vm_config.compose_file.len() > 200 { - format!("{}...", &vm_config.compose_file[..200]) - } else { - vm_config.compose_file.clone() - } + compose_preview(&vm_config.compose_file) ) }); } @@ -385,3 +392,25 @@ Compose file content (first 200 chars): Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A compose file is operator input and may hold any UTF-8. Truncating it + /// for an error message must not depend on where a character starts. + #[test] + fn a_compose_preview_truncates_by_character() { + let head = "\u{4e2d}".repeat(250); + assert!( + head.chars().count() > 200, + "the preview has to actually truncate" + ); + let preview = compose_preview(&head); + assert_eq!(preview.chars().count(), 203, "{preview}"); + assert!(preview.ends_with("...")); + + let short = "{\"name\": \"\u{e9}\"}"; + assert_eq!(compose_preview(short), short); + } +} From 21dfe3c53bd6031098197e9be85ce692f0b2a2d0 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 02:07:18 -0700 Subject: [PATCH 2/2] refactor(vmm): trim compose preview comment and test --- dstack/vmm/src/one_shot.rs | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/dstack/vmm/src/one_shot.rs b/dstack/vmm/src/one_shot.rs index 8d40fe505..4aa36741b 100644 --- a/dstack/vmm/src/one_shot.rs +++ b/dstack/vmm/src/one_shot.rs @@ -11,11 +11,7 @@ use crate::main_service; use anyhow::{Context, Result}; use fs_err as fs; -/// The head of a compose file, for an error message about it. -/// -/// Truncation counts characters, not bytes: a compose file is operator input -/// and may hold any UTF-8, and slicing one at byte 200 aborts the process -/// whenever a character straddles that byte. +/// Truncate by character: slicing at byte 200 panics inside a multi-byte character. fn compose_preview(compose_file: &str) -> String { const PREVIEW_CHARS: usize = 200; let preview: String = compose_file.chars().take(PREVIEW_CHARS).collect(); @@ -397,20 +393,9 @@ Compose file content (first 200 chars): mod tests { use super::*; - /// A compose file is operator input and may hold any UTF-8. Truncating it - /// for an error message must not depend on where a character starts. #[test] - fn a_compose_preview_truncates_by_character() { - let head = "\u{4e2d}".repeat(250); - assert!( - head.chars().count() > 200, - "the preview has to actually truncate" - ); - let preview = compose_preview(&head); - assert_eq!(preview.chars().count(), 203, "{preview}"); - assert!(preview.ends_with("...")); - - let short = "{\"name\": \"\u{e9}\"}"; - assert_eq!(compose_preview(short), short); + fn compose_preview_truncates_by_character() { + let preview = compose_preview(&"\u{4e2d}".repeat(250)); + assert_eq!(preview.chars().count(), 203); } }