Skip to content

Validation

A forensic tool's output is only worth the rigour behind it. This page documents exactly how each timeglyph format is checked, against whom, and at what confidence tier — so the standing rests on measurements an independent party can re-run, not on self-assessment.

Evidence tiers

timeglyph classifies every validation by who confirms it (the fleet standard):

Tier Definition Example here
Tier 1 An independent third party authored both the input value and the answer, or it is real-world data. Discord's docs give id 1759288472991170632016-04-30 11:18:25.796; Apple TN1150 states HFS+ max = 2040-02-06 06:28:15.
Tier 2 Real output checked against an independent oracle, but we chose the scenario. A value we picked, confirmed by an independent implementation (e.g. CPython).
Tier 3 We authored both the fixture and the expected answer, nothing independent vouches. (Avoided — these are the self-deception trap.)

The goal is tier 1 wherever reachable; tier-2 rows are labelled honestly.

Enforced, not just claimed

"Tier-3 avoided" is a gate, not prose. docs/validation-tiers.tsv records every registry format's highest-trust independent oracle and tier; tests/validation_tiers.rs proves — on every build — that (1) every format is audited, (2) none is tier-3-only, (3) each tier claim is bound to the oracle file that actually references the format (a citation can't be faked), and (4) formats leaning on a single independent oracle are a tracked, shrinking allowlist (19 today) so a new format can't silently ship single-anchored.

Reliability is reported PER FAMILY, never one global number

A single global top-3 (~92%, n=336) hides that some families' true format label is systematically out-ranked. tests/calibration.rs per_family_reliability_is_reported_and_floored measures top-1/top-3 for each of the 36 families and floors each at 80% top-3 — except five families where the decoded instant is still correct but a more-common same-instant/same-window format wins the label (active is the same FILETIME instant as filetime; dotnet_ticks/dttm/ dvr/nokiale share crowded windows). Those are named and gated, not averaged away.

Independent oracles

Oracle Author / license Role Verified
time-decode v10.4.0 Corey Forman (digitalsleuth), MITgithub A separate implementation of 73 timestamp formats; the primary differential oracle (the reference named in ADR 0007). run in this battery
CPython datetime Python Software Foundation, PSF-2.0 — docs Independent implementation for formats time-decode lacks a flag for (PostgreSQL, Unix-ns). run
unfurl (dfir-unfurl) Ryan Benson, Apache-2.0github A second independent oracle for the embedded-ID family (Twitter/Discord snowflakes); raises those from single- to dual-oracle. Env-gated (tests/unfurl_oracle.rs). run when installed
Format specifications the spec authors (Discord, Microsoft, Apple, RFCs, …) Spec worked examples: the author states value → answer directly (tier-1 gold). see References

Differential validation between two independent implementations is strong but not infallible — a shared misreading of a spec could make both agree on a wrong answer. The tier-1 spec worked examples below anchor the trickiest epochs independently of any tool, which is why both kinds of check are used together.

Format coverage

timeglyph implements 45 numeric/packed formats — catalogued authoritatively in forensicnomicon::temporal_formats::TIME_FORMATS (the zero-dep knowledge table) and decoded by the engine in src/registry.rs — plus the self-describing string forms in interpret.rs (ISO-8601/RFC-3339, RFC-2822, HTTP-date, EXIF, ASN.1, ULID, UUIDv1, ObjectId, Google ei=, Apache CLF, PDF date, DMTF/WMI CIM, JWT iat/exp/nbf). tests/docs_sync.rs fails the build if any registry format drifts out of this list, so coverage cannot fall silently behind:

  • Linear epochs (seconds → nanoseconds since a fixed point): unix, unix_ms, unix_us, unix_ns, unix_float, filetime, webkit, cocoa, cocoa_float, hfsplus, hfs, dotnet_ticks, active, prtime, iostime, postgres, dhcp6, ole, excel1904, mjd, sqlite_julian, ksuid, nokiale.
  • Embedded-ID (epoch + bit-shift within a larger ID): snowflake, discord, mastodon, linkedin, tiktok, sony, gmsgid.
  • Composite (two-word) (src/compose.rs; a value split across two integer fields): filetime_hilodecode filetime_hilo "low:high" reassembles a FILETIME's two 32-bit halves (.reg exports / IE cookies; validated against time-decode --filetimelohi); unix_sec_nsecdecode unix_sec_nsec "sec:nsec" reassembles a struct timespec (ext4/BTRFS/ZFS stat, protobuf Timestamp, Java Instant).
  • Anchored-relative (src/compose.rs; a boot/epoch-relative duration resolved against an anchor): elapsed_realtime (Android, ms since boot) and mach_continuous (Apple, ns since boot) via decode <id> "<ticks>@<anchor>", where <anchor> is an ISO-8601 instant.
  • VMware VMSD (compose::vmsd): decode vmsd "<createTimeHigh>,<createTimeLow>" reassembles a .vmsd snapshot time (µs since 1970 split across two 32-bit fields). Validated against time-decode --vm.
  • Syslog RFC 3164 (interpret::parse_syslog_with_reference): decode syslog "<Mon DD HH:MM:SS>@<reference>" infers the omitted year as the most recent one at-or-before the ISO-8601 reference (firewall/router/Linux logs).
  • GPS week + time-of-week (compose::gps_week_tow, leap feature): decode gps_week_tow "<week>:<tow>" — GNSS receiver time (u-blox, NMEA, Berla iVe, drone logs), leap-corrected to UTC. Validated against time-decode --gps.
  • Packed bit-field / civil: fat, exfat, dttm, bitdate, bitdec, bcd, moto, symantec, dvr, ns40, ns40le, logtime, semioctet, gsm, sqlserver.

The differential battery below validates a subset against time-decode; formats without a matching oracle flag are tier-2 (spec worked example) — see the note after the table.

Raw-double hex lane

identify/decode --as hex also reinterpret the first 8 bytes as an IEEE-754 double (both byte orders) and run it through the float formats (cocoa_float, unix_float, ole, sqlite_julian) — a raw f64 is how Apple Biome/SEGB, binary plists, and CFAbsoluteTime blobs store time. Non-finite / out-of-range doubles yield no reading, so the lane is self-gating.

Artifact hints (forensicnomicon linkage)

With the artifact-hints feature, the --artifact hint consults forensicnomicon's authoritative artifact→timestamp-format catalog: --artifact "imessage" pins iostime, "ntfs mft" pins filetime, etc. — the format the artifact ACTUALLY stores, where a keyword heuristic can't. Forensic knowledge lives in forensicnomicon; timeglyph is the decoder that consumes it. Off by default (opt-in dependency).

The differential battery

Every input below is time-decode's own published example value for that format (time-decode --formats <fmt>), so the input is authored by the independent third party — not chosen by us — and the expected answer is its tool's output. timeglyph agrees with all of them to the second. The battery is encoded as an env-gated test (tests/oracle.rs).

timeglyph format oracle flag third-party input agreed answer (UTC) tier
unix --unixsec 1746371930 2025-05-04 15:18:50 1
unix_ms --unixmilli 1746371930064 2025-05-04 15:18:50.064 1
unix_us --prtime 1746371930064939 2025-05-04 15:18:50.064939 1
filetime --active 133908455300649390 2025-05-04 15:18:50.064939 1
webkit --chrome 13390845530064940 2025-05-04 15:18:50.064940 1
hfsplus --hfsdec 3829216730 2025-05-04 15:18:50 1
hfsplus (max) --hfsdec 4294967295 2040-02-06 06:28:15 1 (spec + oracle)
dotnet_ticks --dotnet 638819687300649472 2025-05-04 15:18:50.064947 1
cocoa_float --mac 768064730.064939 2025-05-04 15:18:50.064939 1
sqlite_julian --juliandec 2460800.1380787035 2025-05-04 15:18:49.999986 1
ole --oleauto 45781.638079455312 2025-05-04 15:18:50.064939 1
discord --discord 1102608904745127937 2023-05-01 14:54:08.374 1
snowflake --twitter 1189581422684274688 2019-10-30 16:34:47.179 1
fat --fat a45a597a 2025-05-04 15:18:50 (local) 1
gps --gps 1430407111 2025-05-04 15:18:13 1
ntp --ntp 3981841662.020607 2026-03-07 03:07:42 1
tai64 --tai 1599755800 2020-09-10 16:36:03 1

Formats time-decode has no flag for

postgres (µs since 2000) and unix_ns are validated against CPython datetime instead, with values we chose — tier 2: postgres 6311520000000002020-01-01 00:00:00 and unix_ns 15778368000000000002020-01-01 00:00:00. To reach tier 1, source a third-party-published worked example for each.

Tier-1 spec worked examples

These come straight from the format's own specification — the author states the value and the answer, so no tool is involved:

  • Discord — the developer docs decode 1759288472991170632016-04-30 11:18:25.796 UTC (Discord docs).
  • OLE Automation — Microsoft states 2.0 = 1900-01-01 and 2.5 = noon 1900-01-01 (VariantTimeToSystemTime).
  • HFS+ — Apple TN1150 states the maximum date is 2040-02-06 06:28:15 GMT (the u32 overflow), matched by hfsplus 4294967295 (TN1150).

These are encoded in tests/anchors.rs and run on every build (no oracle needed).

Input conventions (reconciled divergences)

The same instant can be presented to a decoder in different encodings. Where timeglyph and the oracle take different encodings, the battery converts between them — these are convention differences, not disagreements about the time:

  • FAT — time-decode reads the 4 on-disk bytes a4 5a 59 7a as date = LE(a4,5a) = 0x5AA4, time = LE(59,7a) = 0x7A59. timeglyph takes those two 16-bit words packed into one integer (date in the high word) = 0x5AA47A59. Both decode to the same instant.
  • TAI64 — the external label is 2^62 + s (s = TAI seconds since 1970 TAI), while time-decode's --tai takes s directly; the battery passes label − 2^62.
  • NTP — time-decode's example carries a fractional part (…​.020607); timeglyph decodes the integer seconds field. They agree to the second.

The broader question of robustly handling endianness, hex-vs-decimal, and packed layouts is a design topic tracked separately.

How to run

# 1. Install the independent oracle (MIT).
pip install time-decode

# 2. Run the env-gated differential battery (skips cleanly if absent).
cargo test --features leap --test oracle

# 3. The tier-1 spec anchors run with the normal suite.
cargo test --all-features

Appendix — epoch offsets (seconds to the Unix epoch)

A cross-check reference; positive = after 1970, negative = before.

Epoch Used by Offset to 1970
0001-01-01 .NET ticks −62 135 596 800 s
1582-10-15 UUIDv1/v6 −12 219 292 800 s (×10⁷ as 100-ns)
1601-01-01 FILETIME, WebKit −11 644 473 600 s
1899-12-30 OLE Automation −2 209 161 600 s
1900-01-01 NTP −2 208 988 800 s
1904-01-01 HFS+ −2 082 844 800 s
1970-01-01 Unix 0
1980-01-01 FAT/DOS +315 532 800 s
1980-01-06 GPS +315 964 800 s
2000-01-01 PostgreSQL +946 684 800 s
2001-01-01 Cocoa / CFAbsoluteTime +978 307 200 s

Julian Day 2440587.5 = the Unix epoch. Leap offsets (2017→): TAI − UTC = 37 s, GPS − UTC = 18 s. See Time scales and Calendars.