ogham/exa · error

Offset out of range

Error message

Offset out of range

What it means

This is a Rust panic from expect() in src/output/time.rs:130 inside full_zoned(). The datetime crate's Offset::of_seconds() returns Option<Offset>: it is None when the offset in seconds cannot be represented as a ±24-hour UTC offset (or when the i64-to-i32 cast mangles an absurd value). exa turns that None into a panic while formatting a full-iso timestamp with a timezone. The offset value itself comes from the TimeZone loaded at startup from the $TZ environment variable or /etc/localtime (src/output/table.rs:342-360), so the panic signals that the loaded zone data produced an impossible offset.

Source

Thrown at src/output/time.rs:130

            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute())
}

#[allow(trivial_numeric_casts)]
fn full_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));
    format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09}",
            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute(), date.second(), systemtime_nanos(time))
}

#[allow(trivial_numeric_casts)]
fn full_zoned(time: SystemTime, zone: &TimeZone) -> String {
    use datetime::Offset;

    let local = LocalDateTime::at(systemtime_epoch(time));
    let date = zone.to_zoned(local);
    let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");
    format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:09} {:+03}{:02}",
            date.year(), date.month() as usize, date.day(),
            date.hour(), date.minute(), date.second(), systemtime_nanos(time),
            offset.hours(), offset.minutes().abs())
}

#[allow(trivial_numeric_casts)]
fn iso_local(time: SystemTime) -> String {
    let date = LocalDateTime::at(systemtime_epoch(time));

    if is_recent(&date) {
        format!("{:02}-{:02} {:02}:{:02}",
                date.month() as usize, date.day(),
                date.hour(), date.minute())
    }
    else {
        format!("{:04}-{:02}-{:02}",
                date.year(), date.month() as usize, date.day())

View on GitHub (pinned to 3d1edbb470)

Solutions

  1. Test the environment first: run with TZ=UTC (for example TZ=UTC exa --time-style=full-iso). If that works, your previous $TZ or /etc/localtime is the culprit.
  2. Point $TZ at a known-good zone file or reinstall tzdata (Debian/Ubuntu: apt install --reinstall tzdata; Fedora: dnf reinstall tzdata), and verify with file /etc/localtime and by diffing against /usr/share/zoneinfo/UTC.
  3. If you embed this code: replace the expect with a safe fallback (offset 0 or fall back to the unzoned full_local format) so one bad zone cannot abort the whole listing.
  4. If the panic persists on valid tzdata, report it upstream with the exact $TZ value and the zone file, since zone.offset() returning an out-of-range value from a valid TZif would be a datetime-crate bug.

Example fix

// before (src/output/time.rs)
let offset = Offset::of_seconds(zone.offset(local) as i32).expect("Offset out of range");

// after: degrade gracefully instead of panicking
let offset = Offset::of_seconds(zone.offset(local) as i32)
    .unwrap_or_else(|| Offset::of_seconds(0).expect("UTC offset is always in range"));
// or: fall back to the unzoned formatter
// if Offset::of_seconds(zone.offset(local) as i32).is_none() {
//     return full_local(time);
// }
Defensive patterns

Strategy: fallback

Validate before calling

// Before rendering with --time-style=full-iso, sanity-check the active zone:
use datetime::{LocalDateTime, TimeZone, Offset};
use std::time::SystemTime;

fn zone_offset_is_representable(zone: &TimeZone) -> bool {
    let now = LocalDateTime::at(
        SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap()
    );
    let secs = zone.offset(now);
    // Offset::of_seconds rejects anything beyond +/- 24 hours (i32 range)
    secs.abs() < 24 * 60 * 60 && Offset::of_seconds(secs as i32).is_some()
}

if !zone_offset_is_representable(&tz) {
    // fall back to UTC or to the unzoned formatter before rendering
}

Type guard

fn usable_for_full_iso(zone: &TimeZone) -> bool {
    // Probe the exact conversion that full_zoned() (src/output/time.rs:130)
    // performs with .expect(); if it would be None, exa would panic.
    let local = LocalDateTime::at(std::time::Duration::from_secs(0));
    Offset::of_seconds(zone.offset(local) as i32).is_some()
}

Try / catch

// The failure is a panic, not an Err; guard with catch_unwind and keep listing:
let formatted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    TimeFormat::FullISO.format_zoned(time, &zone)
})).unwrap_or_else(|_| {
    TimeFormat::FullISO.format_local(time)  // fallback: drop the zone suffix
});

Prevention

When it happens

Trigger: Running exa with --time-style=full-iso (full_zoned is the only formatting path that calls Offset::of_seconds) on Unix, in a process where determine_time_zone() succeeded but the zone data is broken: a corrupt or truncated TZif file, a hand-crafted/custom zone file with offsets at or beyond ±86400 seconds, or $TZ pointing at such a file. The panic happens per file, at the moment its mtime is formatted.

Common situations: Systems with a damaged /etc/localtime (interrupted tzdata update, container image with a truncated zoneinfo file), $TZ pointing to a stale custom file, or minimal containers where /usr/share/zoneinfo is partially copied. Rare with stock distro tzdata, because real zone offsets stay within ±14 hours; essentially always an environment/data-integrity problem rather than a code bug in the caller.

Related errors


AI-assisted analysis of ogham/exa@3d1edbb470 (2026-08-16). Data as JSON: /api/errors/c07ff6c21977a4d9. Report an issue: GitHub.