From cd00d988cf527bb2e0d7646aed92c9bc978a6d76 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:54:30 +0700 Subject: [PATCH] Guard MiniMax reset arithmetic against unrepresentable timestamps --- rust/src/providers/minimax/coding_plan.rs | 39 ++++++++++++++++++- .../src/providers/minimax/coding_plan_html.rs | 35 +++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/rust/src/providers/minimax/coding_plan.rs b/rust/src/providers/minimax/coding_plan.rs index 5169101e71..fc3f2906e5 100644 --- a/rust/src/providers/minimax/coding_plan.rs +++ b/rust/src/providers/minimax/coding_plan.rs @@ -132,12 +132,17 @@ fn resets_at( remains as f64 }; // Display/rounding conversion of an epoch value; sub-second precision is - // intentionally dropped. + // intentionally dropped. An offset that cannot be represented as a + // timestamp (oversized `remains`) yields no reset line and keeps the usage + // percentage (upstream #3758/#3764: safely handle unrepresentable + // timestamps). #[allow( clippy::cast_possible_truncation, reason = "epoch seconds truncated to whole seconds by design" )] - Some(now + Duration::seconds(seconds as i64)) + let total_seconds = Duration::try_seconds(seconds as i64)?; + now.checked_add_signed(total_seconds) + .filter(|reset| *reset > now) } /// Upstream `mapModelNameToServiceType`. @@ -693,6 +698,36 @@ pub(crate) fn is_token_plan_without_coding_plan(err: &ProviderError) -> bool { mod tests { use super::*; + #[test] + fn oversized_remains_yields_no_reset_line_and_keeps_usage() { + // `remains` above ~8.3e15 ms cannot be represented as a timestamp: + // `Duration::try_seconds` must return None, so `resets_at` is None and + // the usage percentage survives (upstream #3758/#3764). + assert_eq!(resets_at(None, Some(i64::MAX), now()), None); + } + + #[test] + fn negative_remains_yields_no_reset_line() { + assert_eq!(resets_at(None, Some(-5), now()), None); + assert_eq!(resets_at(None, Some(0), now()), None); + } + + #[test] + fn positive_remains_keeps_the_reset_line() { + // 90_000 > 1_000_000? No: below the heuristic, treated as seconds. + let reset = resets_at(None, Some(60), now()).expect("small remains"); + assert_eq!(reset, now() + Duration::seconds(60)); + // Above the heuristic: treated as milliseconds. + let reset_ms = resets_at(None, Some(2_000_000), now()).expect("ms remains"); + assert_eq!(reset_ms, now() + Duration::seconds(2_000)); + } + + #[test] + fn end_time_in_the_future_wins_over_remains() { + let end = now() + Duration::seconds(120); + assert_eq!(resets_at(Some(end), Some(60), now()), Some(end)); + } + fn now() -> DateTime { Utc.with_ymd_and_hms(2026, 8, 3, 12, 0, 0).unwrap() } diff --git a/rust/src/providers/minimax/coding_plan_html.rs b/rust/src/providers/minimax/coding_plan_html.rs index 0c4ee6c7cc..a2fafdb909 100644 --- a/rust/src/providers/minimax/coding_plan_html.rs +++ b/rust/src/providers/minimax/coding_plan_html.rs @@ -228,13 +228,20 @@ fn parse_resets_at_from_text(text: &str, now: DateTime) -> Option" values are parsed from dashboard text and - // rounded to whole seconds. + // rounded to whole seconds. An offset that cannot be represented as a + // timestamp (oversized `N`, `seconds as i64` saturating) yields no reset + // line and keeps the usage percentage (upstream #3758/#3764: safely + // handle unrepresentable timestamps). #[allow( clippy::cast_possible_truncation, reason = "reset countdown truncated to whole seconds by design" )] - let total_seconds = seconds as i64; - return Some(now + Duration::seconds(total_seconds)); + let total_seconds = Duration::try_seconds(seconds as i64)?; + let reset = now.checked_add_signed(total_seconds)?; + if reset <= now { + return None; + } + return Some(reset); } // "Resets at HH:mm (zone hint)" @@ -482,12 +489,32 @@ pub(super) fn to_usage_snapshot( #[cfg(test)] mod tests { use super::*; - use chrono::{Duration, TimeZone}; + use chrono::TimeZone; fn now() -> DateTime { Utc.with_ymd_and_hms(2026, 8, 3, 12, 0, 0).unwrap() } + #[test] + fn oversized_days_text_yields_no_reset_line() { + // "Resets in 99999999999999999999 days": `seconds as i64` saturates + // and `Duration::seconds` would panic; the guard returns None. + let text = "Resets in 99999999999999999999 days"; + assert_eq!(parse_resets_at_from_text(text, now()), None); + } + + #[test] + fn normal_days_text_keeps_the_reset_line() { + let text = "Resets in 2 days"; + let reset = parse_resets_at_from_text(text, now()).expect("2 days"); + assert_eq!(reset, now() + Duration::seconds(2 * 86_400)); + } + + #[test] + fn text_without_a_reset_count_is_unmatched() { + assert_eq!(parse_resets_at_from_text("no reset info", now()), None); + } + #[test] fn parses_camelcase_next_data_html() { let entry = serde_json::json!({