Skip to content

Commit ef24c8b

Browse files
committed
fix(cli): skip timezone validation without a zoneinfo database
A host with no zoneinfo at all cannot resolve any name, not even UTC, so the check would turn the old silent fallback into a hard failure. The CI integration tests hit this because they spawn the binary with a cleared environment inside the hermetic sandbox. The validator now only runs when jiff reports a non-empty database, and the empty case is unit-tested with an injected TimeZoneDatabase::none().
1 parent ebc680f commit ef24c8b

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

‎docs/guide/cli-options.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ ccusage daily -z Asia/Tokyo
122122
ccusage monthly -z Europe/London
123123
```
124124

125-
The value must be an IANA timezone name, or `local` for the system timezone. An unknown name such as `Not/AZone` is rejected with a non-zero exit code instead of silently falling back to the system timezone, which can group usage under the wrong date. The same check applies to `timezone` in a [configuration file](/guide/config-files).
125+
The value must be an IANA timezone name, or `local` for the system timezone. An unknown name such as `Not/AZone` is rejected with a non-zero exit code instead of silently falling back to the system timezone, which can group usage under the wrong date. The same check applies to `timezone` in a [configuration file](/guide/config-files). On a host with no zoneinfo database, such as a minimal container, names cannot be checked and the report falls back to the system timezone as before.
126126

127127
#### Timezone Effect
128128

‎rust/crates/ccusage-core/src/date_utils.rs‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,15 @@ fn civil_from_days(days: i64) -> (i32, u32, u32) {
295295
/// local zone, which can group usage under the wrong date. `local` is the
296296
/// documented way to ask for the system timezone and keeps that fallback.
297297
pub fn is_valid_timezone(value: &str) -> bool {
298-
value == "local" || JiffTimeZone::get(value).is_ok()
298+
is_valid_timezone_in(jiff::tz::db(), value)
299+
}
300+
301+
/// A host with no zoneinfo database at all (a minimal container, or a test
302+
/// sandbox) cannot resolve any name, not even `UTC`. Rejecting every value
303+
/// there would turn the old silent fallback into a hard failure, so the check
304+
/// only runs when there is a database to check against.
305+
fn is_valid_timezone_in(db: &jiff::tz::TimeZoneDatabase, value: &str) -> bool {
306+
value == "local" || db.is_definitively_empty() || db.get(value).is_ok()
299307
}
300308

301309
pub fn parse_tz(timezone: Option<&str>) -> Option<JiffTimeZone> {
@@ -391,7 +399,8 @@ pub fn am_pm(hour: u32) -> &'static str {
391399
#[cfg(test)]
392400
mod tests {
393401
use super::{
394-
MILLIS_PER_HOUR, date_range_bounds_ms, date_within_range, parse_compact_date, parse_tz,
402+
MILLIS_PER_HOUR, date_range_bounds_ms, date_within_range, is_valid_timezone,
403+
is_valid_timezone_in, parse_compact_date, parse_tz,
395404
};
396405

397406
// 2026-01-02 00:00:00 UTC
@@ -478,4 +487,20 @@ mod tests {
478487
));
479488
assert!(!date_within_range("2025-12-31", Some("2026"), None));
480489
}
490+
491+
#[test]
492+
fn timezone_validation_skips_hosts_without_a_zoneinfo_database() {
493+
let empty = jiff::tz::TimeZoneDatabase::none();
494+
assert!(is_valid_timezone_in(&empty, "Not/AZone"));
495+
assert!(is_valid_timezone_in(&empty, "UTC"));
496+
}
497+
498+
#[test]
499+
fn timezone_validation_accepts_local_and_rejects_unknown_names() {
500+
assert!(is_valid_timezone("local"));
501+
if !jiff::tz::db().is_definitively_empty() {
502+
assert!(is_valid_timezone("UTC"));
503+
assert!(!is_valid_timezone("Not/AZone"));
504+
}
505+
}
481506
}

0 commit comments

Comments
 (0)