fix(cli): avoid panic on multi-byte UTF-8 in --since duration (#2446)

parse_duration_to_ms was moved to commands/common.rs since the
original PR was opened, but it still split the last byte of the
input with split_at(s.len() - 1), which panics when the final
character is multi-byte UTF-8 (e.g. 'openshell logs my-sandbox
--since 5€').

Split off the last character using its UTF-8 length instead, so
invalid units surface the intended 'unknown duration unit' error.
Add regression tests in commands/common.rs.

Signed-off-by: Andrew White <andrewh@cdw.com>
This commit is contained in:
Andrew White
2026-07-29 08:22:58 +00:00
committed by GitHub
parent bc14018cad
commit 101cbc9780
+29 -1
View File
@@ -719,7 +719,10 @@ pub fn parse_duration_to_ms(s: &str) -> Result<i64> {
if s.is_empty() {
return Err(miette::miette!("empty duration string"));
}
let (num_str, unit) = s.split_at(s.len() - 1);
// Split off the last character by its UTF-8 length: indexing by byte
// length would panic on multi-byte units (e.g. "5\u{20ac}").
let last_len = s.chars().last().map_or(0, char::len_utf8);
let (num_str, unit) = s.split_at(s.len() - last_len);
let num: i64 = num_str
.parse()
.map_err(|_| miette::miette!("invalid duration: {s} (expected e.g. 5m, 1h, 30s)"))?;
@@ -948,3 +951,28 @@ pub fn scrub_git_env(command: &mut Command) -> &mut Command {
}
command
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_duration_to_ms_parses_supported_units() {
assert_eq!(parse_duration_to_ms("30s").expect("parse"), 30_000);
assert_eq!(parse_duration_to_ms("5m").expect("parse"), 300_000);
assert_eq!(parse_duration_to_ms("1h").expect("parse"), 3_600_000);
}
#[test]
fn parse_duration_to_ms_rejects_multi_byte_unit_without_panicking() {
let err = parse_duration_to_ms("5\u{20ac}").expect_err("multi-byte unit should error");
assert!(err.to_string().contains("unknown duration unit"));
let err = parse_duration_to_ms("\u{20ac}").expect_err("missing number should error");
assert!(err.to_string().contains("invalid duration"));
}
}