Skip to content

Commit 29191c0

Browse files
authored
fix: unicode issues with bt sync pull
Thread panic when using `bt sync pull --window 1é`
1 parent bf89d86 commit 29191c0

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

src/sync.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2607,6 +2607,34 @@ fn build_root_spans_query(
26072607
parts.join(" | ")
26082608
}
26092609

2610+
fn parse_duration_to_seconds(input: &str) -> Result<u64> {
2611+
let trimmed = input.trim();
2612+
if trimmed.is_empty() {
2613+
bail!("duration cannot be empty");
2614+
}
2615+
if let Ok(seconds) = trimmed.parse::<u64>() {
2616+
return Ok(seconds);
2617+
}
2618+
2619+
let suffix = trimmed.chars().last().filter(|ch| ch.is_ascii_alphabetic());
2620+
let (num_str, unit) = match suffix {
2621+
Some(unit) => (&trimmed[..trimmed.len() - unit.len_utf8()], unit),
2622+
None => (trimmed, 's'),
2623+
};
2624+
let value: u64 = num_str
2625+
.trim()
2626+
.parse()
2627+
.with_context(|| format!("invalid duration '{input}'"))?;
2628+
let multiplier = match unit.to_ascii_lowercase() {
2629+
's' => 1,
2630+
'm' => 60,
2631+
'h' => 60 * 60,
2632+
'd' => 60 * 60 * 24,
2633+
_ => bail!("invalid duration '{input}'. expected suffix s/m/h/d"),
2634+
};
2635+
Ok(value.saturating_mul(multiplier))
2636+
}
2637+
26102638
fn build_time_filter_clause(window: &str, extra_filter: Option<&str>) -> Result<String> {
26112639
let seconds = parse_duration_to_seconds(window)?;
26122640
let time_clause = format!("created >= NOW() - INTERVAL {seconds} SECOND");
@@ -4268,6 +4296,14 @@ fn spinner_bar(message: &str) -> ProgressBar {
42684296
mod tests {
42694297
use super::*;
42704298

4299+
#[test]
4300+
fn parse_duration_to_seconds_rejects_non_ascii_suffix_without_panicking() {
4301+
for input in ["1–", "1é", "1🙂"] {
4302+
let err = parse_duration_to_seconds(input).expect_err("invalid unicode suffix");
4303+
assert!(err.to_string().contains("invalid duration"));
4304+
}
4305+
}
4306+
42714307
#[test]
42724308
fn push_checkpoint_line_offset_advances_only_after_commit() {
42734309
let mut state =

src/traces.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5031,6 +5031,34 @@ fn print_span_text(item: Option<&Map<String, Value>>) {
50315031
}
50325032
}
50335033

5034+
fn parse_duration_to_seconds(input: &str) -> Result<u64> {
5035+
let trimmed = input.trim();
5036+
if trimmed.is_empty() {
5037+
bail!("duration cannot be empty");
5038+
}
5039+
if let Ok(seconds) = trimmed.parse::<u64>() {
5040+
return Ok(seconds);
5041+
}
5042+
5043+
let suffix = trimmed.chars().last().filter(|ch| ch.is_ascii_alphabetic());
5044+
let (num_str, unit) = match suffix {
5045+
Some(unit) => (&trimmed[..trimmed.len() - unit.len_utf8()], unit),
5046+
None => (trimmed, 's'),
5047+
};
5048+
let value: u64 = num_str
5049+
.trim()
5050+
.parse()
5051+
.with_context(|| format!("invalid duration '{input}'"))?;
5052+
let multiplier = match unit.to_ascii_lowercase() {
5053+
's' => 1,
5054+
'm' => 60,
5055+
'h' => 60 * 60,
5056+
'd' => 60 * 60 * 24,
5057+
_ => bail!("invalid duration '{input}'. expected suffix s/m/h/d"),
5058+
};
5059+
Ok(value.saturating_mul(multiplier))
5060+
}
5061+
50345062
fn build_base_filter_clause(
50355063
since: Option<&str>,
50365064
window: &str,
@@ -6156,6 +6184,22 @@ mod tests {
61566184
}
61576185
}
61586186

6187+
#[test]
6188+
fn parse_duration_to_seconds_supports_units() {
6189+
assert_eq!(parse_duration_to_seconds("90").expect("seconds"), 90);
6190+
assert_eq!(parse_duration_to_seconds("15m").expect("minutes"), 900);
6191+
assert_eq!(parse_duration_to_seconds("2h").expect("hours"), 7_200);
6192+
assert_eq!(parse_duration_to_seconds("1d").expect("days"), 86_400);
6193+
}
6194+
6195+
#[test]
6196+
fn parse_duration_to_seconds_rejects_non_ascii_suffix_without_panicking() {
6197+
for input in ["1–", "1é", "1🙂"] {
6198+
let err = parse_duration_to_seconds(input).expect_err("invalid unicode suffix");
6199+
assert!(err.to_string().contains("invalid duration"));
6200+
}
6201+
}
6202+
61596203
#[test]
61606204
fn build_base_filter_clause_uses_window_or_since() {
61616205
let from_window = build_base_filter_clause(None, "1h", Some("metadata.model IS NOT NULL"))

0 commit comments

Comments
 (0)