Repository: BurntSushi/jiff (language: rust)

Issue:
Support time zones in no-alloc
With `chrono_tz` I can do the following, without alloc:

```rust
// time.rs
use embassy_time::{Duration, Instant};

pub type Timestamp = chrono::DateTime<chrono::Utc>; // ugh
pub type UnsignedDuration = core::time::Duration;
pub type SignedDuration = chrono::TimeDelta;

pub const TZ: &chrono_tz::Tz = &chrono_tz::Europe::Zurich;

fn offset() -> Duration {
    // ntp things
}

pub fn now() -> Timestamp {
    Timestamp::from_timestamp_micros((Instant::now() + offset()).as_micros() as i64).unwrap()
}
```

One thing my embedded display does is show sunrise and sunset times. Simplified:
```rust
let sunset: Timestamp = solar::next_sunset(time::now(), lat, long);
display(sunset.with_timezone(TZ));
```

Setting
```toml
CHRONO_TZ_TIMEZONE_FILTER="Europe/Zurich|UTC"
```

during build will only package the `Europe/Zurich` transition rules into my binary, instead of the whole TZDB.

Would POSIX time zones suffice? Yes in this case, because `Europe/Zurich` does not have any upcoming rules changes, and I'm not using it on historical dates. However, if central Europe decided today to abandon DST next year, I would want to redeploy my binary when TZDB incorporates the change (in a few weeks), and be ready for 2026. I don't think POSIX time zones can do that.


Discussion / hints:
Oh, are you `grittybants` on reddit? From [this exchange](https://old.reddit.com/r/rust/comments/1imonui/jiff_020_released_a_relatively_new_datetime/mc62mmo/).

Thank you for filing this issue!

Can you say why you can't or don't want to include an allocator? Like, is there a specific constraint preventing you from doing it? Or you just don't want to?

> I don't think POSIX time zones can do that.

In that case, since you're already okay with re-deploying, you'd just change your POSIX TZ string to not have a DST rule.
So one aspect of your workflow that I am not a huge fan of is setting an environment variable to influence a `build.rs` script. Are you deadset on keeping that exact workflow, or can it be tweaked? The reason why is because I do _not_ like the idea of using `build.rs` scripts to build the tzdb like how `chrono-tz` does. Because then all of the build dependencies get passed on to users. But, by `chrono-tz` doing things this way, it gives you control over what is actually included in the crate. And that in turn affords you an opportunity to reduce binary size. That is valuable.

A barebones approach that I'm considering is just exposing a `TimeZone::tzif_static`, or something, that asks for a `&'static [u8]` that is the corresponding TZif data. That would technically unblock you I think, but you'd have to manage putting the TZif data somewhere in your binary, and you wouldn't get `jiff::tz::TimeZoneDatabase` support at all.

In order to get `TimeZoneDatabase` support, I'd either need to expose a mutable API somehow, or, I guess, provide some way to use a slimmed down bundled tzdb crate. Which is just full circle back to `chrono-tz`'s design. I guess it could be a separate `jiff-tzdb-partial` crate or something, but then it starts feeling over-complicated.

Finally, there's the problem of the size of `TimeZone`. I think the only way to keep `TimeZone` slim and support the different types of time zones in a core-only environment without sacrificing a convenient API is through some kind of pointer tagging.

Getting POSIX time zones working is I think considerably easier. Then all I need to do is figure out the problem of keeping `TimeZone` slim and punt on the IANA tzdb stuff.
> Can you say why you can't or don't want to include an allocator? Like, is there a specific constraint preventing you from doing it? Or you just don't want to?

It's a cost and it introduces fallibility. How much of my precious memory do I want to allocate to an allocator just for jiff? How much does it need? If I allocate too little I might get a runtime panic in three years, if I allocate too much I waste memory.

> In that case, since you're already okay with re-deploying, you'd just change your POSIX TZ string to not have a DST rule.

By that logic I could also use fixed offset time zones and deploy twice a year to get the DST right.


> A barebones approach that I'm considering is just exposing a TimeZone::tzif_static, or something, that asks for a &'static [u8] that is the corresponding TZif data. That would technically unblock you I think, but you'd have to manage putting the TZif data somewhere in your binary, and you wouldn't get jiff::tz::TimeZoneDatabase support at all.

Luckily I don't need a timezone database, I just need a working timezone. This would work for me, and packaging would be trivial:
```rust
const TZ: TimeZone = TimeZone::tzif_static(include_bytes!("/usr/share/zoneinfo/Europe/Zurich"));
```

> By that logic I could also use fixed offset time zones and deploy twice a year to get the DST right.

You wouldn't need to re-deploy twice a year. You mentioned re-deploying in the case of a time zone rule change. That's all you would need when using a POSIX time zone too. I was just going off of the re-deployment strategy you yourself suggested. If there aren't any rule changes, then you wouldn't need to do any re-deployments. It is my understanding that my suggestion requires exactly as many re-deployments as your existing strategy.
POSIX time zones would use less of your memory too AFAIK.
> You wouldn't need to re-deploy twice a year. You mentioned re-deploying in the case of a time zone rule change. That's all you would need when using a POSIX time zone too.

For a POSIX zone I couldn't redeploy today, as I'd need to keep the current POSIX zone until the DST change happens. I'd still have to time the deployment to some timezone specific event.

If jiff wants to internally represent `Europe/Zurich` as a POSIX time zone while that's possible, I won't complain. It just needs to stop doing that when that doesn't fully capture the time zone anymore, and I shouldn't have to figure out if this happens.

> POSIX time zones would use less of your memory too AFAIK.

Embedded devices usually have read-only flash storage, where static data lives, as well as random access memory. The flash storage is usually a lot bigger, so I don't mind having a whole time zone in there if that gives me the peace of mind that it's correct.

Ah yeah that's true, you'd have to time it. I didn't realize that was what you meant. But that makes sense.
One small snag here is that if we have a `TimeZone::tzif_static`, and it is `const`, then I think _ideally_ that would return a `Result<TimeZone, Error>`. But...

1. A `jiff::Error` cannot be constructed in a `const` context because it has an `Arc` internally when `alloc` is enabled. Without some kind of indirection here, the `Error` would become fat, which I am firmly opposed to for perf reasons. So if this returns an error, it has to be a new error type (sigh).
2. Validating the TZif data in a `const` context will be annoying. It would need to be entirely separate from Jiff's parser today (which allocates of course), and unifying them is probably tricky.

Since the data is `'static`, I'm considering just saying that if the data is invalid, then Jiff has unspecified behavior (may panic, may have logic errors). We can still do some basic sanity checks in a `const` context, like ensuring the data begins with the string `TZif`, and either return an error or panic immediately.

(We can't lazily report an error because all of the fundamental methods on a `TimeZone` are infallible.)

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 56e6b2a3..2741656b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -46,9 +46,6 @@ jobs:
         - build: macos
           os: macos-latest
           rust: stable
-        - build: win-msvc
-          os: windows-latest
-          rust: stable
     steps:
     - name: Checkout repository
       uses: actions/checkout@v4
@@ -57,20 +54,30 @@ jobs:
       with:
         toolchain: ${{ matrix.rust }}
     - run: cargo build --verbose
-    - run: cargo doc --features serde --verbose
+    - run: cargo doc --features serde,static --verbose
+    - run: cargo test --verbose --lib --profile testrelease
+    - run: cargo test --verbose --test integration --profile testrelease
+    - run: ./test
+
+  # Test for Windows. We test fewer configurations here because it just
+  # takes forever otherwise.
+  win-msvc:
+    runs-on: windows-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Install Rust
+      uses: dtolnay/rust-toolchain@master
+      with:
+        toolchain: stable
+    - run: cargo build --verbose
+    - run: cargo doc --features serde,static --verbose
     - run: cargo test --verbose --all
-    # Skip on Windows because it takes freaking forever.
-    - if: matrix.build != 'win-msvc'
-      run: cargo test --verbose --lib --profile testrelease
-    - if: matrix.build != 'win-msvc'
-      run: cargo test --verbose --test integration --profile testrelease
-    - if: matrix.build != 'win-msvc'
-      run: ./test
 
   # Tests for stable-x86_64-gnu. It's different enough from the "main" targets
   # to warrant its own section. The main problem is that it is just
   # annoyingly slow. Like, twice as slow as the next slowest runner. So we
-  # run a stripped down version of tests.
+  # run a stripped down version (even more so than msvc) of tests.
   win-gnu:
     runs-on: windows-latest
     steps:
@@ -81,7 +88,7 @@ jobs:
       with:
         toolchain: stable-x86_64-gnu
     - run: cargo build --verbose
-    - run: cargo doc --features serde --verbose
+    - run: cargo doc --features serde,static --verbose
     - run: cargo test --verbose --lib
     - run: cargo test --verbose --test integration
 
@@ -108,7 +115,7 @@ jobs:
     - name: Build jiff-tzdb-platform
       run: cargo build -p jiff-tzdb-platform --verbose
     - name: Build docs
-      run: cargo doc --features serde --verbose
+      run: cargo doc --features serde,static --verbose
     # NOTE: We currently don't track some ecosystem integration crates
     # like `jiff-icu`. They don't support Jiff's MSRV at time of writing
     # (2025-02-02), and I think these sorts of integration crates are at very
@@ -320,6 +327,28 @@ jobs:
       run: |
         cargo bench --manifest-path bench/Cargo.toml -- --test
 
+  # Runs miri on a subset of Jiff's test suite. This doesn't quite cover
+  # everything. In particular, `miri` and `insta` cannot play nice together,
+  # and `insta` is used a lot among Jiff's tests. However, the primary reason
+  # why we want to run `miri` (at present, 2025-02-25) is to check out pointer
+  # tagging representation of `jiff::tz::TimeZone`. And we can run the tests in
+  # `tz`.
+  miri:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+    - name: Install Rust
+      uses: dtolnay/rust-toolchain@master
+      with:
+        # We use nightly here so that we can use miri I guess?
+        toolchain: nightly
+        components: miri
+    - name: "Run tests on `jiff::tz` under miri"
+      run: "cargo miri test tz:: --verbose"
+    - name: Run proc macro integration tests under miri
+      run: cargo miri test --test integration procmacro --verbose
+
   # Check that all files are formatted properly.
   rustfmt:
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index 5734d898..59635f0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ members = [
   "crates/jiff-diesel",
   "crates/jiff-icu",
   "crates/jiff-sqlx",
+  "crates/jiff-static",
   "crates/jiff-tzdb",
   "crates/jiff-tzdb-platform",
   "examples/*",
@@ -68,6 +69,31 @@ logging = ["dep:log"]
 # require extra dependencies. (For example, `windows-sys` on Windows.)
 tz-system = ["std", "dep:windows-sys"]
 
+# When enabled, the `jiff::tz::get` and `jiff::tz::include` proc-macros
+# become available. These proc macros enable creating `TimeZone` values in a
+# `const` context for use in `core`-only environments.
+#
+# Users should generally prefer using Jiff's default zoneinfo integration at
+# runtime. On Unix systems, this will enable applications using Jiff to get
+# automatic tzdb updates when `/usr/share/zoneinfo` is updated without needing
+# to re-compile the application.
+#
+# Note that this introduces a build-time dependency on `jiff-tzdb`.
+static = ["static-tz", "jiff-static?/tzdb"]
+
+# When enabled, the `jiff::tz::include` macro becomes available.
+#
+# This proc-macro parses the TZif data (from a file) at compile time and
+# generates a special static structure that can be used by Jiff at runtime
+# to do tzdb lookups. This effectively provides a way to use time zones in
+# core-only environments without dynamic memory allocation.
+#
+# This is a subset of the functionality provided by `static`. Namely, this
+# doesn't result in a dependency on `jiff-tzdb`. It requires users to include
+# the time zone they want as a file, where as enabling `static` (which also
+# enables this feature, by necessity) permits using Jiff's bundled tzdb.
+static-tz = ["dep:jiff-static"]
+
 # This conditionally bundles tzdb into the binary depending on which platform
 # Jiff is being built for.
 tzdb-bundle-platform = ["dep:jiff-tzdb-platform", "alloc"]
@@ -110,10 +136,26 @@ tzdb-concatenated = ["std"]
 js = ["dep:wasm-bindgen", "dep:js-sys"]
 
 [dependencies]
+jiff-static = { version = "=0.2.1", path = "crates/jiff-static", optional = true }
 jiff-tzdb = { version = "0.1.2", path = "crates/jiff-tzdb", optional = true }
 log = { version = "0.4.21", optional = true, default-features = false }
 serde = { version = "1.0.203", optional = true, default-features = false }
 
+# This ensures that `jiff-static` is always used with a compatible version
+# of `jiff`. Namely, since `jiff-static` emits code that relies on internal
+# Jiff APIs that aren't covered by semver, we only guarantee compatibility for
+# one version of Jiff for each release of `jiff-static`.
+#
+# (This is the same pattern that `serde` and `serde_derive` use as of
+# 2025-02-22.)
+#
+# This also helps with compilation, although in Jiff's case, we don't use
+# `syn` so this is less of a problem.
+#
+# See: https://github.com/matklad/macro-dep-test
+[target.'cfg(any())'.dependencies]
+jiff-static = { version = "=0.2.1", path = "crates/jiff-static" }
+
 # Note that the `cfg` gate for the `tzdb-bundle-platform` must repeat the
 # target gate on this dependency. The intent is that `tzdb-bundle-platform`
 # is enabled by default, but that the `tzdb-bundle-platform` crate is only
@@ -152,8 +194,8 @@ humantime = "2.1.0"
 icu = { version = "1.5.0", features = ["std"] }
 insta = "1.39.0"
 # We force `serde` to be enabled in dev mode so that the docs render and test
-# correctly. This is highly suspicious.
-jiff = { path = "./", default-features = false, features = ["serde"] }
+# correctly. We also enable `static` so that we can test our proc macros.
+jiff = { path = "./", default-features = false, features = ["serde", "static"] }
 # Uncomment if you want to remove `ignore` from `jiff_icu` tests in
 # `COMPARE.md`. Otherwise, this creates a circular dependency and causes
 # `jiff-icu` to get re-compiled all the time.
diff --git a/bench/src/bench.rs b/bench/src/bench.rs
index fcdaec78..88ab55e9 100644
--- a/bench/src/bench.rs
+++ b/bench/src/bench.rs
@@ -6,6 +6,7 @@ mod datetime;
 mod parse;
 mod print;
 mod timestamp;
+mod tz;
 mod zoned;
 
 fn main() {
@@ -29,6 +30,7 @@ fn main() {
     parse::define(&mut c);
     print::define(&mut c);
     timestamp::define(&mut c);
+    tz::define(&mut c);
     zoned::define(&mut c);
     // This is an undocument API. Woohoo.
     c.final_summary();
diff --git a/bench/src/date.rs b/bench/src/date.rs
index 5cb5adde..816ecb93 100644
--- a/bench/src/date.rs
+++ b/bench/src/date.rs
@@ -86,6 +86,13 @@ fn add_days(c: &mut Criterion) {
         1070,
         civil::date(2026, 1, 17),
     );
+    benchmark_with(
+        c,
+        "one",
+        civil::date(2023, 2, 12),
+        1,
+        civil::date(2023, 2, 13),
+    );
 }
 
 /// Measures the timing for adding a `Span` with years, months and days to
diff --git a/bench/src/datetime.rs b/bench/src/datetime.rs
index 66989748..c91aa5ad 100644
--- a/bench/src/datetime.rs
+++ b/bench/src/datetime.rs
@@ -111,7 +111,7 @@ fn add_years_months_days(c: &mut Criterion) {
 /// This is useful when you have a known time zone already and want to get
 /// a specific instant for many distinct civil datetimes in that time zone.
 fn to_timestamp_static(c: &mut Criterion) {
-    const NAME: &str = "civil_datetime/to_datetime_static";
+    const NAME: &str = "civil_datetime/to_timestamp_static";
     const TZNAME: &str = "America/New_York";
     const STAMP: i64 = 1719755160;
     const DATETIME: civil::DateTime = civil::date(2024, 6, 30).at(9, 46, 0, 0);
diff --git a/bench/src/tz.rs b/bench/src/tz.rs
new file mode 100644
index 00000000..c03418c5
--- /dev/null
+++ b/bench/src/tz.rs
@@ -0,0 +1,54 @@
+use std::hint::black_box as bb;
+
+use criterion::Criterion;
+use jiff::{
+    civil,
+    tz::{Offset, TimeZone},
+    Timestamp,
+};
+
+use crate::benchmark;
+
+pub(super) fn define(c: &mut Criterion) {
+    posix_to_offset(c);
+    posix_to_timestamp(c);
+}
+
+/// Measures how long it takes to map a timestamp to an offset using a POSIX
+/// time zone.
+fn posix_to_offset(c: &mut Criterion) {
+    const NAME: &str = "tz/posix_to_offset";
+    const STAMP: Timestamp = Timestamp::constant(1719755160, 0);
+    const TZ: &str = "EST5EDT,M3.2.0,M11.1.0";
+    const EXPECTED: Offset = Offset::constant(-4);
+
+    let tz = TimeZone::posix(TZ).unwrap();
+    {
+        benchmark(c, format!("{NAME}/jiff"), |b| {
+            b.iter(|| {
+                let offset = bb(&tz).to_offset(bb(STAMP));
+                assert_eq!(offset, EXPECTED);
+            })
+        });
+    }
+}
+
+/// Measures how long it takes to map a civil datetime to a possibly ambiguous
+/// timestamp using a POSIX time zone.
+fn posix_to_timestamp(c: &mut Criterion) {
+    const NAME: &str = "tz/posix_to_timestamp";
+    const DATETIME: civil::DateTime = civil::date(2024, 6, 30).at(9, 46, 0, 0);
+    const TZ: &str = "EST5EDT,M3.2.0,M11.1.0";
+    const EXPECTED: Timestamp = Timestamp::constant(1719755160, 0);
+
+    let tz = TimeZone::posix(TZ).unwrap();
+    {
+        benchmark(c, format!("{NAME}/jiff"), |b| {
+            b.iter(|| {
+                let ambiguous = bb(&tz).to_ambiguous_timestamp(bb(DATETIME));
+                let ts = ambiguous.unambiguous().unwrap();
+                assert_eq!(ts, EXPECTED);
+            })
+        });
+    }
+}
diff --git a/crates/jiff-cli/cmd/generate/crc32.rs b/crates/jiff-cli/cmd/generate/crc32.rs
index e914527f..891f4d40 100644
--- a/crates/jiff-cli/cmd/generate/crc32.rs
+++ b/crates/jiff-cli/cmd/generate/crc32.rs
@@ -46,10 +46,11 @@ pub fn run(p: &mut Parser) -> anyhow::Result<()> {
     args::configure(p, USAGE, &mut [&mut config])?;
 
     let jiff = config.jiff();
-    let table_path = jiff.join("src/util/crc32/table.rs");
+    let table_path = jiff.join("src/shared/crc32/table.rs");
     write_crc_tables(&table_path).with_context(|| {
         format!("failed to write CRC32 data table to {}", table_path.display())
     })?;
+    super::rustfmt(&table_path)?;
 
     Ok(())
 }
diff --git a/crates/jiff-cli/cmd/generate/mod.rs b/crates/jiff-cli/cmd/generate/mod.rs
index 7fe09068..27e30d88 100644
--- a/crates/jiff-cli/cmd/generate/mod.rs
+++ b/crates/jiff-cli/cmd/generate/mod.rs
@@ -1,6 +1,9 @@
+use anyhow::Context;
+
 use crate::args;
 
 mod crc32;
+mod shared;
 mod tzdb;
 mod unit_designator_match;
 mod windows_zones;
@@ -16,6 +19,7 @@ USAGE:
 COMMANDS:
     crc32                  Generate CRC32 data tables.
     jiff-tzdb              Generate Rust source code from TZif data for jiff-tzdb
+    shared                 Copy shared code from Jiff to jiff-static proc macro
     unit-designator-match  Generate Rust `match` expression for parsing unit labels
     windows-zones          Generate mapping Windows TZ names to IANA names.
     zoneinfo               Generate TZif data for jiff-tzdb
@@ -25,9 +29,50 @@ pub fn run(p: &mut lexopt::Parser) -> anyhow::Result<()> {
     match &*args::next_as_command(USAGE, p)? {
         "crc32" => crc32::run(p),
         "jiff-tzdb" => tzdb::run(p),
+        "shared" => shared::run(p),
         "unit-designator-match" => unit_designator_match::run(p),
         "windows-zones" => windows_zones::run(p),
         "zoneinfo" => zoneinfo::run(p),
         unk => anyhow::bail!("unrecognized command '{}'", unk),
     }
 }
+
+/// Run rustfmt on the given crate.
+///
+/// I'm not sure why, but running `rustfmt` on files directly sometimes
+/// produces different output than running `cargo fmt`. Maybe `rustfmt`
+/// isn't respecting the configuration somehow?
+fn cargo_fmt(krate: &str) -> anyhow::Result<()> {
+    use std::process::Command;
+
+    let out = Command::new("cargo")
+        .arg("fmt")
+        .arg("-p")
+        .arg(krate)
+        .output()
+        .context("`cargo fmt` command failed")?;
+    anyhow::ensure!(
+        out.status.success(),
+        "cargo fmt -p {krate}: {}",
+        bstr::BString::from(out.stderr),
+    );
+    Ok(())
+}
+
+/// Run rustfmt on the given file path.
+fn rustfmt<P: AsRef<std::path::Path>>(path: P) -> anyhow::Result<()> {
+    use std::process::Command;
+
+    let path = path.as_ref();
+    let out = Command::new("rustfmt")
+        .arg(path)
+        .output()
+        .context("rustfmt command failed")?;
+    anyhow::ensure!(
+        out.status.success(),
+        "rustfmt {}: {}",
+        path.display(),
+        bstr::BString::from(out.stderr),
+    );
+    Ok(())
+}
diff --git a/crates/jiff-cli/cmd/generate/shared.rs b/crates/jiff-cli/cmd/generate/shared.rs
new file mode 100644
index 00000000..da3b559c
--- /dev/null
+++ b/crates/jiff-cli/cmd/generate/shared.rs
@@ -0,0 +1,175 @@
+/*!
+A command for copying shared code from Jiff to the `jiff-static` proc-macro.
+*/
+
+#![allow(warnings)]
+
+use std::{
+    fmt::Write as _,
+    fs::{self, File},
+    io::{self, BufWriter, Write},
+    path::{Path, PathBuf},
+    sync::LazyLock,
+};
+
+use anyhow::Context;
+use jiff::Unit;
+use lexopt::{Arg, Parser};
+use regex_lite::Regex;
+
+use crate::args::{self, Usage};
+
+const USAGE: &'static str = r#"
+Copy shared code from Jiff to the `jiff-static` proc-macro.
+
+USAGE:
+    jiff-cli generate shared [<jiff-dir>]
+
+While unfortunate, this copies code inside of Jiff into the proc-macro so that
+it can be used in both places. Specifically, this includes a handful of
+shared types, as well as a POSIX time zone and TZif parser.
+
+Doing things this way permits Jiff to depend on and re-export `jiff-static`.
+This obviously comes at the cost of a little more compilation time, but
+the amount of code copied is pretty small (<2,000 SLOC at time of writing,
+2025-02-22). An alternative design would have `jiff-static` depend on
+`jiff`, and `jiff` could expose (without making it part of the semver API)
+the necessary parsing routines. But then Jiff couldn't re-export the macro
+and users would need to specifically deal with `jiff-static` explicitly.
+"#;
+
+pub fn run(p: &mut Parser) -> anyhow::Result<()> {
+    let mut config = Config::default();
+    args::configure(p, USAGE, &mut [&mut config])?;
+
+    let jiff = config.jiff();
+    let jiff_dir = jiff.join("src");
+    let macro_dir = jiff.join("crates/jiff-static");
+    let dir = Path::new("shared");
+    copy(&jiff_dir, &macro_dir, dir)?;
+
+    Ok(())
+}
+
+fn copy(srcdir: &Path, dstdir: &Path, dir: &Path) -> anyhow::Result<()> {
+    for result in walkdir::WalkDir::new(srcdir.join(dir)) {
+        let dent = result?;
+        let suffix = dent.path().strip_prefix(srcdir).unwrap();
+        let dstpath = dstdir.join(suffix);
+        if dent.file_type().is_dir() {
+            match fs::create_dir(&dstpath) {
+                Ok(()) => {}
+                Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
+                Err(err) => {
+                    return Err(anyhow::Error::from(err).context(format!(
+                        "failed to create directory {}",
+                        dstpath.display()
+                    )));
+                }
+            }
+            continue;
+        }
+        anyhow::ensure!(
+            dent.file_type().is_file(),
+            "unknow file type {:?}, unsure how to handle",
+            dent.file_type(),
+        );
+        copy_rust_source_file(&dent.path(), &dstpath)?;
+    }
+    super::cargo_fmt("jiff-static")?;
+    Ok(())
+}
+
+fn copy_rust_source_file(src: &Path, dst: &Path) -> anyhow::Result<()> {
+    let code = fs::read_to_string(src)
+        .with_context(|| format!("failed to read {}", src.display()))?;
+    let code = remove_only_jiffs(&remove_cfg_alloc(&code));
+
+    let mut out = String::new();
+    writeln!(out, "// auto-generated by: jiff-cli generate shared")?;
+    writeln!(out)?;
+    writeln!(out, "{code}")?;
+    fs::write(&dst, &out)
+        .with_context(|| format!("failed to write {}", dst.display()))?;
+    Ok(())
+}
+
+fn remove_only_jiffs(code: &str) -> String {
+    static RE: LazyLock<Regex> = LazyLock::new(|| {
+        Regex::new(
+            r"(?xm)
+                ^[\x20\t]*
+                //\s+only-jiff-start\n
+                (?s:.)+?\n
+                [\x20\t]*//\s+only-jiff-end\n
+            ",
+        )
+        .unwrap()
+    });
+    RE.replace_all(code, "").into_owned()
+}
+
+/// Removes all `#[cfg(feature = "alloc")]` gates.
+///
+/// This is because the proc-macro always runs in a context where `alloc`
+/// (and `std`) are enabled.
+fn remove_cfg_alloc(code: &str) -> String {
+    static RE: LazyLock<Regex> = LazyLock::new(|| {
+        Regex::new(r###"#\[cfg\(feature = "alloc"\)\]\n"###).unwrap()
+    });
+    RE.replace_all(code, "").into_owned()
+}
+
+#[derive(Debug)]
+struct Config {
+    jiff: Option<PathBuf>,
+    verbose: bool,
+}
+
+impl Config {
+    fn jiff(&self) -> &Path {
+        self.jiff.as_deref().unwrap_or_else(|| Path::new("./"))
+    }
+}
+
+impl Default for Config {
+    fn default() -> Config {
+        Config { jiff: None, verbose: false }
+    }
+}
+
+impl args::Configurable for Config {
+    fn configure(
+        &mut self,
+        _: &mut Parser,
+        arg: &mut Arg,
+    ) -> anyhow::Result<bool> {
+        match *arg {
+            Arg::Short('v') | Arg::Long("verbose") => {
+                self.verbose = true;
+            }
+            Arg::Value(ref mut value) => {
+                if self.jiff.is_none() {
+                    let path = PathBuf::from(std::mem::take(value));
+                    self.jiff = Some(path);
+                } else {
+                    return Ok(false);
+                }
+            }
+            _ => return Ok(false),
+        }
+        Ok(true)
+    }
+
+    fn usage(&self) -> &[Usage] {
+        const USAGES: &'static [Usage] = &[Usage::new(
+            "-v, --verbose",
+            "Add more output.",
+            r#"
+This is a generic flag that expands output beyond the "normal" amount. Which
+output is added depends on the command.
+"#,
+        )];
+        USAGES
+    }
+}
diff --git a/crates/jiff-cli/cmd/generate/tzdb.rs b/crates/jiff-cli/cmd/generate/tzdb.rs
index 1d5c0655..0a9b283a 100644
--- a/crates/jiff-cli/cmd/generate/tzdb.rs
+++ b/crates/jiff-cli/cmd/generate/tzdb.rs
@@ -188,6 +188,7 @@ pub fn run(p: &mut Parser) -> anyhow::Result<()> {
                 tzname_path.display()
             )
         })?;
+    super::rustfmt(&tzname_path)?;
 
     Ok(())
 }
diff --git a/crates/jiff-cli/cmd/generate/unit_designator_match.rs b/crates/jiff-cli/cmd/generate/unit_designator_match.rs
index 0feade30..3796cbf9 100644
--- a/crates/jiff-cli/cmd/generate/unit_designator_match.rs
+++ b/crates/jiff-cli/cmd/generate/unit_designator_match.rs
@@ -47,6 +47,7 @@ pub fn run(p: &mut Parser) -> anyhow::Result<()> {
     write_match(&path).with_context(|| {
         format!("failed to write `match` expression to {}", path.display())
     })?;
+    super::rustfmt(&path)?;
 
     Ok(())
 }
diff --git a/crates/jiff-cli/cmd/generate/windows_zones.rs b/crates/jiff-cli/cmd/generate/windows_zones.rs
index 2d95003b..87af4a49 100644
--- a/crates/jiff-cli/cmd/generate/windows_zones.rs
+++ b/crates/jiff-cli/cmd/generate/windows_zones.rs
@@ -95,6 +95,7 @@ pub fn run(p: &mut Parser) -> anyhow::Result<()> {
             mapping_path.display()
         )
     })?;
+    super::rustfmt(&mapping_path)?;
 
     Ok(())
 }
diff --git a/crates/jiff-static/COPYING b/crates/jiff-static/COPYING
new file mode 100644
index 00000000..bb9c20a0
--- /dev/null
+++ b/crates/jiff-static/COPYING
@@ -0,0 +1,3 @@
+This project is dual-licensed under the Unlicense and MIT licenses.
+
+You may use this code under the terms of either license.
diff --git a/crates/jiff-static/Cargo.toml b/crates/jiff-static/Cargo.toml
new file mode 100644
index 00000000..5098781d
--- /dev/null
+++ b/crates/jiff-static/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "jiff-static"
+version = "0.2.1"  #:version
+authors = ["Andrew Gallant <jamslam@gmail.com>"]
+license = "Unlicense OR MIT"
+homepage = "https://github.com/BurntSushi/jiff/tree/master/crates/jiff-static"
+repository = "https://github.com/BurntSushi/jiff"
+documentation = "https://docs.rs/jiff-tzdb"
+description = "Create static TimeZone values for Jiff (useful in core-only environments)."
+categories = ["date-and-time"]
+keywords = ["date", "time", "static", "zone", "iana"]
+workspace = "../.."
+edition = "2021"
+rust-version = "1.70"
+include = ["/*.rs", "/*.dat", "COPYING", "LICENSE-MIT", "UNLICENSE"]
+
+[lib]
+name = "jiff_static"
+bench = false
+path = "lib.rs"
+proc-macro = true
+
+[features]
+default = []
+# This forces the jiff-tzdb crate to be included and makes the `get` proc macro
+# available (which pulls from the bundled tzdb).
+tzdb = ["dep:jiff-tzdb"]
+
+[dependencies]
+jiff-tzdb = { version = "0.1.2", path = "../jiff-tzdb", optional = true }
+proc-macro2 = "1.0.93"
+quote = "1.0.38"
+syn = "2.0.98"
diff --git a/crates/jiff-static/LICENSE-MIT b/crates/jiff-static/LICENSE-MIT
new file mode 100644
index 00000000..3b0a5dc0
--- /dev/null
+++ b/crates/jiff-static/LICENSE-MIT
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Andrew Gallant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/crates/jiff-static/README.md b/crates/jiff-static/README.md
new file mode 100644
index 00000000..f2b05d39
--- /dev/null
+++ b/crates/jiff-static/README.md
@@ -0,0 +1,17 @@
+jiff-tzdb
+=========
+This is an optional dependency of `jiff` that embeds the entire [IANA Time Zone
+Database] into the compiled binary. Specifically, it embeds the binary [TZif]
+for each time zone.
+
+This is most typically used on Windows where there is no standard location
+for a system copy of the Time Zone Database. On macOS and Linux, one must
+explicitly opt into using `jiff-tzdb`. By default, in Unix environments, Jiff
+will look for a system copy of the Time Zone Database.
+
+[IANA Time Zone Database]: https://www.iana.org/time-zones
+[TZif]: https://datatracker.ietf.org/doc/html/rfc8536
+
+### Documentation
+
+https://docs.rs/jiff-tzdb
diff --git a/crates/jiff-static/UNLICENSE b/crates/jiff-static/UNLICENSE
new file mode 100644
index 00000000..68a49daa
--- /dev/null
+++ b/crates/jiff-static/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/crates/jiff-static/lib.rs b/crates/jiff-static/lib.rs
new file mode 100644
index 00000000..8352ee11
--- /dev/null
+++ b/crates/jiff-static/lib.rs
@@ -0,0 +1,379 @@
+/*!
+This crate provides macros for defining `static` data structures for Jiff.
+
+The macros in this crate are re-exported in the [`jiff::tz`] sub-module.
+Users should _not_ depend on this crate directly or import from it. Instead,
+enable the `static` or `static-tz` features of Jiff and use the re-exports in
+`jiff::tz`.
+
+At present, the macros in this crate are limited to creating `TimeZone`
+in a `const` context. This works by reading TZif data (e.g., from
+`/usr/share/zoneinfo/America/New_York` or from [`jiff-tzdb`]) at compile
+time and generating Rust source code that builds a `TimeZone`.
+
+# Documentation
+
+The macros defined in this crate are documented on their corresponding
+re-exports in Jiff:
+
+* `get` is documented at [`jiff::tz::get`].
+* `include` is documented at [`jiff::tz::include`].
+
+# Compatibility
+
+The APIs required to build a `TimeZone` in a `const` context are exposed by
+Jiff but not part of Jiff's public API for the purposes of semver (and do not
+appear in `rustdoc`). The only guarantee provided by `jiff` and `jiff-static`
+is that there is exactly one version of `jiff` that `jiff-static` works with.
+Conventionally, this is indicated by the exact same version string. That is,
+`jiff-static 0.2.2` is only guaranteed to work with `jiff 0.2.2`.
+
+This compatibility constraint is managed by Jiff, so that you should never
+need to worry about it. In particular, users should never directly depend on
+this crate. Everything should be managed through the `jiff` crate.
+
+[`jiff-tzdb`]: https://docs.rs/jiff-tzdb
+[`jiff::tz`]: https://docs.rs/jiff/0.2/jiff/tz/index.html
+[`jiff::tz::get`]: https://docs.rs/jiff/0.2/jiff/tz/macro.get.html
+[`jiff::tz::include`]: https://docs.rs/jiff/0.2/jiff/tz/macro.include.html
+*/
+
+extern crate alloc;
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use quote::quote;
+
+use self::shared::{
+    PosixDay, PosixDayTime, PosixDst, PosixRule, PosixTimeZone, TzifFixed,
+    TzifIndicator, TzifLocalTimeType, TzifOwned, TzifTransition,
+};
+
+/// A bundle of code copied from `src/shared`.
+///
+/// The main thing we use in here is the parsing routine for TZif data and
+/// shared data types for representing TZif data.
+mod shared;
+
+// Public API docs are in Jiff.
+#[proc_macro]
+pub fn include(input: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(input as Include);
+    proc_macro::TokenStream::from(input.quote())
+}
+
+// Public API docs are in Jiff.
+#[cfg(feature = "tzdb")]
+#[proc_macro]
+pub fn get(input: TokenStream) -> TokenStream {
+    let input = syn::parse_macro_input!(input as Get);
+    proc_macro::TokenStream::from(input.quote())
+}
+
+/// The entry point for the `include!` macro.
+#[derive(Debug)]
+struct Include {
+    tzif: TzifOwned,
+}
+
+impl Include {
+    fn from_path_only(path: &str) -> Result<Include, String> {
+        const NEEDLE: &str = "zoneinfo/";
+
+        let Some(zoneinfo) = path.rfind(NEEDLE) else {
+            return Err(format!(
+                "could not extract IANA time zone identifier from \
+                 file path `{path}` \
+                 (could not find `zoneinfo` in path), \
+                 please provide IANA time zone identifier as second \
+                 parameter",
+            ));
+        };
+        let idstart = zoneinfo.saturating_add(NEEDLE.len());
+        let id = &path[idstart..];
+        Include::from_path_with_id(id, path)
+    }
+
+    fn from_path_with_id(id: &str, path: &str) -> Result<Include, String> {
+        let id = id.to_string();
+        let data = std::fs::read(path)
+            .map_err(|e| format!("failed to read {path}: {e}"))?;
+        let tzif = TzifOwned::parse(Some(id.clone()), &data).map_err(|e| {
+            format!("failed to parse TZif data from {path}: {e}")
+        })?;
+        Ok(Include { tzif })
+    }
+
+    fn quote(&self) -> proc_macro2::TokenStream {
+        self.tzif.quote()
+    }
+}
+
+impl syn::parse::Parse for Include {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Include> {
+        let lit1 = input.parse::<syn::LitStr>()?.value();
+        if !input.lookahead1().peek(syn::Token![,]) {
+            return Ok(
+                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
+            );
+        }
+        input.parse::<syn::Token![,]>()?;
+        if input.is_empty() {
+            return Ok(
+                Include::from_path_only(&lit1).map_err(|e| input.error(e))?
+            );
+        }
+        let lit2 = input.parse::<syn::LitStr>()?.value();
+        // Permit optional trailing comma.
+        if input.lookahead1().peek(syn::Token![,]) {
+            input.parse::<syn::Token![,]>()?;
+        }
+        Ok(Include::from_path_with_id(&lit2, &lit1)
+            .map_err(|e| input.error(e))?)
+    }
+}
+
+/// The entry point for the `get!` macro.
+#[cfg(feature = "tzdb")]
+#[derive(Debug)]
+struct Get {
+    tzif: TzifOwned,
+}
+
+#[cfg(feature = "tzdb")]
+impl Get {
+    fn from_id(id: &str) -> Result<Get, String> {
+        let (id, data) = jiff_tzdb::get(id).ok_or_else(|| {
+            format!("could not find time zone `{id}` in bundled tzdb")
+        })?;
+        let id = id.to_string();
+        let tzif = TzifOwned::parse(Some(id.clone()), &data).map_err(|e| {
+            format!("failed to parse TZif data from bundled `{id}`: {e}")
+        })?;
+        Ok(Get { tzif })
+    }
+
+    fn quote(&self) -> proc_macro2::TokenStream {
+        self.tzif.quote()
+    }
+}
+
+#[cfg(feature = "tzdb")]
+impl syn::parse::Parse for Get {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Get> {
+        let lit1 = input.parse::<syn::LitStr>()?.value();
+        if input.lookahead1().peek(syn::Token![,]) {
+            input.parse::<syn::Token![,]>()?;
+        }
+        Ok(Get::from_id(&lit1).map_err(|e| input.error(e))?)
+    }
+}
+
+// Everything below at this point is quasi-quoting the `shared` data type
+// values into `static` data structures as Rust source code.
+
+impl TzifOwned {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let fixed = self.fixed.quote();
+        let types = self.types.iter().map(TzifLocalTimeType::quote);
+        let mut transitions = vec![];
+        for (i, this) in self.transitions.iter().enumerate() {
+            let prev = &self.transitions[i.saturating_sub(1)];
+            let prev_offset = self.types[usize::from(prev.type_index)].offset;
+            let this_offset = self.types[usize::from(this.type_index)].offset;
+            transitions.push(this.quote(prev_offset, this_offset));
+        }
+        quote! {
+            {
+                static TZ: jiff::tz::TimeZone =
+                    jiff::tz::TimeZone::__internal_from_tzif(
+                        &#fixed.to_jiff(&[#(#types),*], &[#(#transitions),*])
+                    );
+                // SAFETY: Since we are guaranteed that the `TimeZone` is
+                // constructed above as a static TZif time zone, it follows
+                // that it is safe to memcpy's its internal representation.
+                //
+                // NOTE: We arrange things this way so that `jiff::tz::get!`
+                // can be used "by value" in most contexts. Basically, we
+                // "pin" the time zone to a static so that it has a guaranteed
+                // static lifetime. Otherwise, since `TimeZone` has a `Drop`
+                // impl, it's easy to run afoul of this and have it be dropped
+                // earlier than you like. Since this particular variant of
+                // `TimeZone` can always be memcpy'd internally, we just do
+                // this dance here to save the user from having to write out
+                // their own `static`.
+                //
+                // NOTE: It would be nice if we could make this `copy` routine
+                // safe, or at least panic if it's misused. But to do that, you
+                // need to know the time zone variant. And to know the time
+                // zone variant, you need to "look" at the tag in the pointer.
+                // And looking at the address of a pointer in a `const` context
+                // is precarious.
+                unsafe { TZ.copy() }
+            }
+        }
+    }
+}
+
+impl TzifFixed<String> {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let TzifFixed {
+            ref name,
+            version,
+            checksum,
+            ref designations,
+            ref posix_tz,
+        } = *self;
+        let name = name.as_ref().unwrap();
+        let posix_tz = posix_tz
+            .as_ref()
+            .map(|tz| {
+                let tz = tz.quote();
+                quote!(Some(#tz))
+            })
+            .unwrap_or_else(|| quote!(None));
+        quote! {
+            jiff::shared::TzifFixed {
+                name: Some(#name),
+                version: #version,
+                checksum: #checksum,
+                designations: #designations,
+                posix_tz: #posix_tz,
+            }
+        }
+    }
+}
+
+impl TzifLocalTimeType {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let TzifLocalTimeType {
+            offset,
+            is_dst,
+            ref designation,
+            ref indicator,
+        } = *self;
+        let desig_start = designation.0;
+        let desig_end = designation.1;
+        let indicator = indicator.quote();
+        quote! {
+            jiff::shared::TzifLocalTimeType {
+                offset: #offset,
+                is_dst: #is_dst,
+                designation: (#desig_start, #desig_end),
+                indicator: #indicator,
+            }.to_jiff()
+        }
+    }
+}
+
+impl TzifIndicator {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        match *self {
+            TzifIndicator::LocalWall => quote! {
+                jiff::shared::TzifIndicator::LocalWall
+            },
+            TzifIndicator::LocalStandard => quote! {
+                jiff::shared::TzifIndicator::LocalStandard
+            },
+            TzifIndicator::UTStandard => quote! {
+                jiff::shared::TzifIndicator::UTStandard
+            },
+        }
+    }
+}
+
+impl TzifTransition {
+    fn quote(
+        &self,
+        prev_offset: i32,
+        this_offset: i32,
+    ) -> proc_macro2::TokenStream {
+        let TzifTransition { timestamp, type_index } = *self;
+        quote! {
+            jiff::shared::TzifTransition {
+                timestamp: #timestamp,
+                type_index: #type_index,
+            }.to_jiff(#prev_offset, #this_offset)
+        }
+    }
+}
+
+impl PosixTimeZone<String> {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let PosixTimeZone { ref std_abbrev, std_offset, ref dst } = *self;
+        let dst = dst
+            .as_ref()
+            .map(|dst| {
+                let dst = dst.quote();
+                quote!(Some(#dst))
+            })
+            .unwrap_or_else(|| quote!(None));
+        quote! {
+            jiff::shared::PosixTimeZone {
+                std_abbrev: #std_abbrev,
+                std_offset: #std_offset,
+                dst: #dst,
+            }
+        }
+    }
+}
+
+impl PosixDst<String> {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let PosixDst { ref abbrev, offset, ref rule } = *self;
+        let rule = rule
+            .as_ref()
+            .map(|r| {
+                let r = r.quote();
+                quote!(Some(#r))
+            })
+            .unwrap_or_else(|| quote!(None));
+        quote! {
+            jiff::shared::PosixDst {
+                abbrev: #abbrev,
+                offset: #offset,
+                rule: #rule,
+            }
+        }
+    }
+}
+
+impl PosixRule {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let start = self.start.quote();
+        let end = self.end.quote();
+        quote! {
+            jiff::shared::PosixRule { start: #start, end: #end }
+        }
+    }
+}
+
+impl PosixDayTime {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        let PosixDayTime { ref date, time } = *self;
+        let date = date.quote();
+        quote! {
+            jiff::shared::PosixDayTime { date: #date, time: #time }
+        }
+    }
+}
+
+impl PosixDay {
+    fn quote(&self) -> proc_macro2::TokenStream {
+        match *self {
+            PosixDay::JulianOne(day) => quote! {
+                jiff::shared::PosixDay::JulianOne(#day)
+            },
+            PosixDay::JulianZero(day) => quote! {
+                jiff::shared::PosixDay::JulianZero(#day)
+            },
+            PosixDay::WeekdayOfMonth { month, week, weekday } => quote! {
+                jiff::shared::PosixDay::WeekdayOfMonth {
+                    month: #month,
+                    week: #week,
+                    weekday: #weekday,
+                }
+            },
+        }
+    }
+}
diff --git a/crates/jiff-static/shared/crc32/mod.rs b/crates/jiff-static/shared/crc32/mod.rs
new file mode 100644
index 00000000..3d63946f
--- /dev/null
+++ b/crates/jiff-static/shared/crc32/mod.rs
@@ -0,0 +1,48 @@
+// auto-generated by: jiff-cli generate shared
+
+use self::table::{TABLE, TABLE16};
+
+mod table;
+
+/// Returns the "masked" CRC32 checksum of the slice using the Castagnoli
+/// polynomial.
+///
+/// This "masked" checksum is the same one used by the Snappy frame format.
+/// Masking is supposed to make the checksum robust with respect to data that
+/// contains the checksum itself.
+pub(crate) fn sum(buf: &[u8]) -> u32 {
+    let sum = slice16(0, buf);
+    (sum.wrapping_shr(15) | sum.wrapping_shl(17)).wrapping_add(0xA282EAD8)
+}
+
+/// Returns the CRC32 checksum of `buf` using the Castagnoli polynomial.
+///
+/// This computes the checksum by looking at 16 bytes from the given slice
+/// per iteration.
+fn slice16(prev: u32, mut buf: &[u8]) -> u32 {
+    let mut crc: u32 = !prev;
+    while buf.len() >= 16 {
+        crc ^= u32::from_le_bytes(buf[..4].try_into().unwrap());
+        crc = TABLE16[0][usize::from(buf[15])]
+            ^ TABLE16[1][usize::from(buf[14])]
+            ^ TABLE16[2][usize::from(buf[13])]
+            ^ TABLE16[3][usize::from(buf[12])]
+            ^ TABLE16[4][usize::from(buf[11])]
+            ^ TABLE16[5][usize::from(buf[10])]
+            ^ TABLE16[6][usize::from(buf[9])]
+            ^ TABLE16[7][usize::from(buf[8])]
+            ^ TABLE16[8][usize::from(buf[7])]
+            ^ TABLE16[9][usize::from(buf[6])]
+            ^ TABLE16[10][usize::from(buf[5])]
+            ^ TABLE16[11][usize::from(buf[4])]
+            ^ TABLE16[12][usize::from((crc >> 24) as u8)]
+            ^ TABLE16[13][usize::from((crc >> 16) as u8)]
+            ^ TABLE16[14][usize::from((crc >> 8) as u8)]
+            ^ TABLE16[15][usize::from((crc) as u8)];
+        buf = &buf[16..];
+    }
+    for &b in buf {
+        crc = TABLE[usize::from((crc as u8) ^ b)] ^ (crc >> 8);
+    }
+    !crc
+}
diff --git a/crates/jiff-static/shared/crc32/table.rs b/crates/jiff-static/shared/crc32/table.rs
new file mode 100644
index 00000000..35e95533
--- /dev/null
+++ b/crates/jiff-static/shared/crc32/table.rs
@@ -0,0 +1,798 @@
+// auto-generated by: jiff-cli generate shared
+
+// auto-generated by: jiff-cli generate crc32
+
+pub(super) const TABLE: [u32; 256] = [
+    0, 4067132163, 3778769143, 324072436, 3348797215, 904991772, 648144872,
+    3570033899, 2329499855, 2024987596, 1809983544, 2575936315, 1296289744,
+    3207089363, 2893594407, 1578318884, 274646895, 3795141740, 4049975192,
+    51262619, 3619967088, 632279923, 922689671, 3298075524, 2592579488,
+    1760304291, 2075979607, 2312596564, 1562183871, 2943781820, 3156637768,
+    1313733451, 549293790, 3537243613, 3246849577, 871202090, 3878099393,
+    357341890, 102525238, 4101499445, 2858735121, 1477399826, 1264559846,
+    3107202533, 1845379342, 2677391885, 2361733625, 2125378298, 820201905,
+    3263744690, 3520608582, 598981189, 4151959214, 85089709, 373468761,
+    3827903834, 3124367742, 1213305469, 1526817161, 2842354314, 2107672161,
+    2412447074, 2627466902, 1861252501, 1098587580, 3004210879, 2688576843,
+    1378610760, 2262928035, 1955203488, 1742404180, 2511436119, 3416409459,
+    969524848, 714683780, 3639785095, 205050476, 4266873199, 3976438427,
+    526918040, 1361435347, 2739821008, 2954799652, 1114974503, 2529119692,
+    1691668175, 2005155131, 2247081528, 3690758684, 697762079, 986182379,
+    3366744552, 476452099, 3993867776, 4250756596, 255256311, 1640403810,
+    2477592673, 2164122517, 1922457750, 2791048317, 1412925310, 1197962378,
+    3037525897, 3944729517, 427051182, 170179418, 4165941337, 746937522,
+    3740196785, 3451792453, 1070968646, 1905808397, 2213795598, 2426610938,
+    1657317369, 3053634322, 1147748369, 1463399397, 2773627110, 4215344322,
+    153784257, 444234805, 3893493558, 1021025245, 3467647198, 3722505002,
+    797665321, 2197175160, 1889384571, 1674398607, 2443626636, 1164749927,
+    3070701412, 2757221520, 1446797203, 137323447, 4198817972, 3910406976,
+    461344835, 3484808360, 1037989803, 781091935, 3705997148, 2460548119,
+    1623424788, 1939049696, 2180517859, 1429367560, 2807687179, 3020495871,
+    1180866812, 410100952, 3927582683, 4182430767, 186734380, 3756733383,
+    763408580, 1053836080, 3434856499, 2722870694, 1344288421, 1131464017,
+    2971354706, 1708204729, 2545590714, 2229949006, 1988219213, 680717673,
+    3673779818, 3383336350, 1002577565, 4010310262, 493091189, 238226049,
+    4233660802, 2987750089, 1082061258, 1395524158, 2705686845, 1972364758,
+    2279892693, 2494862625, 1725896226, 952904198, 3399985413, 3656866545,
+    731699698, 4283874585, 222117402, 510512622, 3959836397, 3280807620,
+    837199303, 582374963, 3504198960, 68661723, 4135334616, 3844915500,
+    390545967, 1230274059, 3141532936, 2825850620, 1510247935, 2395924756,
+    2091215383, 1878366691, 2644384480, 3553878443, 565732008, 854102364,
+    3229815391, 340358836, 3861050807, 4117890627, 119113024, 1493875044,
+    2875275879, 3090270611, 1247431312, 2660249211, 1828433272, 2141937292,
+    2378227087, 3811616794, 291187481, 34330861, 4032846830, 615137029,
+    3603020806, 3314634738, 939183345, 1776939221, 2609017814, 2295496738,
+    2058945313, 2926798794, 1545135305, 1330124605, 3173225534, 4084100981,
+    17165430, 307568514, 3762199681, 888469610, 3332340585, 3587147933,
+    665062302, 2042050490, 2346497209, 2559330125, 1793573966, 3190661285,
+    1279665062, 1595330642, 2910671697,
+];
+
+pub(super) const TABLE16: [[u32; 256]; 16] = [
+    [
+        0, 4067132163, 3778769143, 324072436, 3348797215, 904991772,
+        648144872, 3570033899, 2329499855, 2024987596, 1809983544, 2575936315,
+        1296289744, 3207089363, 2893594407, 1578318884, 274646895, 3795141740,
+        4049975192, 51262619, 3619967088, 632279923, 922689671, 3298075524,
+        2592579488, 1760304291, 2075979607, 2312596564, 1562183871,
+        2943781820, 3156637768, 1313733451, 549293790, 3537243613, 3246849577,
+        871202090, 3878099393, 357341890, 102525238, 4101499445, 2858735121,
+        1477399826, 1264559846, 3107202533, 1845379342, 2677391885,
+        2361733625, 2125378298, 820201905, 3263744690, 3520608582, 598981189,
+        4151959214, 85089709, 373468761, 3827903834, 3124367742, 1213305469,
+        1526817161, 2842354314, 2107672161, 2412447074, 2627466902,
+        1861252501, 1098587580, 3004210879, 2688576843, 1378610760,
+        2262928035, 1955203488, 1742404180, 2511436119, 3416409459, 969524848,
+        714683780, 3639785095, 205050476, 4266873199, 3976438427, 526918040,
+        1361435347, 2739821008, 2954799652, 1114974503, 2529119692,
+        1691668175, 2005155131, 2247081528, 3690758684, 697762079, 986182379,
+        3366744552, 476452099, 3993867776, 4250756596, 255256311, 1640403810,
+        2477592673, 2164122517, 1922457750, 2791048317, 1412925310,
+        1197962378, 3037525897, 3944729517, 427051182, 170179418, 4165941337,
+        746937522, 3740196785, 3451792453, 1070968646, 1905808397, 2213795598,
+        2426610938, 1657317369, 3053634322, 1147748369, 1463399397,
+        2773627110, 4215344322, 153784257, 444234805, 3893493558, 1021025245,
+        3467647198, 3722505002, 797665321, 2197175160, 1889384571, 1674398607,
+        2443626636, 1164749927, 3070701412, 2757221520, 1446797203, 137323447,
+        4198817972, 3910406976, 461344835, 3484808360, 1037989803, 781091935,
+        3705997148, 2460548119, 1623424788, 1939049696, 2180517859,
+        1429367560, 2807687179, 3020495871, 1180866812, 410100952, 3927582683,
+        4182430767, 186734380, 3756733383, 763408580, 1053836080, 3434856499,
+        2722870694, 1344288421, 1131464017, 2971354706, 1708204729,
+        2545590714, 2229949006, 1988219213, 680717673, 3673779818, 3383336350,
+        1002577565, 4010310262, 493091189, 238226049, 4233660802, 2987750089,
+        1082061258, 1395524158, 2705686845, 1972364758, 2279892693,
+        2494862625, 1725896226, 952904198, 3399985413, 3656866545, 731699698,
+        4283874585, 222117402, 510512622, 3959836397, 3280807620, 837199303,
+        582374963, 3504198960, 68661723, 4135334616, 3844915500, 390545967,
+        1230274059, 3141532936, 2825850620, 1510247935, 2395924756,
+        2091215383, 1878366691, 2644384480, 3553878443, 565732008, 854102364,
+        3229815391, 340358836, 3861050807, 4117890627, 119113024, 1493875044,
+        2875275879, 3090270611, 1247431312, 2660249211, 1828433272,
+        2141937292, 2378227087, 3811616794, 291187481, 34330861, 4032846830,
+        615137029, 3603020806, 3314634738, 939183345, 1776939221, 2609017814,
+        2295496738, 2058945313, 2926798794, 1545135305, 1330124605,
+        3173225534, 4084100981, 17165430, 307568514, 3762199681, 888469610,
+        3332340585, 3587147933, 665062302, 2042050490, 2346497209, 2559330125,
+        1793573966, 3190661285, 1279665062, 1595330642, 2910671697,
+    ],
+    [
+        0, 329422967, 658845934, 887597209, 1317691868, 1562966443,
+        1775194418, 2054015301, 2635383736, 2394315727, 3125932886,
+        2851302177, 3550388836, 3225172499, 4108030602, 3883469565,
+        1069937025, 744974838, 411091311, 186800408, 1901039709, 1659701290,
+        1443537075, 1168652484, 2731618873, 2977147470, 2241069783,
+        2520160928, 3965408229, 4294560658, 3407766283, 3636263804,
+        2139874050, 1814657909, 1489949676, 1265388443, 822182622, 581114537,
+        373600816, 98970183, 3802079418, 4047354061, 3319402580, 3598223395,
+        2887074150, 3216496913, 2337304968, 2566056447, 1078858371,
+        1408010996, 1728782957, 1957280282, 247755615, 493284136, 696337329,
+        975428550, 3713716539, 3472378188, 4196393429, 3921508770, 2479927527,
+        2154965136, 3029696521, 2805405822, 4279748100, 3971309171,
+        3629315818, 3421531805, 2979899352, 2722054063, 2530776886,
+        2239369025, 1644365244, 1906417099, 1162229074, 1457827109, 747201632,
+        1059847191, 197940366, 409914617, 3235002245, 3547377650, 3885434731,
+        4097154844, 2388153945, 2650459694, 2837276343, 3133144768,
+        1573319741, 1315204170, 2055455955, 1763794084, 323786209, 15601046,
+        873047311, 665533816, 2157716742, 2470362481, 2816021992, 3027996063,
+        3457565914, 3719617709, 3914560564, 4210158659, 495511230, 237665993,
+        986568272, 695160359, 1392674658, 1084235541, 1950857100, 1743073275,
+        3210335367, 2902150384, 2552030313, 2344516638, 4057183579,
+        3799067948, 3600188853, 3308527042, 575477567, 837783368, 84420561,
+        380288934, 1825011427, 2137386644, 1266828813, 1478549114, 4223924985,
+        3898696334, 3699821079, 3475264096, 3041499941, 2800419666,
+        2450303947, 2175677372, 1725380929, 1970643254, 1100089775,
+        1378914776, 677206173, 1006616810, 253257843, 482013188, 3288730488,
+        3617886991, 3812834198, 4041319393, 2324458148, 2569990867,
+        2915654218, 3194733117, 1494403264, 1253068983, 2119694382,
+        1844797529, 395880732, 70922603, 819829234, 595526021, 2219317755,
+        2548728204, 2735548693, 2964304226, 3401742375, 3647004752,
+        3985066185, 4263891134, 425515587, 184435252, 1041885869, 767259354,
+        1473690527, 1148462056, 1888717681, 1664160518, 3146639482,
+        2821681165, 2630408340, 2406105315, 4110911910, 3869577681,
+        3527588168, 3252691263, 647572418, 893105077, 31202092, 310281051,
+        1746094622, 2075251305, 1331067632, 1559552647, 81018109, 393651338,
+        596708371, 808686692, 1247698209, 1509737814, 1830514127, 2126116280,
+        2579562309, 2321704754, 3196440491, 2905036764, 3611991705,
+        3303540462, 4027559543, 3819779584, 991022460, 682841355, 475331986,
+        267806181, 1973136544, 1715025111, 1390320718, 1098646585, 2785349316,
+        3047659187, 2168471082, 2464327261, 3901714200, 4214093679,
+        3486146550, 3697854337, 2069880831, 1761429384, 1545269009,
+        1337489254, 903200291, 645342804, 311463629, 20059834, 3863682119,
+        4125721648, 3238931625, 3534533854, 2831252891, 3143886316,
+        2407812469, 2619790594, 1150955134, 1463334409, 1675566736,
+        1887274727, 168841122, 431151061, 760577868, 1056433979, 3650022854,
+        3391911345, 4274773288, 3983099231, 2533657626, 2225476717,
+        2957098228, 2749572227,
+    ],
+    [
+        0, 2772537982, 1332695565, 3928932467, 2665391130, 1000289892,
+        3518101015, 1961911401, 944848581, 2635115707, 2000579784, 3531603638,
+        2794429151, 63834273, 3923822802, 1285642924, 1889697162, 3588485108,
+        1070411655, 2592914937, 4001159568, 1262308334, 2702412701, 72489443,
+        1223902031, 3987919153, 127668546, 2732426044, 3593332565, 1936487723,
+        2571285848, 1006839590, 3779394324, 1141205354, 2922096921, 191511399,
+        2140823310, 3671838064, 821366019, 2511642493, 3642082769, 2085902255,
+        2524616668, 859506082, 1204511179, 3800757173, 144978886, 2917507512,
+        2447804062, 883365088, 3733574803, 2076722925, 255337092, 2860101882,
+        1079472265, 3843482359, 2847389787, 217459237, 3872975446, 1134131240,
+        929635393, 2452131391, 2013679180, 3712474162, 3345318105, 1646531239,
+        2282410708, 759906474, 1505436867, 4244289213, 383022798, 3012945072,
+        4281646620, 1517628514, 2958814225, 354057839, 1642732038, 3299575928,
+        780486667, 2344934005, 3083337043, 310800173, 4171804510, 1575566624,
+        689527113, 2354629431, 1719012164, 3275200826, 2409022358, 718754280,
+        3237581211, 1706558437, 289957772, 3020551666, 1579627905, 4217808895,
+        639728589, 2204166579, 1766730176, 3423583166, 3103776727, 499010985,
+        4153445850, 1389436836, 510674184, 3140605814, 1360992005, 4099835259,
+        2158944530, 636449644, 3485578015, 1786782049, 1451427399, 4089615417,
+        434918474, 3165505076, 3361579613, 1830563875, 2268262480, 577987118,
+        1859270786, 3415452412, 566061711, 2231171313, 4027358360, 1431113446,
+        3210989205, 438459627, 2334619459, 778495293, 3293062478, 1628026672,
+        368694105, 2964865319, 1519812948, 4292285226, 3010873734, 372759544,
+        4229503883, 1498974709, 766045596, 2297004002, 1657257873, 3347459567,
+        4219800265, 1589942455, 3035257028, 296471226, 1700507347, 3222944941,
+        708115678, 2406837920, 3285464076, 1721083506, 2361091585, 704312447,
+        1560973334, 4165665384, 308658715, 3072610405, 1784908887, 3475119657,
+        621600346, 2152549412, 4106037325, 1375517235, 3151133248, 513009726,
+        1379054226, 4151517420, 492691615, 3088872161, 3438024328, 1772979446,
+        2206418053, 650303227, 448917981, 3212862371, 1437508560, 4042207662,
+        2216646087, 559859641, 3413116874, 1848743348, 579915544, 2278645094,
+        1845468437, 3367898987, 3159255810, 420477308, 4079040783, 1449175921,
+        1279457178, 3909314020, 53323159, 2792110057, 3533460352, 2011021822,
+        2649948557, 951227379, 1947453791, 3511835425, 998021970, 2654800172,
+        3939331397, 1334640443, 2778873672, 14921014, 1021348368, 2577471598,
+        1938806813, 3603843683, 2721984010, 125811828, 3981540359, 1209069177,
+        78755029, 2716870315, 1272899288, 4003427494, 2590970063, 1060012721,
+        3573564098, 1883361468, 2902854798, 138911472, 3798556291, 1193856253,
+        869836948, 2526624490, 2092432025, 3656804583, 2505519691, 806789173,
+        3661127750, 2138698296, 193566289, 2932343855, 1155974236, 3785840162,
+        3718541572, 2028331898, 2462786313, 931836279, 1132123422, 3862644576,
+        202737427, 2840860013, 3858059201, 1085595071, 2862226892, 266047410,
+        2066475995, 3731519909, 876919254, 2433035176,
+    ],
+    [
+        0, 3712330424, 3211207553, 1646430521, 2065838579, 2791807819,
+        3292861042, 419477706, 4131677158, 721537374, 1227047015, 2489772767,
+        2372293141, 1344534701, 838955412, 4014267180, 3915690301, 874584965,
+        1443074748, 2336634884, 2454094030, 1325607542, 757179215, 4033087991,
+        522244827, 3261429859, 2689069402, 2097306594, 1677910824, 3108456848,
+        3680878761, 102787601, 3609531531, 174112307, 1749169930, 3037175218,
+        2886149496, 1900187584, 325060345, 3458575425, 560035693, 4230274517,
+        2651215084, 1128529492, 1514358430, 2265377830, 3844367647, 945934247,
+        1044489654, 3808694030, 2166785591, 1550003343, 1164153925,
+        2552643325, 4194613188, 658578812, 3355821648, 356543720, 2002970065,
+        2854702953, 3005740963, 1851940123, 205575202, 3506798234, 2879807463,
+        1994650975, 348224614, 3380926174, 3498339860, 230802604, 1877167509,
+        2997282605, 1575107585, 2158466745, 3800375168, 1069593912, 650120690,
+        4219840330, 2577870451, 1155695819, 1120071386, 2676442210,
+        4255501659, 551577571, 971038505, 3836048785, 2257058984, 1539462672,
+        3028716860, 1774397316, 199339709, 3601073157, 3483679951, 316741239,
+        1891868494, 2911254006, 2088979308, 2714165716, 3286526189, 513917525,
+        128023199, 3672428583, 3100006686, 1703146406, 2328307850, 1468170802,
+        899681035, 3907363251, 4058323321, 748729281, 1317157624, 2479329344,
+        2515008081, 1218597097, 713087440, 4156912488, 4005940130, 864051482,
+        1369630755, 2363966107, 1671666103, 3202757391, 3703880246, 25235598,
+        411150404, 3317957372, 2816904133, 2057511293, 1386268991, 2414175111,
+        3989301950, 813842438, 696449228, 4106703476, 2531646285, 1268806133,
+        2766449369, 2040594529, 461605208, 3334874080, 3754335018, 42152338,
+        1621211307, 3185840659, 3150215170, 1719784122, 77814659, 3655790907,
+        3236317681, 497279817, 2139187824, 2730803400, 1300241380, 2428875100,
+        4075239525, 799183581, 916597271, 3957817519, 2311391638, 1417716526,
+        2240142772, 1489008396, 987954741, 3886503053, 4272417863, 602031871,
+        1103155142, 2625987966, 1942077010, 2927891690, 3433471443, 300103531,
+        149131169, 3584435481, 3078925344, 1791035032, 1826712713, 2980365873,
+        3548794632, 247719344, 398679418, 3397842882, 2829352699, 1977734211,
+        2594508655, 1205904855, 633482478, 4169631318, 3783736988, 1019384868,
+        1591745821, 2208675749, 4177958616, 608386144, 1180808537, 2602835937,
+        2183440171, 1600195987, 1027835050, 3758501394, 256046398, 3523698566,
+        2955269823, 1835039751, 1952498893, 2837802613, 3406292812, 373444084,
+        274868197, 3441921373, 2936341604, 1916841692, 1799362070, 3053829294,
+        3559339415, 157458223, 3861267459, 996404923, 1497458562, 2214907194,
+        2634315248, 1078058824, 576935537, 4280745161, 774079059, 4083558635,
+        2437194194, 1275136874, 1426174880, 2286164248, 3932590113, 925055641,
+        3630686645, 86133517, 1728102964, 3125110924, 2739261510, 2113960702,
+        472052679, 3244775807, 3343332206, 436378070, 2015367407, 2774907479,
+        3160736413, 1629530149, 50471196, 3729230756, 822300808, 3964074544,
+        2388947721, 1394727345, 1243701627, 2539965379, 4115022586, 671344706,
+    ],
+    [
+        0, 940666796, 1881333592, 1211347188, 3762667184, 3629437212,
+        2422694376, 2826309188, 3311864721, 4252394557, 3041252553,
+        2371140453, 623031585, 489937549, 1426090617, 1829832149, 2401395155,
+        3073576575, 4278238859, 3339833639, 1869078371, 1467396303, 524615739,
+        659845015, 1246063170, 1918109166, 979875098, 41343670, 2852181234,
+        2450635614, 3659664298, 3795018758, 464041303, 599382779, 1804233231,
+        1402668451, 4226616295, 3288097867, 2345653439, 3017718547,
+        3738156742, 3873362282, 2934792606, 2533101106, 1049231478, 110850010,
+        1319690030, 1991880834, 2492126340, 2895887144, 3836218332,
+        3703137392, 1959750196, 1289618840, 82687340, 1023204032, 1374543637,
+        1778167993, 567214157, 434008033, 2980602277, 2310606345, 3247095549,
+        4187738449, 928082606, 255853826, 1198765558, 2137184858, 3608466462,
+        4010130354, 2805336902, 2670159082, 4063650111, 3391557267,
+        2182397543, 3120943563, 309583759, 711110691, 1649475799, 1514172283,
+        3094547325, 2153931985, 3360805925, 4030774153, 1479980493,
+        1613224545, 672426645, 268764473, 2098462956, 1157984064, 221700020,
+        891793432, 2639380060, 2772488688, 3983761668, 3579973288, 754573305,
+        350793813, 1557856417, 1690988301, 3434897737, 4104982245, 3164519953,
+        2224017853, 3919500392, 3515857860, 2579237680, 2712495260, 165374680,
+        835323252, 2046408064, 1105779244, 2749087274, 2613760390, 3556335986,
+        3957853918, 1134428314, 2072997686, 868016066, 195932270, 1723643323,
+        1588451863, 379480803, 781124943, 2264468235, 3202901159, 4141601875,
+        3469392895, 1856165212, 1454619376, 511707652, 647062952, 2397531116,
+        3069576256, 4274369716, 3335838488, 2881871565, 2480189793,
+        3689349525, 3824578105, 1266704509, 1938886609, 1000521509, 62115977,
+        3783308431, 3650214691, 2443340759, 2847081595, 29690431, 970220947,
+        1911018855, 1240906443, 619167518, 485937330, 1422221382, 1825837034,
+        3298951598, 4239617538, 3028344566, 2358358362, 1963614219,
+        1293619111, 86556499, 1027199231, 2505039547, 2908664087, 3849126371,
+        3715919439, 2959960986, 2289828918, 3226449090, 4166966126,
+        1344853290, 1748613766, 537528946, 404448734, 4196925912, 3258543732,
+        2315968128, 2988159276, 443400040, 578605252, 1783586864, 1381896092,
+        1062144585, 123626981, 1332598033, 2004662973, 3742020857, 3877362517,
+        2938661793, 2537096205, 1509146610, 1642254430, 701587626, 297799430,
+        3115712834, 2175233774, 3381976602, 4052070838, 2626991203,
+        2760235983, 3971377979, 3567715479, 2094074579, 1153459583, 217306507,
+        887274023, 3604078113, 4005605773, 2800943481, 2665639637, 915693713,
+        243601213, 1186381769, 2124927077, 330749360, 732412444, 1670646504,
+        1535468868, 4092816128, 3420587180, 2211558488, 3149978612,
+        1113262757, 2051695881, 846845437, 174635601, 2719921173, 2584730553,
+        3527174989, 3928818913, 2268856628, 3207425688, 4145995372,
+        3473912256, 1736032132, 1600704552, 391864540, 793382768, 3447286646,
+        4117234906, 3176903726, 2236275586, 758961606, 355318378, 1562249886,
+        1695507762, 136208615, 806293323, 2017247167, 1076744211, 3898334807,
+        3494556155, 2558066959, 2691198627,
+    ],
+    [
+        0, 4012927769, 3683426499, 884788186, 3002414967, 1573215342,
+        1769576372, 2252995757, 1611012127, 2402710278, 3146430684,
+        1421530053, 3539152744, 1036207217, 159354795, 3863995570, 3222024254,
+        792484647, 461410557, 4105239524, 1928922953, 2647223376, 2843060106,
+        1178979475, 2685020193, 1329218360, 2072414434, 2495013883, 318709590,
+        4258231375, 3379806101, 641979532, 2247366285, 1791262100, 1584969294,
+        2974342487, 922821114, 3627109091, 3968696633, 62777888, 3857845906,
+        180512139, 1048489553, 3511600456, 1460091365, 3090633468, 2357958950,
+        1673261631, 1173890739, 2865253802, 2658436720, 1900342633,
+        4144828868, 406682333, 746696967, 3283212830, 637419180, 3402519989,
+        4268924527, 289600886, 2534083035, 2017157826, 1283959064, 2746728961,
+        235166699, 3778294002, 3582524200, 985174065, 3169938588, 1405159301,
+        1736297567, 2286790470, 1845642228, 2167548141, 3046040375,
+        1522436142, 3707204739, 868687770, 125555776, 3897278297, 3456658389,
+        557318348, 361024278, 4206141455, 2096979106, 2479699899, 2809265249,
+        1212258168, 2920182730, 1094588627, 1971507977, 2595403792, 486229181,
+        4090179492, 3346523262, 675778407, 2347781478, 1690314367, 1350364581,
+        3209463484, 956660241, 3593801992, 3800685266, 230273483, 3958789497,
+        80100960, 813364666, 3746209443, 1493393934, 3056797975, 2190459597,
+        1841277396, 1274838360, 2764838465, 2423315867, 2134947458,
+        4178135599, 372842806, 579201772, 3451224565, 737830215, 3301576286,
+        4034315652, 524725917, 2567918128, 1983854889, 1115943667, 2914228714,
+        470333398, 4080590031, 3347322645, 682916876, 2935849121, 1104014264,
+        1970348130, 2587970427, 2081337289, 2470364368, 2810318602,
+        1219650579, 3472595134, 567014311, 360134781, 4198978404, 3691284456,
+        859106545, 126363435, 3904392242, 1861333151, 2176965510, 3044872284,
+        1515027269, 3154288631, 1395848430, 1737375540, 2294174765, 251111552,
+        3787965337, 3581610051, 978019162, 2583470427, 1993132610, 1114636696,
+        2906679937, 722048556, 3292134709, 4035262191, 531979766, 4193958212,
+        382390877, 578165127, 3443946142, 1259310643, 2755650858, 2424516336,
+        2142455273, 1508938085, 3066100348, 2189177254, 1833720511,
+        3943015954, 70634763, 814286545, 3753471432, 972458362, 3603358307,
+        3799656889, 222970528, 2332278285, 1681118484, 1351556814, 3216995799,
+        302836797, 4248602404, 3380628734, 649078759, 2700729162, 1338617939,
+        2071296905, 2487554192, 1913320482, 2637864763, 2844153057,
+        1186349048, 3237987157, 802138188, 460546966, 4098033807, 3523271683,
+        1026602778, 160201920, 3871086553, 1626729332, 2412085357, 3145288631,
+        1414078638, 2986787868, 1563864837, 1770677471, 2260340678, 15987563,
+        4022573170, 3682554792, 877607089, 2549676720, 2026410409, 1282693747,
+        2739155306, 621661639, 3393038046, 4269894916, 296814109, 4160676527,
+        416188854, 745685612, 3275893109, 1158403544, 2856042177, 2659677467,
+        1907826178, 1475660430, 3099894167, 2356701773, 1665663316,
+        3842113017, 171022048, 1049451834, 3518838307, 938660497, 3636640136,
+        3967709778, 55449931, 2231887334, 1782025983, 1586185509, 2981834300,
+    ],
+    [
+        0, 1745038536, 3490077072, 3087365464, 2782971345, 3454265625,
+        1978047553, 501592201, 1311636819, 640602523, 2653660355, 4129851403,
+        3956095106, 2211320906, 1003184402, 1405636058, 2623273638,
+        4099462766, 1281205046, 610177022, 968572791, 1371018175, 3921503975,
+        2176731695, 3530950645, 3128240957, 40918629, 1785950893, 2006368804,
+        529919724, 2811272116, 3482564476, 1029407677, 1431875445, 3982350893,
+        2237593317, 2562410092, 4038617764, 1220354044, 549335860, 1937145582,
+        460673574, 2742036350, 3413314486, 3600202559, 3197474807, 110158511,
+        1855180391, 2701162779, 3372438995, 1896226955, 419761219, 81837258,
+        1826852866, 3571901786, 3169175954, 4012737608, 2267981952,
+        1059839448, 1462300944, 1254965657, 583953745, 2597001225, 4073206977,
+        2058815354, 313797554, 2863750890, 3266474530, 3747070635, 3075796579,
+        257006395, 1733474291, 882571817, 1553585889, 3835470777, 2359267185,
+        2440708088, 4185461552, 1098671720, 696208032, 3874291164, 2398081300,
+        921347148, 1592363140, 1124874253, 722408645, 2466931101, 4211690837,
+        2831220879, 3233950791, 2026330399, 281310679, 220317022, 1696786838,
+        3710360782, 3039080454, 1206682823, 804235279, 2548751703, 4293521823,
+        3792453910, 2316266974, 839522438, 1510552654, 163674516, 1640125788,
+        3653705732, 2982415564, 2887892037, 3290599565, 2082989525, 337955101,
+        3686235745, 3014939305, 196159473, 1672612665, 2119678896, 374642552,
+        2924601888, 3327315688, 2509931314, 4254707706, 1167907490, 765458026,
+        813319907, 1484352043, 3766230899, 2290037691, 4117630708, 2641175100,
+        627595108, 1298889644, 1351535397, 948824045, 2156434101, 3901472381,
+        3141792679, 3544244079, 1799727671, 54953727, 514012790, 1990204094,
+        3466948582, 2795914030, 1765143634, 20371610, 3107171778, 3509616906,
+        3436523907, 2765495627, 483616787, 1959806171, 655886593, 1327179209,
+        4145959057, 2669509721, 2197343440, 3942375448, 1392416064, 989706632,
+        3358952777, 2687934849, 406050009, 1882257425, 1842694296, 97936464,
+        3184726280, 3587194304, 2249748506, 3994770642, 1444817290,
+        1042089282, 603502027, 1274779907, 4093570139, 2617098387, 1416525807,
+        1013799719, 2221420159, 3966436023, 4052660798, 2576195318, 562621358,
+        1233897318, 440634044, 1916839540, 3393573676, 2722562020, 3215150957,
+        3617612709, 1873090301, 128334389, 2413365646, 3889833286, 1608470558,
+        937196758, 708430943, 1111154839, 4198471119, 2453453063, 3254056157,
+        2851592213, 301116749, 2045870469, 1679044876, 202841540, 3021105308,
+        3692119124, 327349032, 2072109024, 3280251576, 2877785712, 3059889913,
+        3730905649, 1717858153, 241648545, 1571753595, 900473523, 2376685547,
+        3853155107, 4165979050, 2420959074, 675910202, 1078640370, 2994899507,
+        3665929979, 1652872099, 176684907, 392318946, 2137088810, 3345225330,
+        2942778042, 4239357792, 2494323624, 749285104, 1151992376, 1498395313,
+        827104889, 2303322913, 3779774441, 786002069, 1188715613, 4276037893,
+        2531001805, 2335814980, 3812268428, 1530916052, 859619356, 1626639814,
+        150446350, 2968704086, 3639736478, 3306440727, 2903991519, 353505671,
+        2098281807,
+    ],
+    [
+        0, 1228700967, 2457401934, 3678701417, 555582061, 1747058506,
+        3009771555, 4200137988, 1111164122, 185039357, 3494117012, 2575270835,
+        1663469239, 706411408, 4049501433, 3093430750, 2222328244, 3444208787,
+        370078714, 1597148893, 2775288793, 3965187838, 924021143, 2117012656,
+        3326938478, 2406576201, 1412822816, 487164423, 3880816387, 2926375460,
+        1965585741, 1007945834, 218129817, 1144789182, 2675482583, 3594838768,
+        740157428, 1696701139, 3194297786, 4149829789, 1329291587, 101129316,
+        3712195341, 2491409962, 1848042286, 656055817, 4234025312, 3043124295,
+        2306239533, 3226079498, 453940835, 1379068740, 2825645632, 3780612967,
+        974328846, 1932486953, 3410847991, 2188449232, 1496683193, 269086622,
+        3931171482, 2741802941, 2015891668, 823422451, 436259634, 1396487701,
+        2289578364, 3242478683, 991775071, 1914778744, 2842014481, 3763981878,
+        1480314856, 285717199, 3393402278, 2206156929, 2032553349, 807022754,
+        3948853195, 2724383468, 2658583174, 3612000161, 202258632, 1160922607,
+        3211477227, 4132912588, 756267685, 1680852866, 3696084572, 2507258747,
+        1312111634, 118047029, 4249895985, 3026991382, 1864941183, 638894936,
+        385920683, 1581044620, 2239255781, 3427019202, 907881670, 2132890081,
+        2758137480, 3982076847, 1429973617, 470275926, 3343077439, 2390699288,
+        1948657692, 1025135931, 3864973906, 2942480245, 2474026783,
+        3662338616, 17718609, 1211244662, 2993366386, 4216805461, 538173244,
+        1764729371, 3511526341, 2557599458, 1127569803, 168371372, 4031783336,
+        3110886543, 1646844902, 722773697, 872519268, 2101209923, 2792975402,
+        4014280973, 354161673, 1545627950, 2271538759, 3461911392, 1983550142,
+        1057419161, 3829557488, 2910721495, 1462178003, 505114100, 3311397533,
+        2355337146, 2960629712, 4182500087, 571434398, 1798510777, 2439650749,
+        3629539482, 51570675, 1244568276, 4065106698, 3144738349, 1614045508,
+        688397411, 3545307495, 2590860352, 1093264169, 135634446, 956429309,
+        1883082458, 2876836275, 3796202644, 404517264, 1361054903, 2321845214,
+        3277387513, 2067461927, 839289344, 3913420137, 2692640846, 1512535370,
+        320538733, 3361705732, 2170810915, 3178756681, 4098590574, 789512199,
+        1714650400, 2624223268, 3579184387, 236094058, 1194262349, 4283235987,
+        3060827060, 1832125661, 604535290, 3729882366, 2540503513, 1277789872,
+        85326743, 771841366, 1732059249, 3162089240, 4114995775, 253550395,
+        1176543772, 2640586101, 3562559570, 1815763340, 621159595, 4265780162,
+        3078545125, 1294457825, 68921030, 3747553711, 2523094152, 2859947234,
+        3813353925, 940551852, 1899221899, 2339034767, 3260459944, 420621505,
+        1345212902, 3897315384, 2708483359, 2050271862, 856217425, 3377582677,
+        2154671986, 1529423899, 303387964, 587282639, 1782400488, 2977546881,
+        4165320614, 35437218, 1260439429, 2422489324, 3646438859, 1631206421,
+        671498546, 4081239643, 3128867708, 1076346488, 152814431, 3529458742,
+        2606971153, 2809606523, 3997912156, 890227509, 2083763730, 2255139606,
+        3478572593, 336742744, 1563309183, 3846976929, 2893039750, 1999949807,
+        1040757448, 3293689804, 2372782827, 1445547394, 521482405,
+    ],
+    [
+        0, 4097758792, 3985758817, 430902313, 3738157619, 720442491,
+        861804626, 3345010202, 3094606487, 1280124127, 1440884982, 2715614910,
+        1723609252, 2458052332, 2335042245, 2131967117, 1963693023,
+        2167752087, 2560248254, 1822728182, 2881769964, 1610254244,
+        1180011405, 2993380805, 3447218504, 960935680, 552188713, 3570888033,
+        330802043, 3884545331, 4263934234, 169389906, 3927386046, 506065398,
+        126287327, 4088933271, 886629773, 3236312005, 3645456364, 762833316,
+        1382339881, 2790916961, 3220508488, 1271650560, 2360022810,
+        2023080274, 1631243643, 2500074291, 2669458529, 1797415465,
+        1921871360, 2259925064, 1104377426, 3052249114, 2890050611,
+        1484552827, 661604086, 3545338046, 3405683863, 1052789471, 4188046533,
+        228479629, 338779812, 3759114476, 3519166861, 637333445, 1012130796,
+        3362600356, 252574654, 4214435318, 3801883103, 379778967, 1773259546,
+        2643402066, 2216694139, 1881065267, 3078490409, 1128324961,
+        1525666632, 2932933888, 2764679762, 1358396442, 1230532659,
+        3177621115, 2047240289, 2386083369, 2543301120, 1672045640, 481974469,
+        3901001357, 4046160548, 85284076, 3262487286, 910904510, 803487895,
+        3688535775, 1003822643, 3488335995, 3594830930, 578425370, 3843742720,
+        287574600, 143328865, 4239773737, 2208754852, 2006465260, 1849112261,
+        2584338573, 1567174295, 2841114847, 2969105654, 1153835710,
+        1323208172, 3135265700, 2739885965, 1467056581, 2417053663,
+        1680841111, 2105578942, 2310947830, 4138565499, 43231539, 456959258,
+        4009915218, 677559624, 3697044224, 3321063209, 835563873, 2791835115,
+        1381421987, 1274666890, 3217492418, 2024261592, 2358841744,
+        2503351737, 1627966449, 505149308, 3928301876, 4085919005, 129301333,
+        3235132751, 887808775, 759557934, 3648731494, 3546519092, 660422780,
+        1056066645, 3402406429, 229397511, 4187128399, 3762130534, 335763502,
+        1796236451, 2670637803, 2256649922, 1925146762, 3051333264,
+        1105293528, 1481538801, 2893064889, 1283400277, 3091330077,
+        2716792884, 1439706748, 2461065318, 1720596014, 2132883975,
+        2334125135, 4094480578, 3278474, 429722275, 3986939115, 717427441,
+        3741172921, 3344091280, 862723800, 963948938, 3444205506, 3571805163,
+        551271843, 3887821753, 327525873, 170568152, 4262756240, 2164736797,
+        1966708053, 1821809020, 2561167156, 1606975790, 2885048166,
+        2992200527, 1181191431, 2007645286, 2207574574, 2587616775,
+        1845833807, 2842033749, 1566255133, 1156850740, 2966090364,
+        3487158001, 1005000889, 575149200, 3598107352, 286657730, 3844659850,
+        4236760739, 146342123, 44150713, 4137646577, 4012930520, 453944208,
+        3698224522, 676379586, 838842347, 3317784995, 3134348590, 1324125030,
+        1464043343, 2742898951, 1679662877, 2418231637, 2307671420,
+        2108855092, 2646416344, 1770245520, 1881981369, 2215778289,
+        1131600363, 3075215267, 2934113162, 1524487618, 634317135, 3522182919,
+        3361682222, 1013048678, 4211157884, 255851828, 378597661, 3803064149,
+        3904276487, 478699087, 86463078, 4044981294, 913918516, 3259473020,
+        3689451605, 802571805, 1355119248, 2767957208, 3176440049, 1231713977,
+        2383067299, 2050256619, 1671127746, 2544219274,
+    ],
+    [
+        0, 3411442597, 2470478267, 1477900830, 594376071, 3896184354,
+        2955801660, 2071695257, 1188752142, 2374799531, 3583666869, 516690192,
+        1706532489, 2934039852, 4143390514, 1033987223, 2377504284,
+        1189326265, 519395239, 3584240642, 2933433243, 1703860286, 1033380384,
+        4140718469, 3413064978, 3753655, 1479523497, 2474231564, 3892463765,
+        592720688, 2067974446, 2954146443, 512219849, 3587285356, 2378652530,
+        1183916247, 1038790478, 4139570411, 2930388725, 1711035728,
+        1482502599, 2466990690, 3407720572, 4967385, 2066760768, 2959491045,
+        3899704827, 589741662, 2469530837, 1482979184, 7507310, 3408197323,
+        2959046994, 2064188151, 589297897, 3897131852, 3588810715, 515808382,
+        1185441376, 2382241221, 4135948892, 1037298169, 1707414503,
+        2928896066, 1024439698, 4133080631, 2924426281, 1696157580, 509788181,
+        3577002928, 2367832494, 1182022155, 2077580956, 2961384761,
+        3902136103, 600024194, 1488464667, 2481868990, 3422071456, 11456773,
+        2965005198, 2079070251, 603644469, 3903625616, 2480346633, 1484877228,
+        9934770, 3418483735, 4133521536, 1027011365, 1696598331, 2926998174,
+        3574463751, 509314722, 1179483324, 2367358745, 596143963, 3906868478,
+        2965958368, 2073990469, 15014620, 3417530745, 2477103975, 1492377794,
+        1699906645, 2919563248, 4128376302, 1027898955, 1178595794,
+        2372504183, 3581898857, 506006476, 2923280711, 1701561058, 1031616764,
+        4130030425, 2370882752, 1174845285, 504384891, 3578148574, 3907473993,
+        598813164, 2074596338, 2968627287, 3414829006, 14441579, 1489675893,
+        2476531152, 2048879396, 2974355585, 3915377311, 571052346, 1500651171,
+        2451858694, 3392315160, 23389373, 1019576362, 4153670543, 2944729489,
+        1691580980, 531166637, 3573452296, 2364044310, 1203638195, 4155161912,
+        1023198877, 1693072515, 2948351782, 3569862847, 529642266, 1200048388,
+        2362520225, 2976929334, 2049322387, 573626253, 3915820072, 2451383217,
+        1498109972, 22913546, 3389774255, 1687728621, 2949565000, 4158140502,
+        1015958515, 1207288938, 2359541711, 3568649681, 534986356, 574773987,
+        3910410566, 2969754456, 2052366589, 19869540, 3396949185, 2456792799,
+        1496962426, 3912067057, 578493524, 2054022730, 2973474287, 3393196662,
+        18246099, 1493210061, 2455169128, 2952236287, 1688336218, 1018629444,
+        4158748385, 2358966648, 1204585181, 534411459, 3565945702, 1192287926,
+        2353440019, 3562037005, 520496296, 1685957425, 2938884244, 4147980938,
+        1013666095, 30029240, 3399241245, 2458563587, 1507643302, 581386303,
+        3924900762, 2984755588, 2058467873, 3399813290, 32731919, 1508215057,
+        2461266612, 3922230573, 580781704, 2055797910, 2984150835, 2357191588,
+        1193908225, 524247583, 3563657658, 2937230883, 1682238854, 1012012952,
+        4144262205, 1503069311, 2462154714, 3403122116, 25296481, 2063233528,
+        2980842077, 3921342531, 585927654, 525201265, 3558577364, 2349690570,
+        1197151599, 1008769782, 4151763283, 2942311245, 1681285352,
+        3559051875, 527739334, 1197626328, 2352228477, 4149192676, 1008327745,
+        1678714463, 2941869562, 2465741165, 1504592584, 28883158, 3404645235,
+        2979351786, 2059614031, 584437073, 3917723380,
+    ],
+    [
+        0, 2540828609, 722442611, 3162402482, 1444885222, 3245262119,
+        2098244501, 3932249172, 2889770444, 995070477, 2268200127, 272632702,
+        4196489002, 1834000619, 3509505625, 1180645784, 1569766761,
+        3403762344, 1990140954, 3790525403, 193957775, 2633922638, 545265404,
+        3086082365, 4054767781, 1725902692, 3668001238, 1305523735,
+        2813454915, 817896834, 2361291568, 466584817, 3139533522, 743476499,
+        2418992033, 123671648, 3980281908, 2052046837, 3325153607, 1363158662,
+        387915550, 2154752223, 1007715949, 2875290028, 1090530808, 3597785657,
+        1779414155, 4252910410, 3870410683, 1908420730, 3451805384,
+        1523558665, 2964256093, 668926620, 2611047470, 214997999, 1250927223,
+        3724432822, 1635793668, 4143041733, 479236241, 2346805072, 933169634,
+        2700017187, 1940816725, 3839849620, 1486952998, 3486576103, 632402355,
+        2998945394, 247343296, 2580537089, 3750815385, 1222709592, 4104093674,
+        1676576811, 2307904639, 519971774, 2726317324, 905034445, 775831100,
+        3109014013, 87140175, 2453688462, 2015431898, 4015061787, 1395561897,
+        3294585448, 2181061616, 359771185, 2836382339, 1048458562, 3558828310,
+        1131323095, 4279300197, 1751189412, 3364878727, 1610485318,
+        3816841460, 1961989941, 2660289377, 165756064, 3047117330, 586065363,
+        1689361483, 4089473930, 1337853240, 3637506809, 850308781, 2782878060,
+        429995998, 2396045343, 2501854446, 40809263, 3188776349, 694233692,
+        3271587336, 1416724937, 3893358459, 2138970298, 958472482, 2924533475,
+        305051729, 2237616016, 1866339268, 4165985285, 1144097463, 3544218998,
+        3881633450, 1881993579, 3427966937, 1529047064, 2973905996, 640926605,
+        2588781887, 222059262, 1264804710, 3692205223, 1617754645, 4145876436,
+        494686592, 2316150337, 913557747, 2701279026, 3134045123, 767314946,
+        2445419184, 112448881, 3973220645, 2074312420, 3353153622, 1353508759,
+        385080847, 2172791246, 1039943548, 2861412541, 1089268969, 3617397544,
+        1810068890, 4237460059, 1551662200, 3406662585, 2004083979,
+        3758232266, 174280350, 2635250015, 560781293, 3055362092, 4030863796,
+        1731456629, 3679289543, 1279031046, 2791123794, 825023635, 2371007009,
+        438519264, 32293137, 2526885584, 719542370, 3180507043, 1475605495,
+        3229746230, 2096917124, 3951926597, 2916263133, 983782172, 2262646190,
+        296536687, 4224554555, 1824285178, 3502378824, 1202976905, 2498987519,
+        58880574, 3220970636, 680389453, 3270293273, 1436369112, 3923979882,
+        2123553195, 953016371, 2948339698, 331512128, 2226359937, 1859310293,
+        4188218644, 1172130726, 3534535783, 3378722966, 1578291031,
+        3798770149, 1964856868, 2675706480, 135134641, 3027473155, 587359426,
+        1700617562, 4063013531, 1314047017, 3642962920, 859991996, 2754844797,
+        407762639, 2403074318, 802357037, 3097692396, 81618526, 2477560223,
+        2043530699, 4005313034, 1388467384, 3316884345, 2213321441, 345861408,
+        2833449874, 1066595411, 3589515271, 1115840454, 4277940596,
+        1770899125, 1916944964, 3845371269, 1498274615, 3460050166, 610103458,
+        3006039907, 257092049, 2552438288, 3732678536, 1225642057, 4118003451,
+        1644316986, 2288194926, 521331375, 2741799965, 874347484,
+    ],
+    [
+        0, 829543472, 1659086944, 1402109008, 3318173888, 4105602288,
+        2804218016, 2522164368, 2388842353, 3205694273, 3967909649,
+        3723537185, 1269139377, 2060735361, 692465617, 406322145, 422172691,
+        676858915, 2076864627, 1253811267, 3706620115, 3986644195, 3188531379,
+        2407330947, 2538278754, 2788875090, 4121470722, 3302585138,
+        1384931234, 1677560722, 812644290, 18752498, 844345382, 52585494,
+        1353717830, 1640090742, 4153729254, 3336910038, 2507622534,
+        2751896758, 3157354327, 2369827687, 3738357559, 4020443911,
+        2046179223, 1216865191, 454402039, 711216071, 729442357, 436976645,
+        1234813013, 2028475493, 4005378293, 3754749125, 2355007637,
+        3173991589, 2769862468, 2489936756, 3355121444, 4136289044,
+        1625288580, 1370373044, 37504996, 860722132, 1688690764, 1440134268,
+        105170988, 926227484, 2707435660, 2417091772, 3280181484, 4075965660,
+        3938826045, 3686032141, 2284199773, 3109538669, 788719613, 510866381,
+        1306611613, 2089851821, 2106505311, 1291807855, 527241279, 773637135,
+        3091851423, 2302164143, 3668590847, 3957036239, 4092358446,
+        3265116958, 2433730382, 2692617086, 908804078, 123399134, 1422432142,
+        1706640318, 1458884714, 1736770650, 873953290, 90614842, 2469626026,
+        2722256026, 4056950986, 3231841530, 3633710875, 3924284203,
+        3128274811, 2332326731, 491870171, 740328427, 2142437307, 1321413515,
+        1339885689, 2125257801, 759078937, 474969129, 2316982457, 3144387721,
+        3908694233, 3649578217, 3250577160, 4040035128, 2740746088,
+        2452464472, 75009992, 889805816, 1721444264, 1475015576, 3377381528,
+        4164883624, 2880268536, 2598157512, 210341976, 1039680616, 1852454968,
+        1595665416, 1194072041, 1985856473, 634371977, 348023737, 2196457257,
+        3013251865, 3758681929, 3514383225, 3496417419, 3776367803,
+        2995040491, 2213897435, 362825803, 617716859, 2000937003, 1177695259,
+        1577439226, 1869880266, 1021732762, 228045738, 2613223226, 2863876874,
+        4179703642, 3360744298, 4213010622, 3396117646, 2583615710,
+        2827947246, 1054482558, 262927438, 1547274270, 1833458734, 1971300303,
+        1141797887, 396103599, 653122463, 2964911887, 2177442623, 3529203567,
+        3811216223, 3795101869, 3544546461, 2161574093, 2980500733, 670300269,
+        377629789, 1158696973, 1952547901, 1817608156, 1562881004, 246798268,
+        1069810572, 2844864284, 2564881196, 3413280636, 4194521932,
+        2917769428, 2627237092, 3473541300, 4269530244, 1747906580,
+        1499407396, 181229684, 1002212420, 596342693, 318415765, 1097392069,
+        1880689653, 3863750501, 3611161429, 2226097925, 3051248437,
+        3032512711, 2243013879, 3592671399, 3880912023, 1896294407,
+        1081539639, 333742183, 580211799, 983740342, 198409094, 1480656854,
+        1764807654, 4284874614, 3457428294, 2642827030, 2901902118,
+        2679771378, 2932589762, 4250515602, 3425201314, 1518157874,
+        1795986434, 949938258, 166673506, 299419523, 547951539, 1933275107,
+        1112194003, 3558840131, 3849208691, 3069984547, 2274224915,
+        2257832161, 3085049041, 3832569985, 3573658801, 1129617441,
+        1915046929, 565653569, 281470065, 150019984, 964742048, 1779611632,
+        1533240256, 3442888528, 4232551264, 2950031152, 2661561088,
+    ],
+    [
+        0, 819083365, 1638166730, 1366706351, 3276333460, 4087011825,
+        2733412702, 2453580091, 2206053849, 3014626748, 3805922579,
+        3524001142, 1077236813, 1894214696, 563160199, 289610978, 51846467,
+        868558118, 1655926153, 1382110700, 3227516119, 4035822770, 2717617181,
+        2435429496, 2154473626, 2964885759, 3788429392, 3508330549,
+        1126320398, 1945137515, 579221956, 307495329, 103692934, 922485475,
+        1737116236, 1465414185, 3311852306, 4122272631, 2764221400,
+        2484114365, 2236845919, 3045177146, 3841458069, 3559245808,
+        1176202955, 1992906414, 666836481, 393029220, 87631813, 904601504,
+        1688033039, 1414492010, 3329345105, 4137942580, 2815800987,
+        2533854974, 2252640796, 3063327353, 3890275030, 3610434227,
+        1158443912, 1977502701, 614990658, 343554855, 207385868, 1015958889,
+        1844970950, 1563049379, 3474232472, 4291210493, 2930828370,
+        2657279031, 2401353941, 3220437168, 4001738783, 3730278522,
+        1281958209, 2092636452, 768430475, 488597998, 256600143, 1067012138,
+        1861163141, 1581064416, 3422783963, 4241600958, 2913466641,
+        2641740148, 2352405910, 3169117683, 3985812828, 3711997241,
+        1333672962, 2141979751, 786058440, 503870637, 175263626, 983594991,
+        1809203008, 1526990629, 3376066078, 4192769659, 2828984020,
+        2555176625, 2299525715, 3118318134, 3903556249, 3631854332,
+        1246174151, 2056594338, 736324365, 456217448, 157635273, 968321708,
+        1757487619, 1477646950, 3391992669, 4211051320, 2877932439,
+        2606496754, 2316887824, 3133857653, 3955005402, 3681464255,
+        1229981316, 2038578913, 687109710, 405163563, 414771736, 678089341,
+        2031917778, 1238278839, 3689941900, 3944887273, 3126098758,
+        2324054819, 2613403585, 2870437796, 4200673035, 3400734574,
+        1485684309, 1751090736, 959041183, 167507706, 464516955, 729665342,
+        2047575953, 1255784436, 3639023311, 3895799466, 3108201989,
+        2308005472, 2563916418, 2818603751, 4185272904, 3382970925,
+        1536860950, 1799920499, 977195996, 183299001, 513200286, 776268027,
+        2134024276, 1340119089, 3722326282, 3976989039, 3162128832,
+        2359851429, 2649449799, 2906217762, 4233041293, 3432852968,
+        1587774675, 1852947638, 1057485849, 265669756, 495046109, 760477112,
+        2082848023, 1291289970, 3737726025, 3994752044, 3211615363,
+        2411685094, 2667345924, 2922266721, 4283959502, 3481940139,
+        1572116880, 1835442677, 1007741274, 214094143, 350527252, 607561585,
+        1967189982, 1167251387, 3618406016, 3883812581, 3053981258,
+        2262447663, 2543397581, 2806715048, 4131215879, 3337577058,
+        1423035225, 1677980476, 896908179, 94864374, 401834583, 656521778,
+        1985475229, 1183173368, 3569050563, 3832109990, 3038712585,
+        2244815724, 2492348302, 2757496811, 4113188676, 3321397025,
+        1472648730, 1729425023, 912434896, 112238261, 315270546, 572038647,
+        1936643416, 1136454973, 3514975238, 3780148323, 2955293900,
+        2163477673, 2444693579, 2707761198, 4027801729, 3233896676,
+        1392505311, 1647167930, 861634837, 59357552, 299743441, 554664116,
+        1887029275, 1085010046, 3533003077, 3796328736, 3006343567,
+        2212696554, 2459962632, 2725393773, 4077157826, 3285599655,
+        1374219420, 1631245561, 810327126, 10396723,
+    ],
+    [
+        0, 1409766726, 2819533452, 4228513738, 1441866729, 32929455,
+        4261382501, 2851658787, 2883733458, 4293202580, 65858910, 1475065880,
+        4262683707, 2853450109, 1444794039, 35298289, 1378416981, 103787539,
+        4196815833, 2921399967, 131717820, 1407094778, 2950131760, 4224722294,
+        4190813831, 2915953601, 1371804683, 96682317, 2889588078, 4164732968,
+        70596578, 1345479332, 2756833962, 4032210924, 207575078, 1482165600,
+        4053848387, 2779218949, 1504607183, 229191305, 263435640, 1538580542,
+        2814189556, 4089072306, 1514313361, 239453143, 4065082397, 2789960027,
+        4135128063, 2726190777, 1584898419, 175174709, 2743609366, 4153376080,
+        193364634, 1602344924, 1570458669, 161225067, 4120241825, 2710746087,
+        141193156, 1550662274, 2690958664, 4100165646, 1297060773, 424199907,
+        3846256937, 2974182511, 415150156, 1287251210, 2964331200, 3837218694,
+        3870151799, 2997518641, 1319337723, 446966717, 3009214366, 3881542360,
+        458382610, 1330972756, 526871280, 1264598966, 3077161084, 3815675194,
+        1251363097, 512826463, 3801670549, 3063920339, 3028626722, 3766646884,
+        478906286, 1217188584, 3782479563, 3044236173, 1232774215, 494792961,
+        3911082255, 3172545609, 1091619715, 353869509, 3169796838, 3907524512,
+        350349418, 1088863532, 1123821277, 385577883, 3941764177, 3203782935,
+        386729268, 1124749426, 3204689848, 3942972158, 3140917338, 4013018396,
+        322450134, 1195337616, 4006067123, 3133206261, 1187582271, 315507833,
+        282386312, 1154714318, 3101324548, 3973914690, 1160117345, 287484199,
+        3979040493, 3106669483, 2594121546, 3466097164, 848399814, 1721161856,
+        3480093859, 2607370725, 1734389295, 862452585, 830300312, 1702507998,
+        2574502420, 3446972242, 1686913905, 814421559, 3431131645, 2558901435,
+        3367493151, 2628815705, 1622766739, 884875733, 2638675446, 3376523440,
+        893933434, 1632567868, 1666554317, 928173195, 3411751745, 2673632775,
+        916765220, 1654910818, 2661945512, 3400353262, 1053742560, 1791590566,
+        2529197932, 3267832362, 1799353865, 1060676431, 3274792069,
+        2536901059, 2502726194, 3240871796, 1025652926, 1764060664,
+        3235754459, 2497373341, 1758665559, 1020546577, 1827024053, 954300915,
+        3303573049, 2431636351, 957812572, 1829788186, 2434377168, 3307139222,
+        3338955623, 2466463265, 1862975979, 990745773, 2465548430, 3337756104,
+        989585922, 1862055748, 3620779247, 2211963305, 2145263203, 735660837,
+        2183239430, 3592864320, 707739018, 2116577484, 2083713853, 674604667,
+        3560724913, 2151353591, 700698836, 2110031250, 2177727064, 3586797342,
+        2247642554, 3523156220, 771155766, 2045882992, 3490278995, 2215525141,
+        2013775071, 738234777, 773458536, 2048745262, 2249498852, 3524523426,
+        2079009153, 804027591, 3555033869, 2279790155, 1937851973, 663098115,
+        3683642569, 2408102287, 644900268, 1920413930, 2390675232, 3665402470,
+        3630367127, 2355385553, 1886235419, 610991709, 2375164542, 3650451256,
+        631015666, 1906040244, 564772624, 1974397526, 2309428636, 3718267098,
+        1951964409, 543148479, 3696637557, 2287035187, 2320234690, 3729567108,
+        574968398, 1984038664, 3753564971, 2344455789, 2008314279, 598942945,
+    ],
+    [
+        0, 1737424129, 3474848258, 2828207875, 2614592245, 4233723892,
+        1422555383, 860128758, 843281179, 1439534618, 4250831129, 2597352472,
+        2845110766, 3457813743, 1720257516, 17299181, 1686562358, 50862903,
+        2879069236, 3423985973, 4283524291, 2564790722, 810327745, 1472357312,
+        1455789357, 827025452, 2581098287, 4267086382, 3440515032, 2862410457,
+        34598362, 1702957275, 3373124716, 2927833453, 101725806, 1637796719,
+        1390038681, 894743448, 2647110811, 4199106970, 4216242039, 2629845622,
+        877872501, 1407039604, 1620655490, 118997123, 2944714624, 3356113537,
+        2911578714, 3389380443, 1654050904, 85470553, 912042159, 1372738990,
+        4181807789, 2664411052, 2680707393, 4165379136, 1356178243, 928734786,
+        69196724, 1670457013, 3405914550, 2894912695, 2549607977, 4034466600,
+        1491735595, 1063577898, 203451612, 1806602717, 3275593438, 2763221983,
+        2780077362, 3258605619, 1789486896, 220699185, 1046683591, 1508762310,
+        4051625413, 2532317380, 4084271135, 2499802398, 1013772829,
+        1541541660, 1755745002, 254310379, 2814079208, 3224735209, 3241310980,
+        2797372933, 237994246, 1772190727, 1525021169, 1030423792, 2516059123,
+        4067884786, 1593451077, 963960644, 2447890503, 4134085958, 3308101808,
+        2728615345, 170941106, 1841211315, 1824084318, 188197983, 2745477980,
+        3291108957, 4151235499, 2430611114, 947071401, 1610470568, 981255283,
+        1576155506, 4116790897, 2465186672, 2712356486, 3324361607,
+        1857469572, 154681733, 138393448, 1873889897, 3340914026, 2695671915,
+        2481468829, 4100376732, 1559613343, 997929630, 704883363, 1301108642,
+        3843969185, 2190519712, 2983471190, 3596277079, 2127155796, 424095573,
+        406903224, 2144216249, 3613205434, 2966675131, 2207734605, 3826886220,
+        1284153679, 721706062, 1317358741, 688632212, 2174269079, 3860220822,
+        3578973792, 3000775521, 441398370, 2109852003, 2093367182, 457753231,
+        3017524620, 3562354829, 3876699515, 2157920378, 671893369, 1333967480,
+        3809371855, 2223021006, 739482829, 1268605388, 2027545658, 525801787,
+        3083083320, 3494568761, 3511490004, 3066292437, 508620758, 2044596951,
+        1251645217, 756312608, 2240245027, 3792277538, 2273870073, 3758521848,
+        1217755899, 790333434, 475988492, 2077359885, 3544381454, 3033269519,
+        3050042338, 3527741155, 2060847584, 492369121, 773615895, 1234340886,
+        3774974741, 2257548820, 3186902154, 3665475979, 1927921288, 359089033,
+        639878783, 1101871998, 3913170045, 2393948540, 2411149201, 3896101520,
+        1084935571, 656683154, 341882212, 1944995941, 3682422630, 3170087527,
+        3648168636, 3204210621, 376395966, 1910613439, 1118117961, 623631688,
+        2377701963, 3929417546, 3945910695, 2361339046, 606874533, 1134745252,
+        1894142802, 392736339, 3220941136, 3631567953, 1962510566, 326596071,
+        3152311012, 3697971173, 4012771859, 2292250386, 540274705, 1203571984,
+        1186659325, 557057788, 2309423615, 3995729150, 3714939144, 3135472649,
+        309363466, 1979612683, 276786896, 2012320721, 3747779794, 3102501331,
+        2343103525, 3961917732, 1152718375, 591129382, 574365131, 1169350858,
+        3978422217, 2326731464, 3119226686, 3731186239, 1995859260, 293115965,
+    ],
+    [
+        0, 4060876286, 3790892301, 335044851, 3322195179, 872980757,
+        670089702, 3590114328, 2313498407, 2078876377, 1745961514, 2585612244,
+        1340179404, 3186462258, 2920672961, 1545200447, 371599551, 3827967297,
+        4157752754, 98453580, 3491923028, 573475242, 836168025, 3285902503,
+        2680358808, 1842285158, 2117560981, 2352703339, 1506257779,
+        2882250381, 3090400894, 1245694848, 743199102, 3728759936, 3451403379,
+        1068773773, 3930652053, 407171179, 196907160, 4189101414, 2779348569,
+        1470460839, 1146950484, 3058769578, 1672336050, 2443304780,
+        2186919871, 1884664385, 980056513, 3362157631, 3684570316, 697440562,
+        4235121962, 241359060, 496678951, 4019631577, 3012515558, 1099127576,
+        1383807979, 2692167189, 1972107789, 2273834995, 2491389696,
+        1718852350, 1486398204, 2861870850, 3110916081, 1264632335,
+        2661027351, 1821377513, 2137547546, 2372169444, 3514666459, 594640933,
+        814342358, 3263556904, 393814320, 3849661646, 4136455229, 75579843,
+        1321110083, 3165816765, 2940921678, 1564928688, 2293900968,
+        2058758998, 1766738853, 2604811867, 3344672100, 894937242, 649054313,
+        3567502743, 23005583, 4082304113, 3769328770, 312961404, 1960113026,
+        2262367868, 2501942927, 1730974577, 3000000361, 1088180887,
+        1394881124, 2703769498, 4247903397, 255709531, 482718120, 4006198358,
+        993357902, 3375988144, 3670089027, 684527805, 1660083005, 2432620227,
+        2198255152, 1896528846, 2767615958, 1459255848, 1157765851,
+        3071153957, 3944215578, 421263844, 182688023, 4176450793, 756242673,
+        3743372559, 3437704700, 1055602690, 2972796408, 1128089606,
+        1355098357, 2731091211, 2000021779, 2235163885, 2529264670,
+        1691191776, 953445087, 3403179809, 3642755026, 724306476, 4275095092,
+        215796682, 522496825, 3978864327, 2803330375, 1427857593, 1189281866,
+        3035565492, 1628684716, 2468334674, 2162666657, 1928044895, 787628640,
+        3707654046, 3473289069, 1024074387, 3908497035, 452649845, 151159686,
+        4212035192, 2642220166, 1869683064, 2089384331, 2391110773,
+        1534703725, 2843063699, 3129857376, 1216469150, 345521057, 3868472927,
+        4117517996, 123755346, 3533477706, 546347700, 862517831, 3244619705,
+        2338012217, 2035757511, 1789874484, 2560842954, 1298108626,
+        3209927980, 2896952799, 1588064289, 46011166, 4038205152, 3813309971,
+        289829869, 3300573173, 917942795, 625922808, 3611483910, 3920226052,
+        463858426, 140347913, 4199647223, 799886319, 3718333969, 3461949154,
+        1012214556, 1615644707, 2453718493, 2176361774, 1941219536,
+        2789762248, 1413769526, 1203505605, 3048211515, 4287614907, 226738757,
+        511419062, 3967266632, 965436240, 3414650542, 3632205405, 712180643,
+        1986715804, 2221337954, 2543750545, 1704099951, 2960018551,
+        1113735561, 1369055610, 2744528004, 3320166010, 938064772, 605150071,
+        3592279689, 65084049, 4058847087, 3793057692, 270105186, 1275107677,
+        3188495523, 2918511696, 1610152366, 2315531702, 2013804616,
+        1810913467, 2583450949, 3552812741, 567251771, 842527688, 3225157174,
+        365376046, 3888857040, 4097007395, 104813277, 1512485346, 2821372956,
+        3151158511, 1239339281, 2619481353, 1848512759, 2111205380,
+        2413460986,
+    ],
+];
diff --git a/crates/jiff-static/shared/mod.rs b/crates/jiff-static/shared/mod.rs
new file mode 100644
index 00000000..17f1f3e7
--- /dev/null
+++ b/crates/jiff-static/shared/mod.rs
@@ -0,0 +1,189 @@
+// auto-generated by: jiff-cli generate shared
+
+/*!
+Defines data types shared between `jiff` and `jiff-static`.
+
+While this module exposes types that can be imported outside of `jiff` itself,
+there are *no* semver guarantees provided. That is, this module is _not_ part
+of Jiff's public API. The only guarantee of compatibility that is provided
+is that `jiff-static x.y.z` works with one and only one version of Jiff,
+corresponding to `jiff x.y.z` (i.e., the same version number).
+
+# Design
+
+This module is really accomplishing two different things at the same time.
+
+Firstly, it is a way to provide types that can be used to construct a static
+`TimeZone`. The proc macros in `jiff-static` generate code using these
+types (and a few routines).
+
+Secondly, it provides a way to parse TZif data without `jiff-static`
+depending on `jiff` via a Cargo dependency. This actually requires copying
+the code in this module (which is why it is kinda sectioned off from the rest
+of jiff) into the `jiff-static` crate. This can be done automatically with
+`jiff-cli`:
+
+```text
+jiff-cli generate shared
+```
+
+The copying of code is pretty unfortunate, because it means both crates have to
+compile it. However, the alternatives aren't great either.
+
+One alternative is to have `jiff-static` explicitly depend on `jiff` in its
+`Cargo.toml`. Then Jiff could expose the parsing routines, as it does here,
+and `jiff-static` could use them directly. Unfortunately, this means that
+`jiff` cannot depend on `jiff-static`. And that in turn means that `jiff`
+cannot re-export the macros. Users will need to explicitly depend on and use
+`jiff-static`. Moreover, this could result in some potential surprises
+since `jiff-static` will need to have an `=x.y.z` dependency on Jiff for
+compatibility reasons. That in turn means that the version of Jiff actually
+used is not determine by the user's `jiff = "x.y.z"` line, but rather by the
+user's `jiff-static = "x'.y'.z'"` line. This is overall annoying and not a
+good user experience. Plus, it inverts the typical relationship between crates
+and their proc macros (e.g., `serde` and `serde_derive`) and thus could result
+in other unanticipated surprises.
+
+Another obvious alternative is to split this code out into a separate crate
+that both `jiff` and `jiff-static` depend on. However, the API exposed in
+this module does not provide a coherent user experience. It would either need a
+ton of work to turn it into a coherent user experience or it would need to be
+published as a `jiff-internal-use-only` crate that I find to be very annoying
+and confusing. Moreover, a separate crate introduces a new semver boundary
+beneath Jiff. I've found these sorts of things to overall increase maintenance
+burden (see ripgrep and regex for cases where I did this).
+
+I overall decided that the least bad choice was to copy a little code (under
+2,000 source lines of code at present I believe). Since the copy is managed
+automatically via `jiff-cli generate shared`, we remove the downside of the
+code getting out of sync. The only downside is extra compile time. Since I
+generally only expect `jiff-static` to be used in niche circumstances, I
+prefer this trade-off over the other choices.
+
+More context on how I arrived at this design can be found here:
+<https://github.com/BurntSushi/jiff/issues/256>
+
+# Particulars
+
+When this code is copied to `jiff-static`, the following transformations are
+done:
+
+* A header is added to indicate that the copied file is auto-generated.
+* All `#[cfg(feature = "alloc")]` annotations are removed. The `jiff-static`
+  proc macro always runs in a context where the standard library is available.
+* Any code between `// only-jiff-start` and `// only-jiff-end` comments is
+  removed. Nesting isn't supported.
+
+Otherwise, this module is specifically organized in a way that doesn't rely on
+any other part of Jiff. The one exception are routines to convert from these
+exposed types to other internal types inside of Jiff. This is necessary for
+building a static `TimeZone`. But these conversion routines are removed when
+this module is copied to `jiff-static`.
+*/
+
+pub type TzifOwned = Tzif<
+    alloc::string::String,
+    alloc::vec::Vec<TzifLocalTimeType>,
+    alloc::vec::Vec<TzifTransition>,
+>;
+
+#[derive(Debug)]
+pub struct Tzif<STRING, TYPES, TRANS> {
+    pub fixed: TzifFixed<STRING>,
+    pub types: TYPES,
+    pub transitions: TRANS,
+}
+
+#[derive(Debug)]
+pub struct TzifFixed<STRING> {
+    pub name: Option<STRING>,
+    pub version: u8,
+    pub checksum: u32,
+    pub designations: STRING,
+    pub posix_tz: Option<PosixTimeZone<STRING>>,
+}
+
+#[derive(Debug)]
+pub struct TzifLocalTimeType {
+    pub offset: i32,
+    pub is_dst: bool,
+    pub designation: (u8, u8), // inclusive..exclusive
+    pub indicator: TzifIndicator,
+}
+
+#[derive(Debug)]
+pub enum TzifIndicator {
+    LocalWall,
+    LocalStandard,
+    UTStandard,
+}
+
+#[derive(Debug)]
+pub struct TzifTransition {
+    pub timestamp: i64,
+    pub type_index: u8,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixTimeZone<ABBREV> {
+    pub std_abbrev: ABBREV,
+    pub std_offset: i32,
+    pub dst: Option<PosixDst<ABBREV>>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixDst<ABBREV> {
+    pub abbrev: ABBREV,
+    pub offset: i32,
+    pub rule: Option<PosixRule>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixRule {
+    pub start: PosixDayTime,
+    pub end: PosixDayTime,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixDayTime {
+    pub date: PosixDay,
+    pub time: i32,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PosixDay {
+    /// Julian day in a year, no counting for leap days.
+    ///
+    /// Valid range is `1..=365`.
+    JulianOne(i16),
+    /// Julian day in a year, counting for leap days.
+    ///
+    /// Valid range is `0..=365`.
+    JulianZero(i16),
+    /// The nth weekday of a month.
+    WeekdayOfMonth {
+        /// The month.
+        ///
+        /// Valid range is: `1..=12`.
+        month: i8,
+        /// The week.
+        ///
+        /// Valid range is `1..=5`.
+        ///
+        /// One interesting thing to note here (or my interpretation anyway),
+        /// is that a week of `4` means the "4th weekday in a month" where as
+        /// a week of `5` means the "last weekday in a month, even if it's the
+        /// 4th weekday."
+        week: i8,
+        /// The weekday.
+        ///
+        /// Valid range is `0..=6`, with `0` corresponding to Sunday.
+        weekday: i8,
+    },
+}
+
+// Does not require `alloc`, but is only used when `alloc` is enabled.
+pub(crate) mod crc32;
+pub(crate) mod posix;
+pub(crate) mod tzif;
+pub(crate) mod util;
diff --git a/crates/jiff-static/shared/posix.rs b/crates/jiff-static/shared/posix.rs
new file mode 100644
index 00000000..bf9978b4
--- /dev/null
+++ b/crates/jiff-static/shared/posix.rs
@@ -0,0 +1,1930 @@
+// auto-generated by: jiff-cli generate shared
+
+use alloc::string::String;
+
+use super::{
+    util::{Byte, Bytes},
+    PosixDay, PosixDayTime, PosixDst, PosixRule, PosixTimeZone,
+};
+
+macro_rules! err {
+    ($($tt:tt)*) => {{
+        self::Error(alloc::format!($($tt)*))
+    }}
+}
+
+/// An error that can be returned when parsing.
+#[derive(Debug)]
+pub struct Error(String);
+
+impl core::fmt::Display for Error {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        core::fmt::Display::fmt(&self.0, f)
+    }
+}
+
+impl PosixTimeZone<String> {
+    /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not
+    /// an implementation defined value, from the given bytes.
+    pub fn parse(bytes: &[u8]) -> Result<PosixTimeZone<String>, Error> {
+        // We enable the IANA v3+ extensions here. (Namely, that the time
+        // specification hour value has the range `-167..=167` instead of
+        // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary
+        // since the extension is a strict superset. Plus, GNU tooling
+        // seems to accept the extension.
+        let parser =
+            Parser { ianav3plus: true, ..Parser::new(bytes.as_ref()) };
+        parser.parse()
+    }
+}
+
+/// A parser for POSIX time zones.
+#[derive(Debug)]
+struct Parser<'s> {
+    /// The `TZ` string that we're parsing.
+    tz: &'s [u8],
+    /// The parser's current position in `tz`.
+    pos: core::cell::Cell<usize>,
+    /// Whether to use IANA rules, i.e., when parsing a TZ string in a TZif
+    /// file of version 3 or greater. From `tzfile(5)`:
+    ///
+    /// > First, the hours part of its transition times may be signed and range
+    /// > from `-167` through `167` instead of the POSIX-required unsigned
+    /// > values from `0` through `24`. Second, DST is in effect all year if
+    /// > it starts January 1 at 00:00 and ends December 31 at 24:00 plus the
+    /// > difference between daylight saving and standard time.
+    ///
+    /// At time of writing, I don't think I understand the significance of
+    /// the second part above. (RFC 8536 elaborates that it is meant to be an
+    /// explicit clarification of something that POSIX itself implies.) But the
+    /// first part is clear: it permits the hours to be a bigger range.
+    ianav3plus: bool,
+}
+
+impl<'s> Parser<'s> {
+    /// Create a new parser for extracting a POSIX time zone from the given
+    /// bytes.
+    fn new<B: ?Sized + AsRef<[u8]>>(tz: &'s B) -> Parser<'s> {
+        Parser {
+            tz: tz.as_ref(),
+            pos: core::cell::Cell::new(0),
+            ianav3plus: false,
+        }
+    }
+
+    /// Parses a POSIX time zone from the current position of the parser and
+    /// ensures that the entire TZ string corresponds to a single valid POSIX
+    /// time zone.
+    fn parse(&self) -> Result<PosixTimeZone<String>, Error> {
+        let (time_zone, remaining) = self.parse_prefix()?;
+        if !remaining.is_empty() {
+            return Err(err!(
+                "expected entire TZ string to be a valid POSIX \
+                 time zone, but found `{}` after what would otherwise \
+                 be a valid POSIX TZ string",
+                Bytes(remaining),
+            ));
+        }
+        Ok(time_zone)
+    }
+
+    /// Parses a POSIX time zone from the current position of the parser and
+    /// returns the remaining input.
+    fn parse_prefix(
+        &self,
+    ) -> Result<(PosixTimeZone<String>, &'s [u8]), Error> {
+        let time_zone = self.parse_posix_time_zone()?;
+        Ok((time_zone, self.remaining()))
+    }
+
+    /// Parse a POSIX time zone from the current position of the parser.
+    ///
+    /// Upon success, the parser will be positioned immediately following the
+    /// TZ string.
+    fn parse_posix_time_zone(&self) -> Result<PosixTimeZone<String>, Error> {
+        let std_abbrev = self
+            .parse_abbreviation()
+            .map_err(|e| err!("failed to parse standard abbreviation: {e}"))?;
+        let std_offset = self
+            .parse_posix_offset()
+            .map_err(|e| err!("failed to parse standard offset: {e}"))?;
+        let mut dst = None;
+        if !self.is_done()
+            && (self.byte().is_ascii_alphabetic() || self.byte() == b'<')
+        {
+            dst = Some(self.parse_posix_dst(std_offset)?);
+        }
+        Ok(PosixTimeZone { std_abbrev, std_offset, dst })
+    }
+
+    /// Parse a DST zone with an optional explicit transition rule.
+    ///
+    /// This assumes the parser is positioned at the first byte of the DST
+    /// abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the end
+    /// of the DST transition rule (which might just be the abbreviation, but
+    /// might also include explicit start/end datetime specifications).
+    fn parse_posix_dst(
+        &self,
+        std_offset: i32,
+    ) -> Result<PosixDst<String>, Error> {
+        let abbrev = self
+            .parse_abbreviation()
+            .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?;
+        // This is the default: one hour ahead of standard time. We may
+        // override this if the DST portion specifies an offset. (But it
+        // usually doesn't.)
+        let offset = std_offset + 3600;
+        let mut dst = PosixDst { abbrev, offset, rule: None };
+        if self.is_done() {
+            return Ok(dst);
+        }
+        if self.byte() != b',' {
+            dst.offset = self
+                .parse_posix_offset()
+                .map_err(|e| err!("failed to parse DST offset: {e}"))?;
+            if self.is_done() {
+                return Ok(dst);
+            }
+        }
+        if self.byte() != b',' {
+            return Err(err!(
+                "after parsing DST offset in POSIX time zone string, \
+                 found `{}` but expected a ','",
+                Byte(self.byte()),
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "after parsing DST offset in POSIX time zone string, \
+                 found end of string after a trailing ','",
+            ));
+        }
+        dst.rule = Some(self.parse_rule()?);
+        Ok(dst)
+    }
+
+    /// Parse a time zone abbreviation.
+    ///
+    /// This assumes the parser is positioned at the first byte of
+    /// the abbreviation. This is either the first character in the
+    /// abbreviation, or the opening quote of a quoted abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately following
+    /// the abbreviation name.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_abbreviation(&self) -> Result<String, Error> {
+        if self.byte() == b'<' {
+            if !self.bump() {
+                return Err(err!(
+                    "found opening '<' quote for abbreviation in \
+                         POSIX time zone string, and expected a name \
+                         following it, but found the end of string instead"
+                ));
+            }
+            self.parse_quoted_abbreviation()
+        } else {
+            self.parse_unquoted_abbreviation()
+        }
+    }
+
+    /// Parses an unquoted time zone abbreviation.
+    ///
+    /// This assumes the parser is position at the first byte in the
+    /// abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// last byte in the abbreviation.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_unquoted_abbreviation(&self) -> Result<String, Error> {
+        const MAX_LEN: usize = 30;
+
+        let start = self.pos();
+        for i in 0.. {
+            if !self.byte().is_ascii_alphabetic() {
+                break;
+            }
+            if i >= MAX_LEN {
+                return Err(err!(
+                    "expected abbreviation with at most {MAX_LEN} bytes, \
+                         but found a longer abbreviation beginning with `{}`",
+                    Bytes(&self.tz[start..i]),
+                ));
+            }
+            if !self.bump() {
+                break;
+            }
+        }
+        let end = self.pos();
+        let abbrev =
+            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
+                // NOTE: I believe this error is technically impossible
+                // since the loop above restricts letters in an
+                // abbreviation to ASCII. So everything from `start` to
+                // `end` is ASCII and thus should be UTF-8. But it doesn't
+                // cost us anything to report an error here in case the
+                // code above evolves somehow.
+                err!(
+                    "found abbreviation `{}`, but it is not valid UTF-8",
+                    Bytes(&self.tz[start..end]),
+                )
+            })?;
+        if abbrev.len() < 3 {
+            return Err(err!(
+                "expected abbreviation with 3 or more bytes, but found \
+                     abbreviation {:?} with {} bytes",
+                abbrev,
+                abbrev.len(),
+            ));
+        }
+        Ok(String::from(abbrev))
+    }
+
+    /// Parses a quoted time zone abbreviation.
+    ///
+    /// This assumes the parser is positioned immediately after the opening
+    /// `<` quote. That is, at the first byte in the abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// closing `>` quote.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_quoted_abbreviation(&self) -> Result<String, Error> {
+        const MAX_LEN: usize = 30;
+
+        let start = self.pos();
+        for i in 0.. {
+            if !self.byte().is_ascii_alphanumeric()
+                && self.byte() != b'+'
+                && self.byte() != b'-'
+            {
+                break;
+            }
+            if i >= MAX_LEN {
+                return Err(err!(
+                    "expected abbreviation with at most {MAX_LEN} bytes, \
+                         but found a longer abbreviation beginning with `{}`",
+                    Bytes(&self.tz[start..i]),
+                ));
+            }
+            if !self.bump() {
+                break;
+            }
+        }
+        let end = self.pos();
+        let abbrev =
+            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
+                // NOTE: I believe this error is technically impossible
+                // since the loop above restricts letters in an
+                // abbreviation to ASCII. So everything from `start` to
+                // `end` is ASCII and thus should be UTF-8. But it doesn't
+                // cost us anything to report an error here in case the
+                // code above evolves somehow.
+                err!(
+                    "found abbreviation `{}`, but it is not valid UTF-8",
+                    Bytes(&self.tz[start..end]),
+                )
+            })?;
+        if self.is_done() {
+            return Err(err!(
+                "found non-empty quoted abbreviation {abbrev:?}, but \
+                     did not find expected end-of-quoted abbreviation \
+                     '>' character",
+            ));
+        }
+        if self.byte() != b'>' {
+            return Err(err!(
+                "found non-empty quoted abbreviation {abbrev:?}, but \
+                     found `{}` instead of end-of-quoted abbreviation '>' \
+                     character",
+                Byte(self.byte()),
+            ));
+        }
+        self.bump();
+        if abbrev.len() < 3 {
+            return Err(err!(
+                "expected abbreviation with 3 or more bytes, but found \
+                     abbreviation {abbrev:?} with {} bytes",
+                abbrev.len(),
+            ));
+        }
+        Ok(String::from(abbrev))
+    }
+
+    /// Parse a POSIX time offset.
+    ///
+    /// This assumes the parser is positioned at the first byte of the
+    /// offset. This can either be a digit (for a positive offset) or the
+    /// sign of the offset (which must be either `-` or `+`).
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// end of the offset.
+    fn parse_posix_offset(&self) -> Result<i32, Error> {
+        let sign = self
+            .parse_optional_sign()
+            .map_err(|e| {
+                err!(
+                    "failed to parse sign for time offset \
+                     in POSIX time zone string: {e}",
+                )
+            })?
+            .unwrap_or(1);
+        let hour = self.parse_hour_posix()?;
+        let (mut minute, mut second) = (0, 0);
+        if self.maybe_byte() == Some(b':') {
+            if !self.bump() {
+                return Err(err!(
+                    "incomplete time in POSIX timezone (missing minutes)",
+                ));
+            }
+            minute = self.parse_minute()?;
+            if self.maybe_byte() == Some(b':') {
+                if !self.bump() {
+                    return Err(err!(
+                        "incomplete time in POSIX timezone (missing seconds)",
+                    ));
+                }
+                second = self.parse_second()?;
+            }
+        }
+        let mut seconds = i32::from(hour) * 3600;
+        seconds += i32::from(minute) * 60;
+        seconds += i32::from(second);
+        // Yes, we flip the sign, because POSIX is backwards.
+        // For example, `EST5` corresponds to `-05:00`.
+        seconds *= i32::from(-sign);
+        // Must be true because the parsing routines for hours, minutes
+        // and seconds enforce they are in the ranges -24..=24, 0..=59
+        // and 0..=59, respectively.
+        assert!(
+            -89999 <= seconds && seconds <= 89999,
+            "offset seconds {seconds} is out of range"
+        );
+        Ok(seconds)
+    }
+
+    /// Parses a POSIX DST transition rule.
+    ///
+    /// This assumes the parser is positioned at the first byte in the
+    /// rule. That is, it comes immediately after the DST abbreviation or
+    /// its optional offset.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// DST transition rule. In typical cases, this corresponds to the end
+    /// of the TZ string.
+    fn parse_rule(&self) -> Result<PosixRule, Error> {
+        let start = self.parse_posix_datetime_spec().map_err(|e| {
+            err!("failed to parse start of DST transition rule: {e}")
+        })?;
+        if self.maybe_byte() != Some(b',') || !self.bump() {
+            return Err(err!(
+                "expected end of DST rule after parsing the start \
+                 of the DST rule"
+            ));
+        }
+        let end = self.parse_posix_datetime_spec().map_err(|e| {
+            err!("failed to parse end of DST transition rule: {e}")
+        })?;
+        Ok(PosixRule { start, end })
+    }
+
+    /// Parses a POSIX datetime specification.
+    ///
+    /// This assumes the parser is position at the first byte where a
+    /// datetime specification is expected to occur.
+    ///
+    /// Upon success, the parser will be positioned after the datetime
+    /// specification. This will either be immediately after the date, or
+    /// if it's present, the time part of the specification.
+    fn parse_posix_datetime_spec(&self) -> Result<PosixDayTime, Error> {
+        let mut daytime = PosixDayTime {
+            date: self.parse_posix_date_spec()?,
+            time: 2 * 3600, // the default if the time is absent
+        };
+        if self.maybe_byte() != Some(b'/') {
+            return Ok(daytime);
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected time specification after '/' following a date
+                 specification in a POSIX time zone DST transition rule",
+            ));
+        }
+        daytime.time = self.parse_posix_time_spec()?;
+        Ok(daytime)
+    }
+
+    /// Parses a POSIX date specification.
+    ///
+    /// This assumes the parser is positioned at the first byte of the date
+    /// specification. This can be `J` (for one based Julian day without
+    /// leap days), `M` (for "weekday of month") or a digit starting the
+    /// zero based Julian day with leap days. This routine will validate
+    /// that the position points to one of these possible values. That is,
+    /// the caller doesn't need to parse the `M` or the `J` or the leading
+    /// digit. The caller should just call this routine when it *expect* a
+    /// date specification to follow.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// date specification.
+    fn parse_posix_date_spec(&self) -> Result<PosixDay, Error> {
+        match self.byte() {
+            b'J' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected one-based Julian day after 'J' in date \
+                         specification of a POSIX time zone DST \
+                         transition rule, but got the end of the string \
+                         instead"
+                    ));
+                }
+                Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?))
+            }
+            b'0'..=b'9' => Ok(PosixDay::JulianZero(
+                self.parse_posix_julian_day_with_leap()?,
+            )),
+            b'M' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected month-week-weekday after 'M' in date \
+                         specification of a POSIX time zone DST \
+                         transition rule, but got the end of the string \
+                         instead"
+                    ));
+                }
+                let (month, week, weekday) = self.parse_weekday_of_month()?;
+                Ok(PosixDay::WeekdayOfMonth { month, week, weekday })
+            }
+            _ => Err(err!(
+                "expected 'J', a digit or 'M' at the beginning of a date \
+                 specification of a POSIX time zone DST transition rule, \
+                 but got `{}` instead",
+                Byte(self.byte()),
+            )),
+        }
+    }
+
+    /// Parses a POSIX Julian day that does not include leap days
+    /// (`1 <= n <= 365`).
+    ///
+    /// This assumes the parser is positioned just after the `J` and at the
+    /// first digit of the Julian day. Upon success, the parser will be
+    /// positioned immediately following the day number.
+    fn parse_posix_julian_day_no_leap(&self) -> Result<i16, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid one based Julian day: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "one based Julian day `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 365) {
+            return Err(err!(
+                "parsed one based Julian day `{number}`, \
+                 but one based Julian day in POSIX time zone \
+                 must be in range 1..=365",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a POSIX Julian day that includes leap days (`0 <= n <=
+    /// 365`).
+    ///
+    /// This assumes the parser is positioned at the first digit of the
+    /// Julian day. Upon success, the parser will be positioned immediately
+    /// following the day number.
+    fn parse_posix_julian_day_with_leap(&self) -> Result<i16, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid zero based Julian day: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "zero based Julian day `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 365) {
+            return Err(err!(
+                "parsed zero based Julian day `{number}`, \
+                 but zero based Julian day in POSIX time zone \
+                 must be in range 0..=365",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a POSIX "weekday of month" specification.
+    ///
+    /// This assumes the parser is positioned just after the `M` byte and
+    /// at the first digit of the month. Upon success, the parser will be
+    /// positioned immediately following the "weekday of the month" that
+    /// was parsed.
+    ///
+    /// The tuple returned is month (1..=12), week (1..=5) and weekday
+    /// (0..=6 with 0=Sunday).
+    fn parse_weekday_of_month(&self) -> Result<(i8, i8, i8), Error> {
+        let month = self.parse_month()?;
+        if self.maybe_byte() != Some(b'.') {
+            return Err(err!(
+                "expected '.' after month `{month}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected week after month `{month}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        let week = self.parse_week()?;
+        if self.maybe_byte() != Some(b'.') {
+            return Err(err!(
+                "expected '.' after week `{week}` in POSIX time zone rule"
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected day-of-week after week `{week}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        let weekday = self.parse_weekday()?;
+        Ok((month, week, weekday))
+    }
+
+    /// This parses a POSIX time specification in the format
+    /// `[+/-]hh?[:mm[:ss]]`.
+    ///
+    /// This assumes the parser is positioned at the first `h` (or the
+    /// sign, if present). Upon success, the parser will be positioned
+    /// immediately following the end of the time specification.
+    fn parse_posix_time_spec(&self) -> Result<i32, Error> {
+        let (sign, hour) = if self.ianav3plus {
+            let sign = self
+                .parse_optional_sign()
+                .map_err(|e| {
+                    err!(
+                        "failed to parse sign for transition time \
+                         in POSIX time zone string: {e}",
+                    )
+                })?
+                .unwrap_or(1);
+            let hour = self.parse_hour_ianav3plus()?;
+            (sign, hour)
+        } else {
+            (1, i16::from(self.parse_hour_posix()?))
+        };
+        let (mut minute, mut second) = (0, 0);
+        if self.maybe_byte() == Some(b':') {
+            if !self.bump() {
+                return Err(err!(
+                    "incomplete transition time in \
+                     POSIX time zone string (missing minutes)",
+                ));
+            }
+            minute = self.parse_minute()?;
+            if self.maybe_byte() == Some(b':') {
+                if !self.bump() {
+                    return Err(err!(
+                        "incomplete transition time in \
+                         POSIX time zone string (missing seconds)",
+                    ));
+                }
+                second = self.parse_second()?;
+            }
+        }
+        let mut seconds = i32::from(hour) * 3600;
+        seconds += i32::from(minute) * 60;
+        seconds += i32::from(second);
+        seconds *= i32::from(sign);
+        // Must be true because the parsing routines for hours, minutes
+        // and seconds enforce they are in the ranges -167..=167, 0..=59
+        // and 0..=59, respectively.
+        assert!(-604799 <= seconds && seconds <= 604799);
+        Ok(seconds)
+    }
+
+    /// Parses a month.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the month (which may contain
+    /// two digits).
+    fn parse_month(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_upto_n_digits(2)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "month `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 12) {
+            return Err(err!(
+                "parsed month `{number}`, but month in \
+                 POSIX time zone must be in range 1..=12",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a week-of-month number.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the week digit.
+    fn parse_week(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_exactly_n_digits(1)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "week `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 5) {
+            return Err(err!(
+                "parsed week `{number}`, but week in \
+                 POSIX time zone must be in range 1..=5"
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a weekday number.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the week digit.
+    ///
+    /// The weekday returned is guaranteed to be in the range `0..=6`, with
+    /// `0` corresponding to Sunday.
+    fn parse_weekday(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_exactly_n_digits(1)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "weekday `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 6) {
+            return Err(err!(
+                "parsed weekday `{number}`, but weekday in \
+                 POSIX time zone must be in range `0..=6` \
+                 (with `0` corresponding to Sunday)",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses an hour from a POSIX time specification with the IANA
+    /// v3+ extension. That is, the hour may be in the range `0..=167`.
+    /// (Callers should parse an optional sign preceding the hour digits
+    /// when IANA V3+ parsing is enabled.)
+    ///
+    /// The hour is allowed to be a single digit (unlike minutes or
+    /// seconds).
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first hour digit should occur. Upon success, the parser will be
+    /// positioned immediately after the last hour digit.
+    fn parse_hour_ianav3plus(&self) -> Result<i16, Error> {
+        // Callers should only be using this method when IANA v3+ parsing
+        // is enabled.
+        assert!(self.ianav3plus);
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid hour digits: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "hour `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 167) {
+            // The error message says -167 but the check above uses 0.
+            // This is because the caller is responsible for parsing
+            // the sign.
+            return Err(err!(
+                "parsed hour `{number}`, but hour in IANA v3+ \
+                 POSIX time zone must be in range `-167..=167`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses an hour from a POSIX time specification, with the allowed
+    /// range being `0..=24`.
+    ///
+    /// The hour is allowed to be a single digit (unlike minutes or
+    /// seconds).
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first hour digit should occur. Upon success, the parser will be
+    /// positioned immediately after the last hour digit.
+    fn parse_hour_posix(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(2)
+            .map_err(|e| err!("invalid hour digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "hour `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 24) {
+            return Err(err!(
+                "parsed hour `{number}`, but hour in \
+                 POSIX time zone must be in range `0..=24`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a minute from a POSIX time specification.
+    ///
+    /// The minute must be exactly two digits.
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first minute digit should occur. Upon success, the parser will be
+    /// positioned immediately after the second minute digit.
+    fn parse_minute(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_exactly_n_digits(2)
+            .map_err(|e| err!("invalid minute digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "minute `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 59) {
+            return Err(err!(
+                "parsed minute `{number}`, but minute in \
+                 POSIX time zone must be in range `0..=59`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a second from a POSIX time specification.
+    ///
+    /// The second must be exactly two digits.
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first second digit should occur. Upon success, the parser will be
+    /// positioned immediately after the second second digit.
+    fn parse_second(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_exactly_n_digits(2)
+            .map_err(|e| err!("invalid second digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "second `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 59) {
+            return Err(err!(
+                "parsed second `{number}`, but second in \
+                 POSIX time zone must be in range `0..=59`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a signed 64-bit integer expressed in exactly `n` digits.
+    ///
+    /// If `n` digits could not be found (or if the `TZ` string ends before
+    /// `n` digits could be found), then this returns an error.
+    ///
+    /// This assumes that `n >= 1` and that the parser is positioned at the
+    /// first digit. Upon success, the parser is positioned immediately
+    /// after the `n`th digit.
+    fn parse_number_with_exactly_n_digits(
+        &self,
+        n: usize,
+    ) -> Result<i32, Error> {
+        assert!(n >= 1, "numbers must have at least 1 digit");
+        let start = self.pos();
+        let mut number: i32 = 0;
+        for i in 0..n {
+            if self.is_done() {
+                return Err(err!("expected {n} digits, but found {i}"));
+            }
+            let byte = self.byte();
+            let digit = match byte.checked_sub(b'0') {
+                None => {
+                    return Err(err!(
+                        "invalid digit, expected 0-9 but got {}",
+                        Byte(byte),
+                    ));
+                }
+                Some(digit) if digit > 9 => {
+                    return Err(err!(
+                        "invalid digit, expected 0-9 but got {}",
+                        Byte(byte),
+                    ))
+                }
+                Some(digit) => {
+                    debug_assert!((0..=9).contains(&digit));
+                    i32::from(digit)
+                }
+            };
+            number = number
+                .checked_mul(10)
+                .and_then(|n| n.checked_add(digit))
+                .ok_or_else(|| {
+                    err!(
+                        "number `{}` too big to parse into 64-bit integer",
+                        Bytes(&self.tz[start..i]),
+                    )
+                })?;
+            self.bump();
+        }
+        Ok(number)
+    }
+
+    /// Parses a signed 64-bit integer expressed with up to `n` digits and
+    /// at least 1 digit.
+    ///
+    /// This assumes that `n >= 1` and that the parser is positioned at the
+    /// first digit. Upon success, the parser is position immediately after
+    /// the last digit (which can be at most `n`).
+    fn parse_number_with_upto_n_digits(&self, n: usize) -> Result<i32, Error> {
+        assert!(n >= 1, "numbers must have at least 1 digit");
+        let start = self.pos();
+        let mut number: i32 = 0;
+        for i in 0..n {
+            if self.is_done() || !self.byte().is_ascii_digit() {
+                if i == 0 {
+                    return Err(err!("invalid number, no digits found"));
+                }
+                break;
+            }
+            let digit = i32::from(self.byte() - b'0');
+            number = number
+                .checked_mul(10)
+                .and_then(|n| n.checked_add(digit))
+                .ok_or_else(|| {
+                    err!(
+                        "number `{}` too big to parse into 64-bit integer",
+                        Bytes(&self.tz[start..i]),
+                    )
+                })?;
+            self.bump();
+        }
+        Ok(number)
+    }
+
+    /// Parses an optional sign.
+    ///
+    /// This assumes the parser is positioned at the position where a
+    /// positive or negative sign is permitted. If one exists, then it
+    /// is consumed and returned. Moreover, if one exists, then this
+    /// guarantees that it is not the last byte in the input. That is, upon
+    /// success, it is valid to call `self.byte()`.
+    fn parse_optional_sign(&self) -> Result<Option<i8>, Error> {
+        if self.is_done() {
+            return Ok(None);
+        }
+        Ok(match self.byte() {
+            b'-' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected digit after '-' sign, \
+                         but got end of input",
+                    ));
+                }
+                Some(-1)
+            }
+            b'+' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected digit after '+' sign, \
+                         but got end of input",
+                    ));
+                }
+                Some(1)
+            }
+            _ => None,
+        })
+    }
+}
+
+/// Helper routines for parsing a POSIX `TZ` string.
+impl<'s> Parser<'s> {
+    /// Bump the parser to the next byte.
+    ///
+    /// If the end of the input has been reached, then `false` is returned.
+    fn bump(&self) -> bool {
+        if self.is_done() {
+            return false;
+        }
+        self.pos.set(
+            self.pos().checked_add(1).expect("pos cannot overflow usize"),
+        );
+        !self.is_done()
+    }
+
+    /// Returns true if the next call to `bump` would return false.
+    fn is_done(&self) -> bool {
+        self.pos() == self.tz.len()
+    }
+
+    /// Return the byte at the current position of the parser.
+    ///
+    /// This panics if the parser is positioned at the end of the TZ
+    /// string.
+    fn byte(&self) -> u8 {
+        self.tz[self.pos()]
+    }
+
+    /// Return the byte at the current position of the parser. If the TZ
+    /// string has been exhausted, then this returns `None`.
+    fn maybe_byte(&self) -> Option<u8> {
+        self.tz.get(self.pos()).copied()
+    }
+
+    /// Return the current byte offset of the parser.
+    ///
+    /// The offset starts at `0` from the beginning of the TZ string.
+    fn pos(&self) -> usize {
+        self.pos.get()
+    }
+
+    /// Returns the remaining bytes of the TZ string.
+    ///
+    /// This includes `self.byte()`. It may be empty.
+    fn remaining(&self) -> &'s [u8] {
+        &self.tz[self.pos()..]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn parser(s: &str) -> Parser<'_> {
+        Parser::new(s.as_bytes())
+    }
+
+    #[test]
+    fn parse() {
+        let p = parser("NZST-12NZDT,J60,J300");
+        assert_eq!(
+            p.parse().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
+        assert!(p.parse().is_err());
+    }
+
+    #[test]
+    fn parse_posix_time_zone() {
+        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 9,
+                                week: 5,
+                                weekday: 0,
+                            },
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 4,
+                                week: 1,
+                                weekday: 0,
+                            },
+                            time: 3 * 60 * 60,
+                        },
+                    })
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 9,
+                                week: 5,
+                                weekday: 0,
+                            },
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 4,
+                                week: 1,
+                                weekday: 0,
+                            },
+                            time: 3 * 60 * 60,
+                        },
+                    })
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+    }
+
+    #[test]
+    fn parse_posix_dst() {
+        let p = Parser::new("NZDT,M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 13 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::WeekdayOfMonth {
+                            month: 9,
+                            week: 5,
+                            weekday: 0,
+                        },
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::WeekdayOfMonth {
+                            month: 4,
+                            week: 1,
+                            weekday: 0,
+                        },
+                        time: 3 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 13 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT-7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT+7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: -7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: -7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT7,");
+        assert!(p.parse_posix_dst(12 * 60 * 60).is_err());
+
+        let p = Parser::new("NZDT7!");
+        assert!(p.parse_posix_dst(12 * 60 * 60).is_err());
+    }
+
+    #[test]
+    fn parse_abbreviation() {
+        let p = Parser::new("ABC");
+        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("<ABC>");
+        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("<+09>");
+        assert_eq!(p.parse_abbreviation().unwrap(), "+09");
+
+        let p = Parser::new("+09");
+        assert!(p.parse_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_unquoted_abbreviation() {
+        let p = Parser::new("ABC");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABCXYZ");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABCXYZ");
+
+        let p = Parser::new("ABC123");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
+
+        let tz = "a".repeat(30);
+        let p = Parser::new(&tz);
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz);
+
+        let p = Parser::new("a");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new("ab");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new("ab1");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let tz = "a".repeat(31);
+        let p = Parser::new(&tz);
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new(b"ab\xFFcd");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_quoted_abbreviation() {
+        // The inputs look a little funny here, but that's because
+        // 'parse_quoted_abbreviation' starts after the opening quote
+        // has been parsed.
+
+        let p = Parser::new("ABC>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABCXYZ>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABCXYZ");
+
+        let p = Parser::new("ABC>123");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABC123>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC123");
+
+        let p = Parser::new("ab1>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ab1");
+
+        let p = Parser::new("+09>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "+09");
+
+        let p = Parser::new("-09>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "-09");
+
+        let tz = alloc::format!("{}>", "a".repeat(30));
+        let p = Parser::new(&tz);
+        assert_eq!(
+            p.parse_quoted_abbreviation().unwrap(),
+            tz.trim_end_matches(">")
+        );
+
+        let p = Parser::new("a>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ab>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let tz = alloc::format!("{}>", "a".repeat(31));
+        let p = Parser::new(&tz);
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new(b"ab\xFFcd>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ABC");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ABC!>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_posix_offset() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_posix_offset().unwrap(), -5 * 60 * 60);
+
+        let p = Parser::new("+5");
+        assert_eq!(p.parse_posix_offset().unwrap(), -5 * 60 * 60);
+
+        let p = Parser::new("-5");
+        assert_eq!(p.parse_posix_offset().unwrap(), 5 * 60 * 60);
+
+        let p = Parser::new("-12:34:56");
+        assert_eq!(
+            p.parse_posix_offset().unwrap(),
+            12 * 60 * 60 + 34 * 60 + 56,
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-25");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+25");
+        assert!(p.parse_posix_offset().is_err());
+
+        // This checks that we don't accidentally permit IANA rules for
+        // offset parsing. Namely, the IANA tzfile v3+ extension only applies
+        // to transition times. But since POSIX says that the "time" for the
+        // offset and transition is the same format, it would be an easy
+        // implementation mistake to implement the more flexible rule for
+        // IANA and have it accidentally also apply to the offset. So we check
+        // that it doesn't here.
+        let p = Parser { ianav3plus: true, ..Parser::new("25") };
+        assert!(p.parse_posix_offset().is_err());
+        let p = Parser { ianav3plus: true, ..Parser::new("+25") };
+        assert!(p.parse_posix_offset().is_err());
+        let p = Parser { ianav3plus: true, ..Parser::new("-25") };
+        assert!(p.parse_posix_offset().is_err());
+    }
+
+    #[test]
+    fn parse_rule() {
+        let p = Parser::new("M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_rule().unwrap(),
+            PosixRule {
+                start: PosixDayTime {
+                    date: PosixDay::WeekdayOfMonth {
+                        month: 9,
+                        week: 5,
+                        weekday: 0,
+                    },
+                    time: 2 * 60 * 60,
+                },
+                end: PosixDayTime {
+                    date: PosixDay::WeekdayOfMonth {
+                        month: 4,
+                        week: 1,
+                        weekday: 0,
+                    },
+                    time: 3 * 60 * 60,
+                },
+            },
+        );
+
+        let p = Parser::new("M9.5.0");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new(",M9.5.0,M4.1.0/3");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new("M9.5.0/");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new("M9.5.0,M4.1.0/");
+        assert!(p.parse_rule().is_err());
+    }
+
+    #[test]
+    fn parse_posix_datetime_spec() {
+        let p = Parser::new("J1");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime { date: PosixDay::JulianOne(1), time: 2 * 60 * 60 },
+        );
+
+        let p = Parser::new("J1/3");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime { date: PosixDay::JulianOne(1), time: 3 * 60 * 60 },
+        );
+
+        let p = Parser::new("M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime {
+                date: PosixDay::WeekdayOfMonth {
+                    month: 4,
+                    week: 1,
+                    weekday: 0,
+                },
+                time: 3 * 60 * 60,
+            },
+        );
+
+        let p = Parser::new("1/3:45:05");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime {
+                date: PosixDay::JulianZero(1),
+                time: 3 * 60 * 60 + 45 * 60 + 5,
+            },
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("J1/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("1/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("M4.1.0/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+    }
+
+    #[test]
+    fn parse_posix_date_spec() {
+        let p = Parser::new("J1");
+        assert_eq!(p.parse_posix_date_spec().unwrap(), PosixDay::JulianOne(1));
+        let p = Parser::new("J365");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianOne(365)
+        );
+
+        let p = Parser::new("0");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(0)
+        );
+        let p = Parser::new("1");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(1)
+        );
+        let p = Parser::new("365");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(365)
+        );
+
+        let p = Parser::new("M9.5.0");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 0 },
+        );
+        let p = Parser::new("M9.5.6");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 },
+        );
+        let p = Parser::new("M09.5.6");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 },
+        );
+        let p = Parser::new("M12.1.1");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 12, week: 1, weekday: 1 },
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("j");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("m");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("n");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("J366");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_date_spec().is_err());
+    }
+
+    #[test]
+    fn parse_posix_julian_day_no_leap() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
+
+        let p = Parser::new("001");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
+
+        let p = Parser::new("365");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
+
+        let p = Parser::new("3655");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
+
+        let p = Parser::new("0");
+        assert!(p.parse_posix_julian_day_no_leap().is_err());
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_julian_day_no_leap().is_err());
+    }
+
+    #[test]
+    fn parse_posix_julian_day_with_leap() {
+        let p = Parser::new("0");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 0);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
+
+        let p = Parser::new("001");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
+
+        let p = Parser::new("365");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
+
+        let p = Parser::new("3655");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_julian_day_with_leap().is_err());
+    }
+
+    #[test]
+    fn parse_weekday_of_month() {
+        let p = Parser::new("9.5.0");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 5, 0));
+
+        let p = Parser::new("9.1.6");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6));
+
+        let p = Parser::new("09.1.6");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6));
+
+        let p = Parser::new("9");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5.");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("0.5.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("13.5.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.0.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.6.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5.7");
+        assert!(p.parse_weekday_of_month().is_err());
+    }
+
+    #[test]
+    fn parse_posix_time_spec() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 5 * 60 * 60);
+
+        let p = Parser::new("22");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 22 * 60 * 60);
+
+        let p = Parser::new("02");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 2 * 60 * 60);
+
+        let p = Parser::new("5:45");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 5 * 60 * 60 + 45 * 60);
+
+        let p = Parser::new("5:45:12");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser::new("5:45:129");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser::new("5:45:12:");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            -(5 * 60 * 60 + 45 * 60 + 12)
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            -(167 * 60 * 60 + 45 * 60 + 12),
+        );
+
+        let p = Parser::new("25");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:2");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:23:5");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:23:");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("168") };
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-168") };
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("+168") };
+        assert!(p.parse_posix_time_spec().is_err());
+    }
+
+    #[test]
+    fn parse_month() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_month().unwrap(), 1);
+
+        // Should this be allowed? POSIX spec is unclear.
+        // We allow it because our parse does stop at 2
+        // digits, so this seems harmless. Namely, '001'
+        // results in an error.
+        let p = Parser::new("01");
+        assert_eq!(p.parse_month().unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_month().unwrap(), 12);
+
+        let p = Parser::new("0");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("00");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("001");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("13");
+        assert!(p.parse_month().is_err());
+    }
+
+    #[test]
+    fn parse_week() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_week().unwrap(), 1);
+
+        let p = Parser::new("5");
+        assert_eq!(p.parse_week().unwrap(), 5);
+
+        let p = Parser::new("55");
+        assert_eq!(p.parse_week().unwrap(), 5);
+
+        let p = Parser::new("0");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("6");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("00");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("01");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("05");
+        assert!(p.parse_week().is_err());
+    }
+
+    #[test]
+    fn parse_weekday() {
+        let p = Parser::new("0");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_weekday().unwrap(), 1);
+
+        let p = Parser::new("6");
+        assert_eq!(p.parse_weekday().unwrap(), 6);
+
+        let p = Parser::new("00");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("06");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("60");
+        assert_eq!(p.parse_weekday().unwrap(), 6);
+
+        let p = Parser::new("7");
+        assert!(p.parse_weekday().is_err());
+    }
+
+    #[test]
+    fn parse_hour_posix() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_hour_posix().unwrap(), 5);
+
+        let p = Parser::new("0");
+        assert_eq!(p.parse_hour_posix().unwrap(), 0);
+
+        let p = Parser::new("00");
+        assert_eq!(p.parse_hour_posix().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_hour_posix().unwrap(), 24);
+
+        let p = Parser::new("100");
+        assert_eq!(p.parse_hour_posix().unwrap(), 10);
+
+        let p = Parser::new("25");
+        assert!(p.parse_hour_posix().is_err());
+
+        let p = Parser::new("99");
+        assert!(p.parse_hour_posix().is_err());
+    }
+
+    #[test]
+    fn parse_hour_ianav3plus() {
+        let new = |input| Parser { ianav3plus: true, ..Parser::new(input) };
+
+        let p = new("5");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 5);
+
+        let p = new("0");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("00");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("000");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("24");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 24);
+
+        let p = new("100");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
+
+        let p = new("1000");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
+
+        let p = new("167");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 167);
+
+        let p = new("168");
+        assert!(p.parse_hour_ianav3plus().is_err());
+
+        let p = new("999");
+        assert!(p.parse_hour_ianav3plus().is_err());
+    }
+
+    #[test]
+    fn parse_minute() {
+        let p = Parser::new("00");
+        assert_eq!(p.parse_minute().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_minute().unwrap(), 24);
+
+        let p = Parser::new("59");
+        assert_eq!(p.parse_minute().unwrap(), 59);
+
+        let p = Parser::new("599");
+        assert_eq!(p.parse_minute().unwrap(), 59);
+
+        let p = Parser::new("0");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("9");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("60");
+        assert!(p.parse_minute().is_err());
+    }
+
+    #[test]
+    fn parse_second() {
+        let p = Parser::new("00");
+        assert_eq!(p.parse_second().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_second().unwrap(), 24);
+
+        let p = Parser::new("59");
+        assert_eq!(p.parse_second().unwrap(), 59);
+
+        let p = Parser::new("599");
+        assert_eq!(p.parse_second().unwrap(), 59);
+
+        let p = Parser::new("0");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("9");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("60");
+        assert!(p.parse_second().is_err());
+    }
+
+    #[test]
+    fn parse_number_with_exactly_n_digits() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_exactly_n_digits(1).unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("123");
+        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("");
+        assert!(p.parse_number_with_exactly_n_digits(1).is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_number_with_exactly_n_digits(2).is_err());
+
+        let p = Parser::new("12");
+        assert!(p.parse_number_with_exactly_n_digits(3).is_err());
+    }
+
+    #[test]
+    fn parse_number_with_upto_n_digits() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_upto_n_digits(1).unwrap(), 1);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_upto_n_digits(3).unwrap(), 12);
+
+        let p = Parser::new("123");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("");
+        assert!(p.parse_number_with_upto_n_digits(1).is_err());
+
+        let p = Parser::new("a");
+        assert!(p.parse_number_with_upto_n_digits(1).is_err());
+    }
+}
diff --git a/crates/jiff-static/shared/tzif.rs b/crates/jiff-static/shared/tzif.rs
new file mode 100644
index 00000000..b83c47b9
--- /dev/null
+++ b/crates/jiff-static/shared/tzif.rs
@@ -0,0 +1,778 @@
+// auto-generated by: jiff-cli generate shared
+
+#![allow(warnings)]
+
+use alloc::{string::String, vec};
+
+use super::{
+    util::{Byte, Bytes},
+    PosixTimeZone, TzifFixed, TzifIndicator, TzifLocalTimeType, TzifOwned,
+    TzifTransition,
+};
+
+macro_rules! err {
+    ($($tt:tt)*) => {{
+        self::Error(alloc::format!($($tt)*))
+    }}
+}
+
+// These are Jiff min and max timestamp (in seconds) values.
+//
+// The TZif parser will clamp timestamps to this range. It's
+// not ideal, but Jiff can't handle values outside of this range
+// and completely refusing to use TZif data with pathological
+// timestamps in typically irrelevant transitions is bad juju.
+//
+// Ref: https://github.com/BurntSushi/jiff/issues/163
+// Ref: https://github.com/BurntSushi/jiff/pull/164
+const TIMESTAMP_MIN: i64 = -377705023201;
+const TIMESTAMP_MAX: i64 = 253402207200;
+
+// Similarly for offsets, although in this case, if we find
+// an offset outside of this range, we do actually error. This
+// is because it could result in true incorrect datetimes for
+// actual transitions.
+//
+// But our supported offset range is `-25:59:59..=+25:59:59`.
+// There's no real time zone with offsets even close to those
+// boundaries.
+//
+// If there is pathological data that we should ignore, then
+// we should wait for a real bug report in order to determine
+// the right way to ignore/clamp it.
+const OFFSET_MIN: i32 = -93599;
+const OFFSET_MAX: i32 = 93599;
+
+/// An error that can be returned when parsing.
+#[derive(Debug)]
+pub struct Error(String);
+
+impl core::fmt::Display for Error {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        core::fmt::Display::fmt(&self.0, f)
+    }
+}
+
+impl TzifOwned {
+    /// Parses the given data as a TZif formatted file.
+    ///
+    /// The name given is attached to the `Tzif` value returned, but is
+    /// otherwise not significant.
+    ///
+    /// If the given data is not recognized to be valid TZif, then an error is
+    /// returned.
+    ///
+    /// In general, callers may assume that it is safe to pass arbitrary or
+    /// even untrusted data to this function and count on it not panicking
+    /// or using resources that aren't limited to a small constant factor of
+    /// the size of the data itself. That is, callers can reliably limit the
+    /// resources used by limiting the size of the data given to this parse
+    /// function.
+    pub(crate) fn parse(
+        name: Option<String>,
+        bytes: &[u8],
+    ) -> Result<TzifOwned, Error> {
+        let original = bytes;
+        let name = name.into();
+        let (header32, rest) = Header::parse(4, bytes)
+            .map_err(|e| err!("failed to parse 32-bit header: {e}"))?;
+        let (mut tzif, rest) = if header32.version == 0 {
+            TzifOwned::parse32(name, header32, rest)?
+        } else {
+            TzifOwned::parse64(name, header32, rest)?
+        };
+        // Compute the checksum using the entire contents of the TZif data.
+        let tzif_raw_len = (rest.as_ptr() as usize)
+            .checked_sub(original.as_ptr() as usize)
+            .unwrap();
+        let tzif_raw_bytes = &original[..tzif_raw_len];
+        tzif.fixed.checksum = super::crc32::sum(tzif_raw_bytes);
+        Ok(tzif)
+    }
+
+    fn parse32<'b>(
+        name: Option<String>,
+        header32: Header,
+        bytes: &'b [u8],
+    ) -> Result<(TzifOwned, &'b [u8]), Error> {
+        let mut tzif = TzifOwned {
+            fixed: TzifFixed {
+                name,
+                version: header32.version,
+                // filled in later
+                checksum: 0,
+                designations: String::new(),
+                posix_tz: None,
+            },
+            types: vec![],
+            transitions: vec![],
+        };
+        let rest = tzif.parse_transitions(&header32, bytes)?;
+        let rest = tzif.parse_transition_types(&header32, rest)?;
+        let rest = tzif.parse_local_time_types(&header32, rest)?;
+        let rest = tzif.parse_time_zone_designations(&header32, rest)?;
+        let rest = tzif.parse_leap_seconds(&header32, rest)?;
+        let rest = tzif.parse_indicators(&header32, rest)?;
+        Ok((tzif, rest))
+    }
+
+    fn parse64<'b>(
+        name: Option<String>,
+        header32: Header,
+        bytes: &'b [u8],
+    ) -> Result<(TzifOwned, &'b [u8]), Error> {
+        let (_, rest) = try_split_at(
+            "V1 TZif data block",
+            bytes,
+            header32.data_block_len()?,
+        )?;
+        let (header64, rest) = Header::parse(8, rest)
+            .map_err(|e| err!("failed to parse 64-bit header: {e}"))?;
+        let mut tzif = TzifOwned {
+            fixed: TzifFixed {
+                name,
+                version: header64.version,
+                // filled in later
+                checksum: 0,
+                designations: String::new(),
+                posix_tz: None,
+            },
+            types: vec![],
+            transitions: vec![],
+        };
+        let rest = tzif.parse_transitions(&header64, rest)?;
+        let rest = tzif.parse_transition_types(&header64, rest)?;
+        let rest = tzif.parse_local_time_types(&header64, rest)?;
+        let rest = tzif.parse_time_zone_designations(&header64, rest)?;
+        let rest = tzif.parse_leap_seconds(&header64, rest)?;
+        let rest = tzif.parse_indicators(&header64, rest)?;
+        let rest = tzif.parse_footer(&header64, rest)?;
+        // Note that we specifically and unfortunately do not "validate"
+        // the POSIX TZ string here. We *should* check that it is
+        // consistent with the last transition. Since:
+        //
+        // RFC 8536 says, "If the string is nonempty and one or more
+        // transitions appear in the version 2+ data, the string MUST be
+        // consistent with the last version 2+ transition."
+        //
+        // But in this context, we don't have any of the infrastructure
+        // to actually do TZ operations on a POSIX time zone. It requires
+        // civil datetimes and a bunch of other bullshit. This means that
+        // this verification step doesn't run when using the `jiff-tzdb-static`
+        // proc macro. However, we do still run it when parsing TZif data
+        // at runtime.
+        //
+        // We otherwise don't check that the TZif data is fully valid. It is
+        // possible for it to contain superfluous information. For example, a
+        // non-zero local time type that is never referenced by a transition.
+        Ok((tzif, rest))
+    }
+
+    fn parse_transitions<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "transition times data block",
+            bytes,
+            header.transition_times_len()?,
+        )?;
+        let mut it = bytes.chunks_exact(header.time_size);
+        // RFC 8536 says: "If there are no transitions, local time for all
+        // timestamps is specified by the TZ string in the footer if present
+        // and nonempty; otherwise, it is specified by time type 0."
+        //
+        // RFC 8536 also says: "Local time for timestamps before the first
+        // transition is specified by the first time type (time type
+        // 0)."
+        //
+        // So if there are no transitions, pushing this dummy one will result
+        // in the desired behavior even when it's the only transition.
+        // Similarly, since this is the minimum timestamp value, it will
+        // trigger for any times before the first transition found in the TZif
+        // data.
+        self.transitions
+            .push(TzifTransition { timestamp: TIMESTAMP_MIN, type_index: 0 });
+        while let Some(chunk) = it.next() {
+            let mut timestamp = if header.is_32bit() {
+                i64::from(from_be_bytes_i32(chunk))
+            } else {
+                from_be_bytes_i64(chunk)
+            };
+            if !(TIMESTAMP_MIN <= timestamp && timestamp <= TIMESTAMP_MAX) {
+                // We really shouldn't error here just because the Unix
+                // timestamp is outside what Jiff supports. Since what Jiff
+                // supports is _somewhat_ arbitrary. But Jiff's supported
+                // range is good enough for all realistic purposes, so we
+                // just clamp an out-of-range Unix timestamp to the Jiff
+                // min or max value.
+                //
+                // This can't result in the sorting order being wrong, but
+                // it can result in a transition that is duplicative with
+                // the dummy transition we inserted above. This should be
+                // fine.
+                let clamped = timestamp.clamp(TIMESTAMP_MIN, TIMESTAMP_MAX);
+                timestamp = clamped;
+            }
+            self.transitions.push(TzifTransition {
+                timestamp,
+                // We can't fill in the type index yet. We fill this in
+                // later when we parse the transition types.
+                type_index: 0,
+            });
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_transition_types<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "transition types data block",
+            bytes,
+            header.transition_types_len()?,
+        )?;
+        // We start our transition indices at 1 because we always insert a
+        // dummy first transition corresponding to `Timestamp::MIN`. Its type
+        // index is always 0, so there's no need to change it here.
+        for (transition_index, &type_index) in (1..).zip(bytes) {
+            if usize::from(type_index) >= header.tzh_typecnt {
+                return Err(err!(
+                    "found transition type index {type_index},
+                     but there are only {} local time types",
+                    header.tzh_typecnt,
+                ));
+            }
+            self.transitions[transition_index].type_index = type_index;
+        }
+        Ok(rest)
+    }
+
+    fn parse_local_time_types<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "local time types data block",
+            bytes,
+            header.local_time_types_len()?,
+        )?;
+        let mut it = bytes.chunks_exact(6);
+        while let Some(chunk) = it.next() {
+            let offset = from_be_bytes_i32(&chunk[..4]);
+            if !(OFFSET_MIN <= offset && offset <= OFFSET_MAX) {
+                return Err(err!(
+                    "found local time type with out-of-bounds offset: {offset}"
+                ));
+            }
+            let is_dst = chunk[4] == 1;
+            let designation = (chunk[5], chunk[5]);
+            self.types.push(TzifLocalTimeType {
+                offset,
+                is_dst,
+                designation,
+                indicator: TzifIndicator::LocalWall,
+            });
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_time_zone_designations<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "time zone designations data block",
+            bytes,
+            header.time_zone_designations_len()?,
+        )?;
+        self.fixed.designations =
+            String::from_utf8(bytes.to_vec()).map_err(|_| {
+                err!(
+                    "time zone designations are not valid UTF-8: {:?}",
+                    Bytes(bytes),
+                )
+            })?;
+        // Holy hell, this is brutal. The boundary conditions are crazy.
+        for (i, typ) in self.types.iter_mut().enumerate() {
+            let start = usize::from(typ.designation.0);
+            let Some(suffix) = self.fixed.designations.get(start..) else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but cannot be more than {}",
+                    self.fixed.designations.len(),
+                ));
+            };
+            let Some(len) = suffix.find('\x00') else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but could not find NUL terminator after it in \
+                     designations: {:?}",
+                    self.fixed.designations,
+                ));
+            };
+            let Some(end) = start.checked_add(len) else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but its length {len} is too big",
+                ));
+            };
+            typ.designation.1 = u8::try_from(end).map_err(|_| {
+                err!(
+                    "local time type {i} has designation range of \
+                     {start}..{end}, but end is too big",
+                )
+            })?;
+        }
+        Ok(rest)
+    }
+
+    /// This parses the leap second corrections in the TZif data.
+    ///
+    /// Note that we only parse and verify them. We don't actually use them.
+    /// Jiff effectively ignores leap seconds.
+    fn parse_leap_seconds<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "leap seconds data block",
+            bytes,
+            header.leap_second_len()?,
+        )?;
+        let chunk_len = header
+            .time_size
+            .checked_add(4)
+            .expect("time_size plus 4 fits in usize");
+        let mut it = bytes.chunks_exact(chunk_len);
+        while let Some(chunk) = it.next() {
+            let (occur_bytes, _corr_bytes) = chunk.split_at(header.time_size);
+            let occur = if header.is_32bit() {
+                i64::from(from_be_bytes_i32(occur_bytes))
+            } else {
+                from_be_bytes_i64(occur_bytes)
+            };
+            if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) {}
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_indicators<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (std_wall_bytes, rest) = try_split_at(
+            "standard/wall indicators data block",
+            bytes,
+            header.standard_wall_len()?,
+        )?;
+        let (ut_local_bytes, rest) = try_split_at(
+            "UT/local indicators data block",
+            rest,
+            header.ut_local_len()?,
+        )?;
+        if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
+            // This is a weird case, but technically possible only if all
+            // UT/local indicators are 0. If any are 1, then it's an error,
+            // because it would require the corresponding std/wall indicator
+            // to be 1 too. Which it can't be, because there aren't any. So
+            // we just check that they're all zeros.
+            for (i, &byte) in ut_local_bytes.iter().enumerate() {
+                if byte != 0 {
+                    return Err(err!(
+                        "found UT/local indicator '{byte}' for local time \
+                         type {i}, but it must be 0 since all std/wall \
+                         indicators are 0",
+                    ));
+                }
+            }
+        } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
+            for (i, &byte) in std_wall_bytes.iter().enumerate() {
+                // Indexing is OK because Header guarantees that the number of
+                // indicators is 0 or equal to the number of types.
+                self.types[i].indicator = if byte == 0 {
+                    TzifIndicator::LocalWall
+                } else if byte == 1 {
+                    TzifIndicator::LocalStandard
+                } else {
+                    return Err(err!(
+                        "found invalid std/wall indicator '{byte}' for \
+                         local time type {i}, it must be 0 or 1",
+                    ));
+                };
+            }
+        } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
+            assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
+            let it = std_wall_bytes.iter().zip(ut_local_bytes);
+            for (i, (&stdwall, &utlocal)) in it.enumerate() {
+                // Indexing is OK because Header guarantees that the number of
+                // indicators is 0 or equal to the number of types.
+                self.types[i].indicator = match (stdwall, utlocal) {
+                    (0, 0) => TzifIndicator::LocalWall,
+                    (1, 0) => TzifIndicator::LocalStandard,
+                    (1, 1) => TzifIndicator::UTStandard,
+                    (0, 1) => {
+                        return Err(err!(
+                            "found illegal ut-wall combination for \
+                             local time type {i}, only local-wall, \
+                             local-standard and ut-standard are allowed",
+                        ))
+                    }
+                    _ => {
+                        return Err(err!(
+                            "found illegal std/wall or ut/local value for \
+                             local time type {i}, each must be 0 or 1",
+                        ))
+                    }
+                };
+            }
+        } else {
+            // If they're both empty then we don't need to do anything. Every
+            // local time type record already has the correct default for this
+            // case set.
+            debug_assert!(std_wall_bytes.is_empty());
+            debug_assert!(ut_local_bytes.is_empty());
+        }
+        Ok(rest)
+    }
+
+    fn parse_footer<'b>(
+        &mut self,
+        _header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        if bytes.is_empty() {
+            return Err(err!(
+                "invalid V2+ TZif footer, expected \\n, \
+                 but found unexpected end of data",
+            ));
+        }
+        if bytes[0] != b'\n' {
+            return Err(err!(
+                "invalid V2+ TZif footer, expected {:?}, but found {:?}",
+                Byte(b'\n'),
+                Byte(bytes[0]),
+            ));
+        }
+        let bytes = &bytes[1..];
+        // Only scan up to 1KB for a NUL terminator in case we somehow got
+        // passed a huge block of bytes.
+        let toscan = &bytes[..bytes.len().min(1024)];
+        let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else {
+            return Err(err!(
+                "invalid V2 TZif footer, could not find {:?} \
+                 terminator in: {:?}",
+                Byte(b'\n'),
+                Bytes(toscan),
+            ));
+        };
+        let (bytes, rest) = bytes.split_at(nlat);
+        if !bytes.is_empty() {
+            let posix_tz =
+                PosixTimeZone::parse(bytes).map_err(|e| err!("{e}"))?;
+            // We could in theory limit TZ strings to their strict POSIX
+            // definition here for TZif V2, but I don't think there is any
+            // harm in allowing the extensions in V2 formatted TZif data. Note
+            // that the GNU tooling allow it via the `TZ` environment variable
+            // even though POSIX doesn't specify it. This all seems okay to me
+            // because the V3+ extension is a strict superset of functionality.
+            if let Some(ref dst) = posix_tz.dst {
+                if dst.rule.is_none() {
+                    return Err(err!(
+                        "TZ string `{}` in v3+ tzfile has DST \
+                         but no transition rules",
+                        Bytes(bytes),
+                    ));
+                }
+            }
+            self.fixed.posix_tz = Some(posix_tz);
+        }
+        Ok(&rest[1..])
+    }
+}
+
+/// The header for a TZif formatted file.
+///
+/// V2+ TZif format have two headers: one for V1 data, and then a second
+/// following the V1 data block that describes another data block which uses
+/// 64-bit timestamps. The two headers both have the same format and both
+/// use 32-bit big-endian encoded integers.
+#[derive(Debug)]
+struct Header {
+    /// The size of the timestamps encoded in the data block.
+    ///
+    /// This is guaranteed to be either 4 (for V1) or 8 (for the 64-bit header
+    /// block in V2+).
+    time_size: usize,
+    /// The file format version.
+    ///
+    /// Note that this is either a NUL byte (for version 1), or an ASCII byte
+    /// corresponding to the version number. That is, `0x32` for `2`, `0x33`
+    /// for `3` or `0x34` for `4`. Note also that just because zoneinfo might
+    /// have been recently generated does not mean it uses the latest format
+    /// version. It seems like newer versions are only compiled by `zic` when
+    /// they are needed. For example, `America/New_York` on my system (as of
+    /// `2024-03-25`) has version `0x32`, but `Asia/Jerusalem` has version
+    /// `0x33`.
+    version: u8,
+    /// Number of UT/local indicators stored in the file.
+    ///
+    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
+    tzh_ttisutcnt: usize,
+    /// The number of standard/wall indicators stored in the file.
+    ///
+    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
+    tzh_ttisstdcnt: usize,
+    /// The number of leap seconds for which data entries are stored in the
+    /// file.
+    tzh_leapcnt: usize,
+    /// The number of transition times for which data entries are stored in
+    /// the file.
+    tzh_timecnt: usize,
+    /// The number of local time types for which data entries are stored in the
+    /// file.
+    ///
+    /// This is checked to be at least `1`.
+    tzh_typecnt: usize,
+    /// The number of bytes of time zone abbreviation strings stored in the
+    /// file.
+    ///
+    /// This is checked to be at least `1`.
+    tzh_charcnt: usize,
+}
+
+impl Header {
+    /// Parse the header record from the given bytes.
+    ///
+    /// Upon success, return the header and all bytes after the header.
+    ///
+    /// The given `time_size` must be 4 or 8, corresponding to either the
+    /// V1 header block or the V2+ header block, respectively.
+    fn parse(
+        time_size: usize,
+        bytes: &[u8],
+    ) -> Result<(Header, &[u8]), Error> {
+        assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
+        if bytes.len() < 44 {
+            return Err(err!("invalid header: too short"));
+        }
+        let (magic, rest) = bytes.split_at(4);
+        if magic != b"TZif" {
+            return Err(err!("invalid header: magic bytes mismatch"));
+        }
+        let (version, rest) = rest.split_at(1);
+        let (_reserved, rest) = rest.split_at(15);
+
+        let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_timecnt_bytes, rest) = rest.split_at(4);
+        let (tzh_typecnt_bytes, rest) = rest.split_at(4);
+        let (tzh_charcnt_bytes, rest) = rest.split_at(4);
+
+        let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_ttisutcnt: {e}"))?;
+        let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_ttisstdcnt: {e}"))?;
+        let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_leapcnt: {e}"))?;
+        let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_timecnt: {e}"))?;
+        let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_typecnt: {e}"))?;
+        let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_charcnt: {e}"))?;
+
+        if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
+            return Err(err!(
+                "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \
+                 or equal to tzh_typecnt={tzh_typecnt}",
+            ));
+        }
+        if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
+            return Err(err!(
+                "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \
+                 or equal to tzh_typecnt={tzh_typecnt}",
+            ));
+        }
+        if tzh_typecnt < 1 {
+            return Err(err!(
+                "expected tzh_typecnt={tzh_typecnt} to be at least 1",
+            ));
+        }
+        if tzh_charcnt < 1 {
+            return Err(err!(
+                "expected tzh_charcnt={tzh_charcnt} to be at least 1",
+            ));
+        }
+
+        let header = Header {
+            time_size,
+            version: version[0],
+            tzh_ttisutcnt,
+            tzh_ttisstdcnt,
+            tzh_leapcnt,
+            tzh_timecnt,
+            tzh_typecnt,
+            tzh_charcnt,
+        };
+        Ok((header, rest))
+    }
+
+    /// Returns true if this header is for a 32-bit data block.
+    ///
+    /// When false, it is guaranteed that this header is for a 64-bit data
+    /// block.
+    fn is_32bit(&self) -> bool {
+        self.time_size == 4
+    }
+
+    /// Returns the size of the data block, in bytes, for this header.
+    ///
+    /// This returns an error if the arithmetic required to compute the
+    /// length would overflow.
+    ///
+    /// This is useful for, e.g., skipping over the 32-bit V1 data block in
+    /// V2+ TZif formatted files.
+    fn data_block_len(&self) -> Result<usize, Error> {
+        let a = self.transition_times_len()?;
+        let b = self.transition_types_len()?;
+        let c = self.local_time_types_len()?;
+        let d = self.time_zone_designations_len()?;
+        let e = self.leap_second_len()?;
+        let f = self.standard_wall_len()?;
+        let g = self.ut_local_len()?;
+        a.checked_add(b)
+            .and_then(|z| z.checked_add(c))
+            .and_then(|z| z.checked_add(d))
+            .and_then(|z| z.checked_add(e))
+            .and_then(|z| z.checked_add(f))
+            .and_then(|z| z.checked_add(g))
+            .ok_or_else(|| {
+                err!(
+                    "length of data block in V{} tzfile is too big",
+                    self.version
+                )
+            })
+    }
+
+    fn transition_times_len(&self) -> Result<usize, Error> {
+        self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| {
+            err!("tzh_timecnt value {} is too big", self.tzh_timecnt)
+        })
+    }
+
+    fn transition_types_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_timecnt)
+    }
+
+    fn local_time_types_len(&self) -> Result<usize, Error> {
+        self.tzh_typecnt.checked_mul(6).ok_or_else(|| {
+            err!("tzh_typecnt value {} is too big", self.tzh_typecnt)
+        })
+    }
+
+    fn time_zone_designations_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_charcnt)
+    }
+
+    fn leap_second_len(&self) -> Result<usize, Error> {
+        let record_len = self
+            .time_size
+            .checked_add(4)
+            .expect("4-or-8 plus 4 always fits in usize");
+        self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| {
+            err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt)
+        })
+    }
+
+    fn standard_wall_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_ttisstdcnt)
+    }
+
+    fn ut_local_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_ttisutcnt)
+    }
+}
+
+/// Splits the given slice of bytes at the index given.
+///
+/// If the index is out of range (greater than `bytes.len()`) then an error is
+/// returned. The error message will include the `what` string given, which is
+/// meant to describe the thing being split.
+fn try_split_at<'b>(
+    what: &'static str,
+    bytes: &'b [u8],
+    at: usize,
+) -> Result<(&'b [u8], &'b [u8]), Error> {
+    if at > bytes.len() {
+        Err(err!(
+            "expected at least {at} bytes for {what}, \
+             but found only {} bytes",
+            bytes.len(),
+        ))
+    } else {
+        Ok(bytes.split_at(at))
+    }
+}
+
+/// Interprets the given slice as an unsigned 32-bit big endian integer,
+/// attempts to convert it to a `usize` and returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+///
+/// # Errors
+///
+/// This errors if the `u32` parsed from the given bytes cannot fit in a
+/// `usize`.
+fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, Error> {
+    let n = from_be_bytes_u32(bytes);
+    usize::try_from(n).map_err(|_| {
+        err!(
+            "failed to parse integer {n} (too big, max allowed is {}",
+            usize::MAX
+        )
+    })
+}
+
+/// Interprets the given slice as an unsigned 32-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
+    u32::from_be_bytes(bytes.try_into().unwrap())
+}
+
+/// Interprets the given slice as a signed 32-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
+    i32::from_be_bytes(bytes.try_into().unwrap())
+}
+
+/// Interprets the given slice as a signed 64-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 8`.
+fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
+    i64::from_be_bytes(bytes.try_into().unwrap())
+}
diff --git a/crates/jiff-static/shared/util.rs b/crates/jiff-static/shared/util.rs
new file mode 100644
index 00000000..5148709c
--- /dev/null
+++ b/crates/jiff-static/shared/util.rs
@@ -0,0 +1,130 @@
+// auto-generated by: jiff-cli generate shared
+
+/// Provides a convenient `Debug` implementation for a `u8`.
+///
+/// The `Debug` impl treats the byte as an ASCII, and emits a human
+/// readable representation of it. If the byte isn't ASCII, then it's
+/// emitted as a hex escape sequence.
+#[derive(Clone, Copy)]
+pub(crate) struct Byte(pub u8);
+
+impl core::fmt::Display for Byte {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        if self.0 == b' ' {
+            return write!(f, " ");
+        }
+        // 10 bytes is enough for any output from ascii::escape_default.
+        let mut bytes = [0u8; 10];
+        let mut len = 0;
+        for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
+            // capitalize \xab to \xAB
+            if i >= 2 && b'a' <= b && b <= b'f' {
+                b -= 32;
+            }
+            bytes[len] = b;
+            len += 1;
+        }
+        write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap())
+    }
+}
+
+impl core::fmt::Debug for Byte {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        write!(f, "\"")?;
+        core::fmt::Display::fmt(self, f)?;
+        write!(f, "\"")?;
+        Ok(())
+    }
+}
+
+/// Provides a convenient `Debug` implementation for `&[u8]`.
+///
+/// This generally works best when the bytes are presumed to be mostly
+/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
+/// they are emitted as hex escape sequences.
+#[derive(Clone, Copy)]
+pub(crate) struct Bytes<'a>(pub &'a [u8]);
+
+impl<'a> core::fmt::Display for Bytes<'a> {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        // This is a sad re-implementation of a similar impl found in bstr.
+        let mut bytes = self.0;
+        while let Some(result) = utf8_decode(bytes) {
+            let ch = match result {
+                Ok(ch) => ch,
+                Err(byte) => {
+                    write!(f, r"\x{:02x}", byte)?;
+                    bytes = &bytes[1..];
+                    continue;
+                }
+            };
+            bytes = &bytes[ch.len_utf8()..];
+            match ch {
+                '\0' => write!(f, "\\0")?,
+                '\x01'..='\x7f' => {
+                    write!(f, "{}", (ch as u8).escape_ascii())?;
+                }
+                _ => write!(f, "{}", ch.escape_debug())?,
+            }
+        }
+        Ok(())
+    }
+}
+
+impl<'a> core::fmt::Debug for Bytes<'a> {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        write!(f, "\"")?;
+        core::fmt::Display::fmt(self, f)?;
+        write!(f, "\"")?;
+        Ok(())
+    }
+}
+
+/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
+///
+/// If no valid encoding of a codepoint exists at the beginning of the
+/// given byte slice, then the first byte is returned instead.
+///
+/// This returns `None` if and only if `bytes` is empty.
+///
+/// This never panics.
+///
+/// *WARNING*: This is not designed for performance. If you're looking for
+/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
+/// this crate, then please file an issue and discuss your use case.
+pub(crate) fn utf8_decode(bytes: &[u8]) -> Option<Result<char, u8>> {
+    /// Given a UTF-8 leading byte, this returns the total number of code
+    /// units in the following encoded codepoint.
+    ///
+    /// If the given byte is not a valid UTF-8 leading byte, then this
+    /// returns `None`.
+    fn utf8_len(byte: u8) -> Option<usize> {
+        if byte <= 0x7F {
+            return Some(1);
+        } else if byte & 0b1100_0000 == 0b1000_0000 {
+            return None;
+        } else if byte <= 0b1101_1111 {
+            Some(2)
+        } else if byte <= 0b1110_1111 {
+            Some(3)
+        } else if byte <= 0b1111_0111 {
+            Some(4)
+        } else {
+            None
+        }
+    }
+
+    if bytes.is_empty() {
+        return None;
+    }
+    let len = match utf8_len(bytes[0]) {
+        None => return Some(Err(bytes[0])),
+        Some(len) if len > bytes.len() => return Some(Err(bytes[0])),
+        Some(1) => return Some(Ok(char::from(bytes[0]))),
+        Some(len) => len,
+    };
+    match core::str::from_utf8(&bytes[..len]) {
+        Ok(s) => Some(Ok(s.chars().next().unwrap())),
+        Err(_) => Some(Err(bytes[0])),
+    }
+}
diff --git a/src/civil/date.rs b/src/civil/date.rs
index 7bac0e5a..8cf3daf4 100644
--- a/src/civil/date.rs
+++ b/src/civil/date.rs
@@ -1487,12 +1487,19 @@ impl Date {
             return Ok(self);
         }
         if span.units().contains_only(Unit::Day) {
-            let epoch_days = self.to_unix_epoch_day();
-            let days = epoch_days.try_checked_add(
-                "days",
-                UnixEpochDay::rfrom(span.get_days_ranged()),
-            )?;
-            return Ok(Date::from_unix_epoch_day(days));
+            let span_days = span.get_days_ranged();
+            return if span_days == -1 {
+                self.yesterday()
+            } else if span_days == 1 {
+                self.tomorrow()
+            } else {
+                let epoch_days = self.to_unix_epoch_day();
+                let days = epoch_days.try_checked_add(
+                    "days",
+                    UnixEpochDay::rfrom(span.get_days_ranged()),
+                )?;
+                Ok(Date::from_unix_epoch_day(days))
+            };
         }
 
         let (month, years) =
@@ -1528,15 +1535,24 @@ impl Date {
         duration: SignedDuration,
     ) -> Result<Date, Error> {
         // OK because 24!={-1,0}.
-        let days = duration.as_hours() / 24;
-        let days = UnixEpochDay::try_new("days", days).with_context(|| {
-            err!(
-                "{days} computed from duration {duration:?} overflows \
-                 Jiff's datetime limits",
-            )
-        })?;
-        let days = self.to_unix_epoch_day().try_checked_add("days", days)?;
-        Ok(Date::from_unix_epoch_day(days))
+        match duration.as_hours() / 24 {
+            0 => Ok(self),
+            -1 => self.yesterday(),
+            1 => self.tomorrow(),
+            days => {
+                let days = UnixEpochDay::try_new("days", days).with_context(
+                    || {
+                        err!(
+                            "{days} computed from duration {duration:?} \
+                             overflows Jiff's datetime limits",
+                        )
+                    },
+                )?;
+                let days =
+                    self.to_unix_epoch_day().try_checked_add("days", days)?;
+                Ok(Date::from_unix_epoch_day(days))
+            }
+        }
     }
 
     /// This routine is identical to [`Date::checked_add`] with the duration
@@ -2128,9 +2144,11 @@ impl Date {
         day: impl RInto<Day>,
     ) -> Result<Date, Error> {
         let (year, month, day) = (year.rinto(), month.rinto(), day.rinto());
-        let max_day = days_in_month(year, month);
-        if day > max_day {
-            return Err(day.to_error_with_bounds("day", 1, max_day));
+        if day > 28 {
+            let max_day = days_in_month(year, month);
+            if day > max_day {
+                return Err(day.to_error_with_bounds("day", 1, max_day));
+            }
         }
         Ok(Date::new_ranged_unchecked(year, month, day))
     }
@@ -3796,6 +3814,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(not(miri))]
     fn all_days_to_date_roundtrip() {
         for rd in -100_000..=100_000 {
             let rd = UnixEpochDay::new(rd).unwrap();
@@ -3806,6 +3825,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(not(miri))]
     fn all_date_to_days_roundtrip() {
         let year_range = 2000..=2500;
         // let year_range = -9999..=9999;
@@ -3824,6 +3844,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(not(miri))]
     fn all_date_to_iso_week_date_roundtrip() {
         let year_range = 2000..=2500;
         for year in year_range {
@@ -4052,6 +4073,7 @@ mod tests {
         }
     }
 
+    #[cfg(not(miri))]
     quickcheck::quickcheck! {
         fn prop_checked_add_then_sub(
             d1: Date,
diff --git a/src/civil/iso_week_date.rs b/src/civil/iso_week_date.rs
index 34000c13..400efa84 100644
--- a/src/civil/iso_week_date.rs
+++ b/src/civil/iso_week_date.rs
@@ -841,6 +841,7 @@ fn is_long_year(year: ISOYear) -> bool {
         || (last.in_leap_year() && weekday == Weekday::Friday)
 }
 
+#[cfg(not(miri))]
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/civil/time.rs b/src/civil/time.rs
index c6fbdefd..0e49c4be 100644
--- a/src/civil/time.rs
+++ b/src/civil/time.rs
@@ -9,12 +9,12 @@ use crate::{
         temporal::{self, DEFAULT_DATETIME_PARSER},
     },
     util::{
-        common::{from_day_nanosecond, to_day_nanosecond},
+        common,
         rangeint::{RFrom, RInto, TryRFrom},
         round::increment,
         t::{
-            self, CivilDayNanosecond, Hour, Microsecond, Millisecond, Minute,
-            Nanosecond, Second, SubsecNanosecond, C,
+            self, CivilDayNanosecond, CivilDaySecond, Hour, Microsecond,
+            Millisecond, Minute, Nanosecond, Second, SubsecNanosecond, C,
         },
     },
     RoundMode, SignedDuration, Span, SpanRound, Unit, Zoned,
@@ -1125,6 +1125,38 @@ impl Time {
     pub(crate) fn overflowing_add_duration(
         self,
         duration: SignedDuration,
+    ) -> Result<(Time, SignedDuration), Error> {
+        if self.subsec_nanosecond() != 0 || duration.subsec_nanos() != 0 {
+            return self.overflowing_add_duration_general(duration);
+        }
+        let start = t::NoUnits::rfrom(self.to_second());
+        let duration_secs = t::NoUnits::new_unchecked(duration.as_secs());
+        // This can fail if the duration is near its min or max values, and
+        // thus we fall back to the more general (but slower) implementation
+        // that uses 128-bit integers.
+        let Some(sum) = start.checked_add(duration_secs) else {
+            return self.overflowing_add_duration_general(duration);
+        };
+        let days = t::SpanDays::try_new(
+            "overflowing-days",
+            sum.div_floor(t::SECONDS_PER_CIVIL_DAY),
+        )?;
+        let time_secs = sum.rem_floor(t::SECONDS_PER_CIVIL_DAY);
+        let time = Time::from_second(time_secs.rinto());
+        // OK because of the constraint imposed by t::SpanDays.
+        let hours = i64::from(days).checked_mul(24).unwrap();
+        Ok((time, SignedDuration::from_hours(hours)))
+    }
+
+    /// Like `overflowing_add`, but with `SignedDuration`.
+    ///
+    /// This is used for datetime arithmetic, when adding to the time
+    /// component overflows into days (always 24 hours).
+    #[inline(never)]
+    #[cold]
+    fn overflowing_add_duration_general(
+        self,
+        duration: SignedDuration,
     ) -> Result<(Time, SignedDuration), Error> {
         let start = t::NoUnits128::rfrom(self.to_nanosecond());
         let duration = t::NoUnits96::new_unchecked(duration.as_nanos());
@@ -1739,6 +1771,81 @@ impl Time {
         t2 - t1
     }
 
+    /// Converts this time value to the number of seconds that has elapsed
+    /// since `00:00:00`. This completely ignores seconds. Callers should
+    /// likely ensure that the fractional second component is zero.
+    ///
+    /// The maximum possible value that can be returned represents the time
+    /// `23:59:59`.
+    #[inline]
+    pub(crate) fn to_second(&self) -> CivilDaySecond {
+        #[cfg(not(debug_assertions))]
+        {
+            CivilDaySecond {
+                val: common::to_day_second(
+                    self.hour.val,
+                    self.minute.val,
+                    self.second.val,
+                ),
+            }
+        }
+        #[cfg(debug_assertions)]
+        {
+            let val = common::to_day_second(
+                self.hour.val,
+                self.minute.val,
+                self.second.val,
+            );
+            let min = common::to_day_second(
+                self.hour.min,
+                self.minute.min,
+                self.second.min,
+            );
+            let max = common::to_day_second(
+                self.hour.max,
+                self.minute.max,
+                self.second.max,
+            );
+            CivilDaySecond { val, min, max }
+        }
+    }
+
+    /// Converts the given second to a time value. The second should correspond
+    /// to the number of seconds that have elapsed since `00:00:00`. The
+    /// fractional second component of the `Time` returned is always `0`.
+    #[inline(always)]
+    pub(crate) fn from_second(second: CivilDaySecond) -> Time {
+        #[cfg(not(debug_assertions))]
+        {
+            let (hour, minute, second) = common::from_day_second(second.val);
+            Time {
+                hour: Hour { val: hour },
+                minute: Minute { val: minute },
+                second: Second { val: second },
+                subsec_nanosecond: SubsecNanosecond { val: 0 },
+            }
+        }
+        #[cfg(debug_assertions)]
+        {
+            let (hour, minute, sec) = common::from_day_second(second.val);
+            let (min_hour, min_minute, min_sec) =
+                common::from_day_second(second.min);
+            let (max_hour, max_minute, max_sec) =
+                common::from_day_second(second.max);
+
+            let hour = Hour { val: hour, min: min_hour, max: max_hour };
+            let minute =
+                Minute { val: minute, min: min_minute, max: max_minute };
+            let second = Second { val: sec, min: min_sec, max: max_sec };
+            Time {
+                hour,
+                minute,
+                second,
+                subsec_nanosecond: SubsecNanosecond::N::<0>(),
+            }
+        }
+    }
+
     /// Converts this time value to the number of nanoseconds that has elapsed
     /// since `00:00:00.000000000`.
     ///
@@ -1749,7 +1856,7 @@ impl Time {
         #[cfg(not(debug_assertions))]
         {
             CivilDayNanosecond {
-                val: to_day_nanosecond(
+                val: common::to_day_nanosecond(
                     self.hour.val,
                     self.minute.val,
                     self.second.val,
@@ -1759,19 +1866,19 @@ impl Time {
         }
         #[cfg(debug_assertions)]
         {
-            let val = to_day_nanosecond(
+            let val = common::to_day_nanosecond(
                 self.hour.val,
                 self.minute.val,
                 self.second.val,
                 self.subsec_nanosecond.val,
             );
-            let min = to_day_nanosecond(
+            let min = common::to_day_nanosecond(
                 self.hour.min,
                 self.minute.min,
                 self.second.min,
                 self.subsec_nanosecond.min,
             );
-            let max = to_day_nanosecond(
+            let max = common::to_day_nanosecond(
                 self.hour.max,
                 self.minute.max,
                 self.second.max,
@@ -1789,7 +1896,7 @@ impl Time {
         #[cfg(not(debug_assertions))]
         {
             let (hour, minute, second, subsec) =
-                from_day_nanosecond(nanosecond.val);
+                common::from_day_nanosecond(nanosecond.val);
             Time {
                 hour: Hour { val: hour },
                 minute: Minute { val: minute },
@@ -1800,11 +1907,11 @@ impl Time {
         #[cfg(debug_assertions)]
         {
             let (hour, minute, second, subsec) =
-                from_day_nanosecond(nanosecond.val);
+                common::from_day_nanosecond(nanosecond.val);
             let (min_hour, min_minute, min_second, min_subsec) =
-                from_day_nanosecond(nanosecond.min);
+                common::from_day_nanosecond(nanosecond.min);
             let (max_hour, max_minute, max_second, max_subsec) =
-                from_day_nanosecond(nanosecond.max);
+                common::from_day_nanosecond(nanosecond.max);
 
             let hour = Hour { val: hour, min: min_hour, max: max_hour };
             let minute =
@@ -3309,6 +3416,7 @@ mod tests {
         );
     }
 
+    #[cfg(not(miri))]
     quickcheck::quickcheck! {
         fn prop_ordering_same_as_civil_nanosecond(
             civil_nanosecond1: CivilDayNanosecond,
diff --git a/src/error.rs b/src/error.rs
index 88e86ff0..cac10ed6 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -54,7 +54,7 @@ pub struct Error {
     /// This also makes clones cheap. And it also make the size of error equal
     /// to one word (although a `Box` would achieve that last goal). This is
     /// why we put the `Arc` here instead of on `std::io::Error` directly.
-    inner: Arc<ErrorInner>,
+    inner: Option<Arc<ErrorInner>>,
 }
 
 #[derive(Debug)]
@@ -176,6 +176,22 @@ impl Error {
         }));
         self.context(err)
     }
+
+    /*
+    /// Creates a new "unknown" Jiff error.
+    ///
+    /// The benefit of this API is that it permits creating an `Error` in a
+    /// `const` context. But the error message quality is currently pretty
+    /// bad: it's just a generic "unknown jiff error" message.
+    ///
+    /// This could be improved to take a `&'static str`, but I believe this
+    /// will require pointer tagging in order to avoid increasing the size of
+    /// `Error`. (Which is important, because of how many perf sensitive
+    /// APIs return a `Result<T, Error>` in Jiff.
+    pub(crate) const fn unknown() -> Error {
+        Error { inner: None }
+    }
+    */
 }
 
 #[cfg(feature = "std")]
@@ -187,8 +203,12 @@ impl core::fmt::Display for Error {
         {
             let mut err = self;
             loop {
-                write!(f, "{}", err.inner.kind)?;
-                err = match err.inner.cause.as_ref() {
+                let Some(ref inner) = err.inner else {
+                    write!(f, "unknown jiff error")?;
+                    break;
+                };
+                write!(f, "{}", inner.kind)?;
+                err = match inner.cause.as_ref() {
                     None => break,
                     Some(err) => err,
                 };
@@ -198,7 +218,10 @@ impl core::fmt::Display for Error {
         }
         #[cfg(not(feature = "alloc"))]
         {
-            write!(f, "{}", self.inner.kind)
+            match self.inner {
+                None => write!(f, "unknown jiff error"),
+                Some(ref inner) => write!(f, "{}", inner.kind),
+            }
         }
     }
 }
@@ -208,18 +231,22 @@ impl core::fmt::Debug for Error {
         if !f.alternate() {
             core::fmt::Display::fmt(self, f)
         } else {
+            let Some(ref inner) = self.inner else {
+                return f
+                    .debug_struct("Error")
+                    .field("kind", &"None")
+                    .finish();
+            };
             #[cfg(feature = "alloc")]
             {
                 f.debug_struct("Error")
-                    .field("kind", &self.inner.kind)
-                    .field("cause", &self.inner.cause)
+                    .field("kind", &inner.kind)
+                    .field("cause", &inner.cause)
                     .finish()
             }
             #[cfg(not(feature = "alloc"))]
             {
-                f.debug_struct("Error")
-                    .field("kind", &self.inner.kind)
-                    .finish()
+                f.debug_struct("Error").field("kind", &inner.kind).finish()
             }
         }
     }
@@ -240,11 +267,11 @@ impl From<ErrorKind> for Error {
     fn from(kind: ErrorKind) -> Error {
         #[cfg(feature = "alloc")]
         {
-            Error { inner: Arc::new(ErrorInner { kind, cause: None }) }
+            Error { inner: Some(Arc::new(ErrorInner { kind, cause: None })) }
         }
         #[cfg(not(feature = "alloc"))]
         {
-            Error { inner: Arc::new(ErrorInner { kind }) }
+            Error { inner: Some(Arc::new(ErrorInner { kind })) }
         }
     }
 }
@@ -528,13 +555,17 @@ impl ErrorContext for Error {
         #[cfg(feature = "alloc")]
         {
             let mut err = consequent.into_error();
+            if err.inner.is_none() {
+                err = err!("unknown jiff error");
+            }
+            let inner = err.inner.as_mut().unwrap();
             assert!(
-                err.inner.cause.is_none(),
+                inner.cause.is_none(),
                 "cause of consequence must be `None`"
             );
             // OK because we just created this error so the Arc
             // has one reference.
-            Arc::get_mut(&mut err.inner).unwrap().cause = Some(self);
+            Arc::get_mut(inner).unwrap().cause = Some(self);
             err
         }
         #[cfg(not(feature = "alloc"))]
@@ -552,13 +583,17 @@ impl ErrorContext for Error {
         #[cfg(feature = "alloc")]
         {
             let mut err = consequent().into_error();
+            if err.inner.is_none() {
+                err = err!("unknown jiff error");
+            }
+            let inner = err.inner.as_mut().unwrap();
             assert!(
-                err.inner.cause.is_none(),
+                inner.cause.is_none(),
                 "cause of consequence must be `None`"
             );
             // OK because we just created this error so the Arc
             // has one reference.
-            Arc::get_mut(&mut err.inner).unwrap().cause = Some(self);
+            Arc::get_mut(inner).unwrap().cause = Some(self);
             err
         }
         #[cfg(not(feature = "alloc"))]
diff --git a/src/fmt/rfc9557.rs b/src/fmt/rfc9557.rs
index b7793d2e..259e92c6 100644
--- a/src/fmt/rfc9557.rs
+++ b/src/fmt/rfc9557.rs
@@ -643,9 +643,7 @@ mod tests {
         Some(
             (
                 TimeZone(
-                    Fixed(
-                        25:59:00,
-                    ),
+                    25:59:00,
                 ),
                 false,
             ),
@@ -655,9 +653,7 @@ mod tests {
         Some(
             (
                 TimeZone(
-                    Fixed(
-                        -25:59:00,
-                    ),
+                    -25:59:00,
                 ),
                 false,
             ),
diff --git a/src/fmt/temporal/parser.rs b/src/fmt/temporal/parser.rs
index f37c5685..d0f33f5e 100644
--- a/src/fmt/temporal/parser.rs
+++ b/src/fmt/temporal/parser.rs
@@ -530,9 +530,11 @@ impl DateTimeParser {
         }
         #[cfg(feature = "alloc")]
         {
-            match crate::tz::posix::IanaTz::parse_v3plus_prefix(consumed) {
-                Ok((iana_tz, input)) => {
-                    let kind = ParsedTimeZoneKind::Posix(iana_tz.into_tz());
+            use crate::tz::posix::ReasonablePosixTimeZone;
+
+            match ReasonablePosixTimeZone::parse_prefix(consumed) {
+                Ok((posix_tz, input)) => {
+                    let kind = ParsedTimeZoneKind::Posix(posix_tz);
                     let value = ParsedTimeZone { input: original, kind };
                     Ok(Parsed { value, input })
                 }
diff --git a/src/lib.rs b/src/lib.rs
index d412f417..10fb69f7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -658,7 +658,18 @@ For more, see the [`fmt::serde`] sub-module. (This requires enabling Jiff's
   environment variables (with sensible default fallbacks) are used to construct
   candidate paths to look for this database. For more on this, see the
   [Android section of the platform support documentation](crate::_documentation::platform#android).
-
+* **static** -
+  When enabled, new procedural macros will be added to the `tz` sub-module for
+  creating static `TimeZone` values at compile-time. This adds a dependency on
+  [`jiff-static`] and [`jiff-tzdb`]. `jiff-static` defines the macros, and Jiff
+  re-exports them. This also enables `static-tz`.
+* **static-tz** -
+  When enabled, a `jiff::tz::include` procedural macro will become available.
+  This takes a TZif file path, like `/usr/share/zoneinfo/Israel`, as input and
+  returns a `TimeZone` value at compile time.
+
+[`jiff-static`]: https://docs.rs/jiff-static
+[`jiff-tzdb`]: https://docs.rs/jiff-tzdb
 [Concatenated Time Zone Database]: https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/util/ZoneInfoDB.java
 */
 
@@ -731,6 +742,8 @@ mod error;
 pub mod fmt;
 #[cfg(feature = "std")]
 mod now;
+#[doc(hidden)]
+pub mod shared;
 mod signed_duration;
 mod span;
 mod timestamp;
@@ -765,7 +778,7 @@ mod tests {
 
     #[cfg(feature = "std")]
     #[test]
-    fn topscratch() {
+    fn ranges() {
         use crate::util::t;
 
         dbg!((t::SpanYears::MIN, t::SpanYears::MAX));
diff --git a/src/util/crc32/mod.rs b/src/shared/crc32/mod.rs
similarity index 97%
rename from src/util/crc32/mod.rs
rename to src/shared/crc32/mod.rs
index ac6e2a01..f37b6be0 100644
--- a/src/util/crc32/mod.rs
+++ b/src/shared/crc32/mod.rs
@@ -1,4 +1,4 @@
-use crate::util::crc32::table::{TABLE, TABLE16};
+use self::table::{TABLE, TABLE16};
 
 mod table;
 
diff --git a/src/util/crc32/table.rs b/src/shared/crc32/table.rs
similarity index 99%
rename from src/util/crc32/table.rs
rename to src/shared/crc32/table.rs
index e7998566..b73de914 100644
--- a/src/util/crc32/table.rs
+++ b/src/shared/crc32/table.rs
@@ -1,4 +1,6 @@
-pub const TABLE: [u32; 256] = [
+// auto-generated by: jiff-cli generate crc32
+
+pub(super) const TABLE: [u32; 256] = [
     0, 4067132163, 3778769143, 324072436, 3348797215, 904991772, 648144872,
     3570033899, 2329499855, 2024987596, 1809983544, 2575936315, 1296289744,
     3207089363, 2893594407, 1578318884, 274646895, 3795141740, 4049975192,
@@ -44,7 +46,7 @@ pub const TABLE: [u32; 256] = [
     1279665062, 1595330642, 2910671697,
 ];
 
-pub const TABLE16: [[u32; 256]; 16] = [
+pub(super) const TABLE16: [[u32; 256]; 16] = [
     [
         0, 4067132163, 3778769143, 324072436, 3348797215, 904991772,
         648144872, 3570033899, 2329499855, 2024987596, 1809983544, 2575936315,
diff --git a/src/shared/mod.rs b/src/shared/mod.rs
new file mode 100644
index 00000000..dacf1d22
--- /dev/null
+++ b/src/shared/mod.rs
@@ -0,0 +1,243 @@
+/*!
+Defines data types shared between `jiff` and `jiff-static`.
+
+While this module exposes types that can be imported outside of `jiff` itself,
+there are *no* semver guarantees provided. That is, this module is _not_ part
+of Jiff's public API. The only guarantee of compatibility that is provided
+is that `jiff-static x.y.z` works with one and only one version of Jiff,
+corresponding to `jiff x.y.z` (i.e., the same version number).
+
+# Design
+
+This module is really accomplishing two different things at the same time.
+
+Firstly, it is a way to provide types that can be used to construct a static
+`TimeZone`. The proc macros in `jiff-static` generate code using these
+types (and a few routines).
+
+Secondly, it provides a way to parse TZif data without `jiff-static`
+depending on `jiff` via a Cargo dependency. This actually requires copying
+the code in this module (which is why it is kinda sectioned off from the rest
+of jiff) into the `jiff-static` crate. This can be done automatically with
+`jiff-cli`:
+
+```text
+jiff-cli generate shared
+```
+
+The copying of code is pretty unfortunate, because it means both crates have to
+compile it. However, the alternatives aren't great either.
+
+One alternative is to have `jiff-static` explicitly depend on `jiff` in its
+`Cargo.toml`. Then Jiff could expose the parsing routines, as it does here,
+and `jiff-static` could use them directly. Unfortunately, this means that
+`jiff` cannot depend on `jiff-static`. And that in turn means that `jiff`
+cannot re-export the macros. Users will need to explicitly depend on and use
+`jiff-static`. Moreover, this could result in some potential surprises
+since `jiff-static` will need to have an `=x.y.z` dependency on Jiff for
+compatibility reasons. That in turn means that the version of Jiff actually
+used is not determine by the user's `jiff = "x.y.z"` line, but rather by the
+user's `jiff-static = "x'.y'.z'"` line. This is overall annoying and not a
+good user experience. Plus, it inverts the typical relationship between crates
+and their proc macros (e.g., `serde` and `serde_derive`) and thus could result
+in other unanticipated surprises.
+
+Another obvious alternative is to split this code out into a separate crate
+that both `jiff` and `jiff-static` depend on. However, the API exposed in
+this module does not provide a coherent user experience. It would either need a
+ton of work to turn it into a coherent user experience or it would need to be
+published as a `jiff-internal-use-only` crate that I find to be very annoying
+and confusing. Moreover, a separate crate introduces a new semver boundary
+beneath Jiff. I've found these sorts of things to overall increase maintenance
+burden (see ripgrep and regex for cases where I did this).
+
+I overall decided that the least bad choice was to copy a little code (under
+2,000 source lines of code at present I believe). Since the copy is managed
+automatically via `jiff-cli generate shared`, we remove the downside of the
+code getting out of sync. The only downside is extra compile time. Since I
+generally only expect `jiff-static` to be used in niche circumstances, I
+prefer this trade-off over the other choices.
+
+More context on how I arrived at this design can be found here:
+<https://github.com/BurntSushi/jiff/issues/256>
+
+# Particulars
+
+When this code is copied to `jiff-static`, the following transformations are
+done:
+
+* A header is added to indicate that the copied file is auto-generated.
+* All `#[cfg(feature = "alloc")]` annotations are removed. The `jiff-static`
+  proc macro always runs in a context where the standard library is available.
+* Any code between `// only-jiff-start` and `// only-jiff-end` comments is
+  removed. Nesting isn't supported.
+
+Otherwise, this module is specifically organized in a way that doesn't rely on
+any other part of Jiff. The one exception are routines to convert from these
+exposed types to other internal types inside of Jiff. This is necessary for
+building a static `TimeZone`. But these conversion routines are removed when
+this module is copied to `jiff-static`.
+*/
+
+// only-jiff-start
+pub type TzifStatic = Tzif<
+    &'static str,
+    &'static [TzifLocalTimeType],
+    &'static [TzifTransition],
+>;
+// only-jiff-end
+
+#[cfg(feature = "alloc")]
+pub type TzifOwned = Tzif<
+    alloc::string::String,
+    alloc::vec::Vec<TzifLocalTimeType>,
+    alloc::vec::Vec<TzifTransition>,
+>;
+
+#[derive(Debug)]
+pub struct Tzif<STRING, TYPES, TRANS> {
+    pub fixed: TzifFixed<STRING>,
+    pub types: TYPES,
+    pub transitions: TRANS,
+}
+
+#[derive(Debug)]
+pub struct TzifFixed<STRING> {
+    pub name: Option<STRING>,
+    pub version: u8,
+    pub checksum: u32,
+    pub designations: STRING,
+    pub posix_tz: Option<PosixTimeZone<STRING>>,
+}
+
+// only-jiff-start
+impl TzifFixed<&'static str> {
+    pub const fn to_jiff(
+        &self,
+        types: &'static [crate::tz::tzif::LocalTimeType],
+        trans: &'static [crate::tz::tzif::Transition],
+    ) -> crate::tz::tzif::TzifStatic {
+        crate::tz::tzif::TzifStatic::from_shared_const(self, types, trans)
+    }
+}
+// only-jiff-end
+
+#[derive(Debug)]
+pub struct TzifLocalTimeType {
+    pub offset: i32,
+    pub is_dst: bool,
+    pub designation: (u8, u8), // inclusive..exclusive
+    pub indicator: TzifIndicator,
+}
+
+// only-jiff-start
+impl TzifLocalTimeType {
+    pub const fn to_jiff(&self) -> crate::tz::tzif::LocalTimeType {
+        crate::tz::tzif::LocalTimeType::from_shared(self)
+    }
+}
+// only-jiff-end
+
+#[derive(Debug)]
+pub enum TzifIndicator {
+    LocalWall,
+    LocalStandard,
+    UTStandard,
+}
+
+#[derive(Debug)]
+pub struct TzifTransition {
+    pub timestamp: i64,
+    pub type_index: u8,
+}
+
+// only-jiff-start
+impl TzifTransition {
+    pub const fn to_jiff(
+        &self,
+        prev_offset: i32,
+        this_offset: i32,
+    ) -> crate::tz::tzif::Transition {
+        crate::tz::tzif::Transition::from_shared(
+            self,
+            prev_offset,
+            this_offset,
+        )
+    }
+}
+// only-jiff-end
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixTimeZone<ABBREV> {
+    pub std_abbrev: ABBREV,
+    pub std_offset: i32,
+    pub dst: Option<PosixDst<ABBREV>>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixDst<ABBREV> {
+    pub abbrev: ABBREV,
+    pub offset: i32,
+    pub rule: Option<PosixRule>,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixRule {
+    pub start: PosixDayTime,
+    pub end: PosixDayTime,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct PosixDayTime {
+    pub date: PosixDay,
+    pub time: i32,
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PosixDay {
+    /// Julian day in a year, no counting for leap days.
+    ///
+    /// Valid range is `1..=365`.
+    JulianOne(i16),
+    /// Julian day in a year, counting for leap days.
+    ///
+    /// Valid range is `0..=365`.
+    JulianZero(i16),
+    /// The nth weekday of a month.
+    WeekdayOfMonth {
+        /// The month.
+        ///
+        /// Valid range is: `1..=12`.
+        month: i8,
+        /// The week.
+        ///
+        /// Valid range is `1..=5`.
+        ///
+        /// One interesting thing to note here (or my interpretation anyway),
+        /// is that a week of `4` means the "4th weekday in a month" where as
+        /// a week of `5` means the "last weekday in a month, even if it's the
+        /// 4th weekday."
+        week: i8,
+        /// The weekday.
+        ///
+        /// Valid range is `0..=6`, with `0` corresponding to Sunday.
+        weekday: i8,
+    },
+}
+
+// only-jiff-start
+impl PosixTimeZone<&'static str> {
+    pub const fn to_jiff(&self) -> crate::tz::posix::ReasonablePosixTimeZone {
+        crate::tz::posix::ReasonablePosixTimeZone::from_shared_const(self)
+    }
+}
+// only-jiff-end
+
+// Does not require `alloc`, but is only used when `alloc` is enabled.
+#[cfg(feature = "alloc")]
+pub(crate) mod crc32;
+#[cfg(feature = "alloc")]
+pub(crate) mod posix;
+#[cfg(feature = "alloc")]
+pub(crate) mod tzif;
+pub(crate) mod util;
diff --git a/src/shared/posix.rs b/src/shared/posix.rs
new file mode 100644
index 00000000..bcd4056b
--- /dev/null
+++ b/src/shared/posix.rs
@@ -0,0 +1,1941 @@
+use alloc::string::String;
+
+use super::{
+    util::{Byte, Bytes},
+    PosixDay, PosixDayTime, PosixDst, PosixRule, PosixTimeZone,
+};
+
+macro_rules! err {
+    ($($tt:tt)*) => {{
+        self::Error(alloc::format!($($tt)*))
+    }}
+}
+
+/// An error that can be returned when parsing.
+#[derive(Debug)]
+pub struct Error(String);
+
+impl core::fmt::Display for Error {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        core::fmt::Display::fmt(&self.0, f)
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl PosixTimeZone<String> {
+    /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not
+    /// an implementation defined value, from the given bytes.
+    pub fn parse(bytes: &[u8]) -> Result<PosixTimeZone<String>, Error> {
+        // We enable the IANA v3+ extensions here. (Namely, that the time
+        // specification hour value has the range `-167..=167` instead of
+        // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary
+        // since the extension is a strict superset. Plus, GNU tooling
+        // seems to accept the extension.
+        let parser =
+            Parser { ianav3plus: true, ..Parser::new(bytes.as_ref()) };
+        parser.parse()
+    }
+
+    // only-jiff-start
+    /// Like parse, but parses a prefix of the input given and returns whatever
+    /// is remaining.
+    pub fn parse_prefix<'b>(
+        bytes: &'b [u8],
+    ) -> Result<(PosixTimeZone<String>, &'b [u8]), Error> {
+        let parser =
+            Parser { ianav3plus: true, ..Parser::new(bytes.as_ref()) };
+        parser.parse_prefix()
+    }
+    // only-jiff-end
+}
+
+/// A parser for POSIX time zones.
+#[derive(Debug)]
+struct Parser<'s> {
+    /// The `TZ` string that we're parsing.
+    tz: &'s [u8],
+    /// The parser's current position in `tz`.
+    pos: core::cell::Cell<usize>,
+    /// Whether to use IANA rules, i.e., when parsing a TZ string in a TZif
+    /// file of version 3 or greater. From `tzfile(5)`:
+    ///
+    /// > First, the hours part of its transition times may be signed and range
+    /// > from `-167` through `167` instead of the POSIX-required unsigned
+    /// > values from `0` through `24`. Second, DST is in effect all year if
+    /// > it starts January 1 at 00:00 and ends December 31 at 24:00 plus the
+    /// > difference between daylight saving and standard time.
+    ///
+    /// At time of writing, I don't think I understand the significance of
+    /// the second part above. (RFC 8536 elaborates that it is meant to be an
+    /// explicit clarification of something that POSIX itself implies.) But the
+    /// first part is clear: it permits the hours to be a bigger range.
+    ianav3plus: bool,
+}
+
+impl<'s> Parser<'s> {
+    /// Create a new parser for extracting a POSIX time zone from the given
+    /// bytes.
+    fn new<B: ?Sized + AsRef<[u8]>>(tz: &'s B) -> Parser<'s> {
+        Parser {
+            tz: tz.as_ref(),
+            pos: core::cell::Cell::new(0),
+            ianav3plus: false,
+        }
+    }
+
+    /// Parses a POSIX time zone from the current position of the parser and
+    /// ensures that the entire TZ string corresponds to a single valid POSIX
+    /// time zone.
+    fn parse(&self) -> Result<PosixTimeZone<String>, Error> {
+        let (time_zone, remaining) = self.parse_prefix()?;
+        if !remaining.is_empty() {
+            return Err(err!(
+                "expected entire TZ string to be a valid POSIX \
+                 time zone, but found `{}` after what would otherwise \
+                 be a valid POSIX TZ string",
+                Bytes(remaining),
+            ));
+        }
+        Ok(time_zone)
+    }
+
+    /// Parses a POSIX time zone from the current position of the parser and
+    /// returns the remaining input.
+    fn parse_prefix(
+        &self,
+    ) -> Result<(PosixTimeZone<String>, &'s [u8]), Error> {
+        let time_zone = self.parse_posix_time_zone()?;
+        Ok((time_zone, self.remaining()))
+    }
+
+    /// Parse a POSIX time zone from the current position of the parser.
+    ///
+    /// Upon success, the parser will be positioned immediately following the
+    /// TZ string.
+    fn parse_posix_time_zone(&self) -> Result<PosixTimeZone<String>, Error> {
+        let std_abbrev = self
+            .parse_abbreviation()
+            .map_err(|e| err!("failed to parse standard abbreviation: {e}"))?;
+        let std_offset = self
+            .parse_posix_offset()
+            .map_err(|e| err!("failed to parse standard offset: {e}"))?;
+        let mut dst = None;
+        if !self.is_done()
+            && (self.byte().is_ascii_alphabetic() || self.byte() == b'<')
+        {
+            dst = Some(self.parse_posix_dst(std_offset)?);
+        }
+        Ok(PosixTimeZone { std_abbrev, std_offset, dst })
+    }
+
+    /// Parse a DST zone with an optional explicit transition rule.
+    ///
+    /// This assumes the parser is positioned at the first byte of the DST
+    /// abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the end
+    /// of the DST transition rule (which might just be the abbreviation, but
+    /// might also include explicit start/end datetime specifications).
+    fn parse_posix_dst(
+        &self,
+        std_offset: i32,
+    ) -> Result<PosixDst<String>, Error> {
+        let abbrev = self
+            .parse_abbreviation()
+            .map_err(|e| err!("failed to parse DST abbreviation: {e}"))?;
+        // This is the default: one hour ahead of standard time. We may
+        // override this if the DST portion specifies an offset. (But it
+        // usually doesn't.)
+        let offset = std_offset + 3600;
+        let mut dst = PosixDst { abbrev, offset, rule: None };
+        if self.is_done() {
+            return Ok(dst);
+        }
+        if self.byte() != b',' {
+            dst.offset = self
+                .parse_posix_offset()
+                .map_err(|e| err!("failed to parse DST offset: {e}"))?;
+            if self.is_done() {
+                return Ok(dst);
+            }
+        }
+        if self.byte() != b',' {
+            return Err(err!(
+                "after parsing DST offset in POSIX time zone string, \
+                 found `{}` but expected a ','",
+                Byte(self.byte()),
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "after parsing DST offset in POSIX time zone string, \
+                 found end of string after a trailing ','",
+            ));
+        }
+        dst.rule = Some(self.parse_rule()?);
+        Ok(dst)
+    }
+
+    /// Parse a time zone abbreviation.
+    ///
+    /// This assumes the parser is positioned at the first byte of
+    /// the abbreviation. This is either the first character in the
+    /// abbreviation, or the opening quote of a quoted abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately following
+    /// the abbreviation name.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_abbreviation(&self) -> Result<String, Error> {
+        if self.byte() == b'<' {
+            if !self.bump() {
+                return Err(err!(
+                    "found opening '<' quote for abbreviation in \
+                         POSIX time zone string, and expected a name \
+                         following it, but found the end of string instead"
+                ));
+            }
+            self.parse_quoted_abbreviation()
+        } else {
+            self.parse_unquoted_abbreviation()
+        }
+    }
+
+    /// Parses an unquoted time zone abbreviation.
+    ///
+    /// This assumes the parser is position at the first byte in the
+    /// abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// last byte in the abbreviation.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_unquoted_abbreviation(&self) -> Result<String, Error> {
+        const MAX_LEN: usize = 30;
+
+        let start = self.pos();
+        for i in 0.. {
+            if !self.byte().is_ascii_alphabetic() {
+                break;
+            }
+            if i >= MAX_LEN {
+                return Err(err!(
+                    "expected abbreviation with at most {MAX_LEN} bytes, \
+                         but found a longer abbreviation beginning with `{}`",
+                    Bytes(&self.tz[start..i]),
+                ));
+            }
+            if !self.bump() {
+                break;
+            }
+        }
+        let end = self.pos();
+        let abbrev =
+            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
+                // NOTE: I believe this error is technically impossible
+                // since the loop above restricts letters in an
+                // abbreviation to ASCII. So everything from `start` to
+                // `end` is ASCII and thus should be UTF-8. But it doesn't
+                // cost us anything to report an error here in case the
+                // code above evolves somehow.
+                err!(
+                    "found abbreviation `{}`, but it is not valid UTF-8",
+                    Bytes(&self.tz[start..end]),
+                )
+            })?;
+        if abbrev.len() < 3 {
+            return Err(err!(
+                "expected abbreviation with 3 or more bytes, but found \
+                     abbreviation {:?} with {} bytes",
+                abbrev,
+                abbrev.len(),
+            ));
+        }
+        Ok(String::from(abbrev))
+    }
+
+    /// Parses a quoted time zone abbreviation.
+    ///
+    /// This assumes the parser is positioned immediately after the opening
+    /// `<` quote. That is, at the first byte in the abbreviation.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// closing `>` quote.
+    ///
+    /// The string returned is guaranteed to be no more than 30 bytes.
+    /// (This restriction is somewhat arbitrary, but it's so we can put
+    /// the abbreviation in a fixed capacity array.)
+    fn parse_quoted_abbreviation(&self) -> Result<String, Error> {
+        const MAX_LEN: usize = 30;
+
+        let start = self.pos();
+        for i in 0.. {
+            if !self.byte().is_ascii_alphanumeric()
+                && self.byte() != b'+'
+                && self.byte() != b'-'
+            {
+                break;
+            }
+            if i >= MAX_LEN {
+                return Err(err!(
+                    "expected abbreviation with at most {MAX_LEN} bytes, \
+                         but found a longer abbreviation beginning with `{}`",
+                    Bytes(&self.tz[start..i]),
+                ));
+            }
+            if !self.bump() {
+                break;
+            }
+        }
+        let end = self.pos();
+        let abbrev =
+            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
+                // NOTE: I believe this error is technically impossible
+                // since the loop above restricts letters in an
+                // abbreviation to ASCII. So everything from `start` to
+                // `end` is ASCII and thus should be UTF-8. But it doesn't
+                // cost us anything to report an error here in case the
+                // code above evolves somehow.
+                err!(
+                    "found abbreviation `{}`, but it is not valid UTF-8",
+                    Bytes(&self.tz[start..end]),
+                )
+            })?;
+        if self.is_done() {
+            return Err(err!(
+                "found non-empty quoted abbreviation {abbrev:?}, but \
+                     did not find expected end-of-quoted abbreviation \
+                     '>' character",
+            ));
+        }
+        if self.byte() != b'>' {
+            return Err(err!(
+                "found non-empty quoted abbreviation {abbrev:?}, but \
+                     found `{}` instead of end-of-quoted abbreviation '>' \
+                     character",
+                Byte(self.byte()),
+            ));
+        }
+        self.bump();
+        if abbrev.len() < 3 {
+            return Err(err!(
+                "expected abbreviation with 3 or more bytes, but found \
+                     abbreviation {abbrev:?} with {} bytes",
+                abbrev.len(),
+            ));
+        }
+        Ok(String::from(abbrev))
+    }
+
+    /// Parse a POSIX time offset.
+    ///
+    /// This assumes the parser is positioned at the first byte of the
+    /// offset. This can either be a digit (for a positive offset) or the
+    /// sign of the offset (which must be either `-` or `+`).
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// end of the offset.
+    fn parse_posix_offset(&self) -> Result<i32, Error> {
+        let sign = self
+            .parse_optional_sign()
+            .map_err(|e| {
+                err!(
+                    "failed to parse sign for time offset \
+                     in POSIX time zone string: {e}",
+                )
+            })?
+            .unwrap_or(1);
+        let hour = self.parse_hour_posix()?;
+        let (mut minute, mut second) = (0, 0);
+        if self.maybe_byte() == Some(b':') {
+            if !self.bump() {
+                return Err(err!(
+                    "incomplete time in POSIX timezone (missing minutes)",
+                ));
+            }
+            minute = self.parse_minute()?;
+            if self.maybe_byte() == Some(b':') {
+                if !self.bump() {
+                    return Err(err!(
+                        "incomplete time in POSIX timezone (missing seconds)",
+                    ));
+                }
+                second = self.parse_second()?;
+            }
+        }
+        let mut seconds = i32::from(hour) * 3600;
+        seconds += i32::from(minute) * 60;
+        seconds += i32::from(second);
+        // Yes, we flip the sign, because POSIX is backwards.
+        // For example, `EST5` corresponds to `-05:00`.
+        seconds *= i32::from(-sign);
+        // Must be true because the parsing routines for hours, minutes
+        // and seconds enforce they are in the ranges -24..=24, 0..=59
+        // and 0..=59, respectively.
+        assert!(
+            -89999 <= seconds && seconds <= 89999,
+            "offset seconds {seconds} is out of range"
+        );
+        Ok(seconds)
+    }
+
+    /// Parses a POSIX DST transition rule.
+    ///
+    /// This assumes the parser is positioned at the first byte in the
+    /// rule. That is, it comes immediately after the DST abbreviation or
+    /// its optional offset.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// DST transition rule. In typical cases, this corresponds to the end
+    /// of the TZ string.
+    fn parse_rule(&self) -> Result<PosixRule, Error> {
+        let start = self.parse_posix_datetime_spec().map_err(|e| {
+            err!("failed to parse start of DST transition rule: {e}")
+        })?;
+        if self.maybe_byte() != Some(b',') || !self.bump() {
+            return Err(err!(
+                "expected end of DST rule after parsing the start \
+                 of the DST rule"
+            ));
+        }
+        let end = self.parse_posix_datetime_spec().map_err(|e| {
+            err!("failed to parse end of DST transition rule: {e}")
+        })?;
+        Ok(PosixRule { start, end })
+    }
+
+    /// Parses a POSIX datetime specification.
+    ///
+    /// This assumes the parser is position at the first byte where a
+    /// datetime specification is expected to occur.
+    ///
+    /// Upon success, the parser will be positioned after the datetime
+    /// specification. This will either be immediately after the date, or
+    /// if it's present, the time part of the specification.
+    fn parse_posix_datetime_spec(&self) -> Result<PosixDayTime, Error> {
+        let mut daytime = PosixDayTime {
+            date: self.parse_posix_date_spec()?,
+            time: 2 * 3600, // the default if the time is absent
+        };
+        if self.maybe_byte() != Some(b'/') {
+            return Ok(daytime);
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected time specification after '/' following a date
+                 specification in a POSIX time zone DST transition rule",
+            ));
+        }
+        daytime.time = self.parse_posix_time_spec()?;
+        Ok(daytime)
+    }
+
+    /// Parses a POSIX date specification.
+    ///
+    /// This assumes the parser is positioned at the first byte of the date
+    /// specification. This can be `J` (for one based Julian day without
+    /// leap days), `M` (for "weekday of month") or a digit starting the
+    /// zero based Julian day with leap days. This routine will validate
+    /// that the position points to one of these possible values. That is,
+    /// the caller doesn't need to parse the `M` or the `J` or the leading
+    /// digit. The caller should just call this routine when it *expect* a
+    /// date specification to follow.
+    ///
+    /// Upon success, the parser will be positioned immediately after the
+    /// date specification.
+    fn parse_posix_date_spec(&self) -> Result<PosixDay, Error> {
+        match self.byte() {
+            b'J' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected one-based Julian day after 'J' in date \
+                         specification of a POSIX time zone DST \
+                         transition rule, but got the end of the string \
+                         instead"
+                    ));
+                }
+                Ok(PosixDay::JulianOne(self.parse_posix_julian_day_no_leap()?))
+            }
+            b'0'..=b'9' => Ok(PosixDay::JulianZero(
+                self.parse_posix_julian_day_with_leap()?,
+            )),
+            b'M' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected month-week-weekday after 'M' in date \
+                         specification of a POSIX time zone DST \
+                         transition rule, but got the end of the string \
+                         instead"
+                    ));
+                }
+                let (month, week, weekday) = self.parse_weekday_of_month()?;
+                Ok(PosixDay::WeekdayOfMonth { month, week, weekday })
+            }
+            _ => Err(err!(
+                "expected 'J', a digit or 'M' at the beginning of a date \
+                 specification of a POSIX time zone DST transition rule, \
+                 but got `{}` instead",
+                Byte(self.byte()),
+            )),
+        }
+    }
+
+    /// Parses a POSIX Julian day that does not include leap days
+    /// (`1 <= n <= 365`).
+    ///
+    /// This assumes the parser is positioned just after the `J` and at the
+    /// first digit of the Julian day. Upon success, the parser will be
+    /// positioned immediately following the day number.
+    fn parse_posix_julian_day_no_leap(&self) -> Result<i16, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid one based Julian day: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "one based Julian day `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 365) {
+            return Err(err!(
+                "parsed one based Julian day `{number}`, \
+                 but one based Julian day in POSIX time zone \
+                 must be in range 1..=365",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a POSIX Julian day that includes leap days (`0 <= n <=
+    /// 365`).
+    ///
+    /// This assumes the parser is positioned at the first digit of the
+    /// Julian day. Upon success, the parser will be positioned immediately
+    /// following the day number.
+    fn parse_posix_julian_day_with_leap(&self) -> Result<i16, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid zero based Julian day: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "zero based Julian day `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 365) {
+            return Err(err!(
+                "parsed zero based Julian day `{number}`, \
+                 but zero based Julian day in POSIX time zone \
+                 must be in range 0..=365",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a POSIX "weekday of month" specification.
+    ///
+    /// This assumes the parser is positioned just after the `M` byte and
+    /// at the first digit of the month. Upon success, the parser will be
+    /// positioned immediately following the "weekday of the month" that
+    /// was parsed.
+    ///
+    /// The tuple returned is month (1..=12), week (1..=5) and weekday
+    /// (0..=6 with 0=Sunday).
+    fn parse_weekday_of_month(&self) -> Result<(i8, i8, i8), Error> {
+        let month = self.parse_month()?;
+        if self.maybe_byte() != Some(b'.') {
+            return Err(err!(
+                "expected '.' after month `{month}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected week after month `{month}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        let week = self.parse_week()?;
+        if self.maybe_byte() != Some(b'.') {
+            return Err(err!(
+                "expected '.' after week `{week}` in POSIX time zone rule"
+            ));
+        }
+        if !self.bump() {
+            return Err(err!(
+                "expected day-of-week after week `{week}` in \
+                 POSIX time zone rule"
+            ));
+        }
+        let weekday = self.parse_weekday()?;
+        Ok((month, week, weekday))
+    }
+
+    /// This parses a POSIX time specification in the format
+    /// `[+/-]hh?[:mm[:ss]]`.
+    ///
+    /// This assumes the parser is positioned at the first `h` (or the
+    /// sign, if present). Upon success, the parser will be positioned
+    /// immediately following the end of the time specification.
+    fn parse_posix_time_spec(&self) -> Result<i32, Error> {
+        let (sign, hour) = if self.ianav3plus {
+            let sign = self
+                .parse_optional_sign()
+                .map_err(|e| {
+                    err!(
+                        "failed to parse sign for transition time \
+                         in POSIX time zone string: {e}",
+                    )
+                })?
+                .unwrap_or(1);
+            let hour = self.parse_hour_ianav3plus()?;
+            (sign, hour)
+        } else {
+            (1, i16::from(self.parse_hour_posix()?))
+        };
+        let (mut minute, mut second) = (0, 0);
+        if self.maybe_byte() == Some(b':') {
+            if !self.bump() {
+                return Err(err!(
+                    "incomplete transition time in \
+                     POSIX time zone string (missing minutes)",
+                ));
+            }
+            minute = self.parse_minute()?;
+            if self.maybe_byte() == Some(b':') {
+                if !self.bump() {
+                    return Err(err!(
+                        "incomplete transition time in \
+                         POSIX time zone string (missing seconds)",
+                    ));
+                }
+                second = self.parse_second()?;
+            }
+        }
+        let mut seconds = i32::from(hour) * 3600;
+        seconds += i32::from(minute) * 60;
+        seconds += i32::from(second);
+        seconds *= i32::from(sign);
+        // Must be true because the parsing routines for hours, minutes
+        // and seconds enforce they are in the ranges -167..=167, 0..=59
+        // and 0..=59, respectively.
+        assert!(-604799 <= seconds && seconds <= 604799);
+        Ok(seconds)
+    }
+
+    /// Parses a month.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the month (which may contain
+    /// two digits).
+    fn parse_month(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_upto_n_digits(2)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "month `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 12) {
+            return Err(err!(
+                "parsed month `{number}`, but month in \
+                 POSIX time zone must be in range 1..=12",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a week-of-month number.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the week digit.
+    fn parse_week(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_exactly_n_digits(1)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "week `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(1 <= number && number <= 5) {
+            return Err(err!(
+                "parsed week `{number}`, but week in \
+                 POSIX time zone must be in range 1..=5"
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a weekday number.
+    ///
+    /// This is expected to be positioned at the first digit. Upon success,
+    /// the parser will be positioned after the week digit.
+    ///
+    /// The weekday returned is guaranteed to be in the range `0..=6`, with
+    /// `0` corresponding to Sunday.
+    fn parse_weekday(&self) -> Result<i8, Error> {
+        let number = self.parse_number_with_exactly_n_digits(1)?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "weekday `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 6) {
+            return Err(err!(
+                "parsed weekday `{number}`, but weekday in \
+                 POSIX time zone must be in range `0..=6` \
+                 (with `0` corresponding to Sunday)",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses an hour from a POSIX time specification with the IANA
+    /// v3+ extension. That is, the hour may be in the range `0..=167`.
+    /// (Callers should parse an optional sign preceding the hour digits
+    /// when IANA V3+ parsing is enabled.)
+    ///
+    /// The hour is allowed to be a single digit (unlike minutes or
+    /// seconds).
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first hour digit should occur. Upon success, the parser will be
+    /// positioned immediately after the last hour digit.
+    fn parse_hour_ianav3plus(&self) -> Result<i16, Error> {
+        // Callers should only be using this method when IANA v3+ parsing
+        // is enabled.
+        assert!(self.ianav3plus);
+        let number = self
+            .parse_number_with_upto_n_digits(3)
+            .map_err(|e| err!("invalid hour digits: {e}"))?;
+        let number = i16::try_from(number).map_err(|_| {
+            err!(
+                "hour `{number}` in POSIX time zone \
+                 does not fit into 16-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 167) {
+            // The error message says -167 but the check above uses 0.
+            // This is because the caller is responsible for parsing
+            // the sign.
+            return Err(err!(
+                "parsed hour `{number}`, but hour in IANA v3+ \
+                 POSIX time zone must be in range `-167..=167`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses an hour from a POSIX time specification, with the allowed
+    /// range being `0..=24`.
+    ///
+    /// The hour is allowed to be a single digit (unlike minutes or
+    /// seconds).
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first hour digit should occur. Upon success, the parser will be
+    /// positioned immediately after the last hour digit.
+    fn parse_hour_posix(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_upto_n_digits(2)
+            .map_err(|e| err!("invalid hour digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "hour `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 24) {
+            return Err(err!(
+                "parsed hour `{number}`, but hour in \
+                 POSIX time zone must be in range `0..=24`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a minute from a POSIX time specification.
+    ///
+    /// The minute must be exactly two digits.
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first minute digit should occur. Upon success, the parser will be
+    /// positioned immediately after the second minute digit.
+    fn parse_minute(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_exactly_n_digits(2)
+            .map_err(|e| err!("invalid minute digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "minute `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 59) {
+            return Err(err!(
+                "parsed minute `{number}`, but minute in \
+                 POSIX time zone must be in range `0..=59`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a second from a POSIX time specification.
+    ///
+    /// The second must be exactly two digits.
+    ///
+    /// This assumes the parser is positioned at the position where the
+    /// first second digit should occur. Upon success, the parser will be
+    /// positioned immediately after the second second digit.
+    fn parse_second(&self) -> Result<i8, Error> {
+        let number = self
+            .parse_number_with_exactly_n_digits(2)
+            .map_err(|e| err!("invalid second digits: {e}"))?;
+        let number = i8::try_from(number).map_err(|_| {
+            err!(
+                "second `{number}` in POSIX time zone \
+                 does not fit into 8-bit integer"
+            )
+        })?;
+        if !(0 <= number && number <= 59) {
+            return Err(err!(
+                "parsed second `{number}`, but second in \
+                 POSIX time zone must be in range `0..=59`",
+            ));
+        }
+        Ok(number)
+    }
+
+    /// Parses a signed 64-bit integer expressed in exactly `n` digits.
+    ///
+    /// If `n` digits could not be found (or if the `TZ` string ends before
+    /// `n` digits could be found), then this returns an error.
+    ///
+    /// This assumes that `n >= 1` and that the parser is positioned at the
+    /// first digit. Upon success, the parser is positioned immediately
+    /// after the `n`th digit.
+    fn parse_number_with_exactly_n_digits(
+        &self,
+        n: usize,
+    ) -> Result<i32, Error> {
+        assert!(n >= 1, "numbers must have at least 1 digit");
+        let start = self.pos();
+        let mut number: i32 = 0;
+        for i in 0..n {
+            if self.is_done() {
+                return Err(err!("expected {n} digits, but found {i}"));
+            }
+            let byte = self.byte();
+            let digit = match byte.checked_sub(b'0') {
+                None => {
+                    return Err(err!(
+                        "invalid digit, expected 0-9 but got {}",
+                        Byte(byte),
+                    ));
+                }
+                Some(digit) if digit > 9 => {
+                    return Err(err!(
+                        "invalid digit, expected 0-9 but got {}",
+                        Byte(byte),
+                    ))
+                }
+                Some(digit) => {
+                    debug_assert!((0..=9).contains(&digit));
+                    i32::from(digit)
+                }
+            };
+            number = number
+                .checked_mul(10)
+                .and_then(|n| n.checked_add(digit))
+                .ok_or_else(|| {
+                    err!(
+                        "number `{}` too big to parse into 64-bit integer",
+                        Bytes(&self.tz[start..i]),
+                    )
+                })?;
+            self.bump();
+        }
+        Ok(number)
+    }
+
+    /// Parses a signed 64-bit integer expressed with up to `n` digits and
+    /// at least 1 digit.
+    ///
+    /// This assumes that `n >= 1` and that the parser is positioned at the
+    /// first digit. Upon success, the parser is position immediately after
+    /// the last digit (which can be at most `n`).
+    fn parse_number_with_upto_n_digits(&self, n: usize) -> Result<i32, Error> {
+        assert!(n >= 1, "numbers must have at least 1 digit");
+        let start = self.pos();
+        let mut number: i32 = 0;
+        for i in 0..n {
+            if self.is_done() || !self.byte().is_ascii_digit() {
+                if i == 0 {
+                    return Err(err!("invalid number, no digits found"));
+                }
+                break;
+            }
+            let digit = i32::from(self.byte() - b'0');
+            number = number
+                .checked_mul(10)
+                .and_then(|n| n.checked_add(digit))
+                .ok_or_else(|| {
+                    err!(
+                        "number `{}` too big to parse into 64-bit integer",
+                        Bytes(&self.tz[start..i]),
+                    )
+                })?;
+            self.bump();
+        }
+        Ok(number)
+    }
+
+    /// Parses an optional sign.
+    ///
+    /// This assumes the parser is positioned at the position where a
+    /// positive or negative sign is permitted. If one exists, then it
+    /// is consumed and returned. Moreover, if one exists, then this
+    /// guarantees that it is not the last byte in the input. That is, upon
+    /// success, it is valid to call `self.byte()`.
+    fn parse_optional_sign(&self) -> Result<Option<i8>, Error> {
+        if self.is_done() {
+            return Ok(None);
+        }
+        Ok(match self.byte() {
+            b'-' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected digit after '-' sign, \
+                         but got end of input",
+                    ));
+                }
+                Some(-1)
+            }
+            b'+' => {
+                if !self.bump() {
+                    return Err(err!(
+                        "expected digit after '+' sign, \
+                         but got end of input",
+                    ));
+                }
+                Some(1)
+            }
+            _ => None,
+        })
+    }
+}
+
+/// Helper routines for parsing a POSIX `TZ` string.
+impl<'s> Parser<'s> {
+    /// Bump the parser to the next byte.
+    ///
+    /// If the end of the input has been reached, then `false` is returned.
+    fn bump(&self) -> bool {
+        if self.is_done() {
+            return false;
+        }
+        self.pos.set(
+            self.pos().checked_add(1).expect("pos cannot overflow usize"),
+        );
+        !self.is_done()
+    }
+
+    /// Returns true if the next call to `bump` would return false.
+    fn is_done(&self) -> bool {
+        self.pos() == self.tz.len()
+    }
+
+    /// Return the byte at the current position of the parser.
+    ///
+    /// This panics if the parser is positioned at the end of the TZ
+    /// string.
+    fn byte(&self) -> u8 {
+        self.tz[self.pos()]
+    }
+
+    /// Return the byte at the current position of the parser. If the TZ
+    /// string has been exhausted, then this returns `None`.
+    fn maybe_byte(&self) -> Option<u8> {
+        self.tz.get(self.pos()).copied()
+    }
+
+    /// Return the current byte offset of the parser.
+    ///
+    /// The offset starts at `0` from the beginning of the TZ string.
+    fn pos(&self) -> usize {
+        self.pos.get()
+    }
+
+    /// Returns the remaining bytes of the TZ string.
+    ///
+    /// This includes `self.byte()`. It may be empty.
+    fn remaining(&self) -> &'s [u8] {
+        &self.tz[self.pos()..]
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn parser(s: &str) -> Parser<'_> {
+        Parser::new(s.as_bytes())
+    }
+
+    #[test]
+    fn parse() {
+        let p = parser("NZST-12NZDT,J60,J300");
+        assert_eq!(
+            p.parse().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
+        assert!(p.parse().is_err());
+    }
+
+    #[test]
+    fn parse_posix_time_zone() {
+        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 9,
+                                week: 5,
+                                weekday: 0,
+                            },
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 4,
+                                week: 1,
+                                weekday: 0,
+                            },
+                            time: 3 * 60 * 60,
+                        },
+                    })
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 9,
+                                week: 5,
+                                weekday: 0,
+                            },
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::WeekdayOfMonth {
+                                month: 4,
+                                week: 1,
+                                weekday: 0,
+                            },
+                            time: 3 * 60 * 60,
+                        },
+                    })
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+
+        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
+        assert_eq!(
+            p.parse_posix_time_zone().unwrap(),
+            PosixTimeZone {
+                std_abbrev: "NZST".into(),
+                std_offset: 12 * 60 * 60,
+                dst: Some(PosixDst {
+                    abbrev: "NZDT".into(),
+                    offset: 13 * 60 * 60,
+                    rule: Some(PosixRule {
+                        start: PosixDayTime {
+                            date: PosixDay::JulianOne(60),
+                            time: 2 * 60 * 60,
+                        },
+                        end: PosixDayTime {
+                            date: PosixDay::JulianOne(300),
+                            time: 2 * 60 * 60,
+                        },
+                    }),
+                }),
+            },
+        );
+    }
+
+    #[test]
+    fn parse_posix_dst() {
+        let p = Parser::new("NZDT,M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 13 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::WeekdayOfMonth {
+                            month: 9,
+                            week: 5,
+                            weekday: 0,
+                        },
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::WeekdayOfMonth {
+                            month: 4,
+                            week: 1,
+                            weekday: 0,
+                        },
+                        time: 3 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 13 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT-7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: 7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT+7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: -7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT7,J60,J300");
+        assert_eq!(
+            p.parse_posix_dst(12 * 60 * 60).unwrap(),
+            PosixDst {
+                abbrev: "NZDT".into(),
+                offset: -7 * 60 * 60,
+                rule: Some(PosixRule {
+                    start: PosixDayTime {
+                        date: PosixDay::JulianOne(60),
+                        time: 2 * 60 * 60,
+                    },
+                    end: PosixDayTime {
+                        date: PosixDay::JulianOne(300),
+                        time: 2 * 60 * 60,
+                    },
+                }),
+            },
+        );
+
+        let p = Parser::new("NZDT7,");
+        assert!(p.parse_posix_dst(12 * 60 * 60).is_err());
+
+        let p = Parser::new("NZDT7!");
+        assert!(p.parse_posix_dst(12 * 60 * 60).is_err());
+    }
+
+    #[test]
+    fn parse_abbreviation() {
+        let p = Parser::new("ABC");
+        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("<ABC>");
+        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("<+09>");
+        assert_eq!(p.parse_abbreviation().unwrap(), "+09");
+
+        let p = Parser::new("+09");
+        assert!(p.parse_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_unquoted_abbreviation() {
+        let p = Parser::new("ABC");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABCXYZ");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABCXYZ");
+
+        let p = Parser::new("ABC123");
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
+
+        let tz = "a".repeat(30);
+        let p = Parser::new(&tz);
+        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz);
+
+        let p = Parser::new("a");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new("ab");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new("ab1");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let tz = "a".repeat(31);
+        let p = Parser::new(&tz);
+        assert!(p.parse_unquoted_abbreviation().is_err());
+
+        let p = Parser::new(b"ab\xFFcd");
+        assert!(p.parse_unquoted_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_quoted_abbreviation() {
+        // The inputs look a little funny here, but that's because
+        // 'parse_quoted_abbreviation' starts after the opening quote
+        // has been parsed.
+
+        let p = Parser::new("ABC>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABCXYZ>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABCXYZ");
+
+        let p = Parser::new("ABC>123");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
+
+        let p = Parser::new("ABC123>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC123");
+
+        let p = Parser::new("ab1>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ab1");
+
+        let p = Parser::new("+09>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "+09");
+
+        let p = Parser::new("-09>");
+        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "-09");
+
+        let tz = alloc::format!("{}>", "a".repeat(30));
+        let p = Parser::new(&tz);
+        assert_eq!(
+            p.parse_quoted_abbreviation().unwrap(),
+            tz.trim_end_matches(">")
+        );
+
+        let p = Parser::new("a>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ab>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let tz = alloc::format!("{}>", "a".repeat(31));
+        let p = Parser::new(&tz);
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new(b"ab\xFFcd>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ABC");
+        assert!(p.parse_quoted_abbreviation().is_err());
+
+        let p = Parser::new("ABC!>");
+        assert!(p.parse_quoted_abbreviation().is_err());
+    }
+
+    #[test]
+    fn parse_posix_offset() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_posix_offset().unwrap(), -5 * 60 * 60);
+
+        let p = Parser::new("+5");
+        assert_eq!(p.parse_posix_offset().unwrap(), -5 * 60 * 60);
+
+        let p = Parser::new("-5");
+        assert_eq!(p.parse_posix_offset().unwrap(), 5 * 60 * 60);
+
+        let p = Parser::new("-12:34:56");
+        assert_eq!(
+            p.parse_posix_offset().unwrap(),
+            12 * 60 * 60 + 34 * 60 + 56,
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+a");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("-25");
+        assert!(p.parse_posix_offset().is_err());
+
+        let p = Parser::new("+25");
+        assert!(p.parse_posix_offset().is_err());
+
+        // This checks that we don't accidentally permit IANA rules for
+        // offset parsing. Namely, the IANA tzfile v3+ extension only applies
+        // to transition times. But since POSIX says that the "time" for the
+        // offset and transition is the same format, it would be an easy
+        // implementation mistake to implement the more flexible rule for
+        // IANA and have it accidentally also apply to the offset. So we check
+        // that it doesn't here.
+        let p = Parser { ianav3plus: true, ..Parser::new("25") };
+        assert!(p.parse_posix_offset().is_err());
+        let p = Parser { ianav3plus: true, ..Parser::new("+25") };
+        assert!(p.parse_posix_offset().is_err());
+        let p = Parser { ianav3plus: true, ..Parser::new("-25") };
+        assert!(p.parse_posix_offset().is_err());
+    }
+
+    #[test]
+    fn parse_rule() {
+        let p = Parser::new("M9.5.0,M4.1.0/3");
+        assert_eq!(
+            p.parse_rule().unwrap(),
+            PosixRule {
+                start: PosixDayTime {
+                    date: PosixDay::WeekdayOfMonth {
+                        month: 9,
+                        week: 5,
+                        weekday: 0,
+                    },
+                    time: 2 * 60 * 60,
+                },
+                end: PosixDayTime {
+                    date: PosixDay::WeekdayOfMonth {
+                        month: 4,
+                        week: 1,
+                        weekday: 0,
+                    },
+                    time: 3 * 60 * 60,
+                },
+            },
+        );
+
+        let p = Parser::new("M9.5.0");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new(",M9.5.0,M4.1.0/3");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new("M9.5.0/");
+        assert!(p.parse_rule().is_err());
+
+        let p = Parser::new("M9.5.0,M4.1.0/");
+        assert!(p.parse_rule().is_err());
+    }
+
+    #[test]
+    fn parse_posix_datetime_spec() {
+        let p = Parser::new("J1");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime { date: PosixDay::JulianOne(1), time: 2 * 60 * 60 },
+        );
+
+        let p = Parser::new("J1/3");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime { date: PosixDay::JulianOne(1), time: 3 * 60 * 60 },
+        );
+
+        let p = Parser::new("M4.1.0/3");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime {
+                date: PosixDay::WeekdayOfMonth {
+                    month: 4,
+                    week: 1,
+                    weekday: 0,
+                },
+                time: 3 * 60 * 60,
+            },
+        );
+
+        let p = Parser::new("1/3:45:05");
+        assert_eq!(
+            p.parse_posix_datetime_spec().unwrap(),
+            PosixDayTime {
+                date: PosixDay::JulianZero(1),
+                time: 3 * 60 * 60 + 45 * 60 + 5,
+            },
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("J1/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("1/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+
+        let p = Parser::new("M4.1.0/");
+        assert!(p.parse_posix_datetime_spec().is_err());
+    }
+
+    #[test]
+    fn parse_posix_date_spec() {
+        let p = Parser::new("J1");
+        assert_eq!(p.parse_posix_date_spec().unwrap(), PosixDay::JulianOne(1));
+        let p = Parser::new("J365");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianOne(365)
+        );
+
+        let p = Parser::new("0");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(0)
+        );
+        let p = Parser::new("1");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(1)
+        );
+        let p = Parser::new("365");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::JulianZero(365)
+        );
+
+        let p = Parser::new("M9.5.0");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 0 },
+        );
+        let p = Parser::new("M9.5.6");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 },
+        );
+        let p = Parser::new("M09.5.6");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 9, week: 5, weekday: 6 },
+        );
+        let p = Parser::new("M12.1.1");
+        assert_eq!(
+            p.parse_posix_date_spec().unwrap(),
+            PosixDay::WeekdayOfMonth { month: 12, week: 1, weekday: 1 },
+        );
+
+        let p = Parser::new("a");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("j");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("m");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("n");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("J366");
+        assert!(p.parse_posix_date_spec().is_err());
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_date_spec().is_err());
+    }
+
+    #[test]
+    fn parse_posix_julian_day_no_leap() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
+
+        let p = Parser::new("001");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
+
+        let p = Parser::new("365");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
+
+        let p = Parser::new("3655");
+        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
+
+        let p = Parser::new("0");
+        assert!(p.parse_posix_julian_day_no_leap().is_err());
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_julian_day_no_leap().is_err());
+    }
+
+    #[test]
+    fn parse_posix_julian_day_with_leap() {
+        let p = Parser::new("0");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 0);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
+
+        let p = Parser::new("001");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
+
+        let p = Parser::new("365");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
+
+        let p = Parser::new("3655");
+        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
+
+        let p = Parser::new("366");
+        assert!(p.parse_posix_julian_day_with_leap().is_err());
+    }
+
+    #[test]
+    fn parse_weekday_of_month() {
+        let p = Parser::new("9.5.0");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 5, 0));
+
+        let p = Parser::new("9.1.6");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6));
+
+        let p = Parser::new("09.1.6");
+        assert_eq!(p.parse_weekday_of_month().unwrap(), (9, 1, 6));
+
+        let p = Parser::new("9");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5.");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("0.5.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("13.5.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.0.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.6.0");
+        assert!(p.parse_weekday_of_month().is_err());
+
+        let p = Parser::new("9.5.7");
+        assert!(p.parse_weekday_of_month().is_err());
+    }
+
+    #[test]
+    fn parse_posix_time_spec() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 5 * 60 * 60);
+
+        let p = Parser::new("22");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 22 * 60 * 60);
+
+        let p = Parser::new("02");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 2 * 60 * 60);
+
+        let p = Parser::new("5:45");
+        assert_eq!(p.parse_posix_time_spec().unwrap(), 5 * 60 * 60 + 45 * 60);
+
+        let p = Parser::new("5:45:12");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser::new("5:45:129");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser::new("5:45:12:");
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            5 * 60 * 60 + 45 * 60 + 12
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            -(5 * 60 * 60 + 45 * 60 + 12)
+        );
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") };
+        assert_eq!(
+            p.parse_posix_time_spec().unwrap(),
+            -(167 * 60 * 60 + 45 * 60 + 12),
+        );
+
+        let p = Parser::new("25");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:2");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:23:5");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser::new("12:23:");
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("168") };
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("-168") };
+        assert!(p.parse_posix_time_spec().is_err());
+
+        let p = Parser { ianav3plus: true, ..Parser::new("+168") };
+        assert!(p.parse_posix_time_spec().is_err());
+    }
+
+    #[test]
+    fn parse_month() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_month().unwrap(), 1);
+
+        // Should this be allowed? POSIX spec is unclear.
+        // We allow it because our parse does stop at 2
+        // digits, so this seems harmless. Namely, '001'
+        // results in an error.
+        let p = Parser::new("01");
+        assert_eq!(p.parse_month().unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_month().unwrap(), 12);
+
+        let p = Parser::new("0");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("00");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("001");
+        assert!(p.parse_month().is_err());
+
+        let p = Parser::new("13");
+        assert!(p.parse_month().is_err());
+    }
+
+    #[test]
+    fn parse_week() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_week().unwrap(), 1);
+
+        let p = Parser::new("5");
+        assert_eq!(p.parse_week().unwrap(), 5);
+
+        let p = Parser::new("55");
+        assert_eq!(p.parse_week().unwrap(), 5);
+
+        let p = Parser::new("0");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("6");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("00");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("01");
+        assert!(p.parse_week().is_err());
+
+        let p = Parser::new("05");
+        assert!(p.parse_week().is_err());
+    }
+
+    #[test]
+    fn parse_weekday() {
+        let p = Parser::new("0");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_weekday().unwrap(), 1);
+
+        let p = Parser::new("6");
+        assert_eq!(p.parse_weekday().unwrap(), 6);
+
+        let p = Parser::new("00");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("06");
+        assert_eq!(p.parse_weekday().unwrap(), 0);
+
+        let p = Parser::new("60");
+        assert_eq!(p.parse_weekday().unwrap(), 6);
+
+        let p = Parser::new("7");
+        assert!(p.parse_weekday().is_err());
+    }
+
+    #[test]
+    fn parse_hour_posix() {
+        let p = Parser::new("5");
+        assert_eq!(p.parse_hour_posix().unwrap(), 5);
+
+        let p = Parser::new("0");
+        assert_eq!(p.parse_hour_posix().unwrap(), 0);
+
+        let p = Parser::new("00");
+        assert_eq!(p.parse_hour_posix().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_hour_posix().unwrap(), 24);
+
+        let p = Parser::new("100");
+        assert_eq!(p.parse_hour_posix().unwrap(), 10);
+
+        let p = Parser::new("25");
+        assert!(p.parse_hour_posix().is_err());
+
+        let p = Parser::new("99");
+        assert!(p.parse_hour_posix().is_err());
+    }
+
+    #[test]
+    fn parse_hour_ianav3plus() {
+        let new = |input| Parser { ianav3plus: true, ..Parser::new(input) };
+
+        let p = new("5");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 5);
+
+        let p = new("0");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("00");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("000");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
+
+        let p = new("24");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 24);
+
+        let p = new("100");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
+
+        let p = new("1000");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
+
+        let p = new("167");
+        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 167);
+
+        let p = new("168");
+        assert!(p.parse_hour_ianav3plus().is_err());
+
+        let p = new("999");
+        assert!(p.parse_hour_ianav3plus().is_err());
+    }
+
+    #[test]
+    fn parse_minute() {
+        let p = Parser::new("00");
+        assert_eq!(p.parse_minute().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_minute().unwrap(), 24);
+
+        let p = Parser::new("59");
+        assert_eq!(p.parse_minute().unwrap(), 59);
+
+        let p = Parser::new("599");
+        assert_eq!(p.parse_minute().unwrap(), 59);
+
+        let p = Parser::new("0");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("9");
+        assert!(p.parse_minute().is_err());
+
+        let p = Parser::new("60");
+        assert!(p.parse_minute().is_err());
+    }
+
+    #[test]
+    fn parse_second() {
+        let p = Parser::new("00");
+        assert_eq!(p.parse_second().unwrap(), 0);
+
+        let p = Parser::new("24");
+        assert_eq!(p.parse_second().unwrap(), 24);
+
+        let p = Parser::new("59");
+        assert_eq!(p.parse_second().unwrap(), 59);
+
+        let p = Parser::new("599");
+        assert_eq!(p.parse_second().unwrap(), 59);
+
+        let p = Parser::new("0");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("9");
+        assert!(p.parse_second().is_err());
+
+        let p = Parser::new("60");
+        assert!(p.parse_second().is_err());
+    }
+
+    #[test]
+    fn parse_number_with_exactly_n_digits() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_exactly_n_digits(1).unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("123");
+        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("");
+        assert!(p.parse_number_with_exactly_n_digits(1).is_err());
+
+        let p = Parser::new("1");
+        assert!(p.parse_number_with_exactly_n_digits(2).is_err());
+
+        let p = Parser::new("12");
+        assert!(p.parse_number_with_exactly_n_digits(3).is_err());
+    }
+
+    #[test]
+    fn parse_number_with_upto_n_digits() {
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_upto_n_digits(1).unwrap(), 1);
+
+        let p = Parser::new("1");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 1);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("12");
+        assert_eq!(p.parse_number_with_upto_n_digits(3).unwrap(), 12);
+
+        let p = Parser::new("123");
+        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
+
+        let p = Parser::new("");
+        assert!(p.parse_number_with_upto_n_digits(1).is_err());
+
+        let p = Parser::new("a");
+        assert!(p.parse_number_with_upto_n_digits(1).is_err());
+    }
+}
diff --git a/src/shared/tzif.rs b/src/shared/tzif.rs
new file mode 100644
index 00000000..de010f52
--- /dev/null
+++ b/src/shared/tzif.rs
@@ -0,0 +1,789 @@
+#![allow(warnings)]
+
+use alloc::{string::String, vec};
+
+use super::{
+    util::{Byte, Bytes},
+    PosixTimeZone, TzifFixed, TzifIndicator, TzifLocalTimeType, TzifOwned,
+    TzifTransition,
+};
+
+macro_rules! err {
+    ($($tt:tt)*) => {{
+        self::Error(alloc::format!($($tt)*))
+    }}
+}
+
+// These are Jiff min and max timestamp (in seconds) values.
+//
+// The TZif parser will clamp timestamps to this range. It's
+// not ideal, but Jiff can't handle values outside of this range
+// and completely refusing to use TZif data with pathological
+// timestamps in typically irrelevant transitions is bad juju.
+//
+// Ref: https://github.com/BurntSushi/jiff/issues/163
+// Ref: https://github.com/BurntSushi/jiff/pull/164
+const TIMESTAMP_MIN: i64 = -377705023201;
+const TIMESTAMP_MAX: i64 = 253402207200;
+
+// Similarly for offsets, although in this case, if we find
+// an offset outside of this range, we do actually error. This
+// is because it could result in true incorrect datetimes for
+// actual transitions.
+//
+// But our supported offset range is `-25:59:59..=+25:59:59`.
+// There's no real time zone with offsets even close to those
+// boundaries.
+//
+// If there is pathological data that we should ignore, then
+// we should wait for a real bug report in order to determine
+// the right way to ignore/clamp it.
+const OFFSET_MIN: i32 = -93599;
+const OFFSET_MAX: i32 = 93599;
+
+/// An error that can be returned when parsing.
+#[derive(Debug)]
+pub struct Error(String);
+
+impl core::fmt::Display for Error {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        core::fmt::Display::fmt(&self.0, f)
+    }
+}
+
+impl TzifOwned {
+    /// Parses the given data as a TZif formatted file.
+    ///
+    /// The name given is attached to the `Tzif` value returned, but is
+    /// otherwise not significant.
+    ///
+    /// If the given data is not recognized to be valid TZif, then an error is
+    /// returned.
+    ///
+    /// In general, callers may assume that it is safe to pass arbitrary or
+    /// even untrusted data to this function and count on it not panicking
+    /// or using resources that aren't limited to a small constant factor of
+    /// the size of the data itself. That is, callers can reliably limit the
+    /// resources used by limiting the size of the data given to this parse
+    /// function.
+    pub(crate) fn parse(
+        name: Option<String>,
+        bytes: &[u8],
+    ) -> Result<TzifOwned, Error> {
+        let original = bytes;
+        let name = name.into();
+        let (header32, rest) = Header::parse(4, bytes)
+            .map_err(|e| err!("failed to parse 32-bit header: {e}"))?;
+        let (mut tzif, rest) = if header32.version == 0 {
+            TzifOwned::parse32(name, header32, rest)?
+        } else {
+            TzifOwned::parse64(name, header32, rest)?
+        };
+        // Compute the checksum using the entire contents of the TZif data.
+        let tzif_raw_len = (rest.as_ptr() as usize)
+            .checked_sub(original.as_ptr() as usize)
+            .unwrap();
+        let tzif_raw_bytes = &original[..tzif_raw_len];
+        tzif.fixed.checksum = super::crc32::sum(tzif_raw_bytes);
+        Ok(tzif)
+    }
+
+    fn parse32<'b>(
+        name: Option<String>,
+        header32: Header,
+        bytes: &'b [u8],
+    ) -> Result<(TzifOwned, &'b [u8]), Error> {
+        let mut tzif = TzifOwned {
+            fixed: TzifFixed {
+                name,
+                version: header32.version,
+                // filled in later
+                checksum: 0,
+                designations: String::new(),
+                posix_tz: None,
+            },
+            types: vec![],
+            transitions: vec![],
+        };
+        let rest = tzif.parse_transitions(&header32, bytes)?;
+        let rest = tzif.parse_transition_types(&header32, rest)?;
+        let rest = tzif.parse_local_time_types(&header32, rest)?;
+        let rest = tzif.parse_time_zone_designations(&header32, rest)?;
+        let rest = tzif.parse_leap_seconds(&header32, rest)?;
+        let rest = tzif.parse_indicators(&header32, rest)?;
+        Ok((tzif, rest))
+    }
+
+    fn parse64<'b>(
+        name: Option<String>,
+        header32: Header,
+        bytes: &'b [u8],
+    ) -> Result<(TzifOwned, &'b [u8]), Error> {
+        let (_, rest) = try_split_at(
+            "V1 TZif data block",
+            bytes,
+            header32.data_block_len()?,
+        )?;
+        let (header64, rest) = Header::parse(8, rest)
+            .map_err(|e| err!("failed to parse 64-bit header: {e}"))?;
+        let mut tzif = TzifOwned {
+            fixed: TzifFixed {
+                name,
+                version: header64.version,
+                // filled in later
+                checksum: 0,
+                designations: String::new(),
+                posix_tz: None,
+            },
+            types: vec![],
+            transitions: vec![],
+        };
+        let rest = tzif.parse_transitions(&header64, rest)?;
+        let rest = tzif.parse_transition_types(&header64, rest)?;
+        let rest = tzif.parse_local_time_types(&header64, rest)?;
+        let rest = tzif.parse_time_zone_designations(&header64, rest)?;
+        let rest = tzif.parse_leap_seconds(&header64, rest)?;
+        let rest = tzif.parse_indicators(&header64, rest)?;
+        let rest = tzif.parse_footer(&header64, rest)?;
+        // Note that we specifically and unfortunately do not "validate"
+        // the POSIX TZ string here. We *should* check that it is
+        // consistent with the last transition. Since:
+        //
+        // RFC 8536 says, "If the string is nonempty and one or more
+        // transitions appear in the version 2+ data, the string MUST be
+        // consistent with the last version 2+ transition."
+        //
+        // But in this context, we don't have any of the infrastructure
+        // to actually do TZ operations on a POSIX time zone. It requires
+        // civil datetimes and a bunch of other bullshit. This means that
+        // this verification step doesn't run when using the `jiff-tzdb-static`
+        // proc macro. However, we do still run it when parsing TZif data
+        // at runtime.
+        //
+        // We otherwise don't check that the TZif data is fully valid. It is
+        // possible for it to contain superfluous information. For example, a
+        // non-zero local time type that is never referenced by a transition.
+        Ok((tzif, rest))
+    }
+
+    fn parse_transitions<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "transition times data block",
+            bytes,
+            header.transition_times_len()?,
+        )?;
+        let mut it = bytes.chunks_exact(header.time_size);
+        // RFC 8536 says: "If there are no transitions, local time for all
+        // timestamps is specified by the TZ string in the footer if present
+        // and nonempty; otherwise, it is specified by time type 0."
+        //
+        // RFC 8536 also says: "Local time for timestamps before the first
+        // transition is specified by the first time type (time type
+        // 0)."
+        //
+        // So if there are no transitions, pushing this dummy one will result
+        // in the desired behavior even when it's the only transition.
+        // Similarly, since this is the minimum timestamp value, it will
+        // trigger for any times before the first transition found in the TZif
+        // data.
+        self.transitions
+            .push(TzifTransition { timestamp: TIMESTAMP_MIN, type_index: 0 });
+        while let Some(chunk) = it.next() {
+            let mut timestamp = if header.is_32bit() {
+                i64::from(from_be_bytes_i32(chunk))
+            } else {
+                from_be_bytes_i64(chunk)
+            };
+            if !(TIMESTAMP_MIN <= timestamp && timestamp <= TIMESTAMP_MAX) {
+                // We really shouldn't error here just because the Unix
+                // timestamp is outside what Jiff supports. Since what Jiff
+                // supports is _somewhat_ arbitrary. But Jiff's supported
+                // range is good enough for all realistic purposes, so we
+                // just clamp an out-of-range Unix timestamp to the Jiff
+                // min or max value.
+                //
+                // This can't result in the sorting order being wrong, but
+                // it can result in a transition that is duplicative with
+                // the dummy transition we inserted above. This should be
+                // fine.
+                let clamped = timestamp.clamp(TIMESTAMP_MIN, TIMESTAMP_MAX);
+                // only-jiff-start
+                warn!(
+                    "found Unix timestamp {timestamp} that is outside \
+                     Jiff's supported range, clamping to {clamped}",
+                );
+                // only-jiff-end
+                timestamp = clamped;
+            }
+            self.transitions.push(TzifTransition {
+                timestamp,
+                // We can't fill in the type index yet. We fill this in
+                // later when we parse the transition types.
+                type_index: 0,
+            });
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_transition_types<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "transition types data block",
+            bytes,
+            header.transition_types_len()?,
+        )?;
+        // We start our transition indices at 1 because we always insert a
+        // dummy first transition corresponding to `Timestamp::MIN`. Its type
+        // index is always 0, so there's no need to change it here.
+        for (transition_index, &type_index) in (1..).zip(bytes) {
+            if usize::from(type_index) >= header.tzh_typecnt {
+                return Err(err!(
+                    "found transition type index {type_index},
+                     but there are only {} local time types",
+                    header.tzh_typecnt,
+                ));
+            }
+            self.transitions[transition_index].type_index = type_index;
+        }
+        Ok(rest)
+    }
+
+    fn parse_local_time_types<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "local time types data block",
+            bytes,
+            header.local_time_types_len()?,
+        )?;
+        let mut it = bytes.chunks_exact(6);
+        while let Some(chunk) = it.next() {
+            let offset = from_be_bytes_i32(&chunk[..4]);
+            if !(OFFSET_MIN <= offset && offset <= OFFSET_MAX) {
+                return Err(err!(
+                    "found local time type with out-of-bounds offset: {offset}"
+                ));
+            }
+            let is_dst = chunk[4] == 1;
+            let designation = (chunk[5], chunk[5]);
+            self.types.push(TzifLocalTimeType {
+                offset,
+                is_dst,
+                designation,
+                indicator: TzifIndicator::LocalWall,
+            });
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_time_zone_designations<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "time zone designations data block",
+            bytes,
+            header.time_zone_designations_len()?,
+        )?;
+        self.fixed.designations =
+            String::from_utf8(bytes.to_vec()).map_err(|_| {
+                err!(
+                    "time zone designations are not valid UTF-8: {:?}",
+                    Bytes(bytes),
+                )
+            })?;
+        // Holy hell, this is brutal. The boundary conditions are crazy.
+        for (i, typ) in self.types.iter_mut().enumerate() {
+            let start = usize::from(typ.designation.0);
+            let Some(suffix) = self.fixed.designations.get(start..) else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but cannot be more than {}",
+                    self.fixed.designations.len(),
+                ));
+            };
+            let Some(len) = suffix.find('\x00') else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but could not find NUL terminator after it in \
+                     designations: {:?}",
+                    self.fixed.designations,
+                ));
+            };
+            let Some(end) = start.checked_add(len) else {
+                return Err(err!(
+                    "local time type {i} has designation index of {start}, \
+                     but its length {len} is too big",
+                ));
+            };
+            typ.designation.1 = u8::try_from(end).map_err(|_| {
+                err!(
+                    "local time type {i} has designation range of \
+                     {start}..{end}, but end is too big",
+                )
+            })?;
+        }
+        Ok(rest)
+    }
+
+    /// This parses the leap second corrections in the TZif data.
+    ///
+    /// Note that we only parse and verify them. We don't actually use them.
+    /// Jiff effectively ignores leap seconds.
+    fn parse_leap_seconds<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (bytes, rest) = try_split_at(
+            "leap seconds data block",
+            bytes,
+            header.leap_second_len()?,
+        )?;
+        let chunk_len = header
+            .time_size
+            .checked_add(4)
+            .expect("time_size plus 4 fits in usize");
+        let mut it = bytes.chunks_exact(chunk_len);
+        while let Some(chunk) = it.next() {
+            let (occur_bytes, _corr_bytes) = chunk.split_at(header.time_size);
+            let occur = if header.is_32bit() {
+                i64::from(from_be_bytes_i32(occur_bytes))
+            } else {
+                from_be_bytes_i64(occur_bytes)
+            };
+            if !(TIMESTAMP_MIN <= occur && occur <= TIMESTAMP_MAX) {
+                // only-jiff-start
+                warn!(
+                    "leap second occurrence {occur} is \
+                     not in Jiff's supported range"
+                )
+                // only-jiff-end
+            }
+        }
+        assert!(it.remainder().is_empty());
+        Ok(rest)
+    }
+
+    fn parse_indicators<'b>(
+        &mut self,
+        header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        let (std_wall_bytes, rest) = try_split_at(
+            "standard/wall indicators data block",
+            bytes,
+            header.standard_wall_len()?,
+        )?;
+        let (ut_local_bytes, rest) = try_split_at(
+            "UT/local indicators data block",
+            rest,
+            header.ut_local_len()?,
+        )?;
+        if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
+            // This is a weird case, but technically possible only if all
+            // UT/local indicators are 0. If any are 1, then it's an error,
+            // because it would require the corresponding std/wall indicator
+            // to be 1 too. Which it can't be, because there aren't any. So
+            // we just check that they're all zeros.
+            for (i, &byte) in ut_local_bytes.iter().enumerate() {
+                if byte != 0 {
+                    return Err(err!(
+                        "found UT/local indicator '{byte}' for local time \
+                         type {i}, but it must be 0 since all std/wall \
+                         indicators are 0",
+                    ));
+                }
+            }
+        } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
+            for (i, &byte) in std_wall_bytes.iter().enumerate() {
+                // Indexing is OK because Header guarantees that the number of
+                // indicators is 0 or equal to the number of types.
+                self.types[i].indicator = if byte == 0 {
+                    TzifIndicator::LocalWall
+                } else if byte == 1 {
+                    TzifIndicator::LocalStandard
+                } else {
+                    return Err(err!(
+                        "found invalid std/wall indicator '{byte}' for \
+                         local time type {i}, it must be 0 or 1",
+                    ));
+                };
+            }
+        } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
+            assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
+            let it = std_wall_bytes.iter().zip(ut_local_bytes);
+            for (i, (&stdwall, &utlocal)) in it.enumerate() {
+                // Indexing is OK because Header guarantees that the number of
+                // indicators is 0 or equal to the number of types.
+                self.types[i].indicator = match (stdwall, utlocal) {
+                    (0, 0) => TzifIndicator::LocalWall,
+                    (1, 0) => TzifIndicator::LocalStandard,
+                    (1, 1) => TzifIndicator::UTStandard,
+                    (0, 1) => {
+                        return Err(err!(
+                            "found illegal ut-wall combination for \
+                             local time type {i}, only local-wall, \
+                             local-standard and ut-standard are allowed",
+                        ))
+                    }
+                    _ => {
+                        return Err(err!(
+                            "found illegal std/wall or ut/local value for \
+                             local time type {i}, each must be 0 or 1",
+                        ))
+                    }
+                };
+            }
+        } else {
+            // If they're both empty then we don't need to do anything. Every
+            // local time type record already has the correct default for this
+            // case set.
+            debug_assert!(std_wall_bytes.is_empty());
+            debug_assert!(ut_local_bytes.is_empty());
+        }
+        Ok(rest)
+    }
+
+    fn parse_footer<'b>(
+        &mut self,
+        _header: &Header,
+        bytes: &'b [u8],
+    ) -> Result<&'b [u8], Error> {
+        if bytes.is_empty() {
+            return Err(err!(
+                "invalid V2+ TZif footer, expected \\n, \
+                 but found unexpected end of data",
+            ));
+        }
+        if bytes[0] != b'\n' {
+            return Err(err!(
+                "invalid V2+ TZif footer, expected {:?}, but found {:?}",
+                Byte(b'\n'),
+                Byte(bytes[0]),
+            ));
+        }
+        let bytes = &bytes[1..];
+        // Only scan up to 1KB for a NUL terminator in case we somehow got
+        // passed a huge block of bytes.
+        let toscan = &bytes[..bytes.len().min(1024)];
+        let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else {
+            return Err(err!(
+                "invalid V2 TZif footer, could not find {:?} \
+                 terminator in: {:?}",
+                Byte(b'\n'),
+                Bytes(toscan),
+            ));
+        };
+        let (bytes, rest) = bytes.split_at(nlat);
+        if !bytes.is_empty() {
+            let posix_tz =
+                PosixTimeZone::parse(bytes).map_err(|e| err!("{e}"))?;
+            // We could in theory limit TZ strings to their strict POSIX
+            // definition here for TZif V2, but I don't think there is any
+            // harm in allowing the extensions in V2 formatted TZif data. Note
+            // that the GNU tooling allow it via the `TZ` environment variable
+            // even though POSIX doesn't specify it. This all seems okay to me
+            // because the V3+ extension is a strict superset of functionality.
+            if let Some(ref dst) = posix_tz.dst {
+                if dst.rule.is_none() {
+                    return Err(err!(
+                        "TZ string `{}` in v3+ tzfile has DST \
+                         but no transition rules",
+                        Bytes(bytes),
+                    ));
+                }
+            }
+            self.fixed.posix_tz = Some(posix_tz);
+        }
+        Ok(&rest[1..])
+    }
+}
+
+/// The header for a TZif formatted file.
+///
+/// V2+ TZif format have two headers: one for V1 data, and then a second
+/// following the V1 data block that describes another data block which uses
+/// 64-bit timestamps. The two headers both have the same format and both
+/// use 32-bit big-endian encoded integers.
+#[derive(Debug)]
+struct Header {
+    /// The size of the timestamps encoded in the data block.
+    ///
+    /// This is guaranteed to be either 4 (for V1) or 8 (for the 64-bit header
+    /// block in V2+).
+    time_size: usize,
+    /// The file format version.
+    ///
+    /// Note that this is either a NUL byte (for version 1), or an ASCII byte
+    /// corresponding to the version number. That is, `0x32` for `2`, `0x33`
+    /// for `3` or `0x34` for `4`. Note also that just because zoneinfo might
+    /// have been recently generated does not mean it uses the latest format
+    /// version. It seems like newer versions are only compiled by `zic` when
+    /// they are needed. For example, `America/New_York` on my system (as of
+    /// `2024-03-25`) has version `0x32`, but `Asia/Jerusalem` has version
+    /// `0x33`.
+    version: u8,
+    /// Number of UT/local indicators stored in the file.
+    ///
+    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
+    tzh_ttisutcnt: usize,
+    /// The number of standard/wall indicators stored in the file.
+    ///
+    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
+    tzh_ttisstdcnt: usize,
+    /// The number of leap seconds for which data entries are stored in the
+    /// file.
+    tzh_leapcnt: usize,
+    /// The number of transition times for which data entries are stored in
+    /// the file.
+    tzh_timecnt: usize,
+    /// The number of local time types for which data entries are stored in the
+    /// file.
+    ///
+    /// This is checked to be at least `1`.
+    tzh_typecnt: usize,
+    /// The number of bytes of time zone abbreviation strings stored in the
+    /// file.
+    ///
+    /// This is checked to be at least `1`.
+    tzh_charcnt: usize,
+}
+
+impl Header {
+    /// Parse the header record from the given bytes.
+    ///
+    /// Upon success, return the header and all bytes after the header.
+    ///
+    /// The given `time_size` must be 4 or 8, corresponding to either the
+    /// V1 header block or the V2+ header block, respectively.
+    fn parse(
+        time_size: usize,
+        bytes: &[u8],
+    ) -> Result<(Header, &[u8]), Error> {
+        assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
+        if bytes.len() < 44 {
+            return Err(err!("invalid header: too short"));
+        }
+        let (magic, rest) = bytes.split_at(4);
+        if magic != b"TZif" {
+            return Err(err!("invalid header: magic bytes mismatch"));
+        }
+        let (version, rest) = rest.split_at(1);
+        let (_reserved, rest) = rest.split_at(15);
+
+        let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
+        let (tzh_timecnt_bytes, rest) = rest.split_at(4);
+        let (tzh_typecnt_bytes, rest) = rest.split_at(4);
+        let (tzh_charcnt_bytes, rest) = rest.split_at(4);
+
+        let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_ttisutcnt: {e}"))?;
+        let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_ttisstdcnt: {e}"))?;
+        let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_leapcnt: {e}"))?;
+        let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_timecnt: {e}"))?;
+        let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_typecnt: {e}"))?;
+        let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes)
+            .map_err(|e| err!("failed to parse tzh_charcnt: {e}"))?;
+
+        if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
+            return Err(err!(
+                "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \
+                 or equal to tzh_typecnt={tzh_typecnt}",
+            ));
+        }
+        if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
+            return Err(err!(
+                "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \
+                 or equal to tzh_typecnt={tzh_typecnt}",
+            ));
+        }
+        if tzh_typecnt < 1 {
+            return Err(err!(
+                "expected tzh_typecnt={tzh_typecnt} to be at least 1",
+            ));
+        }
+        if tzh_charcnt < 1 {
+            return Err(err!(
+                "expected tzh_charcnt={tzh_charcnt} to be at least 1",
+            ));
+        }
+
+        let header = Header {
+            time_size,
+            version: version[0],
+            tzh_ttisutcnt,
+            tzh_ttisstdcnt,
+            tzh_leapcnt,
+            tzh_timecnt,
+            tzh_typecnt,
+            tzh_charcnt,
+        };
+        Ok((header, rest))
+    }
+
+    /// Returns true if this header is for a 32-bit data block.
+    ///
+    /// When false, it is guaranteed that this header is for a 64-bit data
+    /// block.
+    fn is_32bit(&self) -> bool {
+        self.time_size == 4
+    }
+
+    /// Returns the size of the data block, in bytes, for this header.
+    ///
+    /// This returns an error if the arithmetic required to compute the
+    /// length would overflow.
+    ///
+    /// This is useful for, e.g., skipping over the 32-bit V1 data block in
+    /// V2+ TZif formatted files.
+    fn data_block_len(&self) -> Result<usize, Error> {
+        let a = self.transition_times_len()?;
+        let b = self.transition_types_len()?;
+        let c = self.local_time_types_len()?;
+        let d = self.time_zone_designations_len()?;
+        let e = self.leap_second_len()?;
+        let f = self.standard_wall_len()?;
+        let g = self.ut_local_len()?;
+        a.checked_add(b)
+            .and_then(|z| z.checked_add(c))
+            .and_then(|z| z.checked_add(d))
+            .and_then(|z| z.checked_add(e))
+            .and_then(|z| z.checked_add(f))
+            .and_then(|z| z.checked_add(g))
+            .ok_or_else(|| {
+                err!(
+                    "length of data block in V{} tzfile is too big",
+                    self.version
+                )
+            })
+    }
+
+    fn transition_times_len(&self) -> Result<usize, Error> {
+        self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| {
+            err!("tzh_timecnt value {} is too big", self.tzh_timecnt)
+        })
+    }
+
+    fn transition_types_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_timecnt)
+    }
+
+    fn local_time_types_len(&self) -> Result<usize, Error> {
+        self.tzh_typecnt.checked_mul(6).ok_or_else(|| {
+            err!("tzh_typecnt value {} is too big", self.tzh_typecnt)
+        })
+    }
+
+    fn time_zone_designations_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_charcnt)
+    }
+
+    fn leap_second_len(&self) -> Result<usize, Error> {
+        let record_len = self
+            .time_size
+            .checked_add(4)
+            .expect("4-or-8 plus 4 always fits in usize");
+        self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| {
+            err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt)
+        })
+    }
+
+    fn standard_wall_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_ttisstdcnt)
+    }
+
+    fn ut_local_len(&self) -> Result<usize, Error> {
+        Ok(self.tzh_ttisutcnt)
+    }
+}
+
+/// Splits the given slice of bytes at the index given.
+///
+/// If the index is out of range (greater than `bytes.len()`) then an error is
+/// returned. The error message will include the `what` string given, which is
+/// meant to describe the thing being split.
+fn try_split_at<'b>(
+    what: &'static str,
+    bytes: &'b [u8],
+    at: usize,
+) -> Result<(&'b [u8], &'b [u8]), Error> {
+    if at > bytes.len() {
+        Err(err!(
+            "expected at least {at} bytes for {what}, \
+             but found only {} bytes",
+            bytes.len(),
+        ))
+    } else {
+        Ok(bytes.split_at(at))
+    }
+}
+
+/// Interprets the given slice as an unsigned 32-bit big endian integer,
+/// attempts to convert it to a `usize` and returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+///
+/// # Errors
+///
+/// This errors if the `u32` parsed from the given bytes cannot fit in a
+/// `usize`.
+fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, Error> {
+    let n = from_be_bytes_u32(bytes);
+    usize::try_from(n).map_err(|_| {
+        err!(
+            "failed to parse integer {n} (too big, max allowed is {}",
+            usize::MAX
+        )
+    })
+}
+
+/// Interprets the given slice as an unsigned 32-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
+    u32::from_be_bytes(bytes.try_into().unwrap())
+}
+
+/// Interprets the given slice as a signed 32-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 4`.
+fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
+    i32::from_be_bytes(bytes.try_into().unwrap())
+}
+
+/// Interprets the given slice as a signed 64-bit big endian integer and
+/// returns it.
+///
+/// # Panics
+///
+/// When `bytes.len() != 8`.
+fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
+    i64::from_be_bytes(bytes.try_into().unwrap())
+}
diff --git a/src/shared/util.rs b/src/shared/util.rs
new file mode 100644
index 00000000..1dca3d05
--- /dev/null
+++ b/src/shared/util.rs
@@ -0,0 +1,128 @@
+/// Provides a convenient `Debug` implementation for a `u8`.
+///
+/// The `Debug` impl treats the byte as an ASCII, and emits a human
+/// readable representation of it. If the byte isn't ASCII, then it's
+/// emitted as a hex escape sequence.
+#[derive(Clone, Copy)]
+pub(crate) struct Byte(pub u8);
+
+impl core::fmt::Display for Byte {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        if self.0 == b' ' {
+            return write!(f, " ");
+        }
+        // 10 bytes is enough for any output from ascii::escape_default.
+        let mut bytes = [0u8; 10];
+        let mut len = 0;
+        for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
+            // capitalize \xab to \xAB
+            if i >= 2 && b'a' <= b && b <= b'f' {
+                b -= 32;
+            }
+            bytes[len] = b;
+            len += 1;
+        }
+        write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap())
+    }
+}
+
+impl core::fmt::Debug for Byte {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        write!(f, "\"")?;
+        core::fmt::Display::fmt(self, f)?;
+        write!(f, "\"")?;
+        Ok(())
+    }
+}
+
+/// Provides a convenient `Debug` implementation for `&[u8]`.
+///
+/// This generally works best when the bytes are presumed to be mostly
+/// UTF-8, but will work for anything. For any bytes that aren't UTF-8,
+/// they are emitted as hex escape sequences.
+#[derive(Clone, Copy)]
+pub(crate) struct Bytes<'a>(pub &'a [u8]);
+
+impl<'a> core::fmt::Display for Bytes<'a> {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        // This is a sad re-implementation of a similar impl found in bstr.
+        let mut bytes = self.0;
+        while let Some(result) = utf8_decode(bytes) {
+            let ch = match result {
+                Ok(ch) => ch,
+                Err(byte) => {
+                    write!(f, r"\x{:02x}", byte)?;
+                    bytes = &bytes[1..];
+                    continue;
+                }
+            };
+            bytes = &bytes[ch.len_utf8()..];
+            match ch {
+                '\0' => write!(f, "\\0")?,
+                '\x01'..='\x7f' => {
+                    write!(f, "{}", (ch as u8).escape_ascii())?;
+                }
+                _ => write!(f, "{}", ch.escape_debug())?,
+            }
+        }
+        Ok(())
+    }
+}
+
+impl<'a> core::fmt::Debug for Bytes<'a> {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        write!(f, "\"")?;
+        core::fmt::Display::fmt(self, f)?;
+        write!(f, "\"")?;
+        Ok(())
+    }
+}
+
+/// Decodes the next UTF-8 encoded codepoint from the given byte slice.
+///
+/// If no valid encoding of a codepoint exists at the beginning of the
+/// given byte slice, then the first byte is returned instead.
+///
+/// This returns `None` if and only if `bytes` is empty.
+///
+/// This never panics.
+///
+/// *WARNING*: This is not designed for performance. If you're looking for
+/// a fast UTF-8 decoder, this is not it. If you feel like you need one in
+/// this crate, then please file an issue and discuss your use case.
+pub(crate) fn utf8_decode(bytes: &[u8]) -> Option<Result<char, u8>> {
+    /// Given a UTF-8 leading byte, this returns the total number of code
+    /// units in the following encoded codepoint.
+    ///
+    /// If the given byte is not a valid UTF-8 leading byte, then this
+    /// returns `None`.
+    fn utf8_len(byte: u8) -> Option<usize> {
+        if byte <= 0x7F {
+            return Some(1);
+        } else if byte & 0b1100_0000 == 0b1000_0000 {
+            return None;
+        } else if byte <= 0b1101_1111 {
+            Some(2)
+        } else if byte <= 0b1110_1111 {
+            Some(3)
+        } else if byte <= 0b1111_0111 {
+            Some(4)
+        } else {
+            None
+        }
+    }
+
+    if bytes.is_empty() {
+        return None;
+    }
+    let len = match utf8_len(bytes[0]) {
+        None => return Some(Err(bytes[0])),
+        Some(len) if len > bytes.len() => return Some(Err(bytes[0])),
+        Some(1) => return Some(Ok(char::from(bytes[0]))),
+        Some(len) => len,
+    };
+    match core::str::from_utf8(&bytes[..len]) {
+        Ok(s) => Some(Ok(s.chars().next().unwrap())),
+        Err(_) => Some(Err(bytes[0])),
+    }
+}
diff --git a/src/tz/concatenated.rs b/src/tz/concatenated.rs
index 358fc375..84c12572 100644
--- a/src/tz/concatenated.rs
+++ b/src/tz/concatenated.rs
@@ -1117,6 +1117,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg(not(miri))]
     fn read_all_time_zones() {
         let db = ConcatenatedTzif::open(ANDROID_CONCATENATED_TZIF).unwrap();
         let available = db.available(&mut alloc::vec![]).unwrap();
diff --git a/src/tz/db/mod.rs b/src/tz/db/mod.rs
index 3886b90d..e2cf96d1 100644
--- a/src/tz/db/mod.rs
+++ b/src/tz/db/mod.rs
@@ -29,12 +29,12 @@ mod zoneinfo;
 /// assert!(tz::db().get("does-not-exist").is_err());
 /// ```
 pub fn db() -> &'static TimeZoneDatabase {
-    #[cfg(not(feature = "std"))]
+    #[cfg(any(not(feature = "std"), miri))]
     {
         static NONE: TimeZoneDatabase = TimeZoneDatabase::none();
         &NONE
     }
-    #[cfg(feature = "std")]
+    #[cfg(all(feature = "std", not(miri)))]
     {
         use std::sync::OnceLock;
 
diff --git a/src/tz/mod.rs b/src/tz/mod.rs
index 771d39f7..e9b1c922 100644
--- a/src/tz/mod.rs
+++ b/src/tz/mod.rs
@@ -78,7 +78,46 @@ are not supported by Jiff.)
 Moreover, Jiff supports reading the `TZ` environment variable, as specified
 by POSIX, on all systems.
 
-TO get the system's default time zone, use [`TimeZone::system`].
+To get the system's default time zone, use [`TimeZone::system`].
+
+# Core-only environments
+
+By default, Jiff attempts to read time zone rules from `/usr/share/zoneinfo`
+on Unix and a bundled database on other platforms (like on Windows). This happens
+at runtime, and aside from requiring APIs to interact with the file system
+on Unix, it also requires dynamic memory allocation.
+
+For core-only environments that don't have file system APIs or dynamic
+memory allocation, Jiff provides a way to construct `TimeZone` values at
+compile time by compiling time zone rules into your binary. This does mean
+that your program will need to be re-compiled if the time zone rules change
+(in contrast to Jiff's default behavior of reading `/usr/share/zoneinfo` at
+runtime on Unix), but sometimes there isn't a practical alternative.
+
+With the `static` crate feature enabled, the [`jiff::tz::get`](crate::tz::get)
+macro becomes available in this module. This example shows how use it to build
+a `TimeZone` at compile time. Here, we find the next DST transition from a
+particular timestamp in `Europe/Zurich`, and then print that in local time for
+Zurich:
+
+```
+use jiff::{tz::{self, TimeZone}, Timestamp};
+
+static TZ: TimeZone = tz::get!("Europe/Zurich");
+
+let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
+let Some(next_transition) = TZ.following(ts).next() else {
+    return Err("no time zone transitions".into());
+};
+let zdt = next_transition.timestamp().to_zoned(TZ.clone());
+assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
+
+# Ok::<(), Box<dyn std::error::Error>>(())
+```
+
+The above example does not require dynamic memory allocation or access to file
+system APIs. It also _only_ embeds the `Europe/Zurich` time zone into your
+compiled binary.
 
 [IANA Time Zone Database]: https://en.wikipedia.org/wiki/Tz_database
 [`GetDynamicTimeZoneInformation`]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-getdynamictimezoneinformation
@@ -101,19 +140,186 @@ mod ambiguous;
 mod concatenated;
 mod db;
 mod offset;
-#[cfg(feature = "alloc")]
 pub(crate) mod posix;
 #[cfg(feature = "tz-system")]
 mod system;
 #[cfg(all(test, feature = "alloc"))]
 mod testdata;
 mod timezone;
-#[cfg(feature = "alloc")]
-mod tzif;
+pub(crate) mod tzif;
 // See module comment for WIP status. :-(
 #[cfg(test)]
 mod zic;
 
+/// Create a `TimeZone` value from TZif data in [`jiff-tzdb`] at compile time.
+///
+/// This reads the data for the time zone with the IANA identifier given from
+/// [`jiff-tzdb`], parses it as TZif specified by [RFC 9636], and constructs a
+/// `TimeZone` value for use in a `const` context. This enables using IANA time
+/// zones with Jiff in core-only environments. No dynamic memory allocation is
+/// used.
+///
+/// # Input
+///
+/// This macro takes one positional parameter that must be a literal string.
+/// The string should be an IANA time zone identifier, e.g.,
+/// `America/New_York`.
+///
+/// # Return type
+///
+/// This macro returns a value with type `TimeZone`. To get a `&'static
+/// TimeZone`, simply use `&include("...")`.
+///
+/// # Usage
+///
+/// Callers should only call this macro once for each unique IANA time zone
+/// identifier you need. Otherwise, multiple copies of the same embedded
+/// time zone data could appear in your binary. There are no correctness
+/// issues with this, but it could make your binary bigger than it needs to be.
+///
+/// # When should I use this?
+///
+/// Users should only use this macro if they have a _specific need_ for it
+/// (like using a time zone on an embedded device). In particular, this will
+/// embed the time zone transition rules into your binary. If the time zone
+/// rules change, your program will need to be re-compiled.
+///
+/// In contrast, Jiff's default configuration on Unix is to read from
+/// `/usr/share/zoneinfo` at runtime. This means your application will
+/// automatically use time zone updates and doesn't need to be re-compiled.
+///
+/// Using a static `TimeZone` may also be faster in some cases. In particular,
+/// a `TimeZone` created at runtime from a `/usr/share/zoneinfo` uses
+/// automic reference counting internally. In contrast, a `TimeZone` created
+/// with this macro does not.
+///
+/// # Example
+///
+/// This example shows how to find the next DST transition from a particular
+/// timestamp in `Europe/Zurich`, and then print that in local time for Zurich:
+///
+/// ```
+/// use jiff::{tz::{self, TimeZone}, Timestamp};
+///
+/// static TZ: TimeZone = tz::get!("Europe/Zurich");
+///
+/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
+/// let Some(next_transition) = TZ.following(ts).next() else {
+///     return Err("no time zone transitions".into());
+/// };
+/// let zdt = next_transition.timestamp().to_zoned(TZ.clone());
+/// assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// [RFC 9636]: https://datatracker.ietf.org/doc/rfc9636/
+/// [`jiff-tzdb`]: https://docs.rs/jiff-tzdb
+#[cfg(feature = "static")]
+pub use jiff_static::get;
+
+/// Create a `TimeZone` value from TZif data in a file at compile time.
+///
+/// This reads the data in the file path given, parses it as TZif specified by
+/// [RFC 9636], and constructs a `TimeZone` value for use in a `const` context.
+/// This enables using IANA time zones with Jiff in core-only environments. No
+/// dynamic memory allocation is used.
+///
+/// Unlike [`jiff::tz::get`](get), this reads TZif data from a file.
+/// `jiff::tz::get`, in contrast, reads TZif data from the [`jiff-tzdb`] crate.
+/// `jiff::tz::get` is more convenient and doesn't require using managing TZif
+/// files, but it comes at the cost of a dependency on `jiff-tzdb` and being
+/// forced to use whatever data is in `jiff-tzdb`.
+///
+/// # Input
+///
+/// This macro takes two positional parameters that must be literal strings.
+///
+/// The first is required and is a path to a file containing TZif data. For
+/// example, `/usr/share/zoneinfo/America/New_York`.
+///
+/// The second parameter is an IANA time zone identifier, e.g.,
+/// `America/New_York`, and is required only when an IANA time zone identifier
+/// could not be determined from the file path. The macro will automatically
+/// infer an IANA time zone identifier as anything after the last occurrence
+/// of the literal `zoneinfo/` in the file path.
+///
+/// # Return type
+///
+/// This macro returns a value with type `TimeZone`. To get a `&'static
+/// TimeZone`, simply use `&include("...")`.
+///
+/// # Usage
+///
+/// Callers should only call this macro once for each unique IANA time zone
+/// identifier you need. Otherwise, multiple copies of the same embedded
+/// time zone data could appear in your binary. There are no correctness
+/// issues with this, but it could make your binary bigger than it needs to be.
+///
+/// # When should I use this?
+///
+/// Users should only use this macro if they have a _specific need_ for it
+/// (like using a time zone on an embedded device). In particular, this will
+/// embed the time zone transition rules into your binary. If the time zone
+/// rules change, your program will need to be re-compiled.
+///
+/// In contrast, Jiff's default configuration on Unix is to read from
+/// `/usr/share/zoneinfo` at runtime. This means your application will
+/// automatically use time zone updates and doesn't need to be re-compiled.
+///
+/// Using a static `TimeZone` may also be faster in some cases. In particular,
+/// a `TimeZone` created at runtime from a `/usr/share/zoneinfo` uses
+/// automic reference counting internally. In contrast, a `TimeZone` created
+/// with this macro does not.
+///
+/// # Example
+///
+/// This example shows how to find the next DST transition from a particular
+/// timestamp in `Europe/Zurich`, and then print that in local time for Zurich:
+///
+/// ```ignore
+/// use jiff::{tz::{self, TimeZone}, Timestamp};
+///
+/// static TZ: TimeZone = tz::include!("/usr/share/zoneinfo/Europe/Zurich");
+///
+/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
+/// let Some(next_transition) = TZ.following(ts).next() else {
+///     return Err("no time zone transitions".into());
+/// };
+/// let zdt = next_transition.timestamp().to_zoned(TZ.clone());
+/// assert_eq!(zdt.to_string(), "2025-03-30T03:00:00+02:00[Europe/Zurich]");
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// # Example: using `/etc/localtime`
+///
+/// On most Unix systems, `/etc/localtime` is a symbolic link to a file in
+/// your `/usr/share/zoneinfo` directory. This means it is a valid input to
+/// this macro. However, Jiff currently does not detect the IANA time zone
+/// identifier, so you'll need to provide it yourself:
+///
+/// ```ignore
+/// use jiff::{tz::{self, TimeZone}, Timestamp};
+///
+/// static TZ: TimeZone = tz::include!("/etc/localtime", "America/New_York");
+///
+/// let ts: Timestamp = "2025-02-25T00:00Z".parse()?;
+/// let zdt = ts.to_zoned(TZ.clone());
+/// assert_eq!(zdt.to_string(), "2025-02-24T19:00:00-05:00[America/New_York]");
+///
+/// # Ok::<(), Box<dyn std::error::Error>>(())
+/// ```
+///
+/// Note that this is reading `/etc/localtime` _at compile time_, which means
+/// that the program will only use the time zone on the system in which it
+/// was compiled. It will _not_ use the time zone of the system running it.
+///
+/// [RFC 9636]: https://datatracker.ietf.org/doc/rfc9636/
+/// [`jiff-tzdb`]: https://docs.rs/jiff-tzdb
+#[cfg(feature = "static-tz")]
+pub use jiff_static::include;
+
 /// Creates a new time zone offset in a `const` context from a given number
 /// of hours.
 ///
diff --git a/src/tz/offset.rs b/src/tz/offset.rs
index d3816828..53c07f5c 100644
--- a/src/tz/offset.rs
+++ b/src/tz/offset.rs
@@ -232,7 +232,7 @@ impl Offset {
     // warrant its existence. And I think I'd rather `Offset::hms` be const and
     // exported instead of this monstrosity.
     #[inline]
-    const fn constant_seconds(seconds: i32) -> Offset {
+    pub(crate) const fn constant_seconds(seconds: i32) -> Offset {
         if !t::SpanZoneOffset::contains(seconds) {
             panic!("invalid time zone offset seconds")
         }
@@ -1013,7 +1013,7 @@ impl Offset {
     }
 
     #[inline]
-    pub(crate) fn seconds_ranged(self) -> t::SpanZoneOffset {
+    pub(crate) const fn seconds_ranged(self) -> t::SpanZoneOffset {
         self.span
     }
 
diff --git a/src/tz/posix.rs b/src/tz/posix.rs
index 239935de..2b9dc989 100644
--- a/src/tz/posix.rs
+++ b/src/tz/posix.rs
@@ -1,6 +1,13 @@
 /*!
 Provides a parser for [POSIX's `TZ` environment variable][posix-env].
 
+NOTE: Sadly, at time of writing, the actual parser is in `src/shared/posix.rs`.
+This is so it can be shared (via simple code copying) with proc macros like
+the one found in `jiff-tzdb-static`. The parser populates a "lowest common
+denominator" data type. In normal use in Jiff, this type is converted into
+the types defined below. This module still does provide the various time zone
+operations. Only the parsing is written elsewhere.
+
 The `TZ` environment variable is most commonly used to set a time zone. For
 example, `TZ=America/New_York`. But it can also be used to tersely define DST
 transitions. Moreover, the format is not just used as an environment variable,
@@ -47,12 +54,13 @@ other programs do in practice (for example, GNU date).
 
 This module works just fine in `no_std` mode. It also generally works fine
 without `alloc` too, modulo some APIs for parsing from an environment variable
-(which need `std` anyway). The main problem is that the type defined here takes
-up a lot of space (100+ bytes). A good chunk of that comes from representing
-time zone abbreviations inline. In theory, only 6-10 bytes are needed for
-simple cases like `TZ=EST5EDT,M3.2.0,M11.1.0`, but we make room for 30 byte
-length abbreviations (times two). Plus, there's a much of room made for the
-rule representation.
+(which need `std` anyway) and the POSIX TZ parser using `String` to represent
+abbreviations. (The latter could be fixed if necessary.) The main problem is
+that the type defined here takes up a lot of space (100+ bytes). A good chunk
+of that comes from representing time zone abbreviations inline. In theory, only
+6-10 bytes are needed for simple cases like `TZ=EST5EDT,M3.2.0,M11.1.0`, but we
+make room for 30 byte length abbreviations (times two). Plus, there's a much of
+room made for the rule representation.
 
 When you then stuff this inside a `TimeZone` which cannot use heap allocation
 to force an indirection, you wind up with a very chunky `TimeZone`. And this in
@@ -71,16 +79,21 @@ thinking about adding an `Unzoned` type that is just like `Zoned`, but requires
 the caller to pass in a `&TimeZone` for every API call. Less convenient for
 sure, but you get a more flexible type.
 
+ADDENDUM: The above is still mostly true, but it looks like we are going to
+allow static `TimeZone` values via a proc-macro. And this also requires
+parsing POSIX time zones and building them at compile time. In order to make
+this work well with `TimeZone` without indirection, we'll use pointer tagging.
+This should help save `Zoned` in core-only environments.
+
 [posix-env]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03
 [iana-env]: https://data.iana.org/time-zones/tzdb-2024a/theory.html#functions
 [musl-env]: https://wiki.musl-libc.org/environment-variables
 */
 
-use core::cell::Cell;
-
 use crate::{
     civil::{Date, DateTime, Time, Weekday},
     error::{err, Error, ErrorContext},
+    shared,
     timestamp::Timestamp,
     tz::{
         timezone::TimeZoneAbbreviation, AmbiguousOffset, Dst, Offset,
@@ -88,22 +101,18 @@ use crate::{
     },
     util::{
         array_str::Abbreviation,
-        escape::{Byte, Bytes},
+        escape::Bytes,
         parse,
-        rangeint::{ri16, ri8, RFrom, RInto},
-        t::{self, Minute, Month, Second, Sign, SpanZoneOffset, Year, C},
+        rangeint::{ri16, ri32, ri8, RInto},
+        t::{self, Month, SpanZoneOffset, Year, C},
     },
     SignedDuration,
 };
 
-/// POSIX says the hour must be in the range `0..=24`, but that the default
-/// hour for DST is one hour more than standard time. Therefore, the actual
-/// allowed range is `0..=25`. (Although we require `0..=24` during parsing.)
-type PosixHour = ri8<0, 25>;
-type IanaHour = ri16<0, 167>;
 type PosixJulianDayNoLeap = ri16<1, 365>;
 type PosixJulianDayWithLeap = ri16<0, 365>;
 type PosixWeek = ri8<1, 5>;
+type PosixTimeSeconds = ri32<{ -604799 }, 604799>;
 
 /// The result of parsing the POSIX `TZ` environment variable.
 ///
@@ -122,7 +131,7 @@ type PosixWeek = ri8<1, 5>;
 /// returned by parsing this type and layer their own semantics on top.
 #[cfg(feature = "tz-system")]
 #[derive(Debug, Eq, PartialEq)]
-pub(crate) enum PosixTz {
+pub(crate) enum PosixTzEnv {
     /// A valid POSIX time zone with an optional DST transition rule.
     Rule(PosixTimeZone),
     /// An implementation defined string. This occurs when the `TZ` value
@@ -131,9 +140,9 @@ pub(crate) enum PosixTz {
 }
 
 #[cfg(feature = "tz-system")]
-impl PosixTz {
+impl PosixTzEnv {
     /// Parse a POSIX `TZ` environment variable string from the given bytes.
-    pub(crate) fn parse(bytes: impl AsRef<[u8]>) -> Result<PosixTz, Error> {
+    fn parse(bytes: impl AsRef<[u8]>) -> Result<PosixTzEnv, Error> {
         let bytes = bytes.as_ref();
         if bytes.get(0) == Some(&b':') {
             let Ok(string) = core::str::from_utf8(&bytes[1..]) else {
@@ -143,46 +152,83 @@ impl PosixTz {
                     Bytes(&bytes[1..]),
                 ));
             };
-            Ok(PosixTz::Implementation(string.into()))
+            Ok(PosixTzEnv::Implementation(string.into()))
         } else {
-            PosixTimeZone::parse(bytes).map(PosixTz::Rule)
+            PosixTimeZone::parse(bytes).map(PosixTzEnv::Rule)
         }
     }
 
     /// Parse a POSIX `TZ` environment variable string from the given `OsStr`.
     pub(crate) fn parse_os_str(
         osstr: impl AsRef<std::ffi::OsStr>,
-    ) -> Result<PosixTz, Error> {
-        PosixTz::parse(parse::os_str_bytes(osstr.as_ref())?)
+    ) -> Result<PosixTzEnv, Error> {
+        PosixTzEnv::parse(parse::os_str_bytes(osstr.as_ref())?)
     }
 }
 
 #[cfg(feature = "tz-system")]
-impl core::fmt::Display for PosixTz {
+impl core::fmt::Display for PosixTzEnv {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         match *self {
-            PosixTz::Rule(ref tz) => write!(f, "{tz}"),
-            PosixTz::Implementation(ref imp) => write!(f, ":{imp}"),
+            PosixTzEnv::Rule(ref tz) => write!(f, "{tz}"),
+            PosixTzEnv::Implementation(ref imp) => write!(f, ":{imp}"),
         }
     }
 }
 
-/// The result of parsing a V2 or V3+ `TZ` string from IANA `tzfile` data.
+// BREADCRUMBS: For a proc macro to work, we need to provide a way to
+// introduce and eliminate a `ReasonablePosixTimeZone` in a `const` context.
+// I really do not want to make nearly every type in this module completely
+// `pub`, and would prefer to just expose a new type. But it's kind of a pain
+// because of how nested this type is. Short of writing a parser that works
+// in a `const` context, I don't really see a better way.
+
+/// A "reasonable" POSIX time zone.
 ///
-/// A V2 `TZ` string is precisely identical to a POSIX `TZ` environment
-/// variable string. A V3 `TZ` string however supports signed DST transition
-/// times, and hours in the range `0..=167`.
+/// This is the same as a regular POSIX time zone, but requires that if a DST
+/// time zone abbreviation is present, then a transition rule must also be
+/// present for it. In other words, this considers a `TZ` string of `EST5EDT`
+/// as unreasonable because it doesn't say *when* the DST transitions should
+/// occur.
 ///
-/// We also specifically require that IANA `TZ` strings are "reasonable." That
-/// is, if DST exists, then it must have a corresponding rule.
-#[derive(Debug, Eq, PartialEq)]
-pub(crate) struct IanaTz(ReasonablePosixTimeZone);
+/// Generally speaking, we only deal with reasonable POSIX time zones. And
+/// we expect `TZ` strings parsed from IANA v2+ formatted `tzfile`s to also
+/// be reasonable or parsing fails. This also seems to be consistent with the
+/// [GNU C Library]'s treatment of the `TZ` variable: it only documents support
+/// for reasonable POSIX time zone strings.
+///
+/// Note that a V2 `TZ` string is precisely identical to a POSIX `TZ`
+/// environment variable string. A V3 `TZ` string however supports signed DST
+/// transition times, and hours in the range `0..=167`. The V2 and V3 here
+/// reference how `TZ` strings are defined in the TZif format specified by [RFC
+/// 9636]. V2 is the original version of it straight from POSIX, where as V3+
+/// corresponds to an extension added to V3 (and newer versions) of the TZif
+/// format.
+///
+/// [GNU C Library]: https://www.gnu.org/software/libc/manual/2.25/html_node/TZ-Variable.html
+/// [RFC 9636]: https://datatracker.ietf.org/doc/rfc9636/
+#[derive(Clone, Debug, Eq, PartialEq)]
+// NOT part of Jiff's public API
+#[doc(hidden)]
+// This ensures the alignment of this type is always *at least* 8 bytes. This
+// is required for the pointer tagging inside of `TimeZone` to be sound. At
+// time of writing (2024-02-24), this explicit `repr` isn't required on 64-bit
+// systems since the type definition is such that it will have an alignment of
+// at least 8 bytes anyway. But this *is* required for 32-bit systems, where
+// the type definition at present only has an alignment of 4 bytes.
+#[repr(align(8))]
+pub struct ReasonablePosixTimeZone {
+    std_abbrev: Abbreviation,
+    std_offset: PosixOffset,
+    dst: Option<ReasonablePosixDst>,
+}
 
-impl IanaTz {
+impl ReasonablePosixTimeZone {
     /// Parse a IANA tzfile v3+ `TZ` string from the given bytes.
-    pub(crate) fn parse_v3plus(
+    #[cfg(feature = "alloc")]
+    pub(crate) fn parse(
         bytes: impl AsRef<[u8]>,
-    ) -> Result<IanaTz, Error> {
+    ) -> Result<ReasonablePosixTimeZone, Error> {
         let bytes = bytes.as_ref();
         let posix_tz = PosixTimeZone::parse(bytes).map_err(|e| {
             e.context(err!("invalid POSIX TZ string {:?}", Bytes(bytes)))
@@ -193,14 +239,15 @@ impl IanaTz {
                 Bytes(bytes),
             ));
         };
-        Ok(IanaTz(reasonable))
+        Ok(reasonable)
     }
 
-    /// Like `parse_v3plus`, but parses a POSIX TZ string from a prefix of the
+    /// Like `parse`, but parses a POSIX TZ string from a prefix of the
     /// given input. And remaining input is returned.
-    pub(crate) fn parse_v3plus_prefix<'b, B: AsRef<[u8]> + ?Sized + 'b>(
+    #[cfg(feature = "alloc")]
+    pub(crate) fn parse_prefix<'b, B: AsRef<[u8]> + ?Sized + 'b>(
         bytes: &'b B,
-    ) -> Result<(IanaTz, &'b [u8]), Error> {
+    ) -> Result<(ReasonablePosixTimeZone, &'b [u8]), Error> {
         let bytes = bytes.as_ref();
         let (posix_tz, remaining) = PosixTimeZone::parse_prefix(bytes)
             .map_err(|e| {
@@ -212,54 +259,43 @@ impl IanaTz {
                 Bytes(bytes),
             ));
         };
-        Ok((IanaTz(reasonable), remaining))
+        Ok((reasonable, remaining))
     }
 
-    /// Return ownership of the underlying "reasonable" POSIX time zone value.
+    /// Converts from the shared-but-internal API for use in proc macros.
     ///
-    /// If this was parsed as an IANA v3+ `TZ` string, then the DST transition
-    /// rules can extend beyond `00:00:00..=24:59:59`, and instead are in the
-    /// range `-167:59:59..=167:59:59`.
-    pub(crate) fn into_tz(self) -> ReasonablePosixTimeZone {
-        self.0
-    }
-}
+    /// This works in a `const` context by requiring that the time zone
+    /// abbreviations are `static` strings. This is used when converting
+    /// code generated by a proc macro to this Jiff internal type.
+    pub(crate) const fn from_shared_const(
+        sh: &shared::PosixTimeZone<&'static str>,
+    ) -> ReasonablePosixTimeZone {
+        use crate::util::constant::unwrap;
 
-impl core::fmt::Display for IanaTz {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        write!(f, "{}", self.0)
+        let std_abbrev = unwrap!(
+            Abbreviation::new(sh.std_abbrev),
+            "expected short enough std tz abbreviation"
+        );
+        let std_offset = PosixOffset {
+            offset: unwrap!(
+                SpanZoneOffset::new_const(sh.std_offset),
+                "expected std offset in range",
+            ),
+        };
+        let dst = match sh.dst {
+            None => None,
+            Some(ref dst) => Some(ReasonablePosixDst::from_shared_const(dst)),
+        };
+        ReasonablePosixTimeZone { std_abbrev, std_offset, dst }
     }
-}
-
-/// A "reasonable" POSIX time zone.
-///
-/// This is the same as a regular POSIX time zone, but requires that if a DST
-/// time zone abbreviation is present, then a transition rule must also be
-/// present for it. In other words, this considers a `TZ` string of `EST5EDT`
-/// as unreasonable because it doesn't say *when* the DST transitions should
-/// occur.
-///
-/// Generally speaking, we only deal with reasonable POSIX time zones. And
-/// we expect `TZ` strings parsed from IANA v2+ formatted `tzfile`s to also
-/// be reasonable or parsing fails. This also seems to be consistent with the
-/// [GNU C Library]'s treatment of the `TZ` variable: it only documents support
-/// for reasonable POSIX time zone strings.
-///
-/// [GNU C Library]: https://www.gnu.org/software/libc/manual/2.25/html_node/TZ-Variable.html
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub(crate) struct ReasonablePosixTimeZone {
-    std_abbrev: Abbreviation,
-    std_offset: PosixOffset,
-    dst: Option<ReasonablePosixDst>,
-}
 
-impl ReasonablePosixTimeZone {
     /// Returns the appropriate time zone offset to use for the given
     /// timestamp.
     ///
-    /// This also includes whether the offset returned should be considered
-    /// to be "DST" or not, along with the time zone abbreviation (e.g., EST
-    /// for standard time in New York, and EDT for DST in New York).
+    /// If you need information like whether the offset is in
+    /// DST or not, or the time zone abbreviation, then use
+    /// `ReasonablePosixTimeZone::to_offset_info`. But that API may be more
+    /// expensive to use, so only use it if you need the additional data.
     pub(crate) fn to_offset(&self, timestamp: Timestamp) -> Offset {
         if self.dst.is_none() {
             return self.std_offset();
@@ -333,7 +369,7 @@ impl ReasonablePosixTimeZone {
         let Some(dst_info) = self.dst_info_wall(year) else {
             return AmbiguousOffset::Unambiguous { offset: std_offset };
         };
-        let diff = dst_info.offset - std_offset;
+        let diff = dst_info.offset.duration_since(std_offset);
         // When the difference between DST and standard is positive, that means
         // STD->DST results in a gap while DST->STD results in a fold. However,
         // when the difference is negative, that means STD->DST results in a
@@ -348,7 +384,7 @@ impl ReasonablePosixTimeZone {
         // zero case is strictly redundant. Both the diff < 0 and diff > 0
         // cases handle the zero case correctly. But we write it out for
         // clarity.)
-        if diff.get_seconds_ranged() == 0 {
+        if diff.as_secs() == 0 {
             debug_assert_eq!(std_offset, dst_info.offset);
             AmbiguousOffset::Unambiguous { offset: std_offset }
         } else if diff.is_negative() {
@@ -466,7 +502,7 @@ impl ReasonablePosixTimeZone {
 
     /// Returns the offset for standard time in this POSIX time zone.
     fn std_offset(&self) -> Offset {
-        self.std_offset.to_offset()
+        self.std_offset.offset()
     }
 
     /// Returns the range in which DST occurs.
@@ -476,8 +512,8 @@ impl ReasonablePosixTimeZone {
     fn dst_info_utc(&self, year: impl RInto<Year>) -> Option<DstInfo<'_>> {
         let year = year.rinto();
         let dst = self.dst.as_ref()?;
-        let std_offset = self.std_offset.to_offset();
-        let dst_offset = dst.posix_offset(&self.std_offset).to_offset();
+        let std_offset = self.std_offset.offset();
+        let dst_offset = dst.offset.offset();
         // DST time starts with respect to standard time, so offset it by the
         // standard offset.
         let start = dst.rule.start.to_datetime(year, std_offset);
@@ -495,7 +531,7 @@ impl ReasonablePosixTimeZone {
     fn dst_info_wall(&self, year: impl RInto<Year>) -> Option<DstInfo<'_>> {
         let year = year.rinto();
         let dst = self.dst.as_ref()?;
-        let dst_offset = dst.posix_offset(&self.std_offset).to_offset();
+        let dst_offset = dst.offset.offset();
         // POSIX time zones express their DST transitions in terms of wall
         // clock time. Since this method specifically is returning wall
         // clock times, we don't want to offset our datetimes at all.
@@ -521,7 +557,7 @@ impl core::fmt::Display for ReasonablePosixTimeZone {
             self.std_offset
         )?;
         if let Some(ref dst) = self.dst {
-            write!(f, "{dst}")?;
+            dst.display(self.std_offset, f)?;
         }
         Ok(())
     }
@@ -586,35 +622,54 @@ impl<'a> DstInfo<'a> {
 #[derive(Clone, Debug, Eq, PartialEq)]
 struct ReasonablePosixDst {
     abbrev: Abbreviation,
-    offset: Option<PosixOffset>,
+    offset: PosixOffset,
     /// This is the principal change. A "reasonable" DST must include a rule.
     rule: Rule,
 }
 
 impl ReasonablePosixDst {
-    /// Returns the offset for this DST zone.
-    ///
-    /// If one wasn't explicity given, then this returns an offset equivalent
-    /// to one hour east of the given standard time offset.
-    fn posix_offset(&self, std_offset: &PosixOffset) -> PosixOffset {
-        if let Some(ref offset) = self.offset {
-            return *offset;
-        }
-        // When no offset is specified, we default to an offset one hour
-        // east for DST. We subtract one because POSIX time offsets are
-        // backwards.
-        PosixOffset {
-            hour: std_offset.hour - (std_offset.sign() * C(1)),
-            ..*std_offset
-        }
+    /// Converts from the shared-but-internal API for use in proc macros.
+    ///
+    /// This works in a `const` context by requiring that the time zone
+    /// abbreviations are `static` strings. This is used when converting
+    /// code generated by a proc macro to this Jiff internal type.
+    const fn from_shared_const(
+        sh: &shared::PosixDst<&'static str>,
+    ) -> ReasonablePosixDst {
+        use crate::util::constant::unwrap;
+
+        let abbrev = unwrap!(
+            Abbreviation::new(sh.abbrev),
+            "expected short enough dst tz abbreviation"
+        );
+        let offset = PosixOffset {
+            offset: unwrap!(
+                SpanZoneOffset::new_const(sh.offset),
+                "expected dst offset in range",
+            ),
+        };
+        let rule = match sh.rule {
+            None => {
+                panic!("expected reasonable POSIX time zone (DST has a rule)")
+            }
+            Some(ref rule) => Rule::from_shared(rule),
+        };
+        ReasonablePosixDst { abbrev, offset, rule }
     }
-}
 
-impl core::fmt::Display for ReasonablePosixDst {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+    fn display(
+        &self,
+        std_offset: PosixOffset,
+        f: &mut core::fmt::Formatter,
+    ) -> core::fmt::Result {
         write!(f, "{}", AbbreviationDisplay(self.abbrev))?;
-        if let Some(offset) = self.offset {
-            write!(f, "{offset}")?;
+        // The overwhelming common case is that DST is exactly one hour ahead
+        // of standard time. So common that this is the default. So don't write
+        // the offset if we don't need to.
+        let default =
+            PosixOffset { offset: std_offset.offset + t::SECONDS_PER_HOUR };
+        if self.offset != default {
+            write!(f, "{}", self.offset)?;
         }
         write!(f, ",{}", self.rule)?;
         Ok(())
@@ -622,6 +677,17 @@ impl core::fmt::Display for ReasonablePosixDst {
 }
 
 /// A POSIX time zone.
+///
+/// This is effectively identical to a `ReasonablePosixTimeZone`, except if it
+/// has a DST time zone abbreviation provided, then it _may_ be missing the
+/// corresponding transition rule. This is seemingly allowed by POSIX, but Jiff
+/// in practice considers POSIX time zones with a DST time zone abbreviation
+/// but without a DST transition rule to be invalid.
+///
+/// This type generally shouldn't be used. It's only exported (to the rest of
+/// the crate) for use with `PosixTzEnv`. The only thing you can do with a
+/// `PosixTimeZone` is attempt to convert it a `ReasonablePosixTimeZone`. Jiff
+/// will emit helpful log messages when this conversion fails.
 #[derive(Debug, Eq, PartialEq)]
 pub(crate) struct PosixTimeZone {
     std_abbrev: Abbreviation,
@@ -632,25 +698,46 @@ pub(crate) struct PosixTimeZone {
 impl PosixTimeZone {
     /// Parse a POSIX `TZ` environment variable, assuming it's a rule and not
     /// an implementation defined value, from the given bytes.
+    #[cfg(feature = "alloc")]
     fn parse(bytes: impl AsRef<[u8]>) -> Result<PosixTimeZone, Error> {
-        // We enable the IANA v3+ extensions here. (Namely, that the time
-        // specification hour value has the range `-167..=167` instead of
-        // `0..=24`.) Requiring strict POSIX rules doesn't seem necessary
-        // since the extension is a strict superset. Plus, GNU tooling
-        // seems to accept the extension.
-        let parser =
-            Parser { ianav3plus: true, ..Parser::new(bytes.as_ref()) };
-        parser.parse()
+        let shared_tz = crate::shared::PosixTimeZone::parse(bytes.as_ref())
+            .map_err(Error::adhoc)?;
+        let jiff_tz = PosixTimeZone::from_shared_owned(&shared_tz);
+        Ok(jiff_tz)
     }
 
     /// Like parse, but parses a prefix of the input given and returns whatever
     /// is remaining.
+    #[cfg(feature = "alloc")]
     fn parse_prefix<'b, B: AsRef<[u8]> + ?Sized + 'b>(
         bytes: &'b B,
     ) -> Result<(PosixTimeZone, &'b [u8]), Error> {
-        let parser =
-            Parser { ianav3plus: true, ..Parser::new(bytes.as_ref()) };
-        parser.parse_prefix()
+        let (shared_tz, remaining) =
+            crate::shared::PosixTimeZone::parse_prefix(bytes.as_ref())
+                .map_err(Error::adhoc)?;
+        let jiff_tz = PosixTimeZone::from_shared_owned(&shared_tz);
+        Ok((jiff_tz, remaining))
+    }
+
+    /// Converts from the shared-but-internal API for use in proc macros.
+    ///
+    /// This is not `const` since it accepts an owned `String` as a time zone
+    /// abbreviation. This is used when parsing POSIX time zones at runtime.
+    #[cfg(feature = "alloc")]
+    pub(crate) fn from_shared_owned(
+        sh: &shared::PosixTimeZone<alloc::string::String>,
+    ) -> PosixTimeZone {
+        let std_abbrev = Abbreviation::new(&sh.std_abbrev)
+            .expect("expected short enough std tz abbreviation");
+        let std_offset = PosixOffset {
+            offset: SpanZoneOffset::new(sh.std_offset)
+                .expect("expected std offset in range"),
+        };
+        let dst = match sh.dst {
+            None => None,
+            Some(ref dst) => Some(PosixDst::from_shared_owned(dst)),
+        };
+        PosixTimeZone { std_abbrev, std_offset, dst }
     }
 
     /// Transforms this POSIX time zone into a "reasonable" time zone.
@@ -703,7 +790,7 @@ impl core::fmt::Display for PosixTimeZone {
             self.std_offset
         )?;
         if let Some(ref dst) = self.dst {
-            write!(f, "{dst}")?;
+            dst.display(self.std_offset, f)?;
         }
         Ok(())
     }
@@ -713,15 +800,45 @@ impl core::fmt::Display for PosixTimeZone {
 #[derive(Debug, Eq, PartialEq)]
 struct PosixDst {
     abbrev: Abbreviation,
-    offset: Option<PosixOffset>,
+    offset: PosixOffset,
     rule: Option<Rule>,
 }
 
-impl core::fmt::Display for PosixDst {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+impl PosixDst {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    ///
+    /// This is not `const` since it accepts an owned `String` as a time zone
+    /// abbreviation. This is used when parsing POSIX time zones at runtime.
+    #[cfg(feature = "alloc")]
+    fn from_shared_owned(
+        sh: &shared::PosixDst<alloc::string::String>,
+    ) -> PosixDst {
+        let abbrev = Abbreviation::new(&sh.abbrev)
+            .expect("expected short enough dst tz abbreviation");
+        let offset = PosixOffset {
+            offset: SpanZoneOffset::new(sh.offset)
+                .expect("expected dst offset in range"),
+        };
+        let rule = match sh.rule {
+            None => None,
+            Some(ref rule) => Some(Rule::from_shared(rule)),
+        };
+        PosixDst { abbrev, offset, rule }
+    }
+
+    fn display(
+        &self,
+        std_offset: PosixOffset,
+        f: &mut core::fmt::Formatter,
+    ) -> core::fmt::Result {
         write!(f, "{}", AbbreviationDisplay(self.abbrev))?;
-        if let Some(offset) = self.offset {
-            write!(f, "{offset}")?;
+        // The overwhelming common case is that DST is exactly one hour ahead
+        // of standard time. So common that this is the default. So don't write
+        // the offset if we don't need to.
+        let default =
+            PosixOffset { offset: std_offset.offset + t::SECONDS_PER_HOUR };
+        if self.offset != default {
+            write!(f, "{}", self.offset)?;
         }
         if let Some(rule) = self.rule {
             write!(f, ",{rule}")?;
@@ -734,66 +851,45 @@ impl core::fmt::Display for PosixDst {
 /// time zone.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 struct PosixOffset {
-    sign: Option<Sign>,
-    hour: PosixHour,
-    minute: Option<Minute>,
-    second: Option<Second>,
+    offset: SpanZoneOffset,
 }
 
 impl PosixOffset {
-    /// Converts this offset to the standard time zone offset used in this
-    /// crate. This flips the underlying sign to follow `jiff::tz::Offset`'s
-    /// convention (which is the convention that pretty much everyon, except
-    /// of course POSIX, uses).
-    ///
-    /// The sign is flipped because POSIX time zones use negative offsets for
-    /// zones east of the prime meridian. But the much more common convention
-    /// nowadays is to use negative offsets for zones west of the prime
-    /// meridian. In other words, a POSIX time zone like `EST5` corresponds to
-    /// an offset of `-05:00:00`.
-    fn to_offset(&self) -> Offset {
-        let sign = SpanZoneOffset::rfrom(-self.sign());
-        let hour = SpanZoneOffset::rfrom(self.hour);
-        let minute =
-            SpanZoneOffset::rfrom(self.minute.unwrap_or(C(0).rinto()));
-        let second =
-            SpanZoneOffset::rfrom(self.second.unwrap_or(C(0).rinto()));
-        let seconds = (hour * t::SECONDS_PER_HOUR)
-            + (minute * t::SECONDS_PER_MINUTE)
-            + second;
-        Offset::from_seconds_ranged(sign * seconds)
-    }
-
-    /// Returns the sign for this offset, defaulting to positive if one wasn't
-    /// explicitly given.
-    ///
-    /// Note that the sign for a POSIX offset is backwards. EST5, for example,
-    /// corresponds to UTC-05.
-    fn sign(&self) -> Sign {
-        self.sign.unwrap_or(Sign::N::<1>())
+    fn offset(&self) -> Offset {
+        Offset::from_seconds_ranged(self.offset)
     }
 }
 
 impl core::fmt::Display for PosixOffset {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        if let Some(sign) = self.sign {
-            if sign < 0 {
-                write!(f, "-")?;
-            } else {
-                write!(f, "+")?;
-            }
+        let offset = self.offset();
+        // Yes, this is backwards. Blame POSIX.
+        // N.B. `+` is the default, so we don't
+        // need to write that out.
+        if offset.seconds() > 0 {
+            write!(f, "-")?;
         }
-        write!(f, "{}", self.hour)?;
-        if let Some(minute) = self.minute {
-            write!(f, ":{minute:02}")?;
-            if let Some(second) = self.second {
-                write!(f, ":{second:02}")?;
+        let h = offset.part_hours_ranged().get().unsigned_abs();
+        let m = offset.part_minutes_ranged().get().unsigned_abs();
+        let s = offset.part_seconds_ranged().get().unsigned_abs();
+        write!(f, "{h}")?;
+        if m != 0 || s != 0 {
+            write!(f, ":{m:02}")?;
+            if s != 0 {
+                write!(f, ":{s:02}")?;
             }
         }
         Ok(())
     }
 }
 
+#[cfg(test)]
+impl From<Offset> for PosixOffset {
+    fn from(offset: Offset) -> PosixOffset {
+        PosixOffset { offset: offset.seconds_ranged() }
+    }
+}
+
 /// The rule for when a DST transition starts and ends.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 struct Rule {
@@ -801,6 +897,15 @@ struct Rule {
     end: PosixDateTimeSpec,
 }
 
+impl Rule {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    const fn from_shared(sh: &shared::PosixRule) -> Rule {
+        let start = PosixDateTimeSpec::from_shared(&sh.start);
+        let end = PosixDateTimeSpec::from_shared(&sh.end);
+        Rule { start, end }
+    }
+}
+
 impl core::fmt::Display for Rule {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "{},{}", self.start, self.end)
@@ -812,10 +917,18 @@ impl core::fmt::Display for Rule {
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 struct PosixDateTimeSpec {
     date: PosixDateSpec,
-    time: Option<PosixTimeSpec>,
+    time: PosixTimeSpec,
 }
 
 impl PosixDateTimeSpec {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    const fn from_shared(sh: &shared::PosixDayTime) -> PosixDateTimeSpec {
+        PosixDateTimeSpec {
+            date: PosixDateSpec::from_shared(&sh.date),
+            time: PosixTimeSpec::from_shared(sh.time),
+        }
+    }
+
     /// Turns this POSIX datetime spec into a civil datetime in the year given
     /// with the given offset. The datetimes returned are offset by the given
     /// offset. For wall clock time, an offset of `0` should be given. For
@@ -840,22 +953,14 @@ impl PosixDateTimeSpec {
 
         let Some(date) = self.date.to_civil_date(year) else { return mkmax() };
         let mut dt = date.to_datetime(Time::MIN);
-        let dur_transition = self.time().to_duration();
-        let dur_offset = SignedDuration::from(offset);
-        dt = dt.checked_add(dur_transition).unwrap_or_else(|_| {
-            if dur_transition.is_negative() {
+        let dur = self.time().to_duration() - SignedDuration::from(offset);
+        dt = dt.checked_add(dur).unwrap_or_else(|_| {
+            if dur.is_negative() {
                 mkmin()
             } else {
                 mkmax()
             }
         });
-        dt = dt.checked_sub(dur_offset).unwrap_or_else(|_| {
-            if dur_transition.is_negative() {
-                mkmax()
-            } else {
-                mkmin()
-            }
-        });
         if dt.date().year() < year {
             mkmin()
         } else if dt.date().year() > year {
@@ -868,21 +973,17 @@ impl PosixDateTimeSpec {
     /// Returns the time for this spec, falling back to the default of 2:00:00
     /// as specified by POSIX.
     fn time(self) -> PosixTimeSpec {
-        const DEFAULT: PosixTimeSpec = PosixTimeSpec {
-            sign: None,
-            hour: IanaHour::N::<2>(),
-            minute: None,
-            second: None,
-        };
-        self.time.unwrap_or(DEFAULT)
+        self.time
     }
 }
 
 impl core::fmt::Display for PosixDateTimeSpec {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         write!(f, "{}", self.date)?;
-        if let Some(time) = self.time {
-            write!(f, "/{time}")?;
+        // This is the default time, so don't write it if we
+        // don't need to.
+        if self.time != PosixTimeSpec::DEFAULT {
+            write!(f, "/{}", self.time)?;
         }
         Ok(())
     }
@@ -908,6 +1009,53 @@ enum PosixDateSpec {
 }
 
 impl PosixDateSpec {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    const fn from_shared(sh: &shared::PosixDay) -> PosixDateSpec {
+        use crate::util::constant::unwrap;
+
+        match *sh {
+            shared::PosixDay::JulianOne(doy) => {
+                let doy = unwrap!(
+                    PosixJulianDayNoLeap::new_const(doy),
+                    "expected 1-based Julian day in range"
+                );
+                PosixDateSpec::JulianOne(doy)
+            }
+            shared::PosixDay::JulianZero(doy) => {
+                let doy = unwrap!(
+                    PosixJulianDayWithLeap::new_const(doy),
+                    "expected 0-based Julian day in range"
+                );
+                PosixDateSpec::JulianZero(doy)
+            }
+            shared::PosixDay::WeekdayOfMonth { month, week, weekday } => {
+                let month = unwrap!(
+                    Month::new_const(month),
+                    "expected weekday-of-month month in range"
+                );
+                let week = unwrap!(
+                    PosixWeek::new_const(week),
+                    "expected weekday-of-month week in range"
+                );
+                let weekday = match weekday {
+                    0 => Weekday::Sunday,
+                    1 => Weekday::Monday,
+                    2 => Weekday::Tuesday,
+                    3 => Weekday::Wednesday,
+                    4 => Weekday::Thursday,
+                    5 => Weekday::Friday,
+                    6 => Weekday::Saturday,
+                    _ => panic!("expected weekday-of-month weekday in range"),
+                };
+                PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
+                    month,
+                    week,
+                    weekday,
+                })
+            }
+        }
+    }
+
     /// Convert this date specification to a civil date in the year given.
     ///
     /// If this date specification couldn't be turned into a date in the year
@@ -1029,918 +1177,131 @@ impl core::fmt::Display for WeekdayOfMonth {
 /// IANA tzfile v3+ format expands the range to `-167..=167`.
 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
 struct PosixTimeSpec {
-    sign: Option<Sign>,
-    /// The hour component of this time specification. When IANA V3+ parsing
-    /// is enabled, then this can be in any value in the range `0..=167`. But
-    /// otherwise, it is limited to `0..=24`.
-    hour: IanaHour,
-    minute: Option<Minute>,
-    second: Option<Second>,
+    duration: PosixTimeSeconds,
 }
 
 impl PosixTimeSpec {
-    /// Returns this time specification as a duration of time.
-    fn to_duration(&self) -> SignedDuration {
-        let sign = i64::from(self.sign());
-        let hour = sign * i64::from(self.hour);
-        let minute = sign * i64::from(self.minute.unwrap_or(C(0).rinto()));
-        let second = sign * i64::from(self.second.unwrap_or(C(0).rinto()));
-        SignedDuration::from_secs(second + (60 * minute) + (60 * 60 * hour))
+    const DEFAULT: PosixTimeSpec = PosixTimeSpec {
+        duration: PosixTimeSeconds::new_unchecked(2 * 60 * 60),
+    };
+
+    /// Converts from the shared-but-internal API for use in proc macros.
+    const fn from_shared(seconds: i32) -> PosixTimeSpec {
+        let duration = crate::util::constant::unwrap!(
+            PosixTimeSeconds::new_const(seconds),
+            "expected POSIX time spec seconds in range",
+        );
+        PosixTimeSpec { duration }
     }
 
-    /// Returns the sign for this time sepc, defaulting to positive if one
-    /// wasn't explicitly given.
-    fn sign(&self) -> Sign {
-        self.sign.unwrap_or(Sign::N::<1>())
+    fn to_duration(&self) -> SignedDuration {
+        SignedDuration::from_secs(self.duration.get().into())
     }
 }
 
 impl core::fmt::Display for PosixTimeSpec {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        if let Some(sign) = self.sign {
-            if sign < 0 {
-                write!(f, "-")?;
-            } else {
-                write!(f, "+")?;
-            }
+        let duration = self.to_duration();
+        if duration.is_negative() {
+            write!(f, "-")?;
+            // The default is positive, so when
+            // positive, we write nothing.
         }
-        write!(f, "{}", self.hour)?;
-        if let Some(minute) = self.minute {
-            write!(f, ":{minute:02}")?;
-            if let Some(second) = self.second {
-                write!(f, ":{second:02}")?;
+        let h = duration.as_hours().unsigned_abs();
+        let m = duration.as_mins().unsigned_abs() % 60;
+        let s = duration.as_secs().unsigned_abs() % 60;
+        write!(f, "{h}")?;
+        if m != 0 || s != 0 {
+            write!(f, ":{m:02}")?;
+            if s != 0 {
+                write!(f, ":{s:02}")?;
             }
         }
         Ok(())
     }
 }
 
+/// A helper type for formatting a time zone abbreviation.
+///
+/// Basically, this will write the `<` and `>` quotes if necessary, and
+/// otherwise write out the abbreviation in its unquoted form.
 #[derive(Debug)]
-struct Parser<'s> {
-    /// The `TZ` string that we're parsing.
-    tz: &'s [u8],
-    /// The parser's current position in `tz`.
-    pos: Cell<usize>,
-    /// Whether to use IANA rules, i.e., when parsing a TZ string in a TZif
-    /// file of version 3 or greater. From `tzfile(5)`:
-    ///
-    /// > First, the hours part of its transition times may be signed and range
-    /// > from `-167` through `167` instead of the POSIX-required unsigned
-    /// > values from `0` through `24`. Second, DST is in effect all year if
-    /// > it starts January 1 at 00:00 and ends December 31 at 24:00 plus the
-    /// > difference between daylight saving and standard time.
-    ///
-    /// At time of writing, I don't think I understand the significance of
-    /// the second part above. (RFC 8536 elaborates that it is meant to be an
-    /// explicit clarification of something that POSIX itself implies.) But the
-    /// first part is clear: it permits the hours to be a bigger range.
-    ianav3plus: bool,
-}
-
-impl<'s> Parser<'s> {
-    fn new<B: ?Sized + AsRef<[u8]>>(tz: &'s B) -> Parser<'s> {
-        Parser { tz: tz.as_ref(), pos: Cell::new(0), ianav3plus: false }
-    }
+struct AbbreviationDisplay(Abbreviation);
 
-    /// Parses a POSIX time zone from the current position of the parser and
-    /// ensures that the entire TZ string corresponds to a single valid POSIX
-    /// time zone.
-    fn parse(&self) -> Result<PosixTimeZone, Error> {
-        let (time_zone, remaining) = self.parse_prefix()?;
-        if !remaining.is_empty() {
-            return Err(err!(
-                "expected entire TZ string to be a valid POSIX \
-                 time zone, but found '{}' after what would otherwise \
-                 be a valid POSIX TZ string",
-                Bytes(remaining),
-            ));
+impl core::fmt::Display for AbbreviationDisplay {
+    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+        let s = self.0.as_str();
+        if s.chars().any(|ch| ch == '+' || ch == '-') {
+            write!(f, "<{s}>")
+        } else {
+            write!(f, "{s}")
         }
-        Ok(time_zone)
-    }
-
-    /// Parses a POSIX time zone from the current position of the parser and
-    /// returns the remaining input.
-    fn parse_prefix(&self) -> Result<(PosixTimeZone, &'s [u8]), Error> {
-        let time_zone = self.parse_posix_time_zone()?;
-        Ok((time_zone, self.remaining()))
     }
+}
 
-    /// Parse a POSIX time zone from the current position of the parser.
-    ///
-    /// Upon success, the parser will be positioned immediately following the
-    /// TZ string.
-    fn parse_posix_time_zone(&self) -> Result<PosixTimeZone, Error> {
-        let std_abbrev = self
-            .parse_abbreviation()
-            .map_err(|e| e.context("failed to parse standard abbreviation"))?;
-        let std_offset = self
-            .parse_posix_offset()
-            .map_err(|e| e.context("failed to parse standard offset"))?;
-        let mut dst = None;
-        if !self.is_done()
-            && (self.byte().is_ascii_alphabetic() || self.byte() == b'<')
-        {
-            dst = Some(self.parse_posix_dst()?);
-        }
-        Ok(PosixTimeZone { std_abbrev, std_offset, dst })
-    }
+// The tests below all require parsing which requires alloc.
+#[cfg(feature = "alloc")]
+#[cfg(test)]
+mod tests {
+    use alloc::string::ToString;
 
-    /// Parse a DST zone with an optional explicit transition rule.
-    ///
-    /// This assumes the parser is positioned at the first byte of the DST
-    /// abbreviation.
-    ///
-    /// Upon success, the parser will be positioned immediately after the end
-    /// of the DST transition rule (which might just be the abbreviation, but
-    /// might also include explicit start/end datetime specifications).
-    fn parse_posix_dst(&self) -> Result<PosixDst, Error> {
-        let abbrev = self
-            .parse_abbreviation()
-            .map_err(|e| e.context("failed to parse DST abbreviation"))?;
-        let mut dst = PosixDst { abbrev, offset: None, rule: None };
-        if self.is_done() {
-            return Ok(dst);
-        }
-        if self.byte() != b',' {
-            dst.offset = Some(
-                self.parse_posix_offset()
-                    .map_err(|e| e.context("failed to parse DST offset"))?,
-            );
-            if self.is_done() {
-                return Ok(dst);
-            }
-        }
-        if self.byte() != b',' {
-            return Err(err!(
-                "after parsing DST offset in POSIX time zone string, \
-                 found '{}' but expected a ','",
-                Byte(self.byte()),
-            ));
-        }
-        if !self.bump() {
-            return Err(err!(
-                "after parsing DST offset in POSIX time zone string, \
-                 found end of string after a trailing ','",
-            ));
-        }
-        dst.rule = Some(self.parse_rule()?);
-        Ok(dst)
-    }
+    use crate::{civil::date, tz::offset};
 
-    /// Parse a time zone abbreviation.
-    ///
-    /// This assumes the parser is positioned at the first byte of the
-    /// abbreviation. This is either the first character in the abbreviation,
-    /// or the opening quote of a quoted abbreviation.
-    ///
-    /// Upon success, the parser will be positioned immediately following the
-    /// abbreviation name.
-    fn parse_abbreviation(&self) -> Result<Abbreviation, Error> {
-        if self.byte() == b'<' {
-            if !self.bump() {
-                return Err(err!(
-                    "found opening '<' quote for abbreviation in \
-                     POSIX time zone string, and expected a name \
-                     following it, but found the end of string instead"
-                ));
-            }
-            self.parse_quoted_abbreviation()
-        } else {
-            self.parse_unquoted_abbreviation()
-        }
-    }
+    use super::*;
 
-    /// Parses an unquoted time zone abbreviation.
-    ///
-    /// This assumes the parser is position at the first byte in the
-    /// abbreviation.
-    ///
-    /// Upon success, the parser will be positioned immediately after the
-    /// last byte in the abbreviation.
-    fn parse_unquoted_abbreviation(&self) -> Result<Abbreviation, Error> {
-        let start = self.pos();
-        for i in 0.. {
-            if !self.byte().is_ascii_alphabetic() {
-                break;
-            }
-            if i >= Abbreviation::capacity() {
-                return Err(err!(
-                    "expected abbreviation with at most {} bytes, \
-                     but found a longer abbreviation beginning with '{}'",
-                    Abbreviation::capacity(),
-                    Bytes(&self.tz[start..i]),
-                ));
-            }
-            if !self.bump() {
-                break;
-            }
-        }
-        let end = self.pos();
-        let abbrev =
-            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
-                // NOTE: I believe this error is technically impossible since
-                // the loop above restricts letters in an abbreviation to
-                // ASCII. So everything from `start` to `end` is ASCII and
-                // thus should be UTF-8. But it doesn't cost us anything to
-                // report an error here in case the code above evolves somehow.
-                err!(
-                    "found abbreviation '{}', but it is not valid UTF-8",
-                    Bytes(&self.tz[start..end]),
-                )
-            })?;
-        if abbrev.len() < 3 {
-            return Err(err!(
-                "expected abbreviation with 3 or more bytes, but found \
-                 abbreviation {:?} with {} bytes",
-                abbrev,
-                abbrev.len(),
-            ));
-        }
-        // OK because we verified above that the abbreviation
-        // does not exceed `Abbreviation::capacity`.
-        Ok(Abbreviation::new(abbrev).unwrap())
+    fn reasonable_posix_time_zone(
+        input: impl AsRef<[u8]>,
+    ) -> ReasonablePosixTimeZone {
+        let input = core::str::from_utf8(input.as_ref()).unwrap();
+        let tz = ReasonablePosixTimeZone::parse(input).unwrap();
+        // While we're here, assert that converting the TZ back
+        // to a string matches what we got. In the original version
+        // of the POSIX TZ parser, we were very meticulous about
+        // capturing the exact AST of the time zone. But I've
+        // since simplified the data structure considerably such
+        // that it is lossy in terms of what was actually parsed
+        // (but of course, not lossy in terms of the semantic
+        // meaning of the time zone).
+        //
+        // So to account for this, we serialize to a string and
+        // then parse it back. We should get what we started with.
+        let reparsed = ReasonablePosixTimeZone::parse(tz.to_string()).unwrap();
+        assert_eq!(tz, reparsed);
+        assert_eq!(tz.to_string(), reparsed.to_string());
+        tz
     }
 
-    /// Parses a quoted time zone abbreviation.
-    ///
-    /// This assumes the parser is positioned immediately after the opening
-    /// `<` quote. That is, at the first byte in the abbreviation.
-    ///
-    /// Upon success, the parser will be positioned immediately after the
-    /// closing `>` quote.
-    fn parse_quoted_abbreviation(&self) -> Result<Abbreviation, Error> {
-        let start = self.pos();
-        for i in 0.. {
-            if !self.byte().is_ascii_alphanumeric()
-                && self.byte() != b'+'
-                && self.byte() != b'-'
-            {
-                break;
-            }
-            if i >= Abbreviation::capacity() {
-                return Err(err!(
-                    "expected abbreviation with at most {} bytes, \
-                     but found a longer abbreviation beginning with '{}'",
-                    Abbreviation::capacity(),
-                    Bytes(&self.tz[start..i]),
-                ));
-            }
-            if !self.bump() {
-                break;
-            }
-        }
-        let end = self.pos();
-        let abbrev =
-            core::str::from_utf8(&self.tz[start..end]).map_err(|_| {
-                // NOTE: I believe this error is technically impossible since
-                // the loop above restricts letters in an abbreviation to
-                // ASCII. So everything from `start` to `end` is ASCII and
-                // thus should be UTF-8. But it doesn't cost us anything to
-                // report an error here in case the code above evolves somehow.
-                err!(
-                    "found abbreviation '{}', but it is not valid UTF-8",
-                    Bytes(&self.tz[start..end]),
-                )
-            })?;
-        if self.is_done() {
-            return Err(err!(
-                "found non-empty quoted abbreviation {abbrev:?}, but \
-                 did not find expected end-of-quoted abbreviation \
-                 '>' character",
-            ));
-        }
-        if self.byte() != b'>' {
-            return Err(err!(
-                "found non-empty quoted abbreviation {abbrev:?}, but \
-                 found '{}' instead of end-of-quoted abbreviation '>' \
-                 character",
-                Byte(self.byte()),
-            ));
-        }
-        self.bump();
-        if abbrev.len() < 3 {
-            return Err(err!(
-                "expected abbreviation with 3 or more bytes, but found \
-                 abbreviation {abbrev:?} with {} bytes",
-                abbrev.len(),
-            ));
-        }
-        // OK because we verified above that the abbreviation
-        // does not exceed `Abbreviation::capacity()`.
-        Ok(Abbreviation::new(abbrev).unwrap())
-    }
+    #[test]
+    fn reasonable_to_dst_civil_datetime_utc_range() {
+        let tz = reasonable_posix_time_zone("WART4WARST,J1/-3,J365/20");
+        let dst_info = DstInfo {
+            // We test this in other places. It's too annoying to write this
+            // out here, and I didn't adopt snapshot testing until I had
+            // written out these tests by hand. ¯\_(ツ)_/¯
+            dst: tz.dst.as_ref().unwrap(),
+            offset: offset(-3),
+            start: date(2024, 1, 1).at(1, 0, 0, 0),
+            end: date(2024, 12, 31).at(23, 0, 0, 0),
+        };
+        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
 
-    /// Parse a POSIX time offset.
-    ///
-    /// This assumes the parser is positioned at the first byte of the offset.
-    /// This can either be a digit (for a positive offset) or the sign of the
-    /// offset (which must be either `-` or `+`).
-    ///
-    /// Upon success, the parser will be positioned immediately after the end
-    /// of the offset.
-    fn parse_posix_offset(&self) -> Result<PosixOffset, Error> {
-        let sign = self.parse_optional_sign().map_err(|e| {
-            e.context(
-                "failed to parse sign for time offset \
-                 in POSIX time zone string",
-            )
-        })?;
-        let hour = self.parse_hour_posix()?;
-        let offset = PosixOffset { sign, hour, minute: None, second: None };
-        if self.maybe_byte() != Some(b':') {
-            return Ok(offset);
-        }
-        if !self.bump() {
-            return Err(err!(
-                "incomplete time in POSIX timezone (missing minutes)",
-            ));
-        }
-        let minute = Some(self.parse_minute()?);
-        if self.maybe_byte() != Some(b':') {
-            return Ok(PosixOffset { sign, hour, minute, second: None });
-        }
-        if !self.bump() {
-            return Err(err!(
-                "incomplete time in POSIX timezone (missing seconds)",
-            ));
-        }
-        let second = Some(self.parse_second()?);
-        Ok(PosixOffset { sign, hour, minute, second })
-    }
+        let tz = reasonable_posix_time_zone("WART4WARST,J1/-4,J365/21");
+        let dst_info = DstInfo {
+            dst: tz.dst.as_ref().unwrap(),
+            offset: offset(-3),
+            start: date(2024, 1, 1).at(0, 0, 0, 0),
+            end: date(2024, 12, 31).at(23, 59, 59, 999_999_999),
+        };
+        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
 
-    /// Parses a POSIX DST transition rule.
-    ///
-    /// This assumes the parser is positioned at the first byte in the rule.
-    /// That is, it comes immediately after the DST abbreviation or its
-    /// optional offset.
-    ///
-    /// Upon success, the parser will be positioned immediately after the
-    /// DST transition rule. In typical cases, this corresponds to the end of
-    /// the TZ string.
-    fn parse_rule(&self) -> Result<Rule, Error> {
-        let start = self.parse_posix_datetime_spec().map_err(|e| {
-            e.context("failed to parse start of DST transition rule")
-        })?;
-        if self.maybe_byte() != Some(b',') || !self.bump() {
-            return Err(err!(
-                "expected end of DST rule after parsing the start \
-                 of the DST rule"
-            ));
-        }
-        let end = self.parse_posix_datetime_spec().map_err(|e| {
-            e.context("failed to parse end of DST transition rule")
-        })?;
-        Ok(Rule { start, end })
-    }
-
-    /// Parses a POSIX datetime specification.
-    ///
-    /// This assumes the parser is position at the first byte where a datetime
-    /// specification is expected to occur.
-    ///
-    /// Upon success, the parser will be positioned after the datetime
-    /// specification. This will either be immediately after the date, or if
-    /// it's present, the time part of the specification.
-    fn parse_posix_datetime_spec(&self) -> Result<PosixDateTimeSpec, Error> {
-        let date = self.parse_posix_date_spec()?;
-        let mut spec = PosixDateTimeSpec { date, time: None };
-        if self.maybe_byte() != Some(b'/') {
-            return Ok(spec);
-        }
-        if !self.bump() {
-            return Err(err!(
-                "expected time specification after '/' following a date
-                 specification in a POSIX time zone DST transition rule",
-            ));
-        }
-        spec.time = Some(self.parse_posix_time_spec()?);
-        Ok(spec)
-    }
-
-    /// Parses a POSIX date specification.
-    ///
-    /// This assumes the parser is positioned at the first byte of the date
-    /// specification. This can be `J` (for one based Julian day without leap
-    /// days), `M` (for "weekday of month") or a digit starting the zero based
-    /// Julian day with leap days. This routine will validate that the position
-    /// points to one of these possible values. That is, the caller doesn't
-    /// need to parse the `M` or the `J` or the leading digit. The caller
-    /// should just call this routine when it *expect* a date specification to
-    /// follow.
-    ///
-    /// Upon success, the parser will be positioned immediately after the date
-    /// specification.
-    fn parse_posix_date_spec(&self) -> Result<PosixDateSpec, Error> {
-        match self.byte() {
-            b'J' => {
-                if !self.bump() {
-                    return Err(err!(
-                        "expected one-based Julian day after 'J' in date \
-                         specification of a POSIX time zone DST transition \
-                         rule, but got the end of the string instead"
-                    ));
-                }
-                Ok(PosixDateSpec::JulianOne(
-                    self.parse_posix_julian_day_no_leap()?,
-                ))
-            }
-            b'0'..=b'9' => Ok(PosixDateSpec::JulianZero(
-                self.parse_posix_julian_day_with_leap()?,
-            )),
-            b'M' => {
-                if !self.bump() {
-                    return Err(err!(
-                        "expected month-week-weekday after 'M' in date \
-                         specification of a POSIX time zone DST transition \
-                         rule, but got the end of the string instead"
-                    ));
-                }
-                Ok(PosixDateSpec::WeekdayOfMonth(
-                    self.parse_weekday_of_month()?,
-                ))
-            }
-            _ => Err(err!(
-                "expected 'J', a digit or 'M' at the beginning of a date \
-                 specification of a POSIX time zone DST transition rule, \
-                 but got '{}' instead",
-                Byte(self.byte()),
-            )),
-        }
-    }
-
-    /// Parses a POSIX Julian day that does not include leap days
-    /// (`1 <= n <= 365`).
-    ///
-    /// This assumes the parser is positioned just after the `J` and at the
-    /// first digit of the Julian day. Upon success, the parser will be
-    /// positioned immediately following the day number.
-    fn parse_posix_julian_day_no_leap(
-        &self,
-    ) -> Result<PosixJulianDayNoLeap, Error> {
-        let number = self
-            .parse_number_with_upto_n_digits(3)
-            .map_err(|e| e.context("invalid one based Julian day"))?;
-        let day = PosixJulianDayNoLeap::new(number).ok_or_else(|| {
-            err!("invalid one based Julian day (must be in range 1..=365")
-        })?;
-        Ok(day)
-    }
-
-    /// Parses a POSIX Julian day that includes leap days (`0 <= n <= 365`).
-    ///
-    /// This assumes the parser is positioned at the first digit of the Julian
-    /// day. Upon success, the parser will be positioned immediately following
-    /// the day number.
-    fn parse_posix_julian_day_with_leap(
-        &self,
-    ) -> Result<PosixJulianDayWithLeap, Error> {
-        let number = self
-            .parse_number_with_upto_n_digits(3)
-            .map_err(|e| e.context("invalid zero based Julian day"))?;
-        let day = PosixJulianDayWithLeap::new(number).ok_or_else(|| {
-            err!("invalid zero based Julian day (must be in range 0..=365")
-        })?;
-        Ok(day)
-    }
-
-    /// Parses a POSIX "weekday of month" specification.
-    ///
-    /// This assumes the parser is positioned just after the `M` byte and
-    /// at the first digit of the month. Upon success, the parser will be
-    /// positioned immediately following the "weekday of the month" that was
-    /// parsed.
-    fn parse_weekday_of_month(&self) -> Result<WeekdayOfMonth, Error> {
-        let month = self.parse_month()?;
-        if self.maybe_byte() != Some(b'.') {
-            return Err(err!(
-                "expected '.' after month '{month}' in POSIX time zone rule"
-            ));
-        }
-        if !self.bump() {
-            return Err(err!(
-                "expected week after month '{month}' in POSIX time zone rule"
-            ));
-        }
-        let week = self.parse_week()?;
-        if self.maybe_byte() != Some(b'.') {
-            return Err(err!(
-                "expected '.' after week '{week}' in POSIX time zone rule"
-            ));
-        }
-        if !self.bump() {
-            return Err(err!(
-                "expected day-of-week after week '{week}' in \
-                 POSIX time zone rule"
-            ));
-        }
-        let weekday = self.parse_weekday()?;
-        Ok(WeekdayOfMonth { month, week, weekday })
-    }
-
-    /// This parses a POSIX time specification in the format
-    /// `[+/-]hh?[:mm[:ss]]`.
-    ///
-    /// This assumes the parser is positioned at the first `h` (or the sign,
-    /// if present). Upon success, the parser will be positioned immediately
-    /// following the end of the time specification.
-    fn parse_posix_time_spec(&self) -> Result<PosixTimeSpec, Error> {
-        let (sign, hour) = if self.ianav3plus {
-            let sign = self.parse_optional_sign().map_err(|e| {
-                e.context(
-                    "failed to parse sign for transition time \
-                     in POSIX time zone string",
-                )
-            })?;
-            let hour = self.parse_hour_ianav3plus()?;
-            (sign, hour)
-        } else {
-            (None, self.parse_hour_posix()?.rinto())
-        };
-        let spec = PosixTimeSpec { sign, hour, minute: None, second: None };
-        if self.maybe_byte() != Some(b':') {
-            return Ok(spec);
-        }
-        if !self.bump() {
-            return Err(err!(
-                "incomplete transition time in \
-                 POSIX time zone string (missing minutes)",
-            ));
-        }
-        let minute = Some(self.parse_minute()?);
-        if self.maybe_byte() != Some(b':') {
-            return Ok(PosixTimeSpec { sign, hour, minute, second: None });
-        }
-        if !self.bump() {
-            return Err(err!(
-                "incomplete transition time in \
-                 POSIX time zone string (missing seconds)",
-            ));
-        }
-        let second = Some(self.parse_second()?);
-        Ok(PosixTimeSpec { sign, hour, minute, second })
-    }
-
-    /// Parses a month.
-    ///
-    /// This is expected to be positioned at the first digit. Upon success,
-    /// the parser will be positioned after the month (which may contain two
-    /// digits).
-    fn parse_month(&self) -> Result<Month, Error> {
-        let number = self.parse_number_with_upto_n_digits(2)?;
-        let month = Month::new(number).ok_or_else(|| {
-            err!("month in POSIX time zone must be in range 1..=12")
-        })?;
-        Ok(month)
-    }
-
-    /// Parses a week-of-month number.
-    ///
-    /// This is expected to be positioned at the first digit. Upon success,
-    /// the parser will be positioned after the week digit.
-    fn parse_week(&self) -> Result<PosixWeek, Error> {
-        let number = self.parse_number_with_exactly_n_digits(1)?;
-        let week = PosixWeek::new(number).ok_or_else(|| {
-            err!("week in POSIX time zone must be in range 1..=5")
-        })?;
-        Ok(week)
-    }
-
-    /// Parses a week-of-month number.
-    ///
-    /// This is expected to be positioned at the first digit. Upon success,
-    /// the parser will be positioned after the week digit.
-    fn parse_weekday(&self) -> Result<Weekday, Error> {
-        let number = self.parse_number_with_exactly_n_digits(1)?;
-        let number8 = i8::try_from(number).map_err(|_| {
-            err!(
-                "weekday '{number}' in POSIX time zone \
-                 does not fit into 8-bit integer"
-            )
-        })?;
-        let weekday =
-            Weekday::from_sunday_zero_offset(number8).map_err(|_| {
-                err!(
-                    "weekday in POSIX time zone must be in range 0..=6 \
-                     (with 0 corresponding to Sunday), but got {number8}",
-                )
-            })?;
-        Ok(weekday)
-    }
-
-    /// Parses an hour from a POSIX time specification with the IANA v3+
-    /// extension. That is, the hour may be in the range `0..=167`. (Callers
-    /// should parse an optional sign preceding the hour digits when IANA V3+
-    /// parsing is enabled.)
-    ///
-    /// The hour is allowed to be a single digit (unlike minutes or seconds).
-    ///
-    /// This assumes the parser is positioned at the position where the first
-    /// hour digit should occur. Upon success, the parser will be positioned
-    /// immediately after the last hour digit.
-    fn parse_hour_ianav3plus(&self) -> Result<IanaHour, Error> {
-        // Callers should only be using this method when IANA v3+ parsing is
-        // enabled.
-        assert!(self.ianav3plus);
-        let number = self
-            .parse_number_with_upto_n_digits(3)
-            .map_err(|e| e.context("invalid hour digits"))?;
-        let hour = IanaHour::new(number).ok_or_else(|| {
-            err!(
-                "hour in POSIX (IANA v3+ style) \
-                     time zone must be in range -167..=167"
-            )
-        })?;
-        Ok(hour)
-    }
-
-    /// Parses an hour from a POSIX time specification, with the allowed range
-    /// being `0..=24`.
-    ///
-    /// The hour is allowed to be a single digit (unlike minutes or seconds).
-    ///
-    /// This assumes the parser is positioned at the position where the first
-    /// hour digit should occur. Upon success, the parser will be positioned
-    /// immediately after the last hour digit.
-    fn parse_hour_posix(&self) -> Result<PosixHour, Error> {
-        type PosixHour24 = ri8<0, 24>;
-
-        let number = self
-            .parse_number_with_upto_n_digits(2)
-            .map_err(|e| e.context("invalid hour digits"))?;
-        let hour = PosixHour24::new(number).ok_or_else(|| {
-            err!("hour in POSIX time zone must be in range 0..=24")
-        })?;
-        Ok(hour.rinto())
-    }
-
-    /// Parses a minute from a POSIX time specification.
-    ///
-    /// The minute must be exactly two digits.
-    ///
-    /// This assumes the parser is positioned at the position where the first
-    /// minute digit should occur. Upon success, the parser will be positioned
-    /// immediately after the second minute digit.
-    fn parse_minute(&self) -> Result<Minute, Error> {
-        let number = self
-            .parse_number_with_exactly_n_digits(2)
-            .map_err(|e| e.context("invalid minute digits"))?;
-        let minute = Minute::new(number).ok_or_else(|| {
-            err!("minute in POSIX time zone must be in range 0..=59")
-        })?;
-        Ok(minute)
-    }
-
-    /// Parses a second from a POSIX time specification.
-    ///
-    /// The second must be exactly two digits.
-    ///
-    /// This assumes the parser is positioned at the position where the first
-    /// second digit should occur. Upon success, the parser will be positioned
-    /// immediately after the second second digit.
-    fn parse_second(&self) -> Result<Second, Error> {
-        let number = self
-            .parse_number_with_exactly_n_digits(2)
-            .map_err(|e| e.context("invalid second digits"))?;
-        let second = Second::new(number).ok_or_else(|| {
-            err!("second in POSIX time zone must be in range 0..=59")
-        })?;
-        Ok(second)
-    }
-
-    /// Parses a signed 64-bit integer expressed in exactly `n` digits.
-    ///
-    /// If `n` digits could not be found (or if the `TZ` string ends before
-    /// `n` digits could be found), then this returns an error.
-    ///
-    /// This assumes that `n >= 1` and that the parser is positioned at the
-    /// first digit. Upon success, the parser is positioned immediately after
-    /// the `n`th digit.
-    fn parse_number_with_exactly_n_digits(
-        &self,
-        n: usize,
-    ) -> Result<i64, Error> {
-        assert!(n >= 1, "numbers must have at least 1 digit");
-        let start = self.pos();
-        for i in 0..n {
-            if self.is_done() {
-                return Err(err!("expected {n} digits, but found {i}"));
-            }
-            if !self.byte().is_ascii_digit() {
-                return Err(err!("invalid digit '{}'", Byte(self.byte())));
-            }
-            self.bump();
-        }
-        let end = self.pos();
-        parse::i64(&self.tz[start..end])
-    }
-
-    /// Parses a signed 64-bit integer expressed with up to `n` digits and at
-    /// least 1 digit.
-    ///
-    /// This assumes that `n >= 1` and that the parser is positioned at the
-    /// first digit. Upon success, the parser is position immediately after the
-    /// last digit (which can be at most `n`).
-    fn parse_number_with_upto_n_digits(&self, n: usize) -> Result<i64, Error> {
-        assert!(n >= 1, "numbers must have at least 1 digit");
-        let start = self.pos();
-        for _ in 0..n {
-            if self.is_done() || !self.byte().is_ascii_digit() {
-                break;
-            }
-            self.bump();
-        }
-        let end = self.pos();
-        parse::i64(&self.tz[start..end])
-    }
-
-    /// Parses an optional sign.
-    ///
-    /// This assumes the parser is positioned at the position where a positive
-    /// or negative sign is permitted. If one exists, then it is consumed and
-    /// returned. Moreover, if one exists, then this guarantees that it is not
-    /// the last byte in the input. That is, upon success, it is valid to call
-    /// `self.byte()`.
-    fn parse_optional_sign(&self) -> Result<Option<Sign>, Error> {
-        if self.is_done() {
-            return Ok(None);
-        }
-        Ok(match self.byte() {
-            b'-' => {
-                if !self.bump() {
-                    return Err(err!(
-                        "expected digit after '-' sign, but got end of input",
-                    ));
-                }
-                Some(Sign::N::<-1>())
-            }
-            b'+' => {
-                if !self.bump() {
-                    return Err(err!(
-                        "expected digit after '+' sign, but got end of input",
-                    ));
-                }
-                Some(Sign::N::<1>())
-            }
-            _ => None,
-        })
-    }
-}
-
-/// Helper routines for parsing a POSIX `TZ` string.
-impl<'s> Parser<'s> {
-    /// Bump the parser to the next byte.
-    ///
-    /// If the end of the input has been reached, then `false` is returned.
-    fn bump(&self) -> bool {
-        if self.is_done() {
-            return false;
-        }
-        self.pos.set(
-            self.pos().checked_add(1).expect("pos cannot overflow usize"),
-        );
-        !self.is_done()
-    }
-
-    /// Returns true if the next call to `bump` would return false.
-    fn is_done(&self) -> bool {
-        self.pos() == self.tz.len()
-    }
-
-    /// Return the byte at the current position of the parser.
-    ///
-    /// This panics if the parser is positioned at the end of the TZ string.
-    fn byte(&self) -> u8 {
-        self.tz[self.pos()]
-    }
-
-    /// Return the byte at the current position of the parser. If the TZ string
-    /// has been exhausted, then this returns `None`.
-    fn maybe_byte(&self) -> Option<u8> {
-        self.tz.get(self.pos()).copied()
-    }
-
-    /// Return the current byte offset of the parser.
-    ///
-    /// The offset starts at `0` from the beginning of the TZ string.
-    fn pos(&self) -> usize {
-        self.pos.get()
-    }
-
-    /// Returns the remaining bytes of the TZ string.
-    ///
-    /// This includes `self.byte()`. It may be empty.
-    fn remaining(&self) -> &'s [u8] {
-        &self.tz[self.pos()..]
-    }
-}
-
-/// A helper type for formatting a time zone abbreviation.
-///
-/// Basically, this will write the `<` and `>` quotes if necessary, and
-/// otherwise write out the abbreviation in its unquoted form.
-#[derive(Debug)]
-struct AbbreviationDisplay(Abbreviation);
-
-impl core::fmt::Display for AbbreviationDisplay {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        let s = self.0.as_str();
-        if s.chars().any(|ch| ch == '+' || ch == '-') {
-            write!(f, "<{s}>")
-        } else {
-            write!(f, "{s}")
-        }
-    }
-}
-
-// Note that most of the tests below are for the parsing. For the actual time
-// zone transition logic, that's unit tested in tz/mod.rs.
-#[cfg(test)]
-mod tests {
-    use std::string::ToString;
-
-    use crate::civil::date;
-
-    use super::*;
-
-    fn reasonable_posix_time_zone(
-        input: impl AsRef<[u8]>,
-    ) -> ReasonablePosixTimeZone {
-        let input = core::str::from_utf8(input.as_ref()).unwrap();
-        let tz = IanaTz::parse_v3plus(input).unwrap().into_tz();
-        // While we're here, assert that converting the TZ back
-        // to a string matches what we got. This isn't guaranteed
-        // in all cases, but good enough for what we test I think.
-        assert_eq!(tz.to_string(), input);
-        tz
-    }
-
-    /// DEBUG COMMAND
-    ///
-    /// Takes environment variable `JIFF_DEBUG_POSIX_TZ` as input, and prints
-    /// the Rust (extended) debug representation of it after parsing it as a
-    /// POSIX TZ string.
-    #[cfg(feature = "std")]
-    #[test]
-    fn debug_posix_tz() -> anyhow::Result<()> {
-        const ENV: &str = "JIFF_DEBUG_POSIX_TZ";
-        let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
-        let val = val
-            .to_str()
-            .ok_or_else(|| err!("{ENV} contains invalid UTF-8"))?;
-        let tz = Parser::new(val).parse()?;
-        std::eprintln!("{tz:#?}");
-        Ok(())
-    }
-
-    /// DEBUG COMMAND
-    ///
-    /// Takes environment variable `JIFF_DEBUG_IANA_TZ` as input, and prints
-    /// the Rust (extended) debug representation of it after parsing it as a
-    /// POSIX TZ string with IANA tzfile v3+ extensions.
-    #[cfg(feature = "std")]
-    #[test]
-    fn debug_iana_tz() -> anyhow::Result<()> {
-        const ENV: &str = "JIFF_DEBUG_IANA_TZ";
-        let Some(val) = std::env::var_os(ENV) else { return Ok(()) };
-        let val = val
-            .to_str()
-            .ok_or_else(|| err!("{ENV} contains invalid UTF-8"))?;
-        let tz = Parser { ianav3plus: true, ..Parser::new(val) }.parse()?;
-        std::eprintln!("{tz:#?}");
-        Ok(())
-    }
-
-    #[test]
-    fn reasonable_to_dst_civil_datetime_utc_range() {
-        let tz = reasonable_posix_time_zone("WART4WARST,J1/-3,J365/20");
-        let dst_info = DstInfo {
-            // We test this in other places. It's too annoying to write this
-            // out here, and I didn't adopt snapshot testing until I had
-            // written out these tests by hand. ¯\_(ツ)_/¯
-            dst: tz.dst.as_ref().unwrap(),
-            offset: crate::tz::offset(-3),
-            start: date(2024, 1, 1).at(1, 0, 0, 0),
-            end: date(2024, 12, 31).at(23, 0, 0, 0),
-        };
-        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
-
-        let tz = reasonable_posix_time_zone("WART4WARST,J1/-4,J365/21");
-        let dst_info = DstInfo {
-            dst: tz.dst.as_ref().unwrap(),
-            offset: crate::tz::offset(-3),
-            start: date(2024, 1, 1).at(0, 0, 0, 0),
-            end: date(2024, 12, 31).at(23, 59, 59, 999_999_999),
-        };
-        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
-
-        let tz = reasonable_posix_time_zone("EST5EDT,M3.2.0,M11.1.0");
-        let dst_info = DstInfo {
-            dst: tz.dst.as_ref().unwrap(),
-            offset: crate::tz::offset(-4),
-            start: date(2024, 3, 10).at(7, 0, 0, 0),
-            end: date(2024, 11, 3).at(6, 0, 0, 0),
-        };
-        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
+        let tz = reasonable_posix_time_zone("EST5EDT,M3.2.0,M11.1.0");
+        let dst_info = DstInfo {
+            dst: tz.dst.as_ref().unwrap(),
+            offset: offset(-4),
+            start: date(2024, 3, 10).at(7, 0, 0, 0),
+            end: date(2024, 11, 3).at(6, 0, 0, 0),
+        };
+        assert_eq!(tz.dst_info_utc(C(2024)), Some(dst_info));
     }
 
     #[test]
@@ -1960,23 +1321,18 @@ mod tests {
             tz,
             ReasonablePosixTimeZone {
                 std_abbrev: "EST".into(),
-                std_offset: PosixOffset {
-                    sign: None,
-                    hour: C(24).rinto(),
-                    minute: None,
-                    second: None,
-                },
+                std_offset: offset(-24).into(),
                 dst: Some(ReasonablePosixDst {
                     abbrev: "EDT".into(),
-                    offset: None,
+                    offset: offset(-23).into(),
                     rule: Rule {
                         start: PosixDateTimeSpec {
                             date: PosixDateSpec::JulianOne(C(1).rinto()),
-                            time: None,
+                            time: PosixTimeSpec::DEFAULT,
                         },
                         end: PosixDateTimeSpec {
                             date: PosixDateSpec::JulianOne(C(365).rinto()),
-                            time: None,
+                            time: PosixTimeSpec::DEFAULT,
                         },
                     },
                 }),
@@ -1988,23 +1344,18 @@ mod tests {
             tz,
             ReasonablePosixTimeZone {
                 std_abbrev: "EST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(C(-1).rinto()),
-                    hour: C(24).rinto(),
-                    minute: None,
-                    second: None,
-                },
+                std_offset: offset(24).into(),
                 dst: Some(ReasonablePosixDst {
                     abbrev: "EDT".into(),
-                    offset: None,
+                    offset: offset(25).into(),
                     rule: Rule {
                         start: PosixDateTimeSpec {
                             date: PosixDateSpec::JulianOne(C(1).rinto()),
-                            time: None,
+                            time: PosixTimeSpec::DEFAULT,
                         },
                         end: PosixDateTimeSpec {
                             date: PosixDateSpec::JulianOne(C(365).rinto()),
-                            time: None,
+                            time: PosixTimeSpec::DEFAULT,
                         },
                     },
                 }),
@@ -2097,22 +1448,12 @@ mod tests {
     #[test]
     fn posix_date_time_spec_time() {
         let tz = reasonable_posix_time_zone("EST5EDT,J1,J365/5:12:34");
-        assert_eq!(
-            tz.rule().start.time(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(2).rinto(),
-                minute: None,
-                second: None,
-            },
-        );
+        assert_eq!(tz.rule().start.time(), PosixTimeSpec::DEFAULT,);
         assert_eq!(
             tz.rule().end.time(),
             PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: Some(C(12).rinto()),
-                second: Some(C(34).rinto()),
+                duration: PosixTimeSeconds::new(5 * 60 * 60 + 12 * 60 + 34)
+                    .unwrap(),
             },
         );
     }
@@ -2227,50 +1568,41 @@ mod tests {
     #[cfg(feature = "tz-system")]
     #[test]
     fn parse_posix_tz() {
-        let tz = PosixTz::parse("EST5EDT").unwrap();
+        let tz = PosixTzEnv::parse("EST5EDT").unwrap();
         assert_eq!(
             tz,
-            PosixTz::Rule(PosixTimeZone {
+            PosixTzEnv::Rule(PosixTimeZone {
                 std_abbrev: "EST".into(),
-                std_offset: PosixOffset {
-                    sign: None,
-                    hour: C(5).rinto(),
-                    minute: None,
-                    second: None,
-                },
+                std_offset: offset(-5).into(),
                 dst: Some(PosixDst {
                     abbrev: "EDT".into(),
-                    offset: None,
+                    offset: offset(-4).into(),
                     rule: None,
                 }),
             },)
         );
 
-        let tz = PosixTz::parse(":EST5EDT").unwrap();
-        assert_eq!(tz, PosixTz::Implementation("EST5EDT".into()));
+        let tz = PosixTzEnv::parse(":EST5EDT").unwrap();
+        assert_eq!(tz, PosixTzEnv::Implementation("EST5EDT".into()));
 
         // We require implementation strings to be UTF-8, because we're
         // sensible.
-        assert!(PosixTz::parse(b":EST5\xFFEDT").is_err());
+        assert!(PosixTzEnv::parse(b":EST5\xFFEDT").is_err());
     }
 
     #[test]
     fn parse_iana() {
         // Ref: https://github.com/chronotope/chrono/issues/1153
-        let p = IanaTz::parse_v3plus("CRAZY5SHORT,M12.5.0/50,0/2").unwrap();
+        let p = ReasonablePosixTimeZone::parse("CRAZY5SHORT,M12.5.0/50,0/2")
+            .unwrap();
         assert_eq!(
             p,
-            IanaTz(ReasonablePosixTimeZone {
+            ReasonablePosixTimeZone {
                 std_abbrev: "CRAZY".into(),
-                std_offset: PosixOffset {
-                    sign: None,
-                    hour: C(5).rinto(),
-                    minute: None,
-                    second: None,
-                },
+                std_offset: offset(-5).into(),
                 dst: Some(ReasonablePosixDst {
                     abbrev: "SHORT".into(),
-                    offset: None,
+                    offset: offset(-4).into(),
                     rule: Rule {
                         start: PosixDateTimeSpec {
                             date: PosixDateSpec::WeekdayOfMonth(
@@ -2280,1203 +1612,24 @@ mod tests {
                                     weekday: Weekday::Sunday,
                                 },
                             ),
-                            time: Some(PosixTimeSpec {
-                                sign: None,
-                                hour: C(50).rinto(),
-                                minute: None,
-                                second: None,
-                            },),
+                            time: PosixTimeSpec {
+                                duration: PosixTimeSeconds::new(50 * 60 * 60)
+                                    .unwrap(),
+                            },
                         },
                         end: PosixDateTimeSpec {
                             date: PosixDateSpec::JulianZero(C(0).rinto()),
-                            time: Some(PosixTimeSpec {
-                                sign: None,
-                                hour: C(2).rinto(),
-                                minute: None,
-                                second: None,
-                            },),
+                            time: PosixTimeSpec {
+                                duration: PosixTimeSeconds::new(2 * 60 * 60)
+                                    .unwrap(),
+                            },
                         },
                     },
                 }),
-            }),
-        );
-
-        let p = Parser::new("America/New_York");
-        assert!(p.parse().is_err());
-
-        let p = Parser::new(":America/New_York");
-        assert!(p.parse().is_err());
-    }
-
-    #[test]
-    fn parse() {
-        let p = Parser::new("NZST-12NZDT,J60,J300");
-        assert_eq!(
-            p.parse().unwrap(),
-            PosixTimeZone {
-                std_abbrev: "NZST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(12).rinto(),
-                    minute: None,
-                    second: None,
-                },
-                dst: Some(PosixDst {
-                    abbrev: "NZDT".into(),
-                    offset: None,
-                    rule: Some(Rule {
-                        start: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(60).rinto()),
-                            time: None,
-                        },
-                        end: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(300).rinto()),
-                            time: None,
-                        },
-                    }),
-                }),
-            },
-        );
-
-        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
-        assert!(p.parse().is_err());
-    }
-
-    #[test]
-    fn parse_posix_time_zone() {
-        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3");
-        assert_eq!(
-            p.parse_posix_time_zone().unwrap(),
-            PosixTimeZone {
-                std_abbrev: "NZST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(12).rinto(),
-                    minute: None,
-                    second: None,
-                },
-                dst: Some(PosixDst {
-                    abbrev: "NZDT".into(),
-                    offset: None,
-                    rule: Some(Rule {
-                        start: PosixDateTimeSpec {
-                            date: PosixDateSpec::WeekdayOfMonth(
-                                WeekdayOfMonth {
-                                    month: C(9).rinto(),
-                                    week: C(5).rinto(),
-                                    weekday: Weekday::Sunday,
-                                }
-                            ),
-                            time: None,
-                        },
-                        end: PosixDateTimeSpec {
-                            date: PosixDateSpec::WeekdayOfMonth(
-                                WeekdayOfMonth {
-                                    month: C(4).rinto(),
-                                    week: C(1).rinto(),
-                                    weekday: Weekday::Sunday,
-                                }
-                            ),
-                            time: Some(PosixTimeSpec {
-                                sign: None,
-                                hour: C(3).rinto(),
-                                minute: None,
-                                second: None,
-                            }),
-                        },
-                    })
-                }),
             },
         );
 
-        let p = Parser::new("NZST-12NZDT,M9.5.0,M4.1.0/3WAT");
-        assert_eq!(
-            p.parse_posix_time_zone().unwrap(),
-            PosixTimeZone {
-                std_abbrev: "NZST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(12).rinto(),
-                    minute: None,
-                    second: None,
-                },
-                dst: Some(PosixDst {
-                    abbrev: "NZDT".into(),
-                    offset: None,
-                    rule: Some(Rule {
-                        start: PosixDateTimeSpec {
-                            date: PosixDateSpec::WeekdayOfMonth(
-                                WeekdayOfMonth {
-                                    month: C(9).rinto(),
-                                    week: C(5).rinto(),
-                                    weekday: Weekday::Sunday,
-                                }
-                            ),
-                            time: None,
-                        },
-                        end: PosixDateTimeSpec {
-                            date: PosixDateSpec::WeekdayOfMonth(
-                                WeekdayOfMonth {
-                                    month: C(4).rinto(),
-                                    week: C(1).rinto(),
-                                    weekday: Weekday::Sunday,
-                                }
-                            ),
-                            time: Some(PosixTimeSpec {
-                                sign: None,
-                                hour: C(3).rinto(),
-                                minute: None,
-                                second: None,
-                            }),
-                        },
-                    })
-                }),
-            },
-        );
-
-        let p = Parser::new("NZST-12NZDT,J60,J300");
-        assert_eq!(
-            p.parse_posix_time_zone().unwrap(),
-            PosixTimeZone {
-                std_abbrev: "NZST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(12).rinto(),
-                    minute: None,
-                    second: None,
-                },
-                dst: Some(PosixDst {
-                    abbrev: "NZDT".into(),
-                    offset: None,
-                    rule: Some(Rule {
-                        start: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(60).rinto()),
-                            time: None,
-                        },
-                        end: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(300).rinto()),
-                            time: None,
-                        },
-                    }),
-                }),
-            },
-        );
-
-        let p = Parser::new("NZST-12NZDT,J60,J300WAT");
-        assert_eq!(
-            p.parse_posix_time_zone().unwrap(),
-            PosixTimeZone {
-                std_abbrev: "NZST".into(),
-                std_offset: PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(12).rinto(),
-                    minute: None,
-                    second: None,
-                },
-                dst: Some(PosixDst {
-                    abbrev: "NZDT".into(),
-                    offset: None,
-                    rule: Some(Rule {
-                        start: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(60).rinto()),
-                            time: None,
-                        },
-                        end: PosixDateTimeSpec {
-                            date: PosixDateSpec::JulianOne(C(300).rinto()),
-                            time: None,
-                        },
-                    }),
-                }),
-            },
-        );
-    }
-
-    #[test]
-    fn parse_posix_dst() {
-        let p = Parser::new("NZDT,M9.5.0,M4.1.0/3");
-        assert_eq!(
-            p.parse_posix_dst().unwrap(),
-            PosixDst {
-                abbrev: "NZDT".into(),
-                offset: None,
-                rule: Some(Rule {
-                    start: PosixDateTimeSpec {
-                        date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                            month: C(9).rinto(),
-                            week: C(5).rinto(),
-                            weekday: Weekday::Sunday,
-                        }),
-                        time: None,
-                    },
-                    end: PosixDateTimeSpec {
-                        date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                            month: C(4).rinto(),
-                            week: C(1).rinto(),
-                            weekday: Weekday::Sunday,
-                        }),
-                        time: Some(PosixTimeSpec {
-                            sign: None,
-                            hour: C(3).rinto(),
-                            minute: None,
-                            second: None,
-                        }),
-                    },
-                }),
-            },
-        );
-
-        let p = Parser::new("NZDT,J60,J300");
-        assert_eq!(
-            p.parse_posix_dst().unwrap(),
-            PosixDst {
-                abbrev: "NZDT".into(),
-                offset: None,
-                rule: Some(Rule {
-                    start: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(60).rinto()),
-                        time: None,
-                    },
-                    end: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(300).rinto()),
-                        time: None,
-                    },
-                }),
-            },
-        );
-
-        let p = Parser::new("NZDT-7,J60,J300");
-        assert_eq!(
-            p.parse_posix_dst().unwrap(),
-            PosixDst {
-                abbrev: "NZDT".into(),
-                offset: Some(PosixOffset {
-                    sign: Some(Sign::N::<-1>()),
-                    hour: C(7).rinto(),
-                    minute: None,
-                    second: None,
-                }),
-                rule: Some(Rule {
-                    start: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(60).rinto()),
-                        time: None,
-                    },
-                    end: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(300).rinto()),
-                        time: None,
-                    },
-                }),
-            },
-        );
-
-        let p = Parser::new("NZDT+7,J60,J300");
-        assert_eq!(
-            p.parse_posix_dst().unwrap(),
-            PosixDst {
-                abbrev: "NZDT".into(),
-                offset: Some(PosixOffset {
-                    sign: Some(Sign::N::<1>()),
-                    hour: C(7).rinto(),
-                    minute: None,
-                    second: None,
-                }),
-                rule: Some(Rule {
-                    start: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(60).rinto()),
-                        time: None,
-                    },
-                    end: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(300).rinto()),
-                        time: None,
-                    },
-                }),
-            },
-        );
-
-        let p = Parser::new("NZDT7,J60,J300");
-        assert_eq!(
-            p.parse_posix_dst().unwrap(),
-            PosixDst {
-                abbrev: "NZDT".into(),
-                offset: Some(PosixOffset {
-                    sign: None,
-                    hour: C(7).rinto(),
-                    minute: None,
-                    second: None,
-                }),
-                rule: Some(Rule {
-                    start: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(60).rinto()),
-                        time: None,
-                    },
-                    end: PosixDateTimeSpec {
-                        date: PosixDateSpec::JulianOne(C(300).rinto()),
-                        time: None,
-                    },
-                }),
-            },
-        );
-
-        let p = Parser::new("NZDT7,");
-        assert!(p.parse_posix_dst().is_err());
-
-        let p = Parser::new("NZDT7!");
-        assert!(p.parse_posix_dst().is_err());
-    }
-
-    #[test]
-    fn parse_abbreviation() {
-        let p = Parser::new("ABC");
-        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
-
-        let p = Parser::new("<ABC>");
-        assert_eq!(p.parse_abbreviation().unwrap(), "ABC");
-
-        let p = Parser::new("<+09>");
-        assert_eq!(p.parse_abbreviation().unwrap(), "+09");
-
-        let p = Parser::new("+09");
-        assert!(p.parse_abbreviation().is_err());
-    }
-
-    #[test]
-    fn parse_unquoted_abbreviation() {
-        let p = Parser::new("ABC");
-        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
-
-        let p = Parser::new("ABCXYZ");
-        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABCXYZ");
-
-        let p = Parser::new("ABC123");
-        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), "ABC");
-
-        let tz = "a".repeat(30);
-        let p = Parser::new(&tz);
-        assert_eq!(p.parse_unquoted_abbreviation().unwrap(), &*tz);
-
-        let p = Parser::new("a");
-        assert!(p.parse_unquoted_abbreviation().is_err());
-
-        let p = Parser::new("ab");
-        assert!(p.parse_unquoted_abbreviation().is_err());
-
-        let p = Parser::new("ab1");
-        assert!(p.parse_unquoted_abbreviation().is_err());
-
-        let tz = "a".repeat(31);
-        let p = Parser::new(&tz);
-        assert!(p.parse_unquoted_abbreviation().is_err());
-
-        let p = Parser::new(b"ab\xFFcd");
-        assert!(p.parse_unquoted_abbreviation().is_err());
-    }
-
-    #[test]
-    fn parse_quoted_abbreviation() {
-        // The inputs look a little funny here, but that's because
-        // 'parse_quoted_abbreviation' starts after the opening quote
-        // has been parsed.
-
-        let p = Parser::new("ABC>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
-
-        let p = Parser::new("ABCXYZ>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABCXYZ");
-
-        let p = Parser::new("ABC>123");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC");
-
-        let p = Parser::new("ABC123>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ABC123");
-
-        let p = Parser::new("ab1>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "ab1");
-
-        let p = Parser::new("+09>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "+09");
-
-        let p = Parser::new("-09>");
-        assert_eq!(p.parse_quoted_abbreviation().unwrap(), "-09");
-
-        let tz = alloc::format!("{}>", "a".repeat(30));
-        let p = Parser::new(&tz);
-        assert_eq!(
-            p.parse_quoted_abbreviation().unwrap(),
-            tz.trim_end_matches(">")
-        );
-
-        let p = Parser::new("a>");
-        assert!(p.parse_quoted_abbreviation().is_err());
-
-        let p = Parser::new("ab>");
-        assert!(p.parse_quoted_abbreviation().is_err());
-
-        let tz = alloc::format!("{}>", "a".repeat(31));
-        let p = Parser::new(&tz);
-        assert!(p.parse_quoted_abbreviation().is_err());
-
-        let p = Parser::new(b"ab\xFFcd>");
-        assert!(p.parse_quoted_abbreviation().is_err());
-
-        let p = Parser::new("ABC");
-        assert!(p.parse_quoted_abbreviation().is_err());
-
-        let p = Parser::new("ABC!>");
-        assert!(p.parse_quoted_abbreviation().is_err());
-    }
-
-    #[test]
-    fn parse_posix_offset() {
-        let p = Parser::new("5");
-        assert_eq!(
-            p.parse_posix_offset().unwrap(),
-            PosixOffset {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: None,
-                second: None,
-            },
-        );
-
-        let p = Parser::new("+5");
-        assert_eq!(
-            p.parse_posix_offset().unwrap(),
-            PosixOffset {
-                sign: Some(Sign::N::<1>()),
-                hour: C(5).rinto(),
-                minute: None,
-                second: None,
-            },
-        );
-
-        let p = Parser::new("-5");
-        assert_eq!(
-            p.parse_posix_offset().unwrap(),
-            PosixOffset {
-                sign: Some(Sign::N::<-1>()),
-                hour: C(5).rinto(),
-                minute: None,
-                second: None,
-            },
-        );
-
-        let p = Parser::new("-12:34:56");
-        assert_eq!(
-            p.parse_posix_offset().unwrap(),
-            PosixOffset {
-                sign: Some(Sign::N::<-1>()),
-                hour: C(12).rinto(),
-                minute: Some(C(34).rinto()),
-                second: Some(C(56).rinto()),
-            },
-        );
-
-        let p = Parser::new("a");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("-");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("+");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("-a");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("+a");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("-25");
-        assert!(p.parse_posix_offset().is_err());
-
-        let p = Parser::new("+25");
-        assert!(p.parse_posix_offset().is_err());
-
-        // This checks that we don't accidentally permit IANA rules for
-        // offset parsing. Namely, the IANA tzfile v3+ extension only applies
-        // to transition times. But since POSIX says that the "time" for the
-        // offset and transition is the same format, it would be an easy
-        // implementation mistake to implement the more flexible rule for
-        // IANA and have it accidentally also apply to the offset. So we check
-        // that it doesn't here.
-        let p = Parser { ianav3plus: true, ..Parser::new("25") };
-        assert!(p.parse_posix_offset().is_err());
-        let p = Parser { ianav3plus: true, ..Parser::new("+25") };
-        assert!(p.parse_posix_offset().is_err());
-        let p = Parser { ianav3plus: true, ..Parser::new("-25") };
-        assert!(p.parse_posix_offset().is_err());
-    }
-
-    #[test]
-    fn parse_rule() {
-        let p = Parser::new("M9.5.0,M4.1.0/3");
-        assert_eq!(
-            p.parse_rule().unwrap(),
-            Rule {
-                start: PosixDateTimeSpec {
-                    date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                        month: C(9).rinto(),
-                        week: C(5).rinto(),
-                        weekday: Weekday::Sunday,
-                    }),
-                    time: None,
-                },
-                end: PosixDateTimeSpec {
-                    date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                        month: C(4).rinto(),
-                        week: C(1).rinto(),
-                        weekday: Weekday::Sunday,
-                    }),
-                    time: Some(PosixTimeSpec {
-                        sign: None,
-                        hour: C(3).rinto(),
-                        minute: None,
-                        second: None,
-                    }),
-                },
-            },
-        );
-
-        let p = Parser::new("M9.5.0");
-        assert!(p.parse_rule().is_err());
-
-        let p = Parser::new(",M9.5.0,M4.1.0/3");
-        assert!(p.parse_rule().is_err());
-
-        let p = Parser::new("M9.5.0/");
-        assert!(p.parse_rule().is_err());
-
-        let p = Parser::new("M9.5.0,M4.1.0/");
-        assert!(p.parse_rule().is_err());
-    }
-
-    #[test]
-    fn parse_posix_datetime_spec() {
-        let p = Parser::new("J1");
-        assert_eq!(
-            p.parse_posix_datetime_spec().unwrap(),
-            PosixDateTimeSpec {
-                date: PosixDateSpec::JulianOne(C(1).rinto()),
-                time: None,
-            },
-        );
-
-        let p = Parser::new("J1/3");
-        assert_eq!(
-            p.parse_posix_datetime_spec().unwrap(),
-            PosixDateTimeSpec {
-                date: PosixDateSpec::JulianOne(C(1).rinto()),
-                time: Some(PosixTimeSpec {
-                    sign: None,
-                    hour: C(3).rinto(),
-                    minute: None,
-                    second: None,
-                }),
-            },
-        );
-
-        let p = Parser::new("M4.1.0/3");
-        assert_eq!(
-            p.parse_posix_datetime_spec().unwrap(),
-            PosixDateTimeSpec {
-                date: PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                    month: C(4).rinto(),
-                    week: C(1).rinto(),
-                    weekday: Weekday::Sunday,
-                }),
-                time: Some(PosixTimeSpec {
-                    sign: None,
-                    hour: C(3).rinto(),
-                    minute: None,
-                    second: None,
-                }),
-            },
-        );
-
-        let p = Parser::new("1/3:45:05");
-        assert_eq!(
-            p.parse_posix_datetime_spec().unwrap(),
-            PosixDateTimeSpec {
-                date: PosixDateSpec::JulianZero(C(1).rinto()),
-                time: Some(PosixTimeSpec {
-                    sign: None,
-                    hour: C(3).rinto(),
-                    minute: Some(C(45).rinto()),
-                    second: Some(C(5).rinto()),
-                }),
-            },
-        );
-
-        let p = Parser::new("a");
-        assert!(p.parse_posix_datetime_spec().is_err());
-
-        let p = Parser::new("J1/");
-        assert!(p.parse_posix_datetime_spec().is_err());
-
-        let p = Parser::new("1/");
-        assert!(p.parse_posix_datetime_spec().is_err());
-
-        let p = Parser::new("M4.1.0/");
-        assert!(p.parse_posix_datetime_spec().is_err());
-    }
-
-    #[test]
-    fn parse_posix_date_spec() {
-        let p = Parser::new("J1");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::JulianOne(C(1).rinto())
-        );
-        let p = Parser::new("J365");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::JulianOne(C(365).rinto())
-        );
-
-        let p = Parser::new("0");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::JulianZero(C(0).rinto())
-        );
-        let p = Parser::new("1");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::JulianZero(C(1).rinto())
-        );
-        let p = Parser::new("365");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::JulianZero(C(365).rinto())
-        );
-
-        let p = Parser::new("M9.5.0");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(5).rinto(),
-                weekday: Weekday::Sunday,
-            }),
-        );
-        let p = Parser::new("M9.5.6");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(5).rinto(),
-                weekday: Weekday::Saturday,
-            }),
-        );
-        let p = Parser::new("M09.5.6");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(5).rinto(),
-                weekday: Weekday::Saturday,
-            }),
-        );
-        let p = Parser::new("M12.1.1");
-        assert_eq!(
-            p.parse_posix_date_spec().unwrap(),
-            PosixDateSpec::WeekdayOfMonth(WeekdayOfMonth {
-                month: C(12).rinto(),
-                week: C(1).rinto(),
-                weekday: Weekday::Monday,
-            }),
-        );
-
-        let p = Parser::new("a");
-        assert!(p.parse_posix_date_spec().is_err());
-
-        let p = Parser::new("j");
-        assert!(p.parse_posix_date_spec().is_err());
-
-        let p = Parser::new("m");
-        assert!(p.parse_posix_date_spec().is_err());
-
-        let p = Parser::new("n");
-        assert!(p.parse_posix_date_spec().is_err());
-
-        let p = Parser::new("J366");
-        assert!(p.parse_posix_date_spec().is_err());
-
-        let p = Parser::new("366");
-        assert!(p.parse_posix_date_spec().is_err());
-    }
-
-    #[test]
-    fn parse_posix_julian_day_no_leap() {
-        let p = Parser::new("1");
-        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
-
-        let p = Parser::new("001");
-        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 1);
-
-        let p = Parser::new("365");
-        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
-
-        let p = Parser::new("3655");
-        assert_eq!(p.parse_posix_julian_day_no_leap().unwrap(), 365);
-
-        let p = Parser::new("0");
-        assert!(p.parse_posix_julian_day_no_leap().is_err());
-
-        let p = Parser::new("366");
-        assert!(p.parse_posix_julian_day_no_leap().is_err());
-    }
-
-    #[test]
-    fn parse_posix_julian_day_with_leap() {
-        let p = Parser::new("0");
-        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 0);
-
-        let p = Parser::new("1");
-        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
-
-        let p = Parser::new("001");
-        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 1);
-
-        let p = Parser::new("365");
-        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
-
-        let p = Parser::new("3655");
-        assert_eq!(p.parse_posix_julian_day_with_leap().unwrap(), 365);
-
-        let p = Parser::new("366");
-        assert!(p.parse_posix_julian_day_with_leap().is_err());
-    }
-
-    #[test]
-    fn parse_weekday_of_month() {
-        let p = Parser::new("9.5.0");
-        assert_eq!(
-            p.parse_weekday_of_month().unwrap(),
-            WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(5).rinto(),
-                weekday: Weekday::Sunday,
-            },
-        );
-
-        let p = Parser::new("9.1.6");
-        assert_eq!(
-            p.parse_weekday_of_month().unwrap(),
-            WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(1).rinto(),
-                weekday: Weekday::Saturday,
-            },
-        );
-
-        let p = Parser::new("09.1.6");
-        assert_eq!(
-            p.parse_weekday_of_month().unwrap(),
-            WeekdayOfMonth {
-                month: C(9).rinto(),
-                week: C(1).rinto(),
-                weekday: Weekday::Saturday,
-            },
-        );
-
-        let p = Parser::new("9");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.5");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.5.");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("0.5.0");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("13.5.0");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.0.0");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.6.0");
-        assert!(p.parse_weekday_of_month().is_err());
-
-        let p = Parser::new("9.5.7");
-        assert!(p.parse_weekday_of_month().is_err());
-    }
-
-    #[test]
-    fn parse_posix_time_spec() {
-        let p = Parser::new("5");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: None,
-                second: None
-            }
-        );
-
-        let p = Parser::new("22");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(22).rinto(),
-                minute: None,
-                second: None
-            }
-        );
-
-        let p = Parser::new("02");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(2).rinto(),
-                minute: None,
-                second: None
-            }
-        );
-
-        let p = Parser::new("5:45");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: None
-            }
-        );
-
-        let p = Parser::new("5:45:12");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser::new("5:45:129");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser::new("5:45:12:");
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: None,
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser { ianav3plus: true, ..Parser::new("+5:45:12") };
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: Some(C(1).rinto()),
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser { ianav3plus: true, ..Parser::new("-5:45:12") };
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: Some(C(-1).rinto()),
-                hour: C(5).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser { ianav3plus: true, ..Parser::new("-167:45:12") };
-        assert_eq!(
-            p.parse_posix_time_spec().unwrap(),
-            PosixTimeSpec {
-                sign: Some(C(-1).rinto()),
-                hour: C(167).rinto(),
-                minute: Some(C(45).rinto()),
-                second: Some(C(12).rinto()),
-            }
-        );
-
-        let p = Parser::new("25");
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser::new("12:2");
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser::new("12:");
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser::new("12:23:5");
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser::new("12:23:");
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser { ianav3plus: true, ..Parser::new("168") };
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser { ianav3plus: true, ..Parser::new("-168") };
-        assert!(p.parse_posix_time_spec().is_err());
-
-        let p = Parser { ianav3plus: true, ..Parser::new("+168") };
-        assert!(p.parse_posix_time_spec().is_err());
-    }
-
-    #[test]
-    fn parse_month() {
-        let p = Parser::new("1");
-        assert_eq!(p.parse_month().unwrap(), 1);
-
-        // Should this be allowed? POSIX spec is unclear.
-        // We allow it because our parse does stop at 2
-        // digits, so this seems harmless. Namely, '001'
-        // results in an error.
-        let p = Parser::new("01");
-        assert_eq!(p.parse_month().unwrap(), 1);
-
-        let p = Parser::new("12");
-        assert_eq!(p.parse_month().unwrap(), 12);
-
-        let p = Parser::new("0");
-        assert!(p.parse_month().is_err());
-
-        let p = Parser::new("00");
-        assert!(p.parse_month().is_err());
-
-        let p = Parser::new("001");
-        assert!(p.parse_month().is_err());
-
-        let p = Parser::new("13");
-        assert!(p.parse_month().is_err());
-    }
-
-    #[test]
-    fn parse_week() {
-        let p = Parser::new("1");
-        assert_eq!(p.parse_week().unwrap(), 1);
-
-        let p = Parser::new("5");
-        assert_eq!(p.parse_week().unwrap(), 5);
-
-        let p = Parser::new("55");
-        assert_eq!(p.parse_week().unwrap(), 5);
-
-        let p = Parser::new("0");
-        assert!(p.parse_week().is_err());
-
-        let p = Parser::new("6");
-        assert!(p.parse_week().is_err());
-
-        let p = Parser::new("00");
-        assert!(p.parse_week().is_err());
-
-        let p = Parser::new("01");
-        assert!(p.parse_week().is_err());
-
-        let p = Parser::new("05");
-        assert!(p.parse_week().is_err());
-    }
-
-    #[test]
-    fn parse_weekday() {
-        let p = Parser::new("0");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
-
-        let p = Parser::new("1");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Monday);
-
-        let p = Parser::new("6");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Saturday);
-
-        let p = Parser::new("00");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
-
-        let p = Parser::new("06");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Sunday);
-
-        let p = Parser::new("60");
-        assert_eq!(p.parse_weekday().unwrap(), Weekday::Saturday);
-
-        let p = Parser::new("7");
-        assert!(p.parse_weekday().is_err());
-    }
-
-    #[test]
-    fn parse_hour_posix() {
-        let p = Parser::new("5");
-        assert_eq!(p.parse_hour_posix().unwrap(), 5);
-
-        let p = Parser::new("0");
-        assert_eq!(p.parse_hour_posix().unwrap(), 0);
-
-        let p = Parser::new("00");
-        assert_eq!(p.parse_hour_posix().unwrap(), 0);
-
-        let p = Parser::new("24");
-        assert_eq!(p.parse_hour_posix().unwrap(), 24);
-
-        let p = Parser::new("100");
-        assert_eq!(p.parse_hour_posix().unwrap(), 10);
-
-        let p = Parser::new("25");
-        assert!(p.parse_hour_posix().is_err());
-
-        let p = Parser::new("99");
-        assert!(p.parse_hour_posix().is_err());
-    }
-
-    #[test]
-    fn parse_hour_ianav3plus() {
-        let new = |input| Parser { ianav3plus: true, ..Parser::new(input) };
-
-        let p = new("5");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 5);
-
-        let p = new("0");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
-
-        let p = new("00");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
-
-        let p = new("000");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 0);
-
-        let p = new("24");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 24);
-
-        let p = new("100");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
-
-        let p = new("1000");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 100);
-
-        let p = new("167");
-        assert_eq!(p.parse_hour_ianav3plus().unwrap(), 167);
-
-        let p = new("168");
-        assert!(p.parse_hour_ianav3plus().is_err());
-
-        let p = new("999");
-        assert!(p.parse_hour_ianav3plus().is_err());
-    }
-
-    #[test]
-    fn parse_minute() {
-        let p = Parser::new("00");
-        assert_eq!(p.parse_minute().unwrap(), 0);
-
-        let p = Parser::new("24");
-        assert_eq!(p.parse_minute().unwrap(), 24);
-
-        let p = Parser::new("59");
-        assert_eq!(p.parse_minute().unwrap(), 59);
-
-        let p = Parser::new("599");
-        assert_eq!(p.parse_minute().unwrap(), 59);
-
-        let p = Parser::new("0");
-        assert!(p.parse_minute().is_err());
-
-        let p = Parser::new("1");
-        assert!(p.parse_minute().is_err());
-
-        let p = Parser::new("9");
-        assert!(p.parse_minute().is_err());
-
-        let p = Parser::new("60");
-        assert!(p.parse_minute().is_err());
-    }
-
-    #[test]
-    fn parse_second() {
-        let p = Parser::new("00");
-        assert_eq!(p.parse_second().unwrap(), 0);
-
-        let p = Parser::new("24");
-        assert_eq!(p.parse_second().unwrap(), 24);
-
-        let p = Parser::new("59");
-        assert_eq!(p.parse_second().unwrap(), 59);
-
-        let p = Parser::new("599");
-        assert_eq!(p.parse_second().unwrap(), 59);
-
-        let p = Parser::new("0");
-        assert!(p.parse_second().is_err());
-
-        let p = Parser::new("1");
-        assert!(p.parse_second().is_err());
-
-        let p = Parser::new("9");
-        assert!(p.parse_second().is_err());
-
-        let p = Parser::new("60");
-        assert!(p.parse_second().is_err());
-    }
-
-    #[test]
-    fn parse_number_with_exactly_n_digits() {
-        let p = Parser::new("1");
-        assert_eq!(p.parse_number_with_exactly_n_digits(1).unwrap(), 1);
-
-        let p = Parser::new("12");
-        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
-
-        let p = Parser::new("123");
-        assert_eq!(p.parse_number_with_exactly_n_digits(2).unwrap(), 12);
-
-        let p = Parser::new("");
-        assert!(p.parse_number_with_exactly_n_digits(1).is_err());
-
-        let p = Parser::new("1");
-        assert!(p.parse_number_with_exactly_n_digits(2).is_err());
-
-        let p = Parser::new("12");
-        assert!(p.parse_number_with_exactly_n_digits(3).is_err());
-    }
-
-    #[test]
-    fn parse_number_with_upto_n_digits() {
-        let p = Parser::new("1");
-        assert_eq!(p.parse_number_with_upto_n_digits(1).unwrap(), 1);
-
-        let p = Parser::new("1");
-        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 1);
-
-        let p = Parser::new("12");
-        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
-
-        let p = Parser::new("12");
-        assert_eq!(p.parse_number_with_upto_n_digits(3).unwrap(), 12);
-
-        let p = Parser::new("123");
-        assert_eq!(p.parse_number_with_upto_n_digits(2).unwrap(), 12);
-
-        let p = Parser::new("");
-        assert!(p.parse_number_with_upto_n_digits(1).is_err());
-
-        let p = Parser::new("a");
-        assert!(p.parse_number_with_upto_n_digits(1).is_err());
+        assert!(ReasonablePosixTimeZone::parse("America/New_York").is_err());
+        assert!(ReasonablePosixTimeZone::parse(":America/New_York").is_err());
     }
 }
diff --git a/src/tz/system/mod.rs b/src/tz/system/mod.rs
index 2a25364d..66ecb058 100644
--- a/src/tz/system/mod.rs
+++ b/src/tz/system/mod.rs
@@ -4,7 +4,7 @@ use alloc::string::ToString;
 
 use crate::{
     error::{err, Error, ErrorContext},
-    tz::{posix::PosixTz, TimeZone, TimeZoneDatabase},
+    tz::{posix::PosixTzEnv, TimeZone, TimeZoneDatabase},
     util::cache::Expiration,
 };
 
@@ -177,7 +177,7 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
     if tzenv.is_empty() {
         return Ok(None);
     }
-    let tz_name_or_path = match PosixTz::parse_os_str(&tzenv) {
+    let tz_name_or_path = match PosixTzEnv::parse_os_str(&tzenv) {
         Err(_err) => {
             trace!(
                 "failed to parse {tzenv:?} as POSIX TZ rule \
@@ -194,18 +194,19 @@ fn get_env_tz(db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error> {
                 })?
                 .to_string()
         }
-        Ok(PosixTz::Implementation(string)) => string.to_string(),
-        Ok(PosixTz::Rule(tz)) => match tz.reasonable() {
+        Ok(PosixTzEnv::Implementation(string)) => string.to_string(),
+        Ok(PosixTzEnv::Rule(tz)) => match tz.reasonable() {
             Ok(reasonable_posix_tz) => {
                 return Ok(Some(TimeZone::from_reasonable_posix_tz(
                     reasonable_posix_tz,
                 )));
             }
-            Err(_) => {
+            Err(_unreasonable_posix_tz) => {
                 warn!(
-                    "parsed {tzenv:?} as POSIX TZ transition string, \
-                     but Jiff considers it unreasonable since \
-                     it specifies DST but without a rule \
+                    "parsed {tzenv:?} as POSIX TZ transition \
+                     {_unreasonable_posix_tz}, but Jiff considers \
+                     it unreasonable since it specifies DST but \
+                     without a rule \
                      (therefore ignoring TZ environment variable)",
                 );
                 return Ok(None);
diff --git a/src/tz/system/unix.rs b/src/tz/system/unix.rs
index 4714b030..e0e724b7 100644
--- a/src/tz/system/unix.rs
+++ b/src/tz/system/unix.rs
@@ -91,6 +91,7 @@ fn read_link_to_zoneinfo(
     Some(tz)
 }
 
+#[cfg(not(miri))]
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/tz/timezone.rs b/src/tz/timezone.rs
index 8c37107b..977f8361 100644
--- a/src/tz/timezone.rs
+++ b/src/tz/timezone.rs
@@ -10,7 +10,9 @@ use crate::{
 };
 
 #[cfg(feature = "alloc")]
-use crate::tz::posix::{IanaTz, ReasonablePosixTimeZone};
+use crate::tz::posix::ReasonablePosixTimeZone;
+
+use self::repr::Repr;
 
 /// A representation of a [time zone].
 ///
@@ -247,14 +249,14 @@ use crate::tz::posix::{IanaTz, ReasonablePosixTimeZone};
 /// [`with` attribute]: https://serde.rs/field-attrs.html#with
 #[derive(Clone, Eq, PartialEq)]
 pub struct TimeZone {
-    kind: Option<Arc<TimeZoneKind>>,
+    repr: Repr,
 }
 
 impl TimeZone {
     /// The UTC time zone.
     ///
     /// The offset of this time is `0` and never has any transitions.
-    pub const UTC: TimeZone = TimeZone { kind: None };
+    pub const UTC: TimeZone = TimeZone { repr: Repr::utc() };
 
     /// Returns the system configured time zone, if available.
     ///
@@ -444,13 +446,13 @@ impl TimeZone {
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
-    pub fn fixed(offset: Offset) -> TimeZone {
-        if offset == Offset::UTC {
+    pub const fn fixed(offset: Offset) -> TimeZone {
+        // Not doing `offset == Offset::UTC` because of `const`.
+        if offset.seconds_ranged().val == 0 {
             return TimeZone::UTC;
         }
-        let fixed = TimeZoneFixed::new(offset);
-        let kind = TimeZoneKind::Fixed(fixed);
-        TimeZone { kind: Some(Arc::new(kind)) }
+        let repr = Repr::fixed(offset);
+        TimeZone { repr }
     }
 
     /// Creates a time zone from a [POSIX TZ] rule string.
@@ -494,9 +496,8 @@ impl TimeZone {
     /// ```
     #[cfg(feature = "alloc")]
     pub fn posix(posix_tz_string: &str) -> Result<TimeZone, Error> {
-        let iana_tz = IanaTz::parse_v3plus(posix_tz_string)?;
-        let reasonable = iana_tz.into_tz();
-        Ok(TimeZone::from_reasonable_posix_tz(reasonable))
+        let posix_tz = ReasonablePosixTimeZone::parse(posix_tz_string)?;
+        Ok(TimeZone::from_reasonable_posix_tz(posix_tz))
     }
 
     /// Creates a time zone from a POSIX tz. Expose so that other parts of Jiff
@@ -505,9 +506,8 @@ impl TimeZone {
     pub(crate) fn from_reasonable_posix_tz(
         posix: ReasonablePosixTimeZone,
     ) -> TimeZone {
-        let posix = TimeZonePosix { posix };
-        let kind = TimeZoneKind::Posix(posix);
-        TimeZone { kind: Some(Arc::new(kind)) }
+        let repr = Repr::arc_posix(Arc::new(posix));
+        TimeZone { repr }
     }
 
     /// Creates a time zone from TZif binary data, whose format is specified
@@ -531,9 +531,10 @@ impl TimeZone {
     pub fn tzif(name: &str, data: &[u8]) -> Result<TimeZone, Error> {
         use alloc::string::ToString;
 
-        let tzif = TimeZoneTzif::new(Some(name.to_string()), data)?;
-        let kind = TimeZoneKind::Tzif(tzif);
-        Ok(TimeZone { kind: Some(Arc::new(kind)) })
+        let name = name.to_string();
+        let tzif = crate::tz::tzif::Tzif::parse(Some(name), data)?;
+        let repr = Repr::arc_tzif(Arc::new(tzif));
+        Ok(TimeZone { repr })
     }
 
     /// Returns a `TimeZone` that is specifially marked as "unknown."
@@ -573,9 +574,9 @@ impl TimeZone {
     /// can use `TimeZone::try_system().unwrap_or(TimeZone::UTC)` instead of
     /// `TimeZone::system`. (Where the latter falls back to the "unknown" time
     /// zone when a system configured time zone could not be found.)
-    pub fn unknown() -> TimeZone {
-        let kind = TimeZoneKind::Unknown;
-        TimeZone { kind: Some(Arc::new(kind)) }
+    pub const fn unknown() -> TimeZone {
+        let repr = Repr::unknown();
+        TimeZone { repr }
     }
 
     /// This creates an unnamed TZif-backed `TimeZone`.
@@ -609,9 +610,9 @@ impl TimeZone {
     /// This returns an error if the given TZif data is invalid.
     #[cfg(feature = "tz-system")]
     pub(crate) fn tzif_system(data: &[u8]) -> Result<TimeZone, Error> {
-        let tzif = TimeZoneTzif::new(None, data)?;
-        let kind = TimeZoneKind::Tzif(tzif);
-        Ok(TimeZone { kind: Some(Arc::new(kind)) })
+        let tzif = crate::tz::tzif::Tzif::parse(None, data)?;
+        let repr = Repr::arc_tzif(Arc::new(tzif));
+        Ok(TimeZone { repr })
     }
 
     #[inline]
@@ -624,19 +625,18 @@ impl TimeZone {
     ///
     /// Basically, this is only `false` when this `TimeZone` was created from
     /// a `/etc/localtime` for which a valid IANA time zone identifier could
-    /// not be extracted. It is also `false` when [`TimeZone::is_unknown`]
-    /// is `true`.
+    /// not be extracted.
     #[cfg(feature = "serde")]
     #[inline]
     pub(crate) fn has_succinct_serialization(&self) -> bool {
-        let Some(ref kind) = self.kind else { return true };
-        match **kind {
-            TimeZoneKind::Unknown => true,
-            TimeZoneKind::Fixed(_) => true,
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(_) => true,
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.name().is_some(),
+        repr::each! {
+            &self.repr,
+            UTC => true,
+            UNKNOWN => true,
+            FIXED(_offset) => true,
+            STATIC_TZIF(tzif) => tzif.name().is_some(),
+            ARC_TZIF(tzif) => tzif.name().is_some(),
+            ARC_POSIX(_posix) => true,
         }
     }
 
@@ -655,15 +655,18 @@ impl TimeZone {
     /// ```
     #[inline]
     pub fn iana_name(&self) -> Option<&str> {
-        let Some(ref kind) = self.kind else { return Some("UTC") };
-        match **kind {
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.name(),
+        repr::each! {
+            &self.repr,
+            UTC => Some("UTC"),
             // Note that while `Etc/Unknown` looks like an IANA time zone
             // identifier, it is specifically and explicitly NOT an IANA time
             // zone identifier. So we do not return it here if we have an
             // unknown time zone identifier.
-            _ => None,
+            UNKNOWN => None,
+            FIXED(_offset) => None,
+            STATIC_TZIF(tzif) => tzif.name(),
+            ARC_TZIF(tzif) => tzif.name(),
+            ARC_POSIX(_posix) => None,
         }
     }
 
@@ -691,8 +694,7 @@ impl TimeZone {
     /// ```
     #[inline]
     pub fn is_unknown(&self) -> bool {
-        let Some(ref kind) = self.kind else { return false };
-        matches!(**kind, TimeZoneKind::Unknown)
+        self.repr.is_unknown()
     }
 
     /// When this time zone is a POSIX time zone, return it.
@@ -704,11 +706,14 @@ impl TimeZone {
     #[cfg(feature = "alloc")]
     #[inline]
     pub(crate) fn posix_tz(&self) -> Option<&ReasonablePosixTimeZone> {
-        let Some(ref kind) = self.kind else { return None };
-        match **kind {
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => Some(&tz.posix),
-            _ => None,
+        repr::each! {
+            &self.repr,
+            UTC => None,
+            UNKNOWN => None,
+            FIXED(_offset) => None,
+            STATIC_TZIF(_tzif) => None,
+            ARC_TZIF(_tzif) => None,
+            ARC_POSIX(posix) => Some(posix),
         }
     }
 
@@ -788,17 +793,15 @@ impl TimeZone {
     /// # Ok::<(), Box<dyn std::error::Error>>(())
     /// ```
     #[inline]
-    pub fn to_offset(&self, _timestamp: Timestamp) -> Offset {
-        let Some(ref kind) = self.kind else {
-            return Offset::UTC;
-        };
-        match **kind {
-            TimeZoneKind::Unknown => Offset::UTC,
-            TimeZoneKind::Fixed(ref tz) => tz.to_offset(),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => tz.to_offset(_timestamp),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.to_offset(_timestamp),
+    pub fn to_offset(&self, timestamp: Timestamp) -> Offset {
+        repr::each! {
+            &self.repr,
+            UTC => Offset::UTC,
+            UNKNOWN => Offset::UTC,
+            FIXED(offset) => offset,
+            STATIC_TZIF(tzif) => tzif.to_offset(timestamp),
+            ARC_TZIF(tzif) => tzif.to_offset(timestamp),
+            ARC_POSIX(posix) => posix.to_offset(timestamp),
         }
     }
 
@@ -844,32 +847,36 @@ impl TimeZone {
     #[inline]
     pub fn to_offset_info<'t>(
         &'t self,
-        _timestamp: Timestamp,
+        timestamp: Timestamp,
     ) -> TimeZoneOffsetInfo<'t> {
-        let Some(ref kind) = self.kind else {
-            return TimeZoneOffsetInfo {
+        repr::each! {
+            &self.repr,
+            UTC => TimeZoneOffsetInfo {
                 offset: Offset::UTC,
                 dst: Dst::No,
                 abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
-            };
-        };
-        match **kind {
-            TimeZoneKind::Unknown => {
+            },
+            UNKNOWN => TimeZoneOffsetInfo {
+                offset: Offset::UTC,
+                dst: Dst::No,
+                // It'd be kinda nice if this were just `ERR` to
+                // indicate an error, but I can't find any precedent
+                // for that. And CLDR says `Etc/Unknown` should behave
+                // like UTC, so... I guess we use UTC here.
+                abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
+            },
+            FIXED(offset) => {
+                let abbreviation =
+                    TimeZoneAbbreviation::Owned(offset.to_array_str());
                 TimeZoneOffsetInfo {
-                    offset: Offset::UTC,
+                    offset,
                     dst: Dst::No,
-                    // It'd be kinda nice if this were just `ERR` to
-                    // indicate an error, but I can't find any precedent
-                    // for that. And CLDR says `Etc/Unknown` should behave
-                    // like UTC, so... I guess we use UTC here.
-                    abbreviation: TimeZoneAbbreviation::Borrowed("UTC"),
+                    abbreviation,
                 }
-            }
-            TimeZoneKind::Fixed(ref tz) => tz.to_offset_info(),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => tz.to_offset_info(_timestamp),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.to_offset_info(_timestamp),
+            },
+            STATIC_TZIF(tzif) => tzif.to_offset_info(timestamp),
+            ARC_TZIF(tzif) => tzif.to_offset_info(timestamp),
+            ARC_POSIX(posix) => posix.to_offset_info(timestamp),
         }
     }
 
@@ -918,17 +925,21 @@ impl TimeZone {
     /// ```
     #[inline]
     pub fn to_fixed_offset(&self) -> Result<Offset, Error> {
-        let Some(ref kind) = self.kind else { return Ok(Offset::UTC) };
-        #[allow(irrefutable_let_patterns)]
-        match **kind {
-            TimeZoneKind::Unknown => Ok(Offset::UTC),
-            TimeZoneKind::Fixed(ref tz) => Ok(tz.to_offset()),
-            #[cfg(feature = "alloc")]
-            _ => Err(err!(
+        let mkerr = || {
+            err!(
                 "cannot convert non-fixed {kind} time zone to offset \
                  without timestamp or civil datetime",
                 kind = self.kind_description(),
-            )),
+            )
+        };
+        repr::each! {
+            &self.repr,
+            UTC => Ok(Offset::UTC),
+            UNKNOWN => Ok(Offset::UTC),
+            FIXED(offset) => Ok(offset),
+            STATIC_TZIF(_tzif) => Err(mkerr()),
+            ARC_TZIF(_tzif) => Err(mkerr()),
+            ARC_POSIX(_posix) => Err(mkerr()),
         }
     }
 
@@ -1163,20 +1174,14 @@ impl TimeZone {
     /// ```
     #[inline]
     pub fn to_ambiguous_timestamp(&self, dt: DateTime) -> AmbiguousTimestamp {
-        let ambiguous_kind = match self.kind {
-            None => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
-            Some(ref kind) => match **kind {
-                TimeZoneKind::Unknown => {
-                    AmbiguousOffset::Unambiguous { offset: Offset::UTC }
-                }
-                TimeZoneKind::Fixed(ref tz) => {
-                    AmbiguousOffset::Unambiguous { offset: tz.to_offset() }
-                }
-                #[cfg(feature = "alloc")]
-                TimeZoneKind::Posix(ref tz) => tz.to_ambiguous_kind(dt),
-                #[cfg(feature = "alloc")]
-                TimeZoneKind::Tzif(ref tz) => tz.to_ambiguous_kind(dt),
-            },
+        let ambiguous_kind = repr::each! {
+            &self.repr,
+            UTC => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
+            UNKNOWN => AmbiguousOffset::Unambiguous { offset: Offset::UTC },
+            FIXED(offset) => AmbiguousOffset::Unambiguous { offset },
+            STATIC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
+            ARC_TZIF(tzif) => tzif.to_ambiguous_kind(dt),
+            ARC_POSIX(posix) => posix.to_ambiguous_kind(dt),
         };
         AmbiguousTimestamp::new(dt, ambiguous_kind)
     }
@@ -1345,15 +1350,16 @@ impl TimeZone {
     #[inline]
     fn previous_transition(
         &self,
-        _timestamp: Timestamp,
+        timestamp: Timestamp,
     ) -> Option<TimeZoneTransition> {
-        match **self.kind.as_ref()? {
-            TimeZoneKind::Unknown => None,
-            TimeZoneKind::Fixed(_) => None,
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => tz.previous_transition(_timestamp),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.previous_transition(_timestamp),
+        repr::each! {
+            &self.repr,
+            UTC => None,
+            UNKNOWN => None,
+            FIXED(_offset) => None,
+            STATIC_TZIF(tzif) => tzif.previous_transition(timestamp),
+            ARC_TZIF(tzif) => tzif.previous_transition(timestamp),
+            ARC_POSIX(posix) => posix.previous_transition(timestamp),
         }
     }
 
@@ -1361,15 +1367,16 @@ impl TimeZone {
     #[inline]
     fn next_transition(
         &self,
-        _timestamp: Timestamp,
+        timestamp: Timestamp,
     ) -> Option<TimeZoneTransition> {
-        match **self.kind.as_ref()? {
-            TimeZoneKind::Unknown => None,
-            TimeZoneKind::Fixed(_) => None,
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => tz.next_transition(_timestamp),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => tz.next_transition(_timestamp),
+        repr::each! {
+            &self.repr,
+            UTC => None,
+            UNKNOWN => None,
+            FIXED(_offset) => None,
+            STATIC_TZIF(tzif) => tzif.next_transition(timestamp),
+            ARC_TZIF(tzif) => tzif.next_transition(timestamp),
+            ARC_POSIX(posix) => posix.next_transition(timestamp),
         }
     }
 
@@ -1377,222 +1384,53 @@ impl TimeZone {
     ///
     /// This is useful in error messages.
     fn kind_description(&self) -> &str {
-        let Some(ref kind) = self.kind else {
-            return "UTC";
-        };
-        match **kind {
-            TimeZoneKind::Unknown => "Etc/Unknown",
-            TimeZoneKind::Fixed(_) => "fixed",
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(_) => "POSIX",
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(_) => "IANA",
+        repr::each! {
+            &self.repr,
+            UTC => "UTC",
+            UNKNOWN => "Etc/Unknown",
+            FIXED(_offset) => "fixed",
+            STATIC_TZIF(_tzif) => "IANA",
+            ARC_TZIF(_tzif) => "IANA",
+            ARC_POSIX(_posix) => "POSIX",
         }
     }
 }
 
-impl core::fmt::Debug for TimeZone {
-    #[inline]
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        let field: &dyn core::fmt::Debug = match self.kind {
-            None => &"UTC",
-            Some(ref kind) => match &**kind {
-                TimeZoneKind::Unknown => &"Etc/Unknown",
-                TimeZoneKind::Fixed(ref tz) => tz,
-                #[cfg(feature = "alloc")]
-                TimeZoneKind::Posix(ref tz) => tz,
-                #[cfg(feature = "alloc")]
-                TimeZoneKind::Tzif(ref tz) => tz,
-            },
-        };
-        f.debug_tuple("TimeZone").field(field).finish()
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-#[cfg_attr(not(feature = "alloc"), derive(Clone))]
-enum TimeZoneKind {
-    // It would be nice if we could represent this similarly to
-    // `TimeZone::UTC`. That is, without putting it behind an `Arc`. But I
-    // didn't see an easy way to do that while retaining the single-word size
-    // of `TimeZone` without pointer tagging, since `Arc` only gives the
-    // compiler a single niche value. Plus, it should be exceptionally rare
-    // for a unknown time zone to be used anyway. It's generally an error
-    // condition.
-    Unknown,
-    Fixed(TimeZoneFixed),
-    #[cfg(feature = "alloc")]
-    Posix(TimeZonePosix),
-    #[cfg(feature = "alloc")]
-    Tzif(TimeZoneTzif),
-}
-
-#[derive(Clone)]
-struct TimeZoneFixed {
-    offset: Offset,
-}
-
-impl TimeZoneFixed {
-    #[inline]
-    fn new(offset: Offset) -> TimeZoneFixed {
-        TimeZoneFixed { offset }
-    }
-
-    #[inline]
-    fn to_offset(&self) -> Offset {
-        self.offset
-    }
-
-    #[inline]
-    fn to_offset_info(&self) -> TimeZoneOffsetInfo<'_> {
-        let abbreviation =
-            TimeZoneAbbreviation::Owned(self.offset.to_array_str());
-        TimeZoneOffsetInfo { offset: self.offset, dst: Dst::No, abbreviation }
-    }
-}
-
-impl core::fmt::Debug for TimeZoneFixed {
-    #[inline]
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        f.debug_tuple("Fixed").field(&self.to_offset()).finish()
-    }
-}
-
-impl core::fmt::Display for TimeZoneFixed {
-    #[inline]
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        core::fmt::Display::fmt(&self.offset, f)
-    }
-}
-
-impl Eq for TimeZoneFixed {}
-
-impl PartialEq for TimeZoneFixed {
-    #[inline]
-    fn eq(&self, rhs: &TimeZoneFixed) -> bool {
-        self.to_offset() == rhs.to_offset()
-    }
-}
-
-#[cfg(feature = "alloc")]
-#[derive(Clone, Eq, PartialEq)]
-struct TimeZonePosix {
-    posix: ReasonablePosixTimeZone,
-}
-
-#[cfg(feature = "alloc")]
-impl TimeZonePosix {
-    #[inline]
-    fn to_offset(&self, timestamp: Timestamp) -> Offset {
-        self.posix.to_offset(timestamp)
-    }
-
-    #[inline]
-    fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
-        self.posix.to_offset_info(timestamp)
-    }
-
-    #[inline]
-    fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
-        self.posix.to_ambiguous_kind(dt)
-    }
-
-    #[inline]
-    fn previous_transition(
-        &self,
-        timestamp: Timestamp,
-    ) -> Option<TimeZoneTransition> {
-        self.posix.previous_transition(timestamp)
-    }
-
-    #[inline]
-    fn next_transition(
-        &self,
-        timestamp: Timestamp,
-    ) -> Option<TimeZoneTransition> {
-        self.posix.next_transition(timestamp)
-    }
-}
-
-// This is implemented by hand because dumping out the full representation of
-// a `ReasonablePosixTimeZone` is way too much noise for users of Jiff.
-#[cfg(feature = "alloc")]
-impl core::fmt::Debug for TimeZonePosix {
-    #[inline]
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        write!(f, "Posix({})", self.posix)
-    }
-}
-
-#[cfg(feature = "alloc")]
-impl core::fmt::Display for TimeZonePosix {
-    #[inline]
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        core::fmt::Display::fmt(&self.posix, f)
-    }
-}
-
-#[cfg(feature = "alloc")]
-#[derive(Eq, PartialEq)]
-struct TimeZoneTzif {
-    tzif: crate::tz::tzif::Tzif,
-}
-
-#[cfg(feature = "alloc")]
-impl TimeZoneTzif {
-    #[inline]
-    fn new(
-        name: Option<alloc::string::String>,
-        bytes: &[u8],
-    ) -> Result<TimeZoneTzif, Error> {
-        let tzif = crate::tz::tzif::Tzif::parse(name, bytes)?;
-        Ok(TimeZoneTzif { tzif })
-    }
-
-    #[inline]
-    fn name(&self) -> Option<&str> {
-        self.tzif.name()
-    }
-
-    #[inline]
-    fn to_offset(&self, timestamp: Timestamp) -> Offset {
-        self.tzif.to_offset(timestamp)
-    }
-
-    #[inline]
-    fn to_offset_info(&self, timestamp: Timestamp) -> TimeZoneOffsetInfo<'_> {
-        self.tzif.to_offset_info(timestamp)
-    }
-
-    #[inline]
-    fn to_ambiguous_kind(&self, dt: DateTime) -> AmbiguousOffset {
-        self.tzif.to_ambiguous_kind(dt)
-    }
-
-    #[inline]
-    fn previous_transition(
-        &self,
-        timestamp: Timestamp,
-    ) -> Option<TimeZoneTransition> {
-        self.tzif.previous_transition(timestamp)
+// Exposed APIs for Jiff's time zone proc macro.
+//
+// These are NOT part of Jiff's public API. There are *zero* semver guarantees
+// for them.
+#[doc(hidden)]
+impl TimeZone {
+    pub const fn __internal_from_tzif(
+        tzif: &'static crate::tz::tzif::TzifStatic,
+    ) -> TimeZone {
+        let repr = Repr::static_tzif(tzif);
+        TimeZone { repr }
     }
 
+    /// Returns a dumb copy of this `TimeZone`.
+    ///
+    /// # Safety
+    ///
+    /// Callers must ensure that this time zone is UTC, unknown, a fixed
+    /// offset or created with `TimeZone::__internal_from_tzif`.
+    ///
+    /// Namely, this specifically does not increment the ref count for
+    /// the `Arc` pointers when the tag is `ARC_TZIF` or `ARC_POSIX`.
+    /// This means that incorrect usage of this routine can lead to
+    /// use-after-free.
     #[inline]
-    fn next_transition(
-        &self,
-        timestamp: Timestamp,
-    ) -> Option<TimeZoneTransition> {
-        self.tzif.next_transition(timestamp)
+    pub const unsafe fn copy(&self) -> TimeZone {
+        // SAFETY: Requirements are forwarded to the caller.
+        unsafe { TimeZone { repr: self.repr.copy() } }
     }
 }
 
-// This is implemented by hand because dumping out the full representation of
-// all TZif data is too much noise for users of Jiff.
-#[cfg(feature = "alloc")]
-impl core::fmt::Debug for TimeZoneTzif {
+impl core::fmt::Debug for TimeZone {
     #[inline]
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        f.debug_tuple("TZif").field(&self.name().unwrap_or("Local")).finish()
+        f.debug_tuple("TimeZone").field(&self.repr).finish()
     }
 }
 
@@ -2038,16 +1876,14 @@ pub(crate) struct DiagnosticName<'a>(&'a TimeZone);
 
 impl<'a> core::fmt::Display for DiagnosticName<'a> {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        let Some(ref kind) = self.0.kind else { return write!(f, "UTC") };
-        match **kind {
-            TimeZoneKind::Unknown => write!(f, "Etc/Unknown"),
-            TimeZoneKind::Fixed(ref tz) => write!(f, "{tz}"),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Posix(ref tz) => write!(f, "{tz}"),
-            #[cfg(feature = "alloc")]
-            TimeZoneKind::Tzif(ref tz) => {
-                write!(f, "{}", tz.name().unwrap_or("Local"))
-            }
+        repr::each! {
+            &self.0.repr,
+            UTC => write!(f, "UTC"),
+            UNKNOWN => write!(f, "Etc/Unknown"),
+            FIXED(offset) => write!(f, "{offset}"),
+            STATIC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
+            ARC_TZIF(tzif) => write!(f, "{}", tzif.name().unwrap_or("Local")),
+            ARC_POSIX(posix) => write!(f, "{posix}"),
         }
     }
 }
@@ -2090,6 +1926,536 @@ impl<'t> TimeZoneAbbreviation<'t> {
     }
 }
 
+/// This module defines the internal representation of a `TimeZone`.
+///
+/// This module exists to _encapsulate_ the representation rigorously and
+/// expose a safe and sound API.
+#[allow(warnings)]
+mod repr {
+    use core::mem::ManuallyDrop;
+
+    use crate::{
+        tz::tzif::TzifStatic,
+        util::{constant::unwrap, t},
+    };
+    #[cfg(feature = "alloc")]
+    use crate::{
+        tz::{posix::ReasonablePosixTimeZone, tzif::TzifOwned},
+        util::sync::Arc,
+    };
+
+    use super::Offset;
+
+    use self::polyfill::{without_provenance, StrictProvenancePolyfill};
+
+    /// A macro for "matching" over the time zone representation variants.
+    ///
+    /// This macro is safe to use.
+    ///
+    /// Note that the `ARC_TZIF` and `ARC_POSIX` branches are automatically
+    /// removed when `alloc` isn't enabled. Users of this macro needn't handle
+    /// the `cfg` themselves.
+    macro_rules! each {
+        (
+            $repr:expr,
+            UTC => $utc:expr,
+            UNKNOWN => $unknown:expr,
+            FIXED($offset:ident) => $fixed:expr,
+            STATIC_TZIF($static_tzif:ident) => $static_tzif_block:expr,
+            ARC_TZIF($arc_tzif:ident) => $arc_tzif_block:expr,
+            ARC_POSIX($arc_posix:ident) => $arc_posix_block:expr,
+        ) => {{
+            let repr = $repr;
+            match repr.tag() {
+                Repr::UTC => $utc,
+                Repr::UNKNOWN => $unknown,
+                Repr::FIXED => {
+                    // SAFETY: We've ensured our pointer tag is correct.
+                    let $offset = unsafe { repr.get_fixed() };
+                    $fixed
+                }
+                Repr::STATIC_TZIF => {
+                    // SAFETY: We've ensured our pointer tag is correct.
+                    let $static_tzif = unsafe { repr.get_static_tzif() };
+                    $static_tzif_block
+                }
+                #[cfg(feature = "alloc")]
+                Repr::ARC_TZIF => {
+                    // SAFETY: We've ensured our pointer tag is correct.
+                    let $arc_tzif = unsafe { repr.get_arc_tzif() };
+                    $arc_tzif_block
+                }
+                #[cfg(feature = "alloc")]
+                Repr::ARC_POSIX => {
+                    // SAFETY: We've ensured our pointer tag is correct.
+                    let $arc_posix = unsafe { repr.get_arc_posix() };
+                    $arc_posix_block
+                }
+                _ => {
+                    debug_assert!(false, "each: invalid time zone repr tag!");
+                    // SAFETY: The constructors for `Repr` guarantee that the
+                    // tag is always one of the values matched above.
+                    unsafe {
+                        core::hint::unreachable_unchecked();
+                    }
+                }
+            }
+        }};
+    }
+    pub(super) use each;
+
+    /// The internal representation of a `TimeZone`.
+    ///
+    /// It has 6 different possible variants: `UTC`, `Etc/Unknown`, fixed
+    /// offset, `static` TZif, `Arc` TZif or `Arc` POSIX time zone.
+    ///
+    /// This design uses pointer tagging so that:
+    ///
+    /// * The size of a `TimeZone` stays no bigger than a single word.
+    /// * In core-only environments, a `TimeZone` can be created from
+    ///   compile-time TZif data without allocating.
+    /// * UTC, unknown and fixed offset time zone does not require allocating.
+    /// * We can still alloc for TZif and POSIX time zones created at runtime.
+    ///   (Allocating for TZif at runtime is the intended common case, and
+    ///   corresponds to reading `/usr/share/zoneinfo` entries.)
+    ///
+    /// We achieve this through pointer tagging and careful use of a strict
+    /// provenance polyfill (because of MSRV). We use the lower 4 bits of a
+    /// pointer to indicate which variant we have. This is sound because we
+    /// require all types that we allocate for to have a minimum alignment of
+    /// 4 bytes.
+    pub(super) struct Repr {
+        ptr: *const u8,
+    }
+
+    impl Repr {
+        const BITS: usize = 0b111;
+        pub(super) const UTC: usize = 1;
+        pub(super) const UNKNOWN: usize = 2;
+        pub(super) const FIXED: usize = 3;
+        pub(super) const STATIC_TZIF: usize = 0;
+        pub(super) const ARC_TZIF: usize = 4;
+        pub(super) const ARC_POSIX: usize = 5;
+
+        // The minimum alignment required for any heap allocated time zone
+        // variants. This is related to the number of tags. We have 6 distinct
+        // values above, which means we need an alignment of at least 6. Since
+        // alignment must be a power of 2, the smallest possible alignment
+        // is 8.
+        const ALIGN: usize = 8;
+
+        /// Creates a representation for a `UTC` time zone.
+        #[inline]
+        pub(super) const fn utc() -> Repr {
+            let ptr = without_provenance(Repr::UTC);
+            Repr { ptr }
+        }
+
+        /// Creates a representation for a `Etc/Unknown` time zone.
+        #[inline]
+        pub(super) const fn unknown() -> Repr {
+            let ptr = without_provenance(Repr::UNKNOWN);
+            Repr { ptr }
+        }
+
+        /// Creates a representation for a fixed offset time zone.
+        #[inline]
+        pub(super) const fn fixed(offset: Offset) -> Repr {
+            let seconds = offset.seconds_ranged().val;
+            // OK because offset is in -93599..=93599.
+            let shifted = unwrap!(
+                seconds.checked_shl(4),
+                "offset small enough for left shift by 4 bits",
+            );
+            assert!(usize::MAX >= 4_294_967_295);
+            // usize cast is okay because Jiff requires 32-bit.
+            let ptr = without_provenance((shifted as usize) | Repr::FIXED);
+            Repr { ptr }
+        }
+
+        /// Creates a representation for a created-at-compile-time TZif time
+        /// zone.
+        ///
+        /// This can only be correctly called by the `jiff-static` proc macro.
+        #[inline]
+        pub(super) const fn static_tzif(tzif: &'static TzifStatic) -> Repr {
+            assert!(core::mem::align_of::<TzifStatic>() >= Repr::ALIGN);
+            let tzif = (tzif as *const TzifStatic).cast::<u8>();
+            // We very specifically do no materialize the pointer address here
+            // because 1) it's UB and 2) the compiler generally prevents. This
+            // is because in a const context, the specific pointer address
+            // cannot be relied upon. Yet, we still want to do pointer tagging.
+            //
+            // Thankfully, this is the only variant that is a pointer that
+            // we want to create in a const context. So we just make this
+            // variant's tag `0`, and thus, no explicit pointer tagging is
+            // required. (Becuase we ensure the alignment is at least 4, and
+            // thus the least significant 3 bits are 0.)
+            //
+            // If this ends up not working out or if we need to support
+            // another `static` variant, then we could perhaps to pointer
+            // tagging with pointer arithmetic (like what the `tagged-pointer`
+            // crate does). I haven't tried it though and I'm unclear if it
+            // work.
+            Repr { ptr: tzif }
+        }
+
+        /// Creates a representation for a TZif time zone.
+        #[cfg(feature = "alloc")]
+        #[inline]
+        pub(super) fn arc_tzif(tzif: Arc<TzifOwned>) -> Repr {
+            assert!(core::mem::align_of::<TzifOwned>() >= Repr::ALIGN);
+            let tzif = Arc::into_raw(tzif).cast::<u8>();
+            assert!(tzif.addr() % 4 == 0);
+            let ptr = tzif.map_addr(|addr| addr | Repr::ARC_TZIF);
+            Repr { ptr }
+        }
+
+        /// Creates a representation for a POSIX time zone.
+        #[cfg(feature = "alloc")]
+        #[inline]
+        pub(super) fn arc_posix(
+            posix_tz: Arc<ReasonablePosixTimeZone>,
+        ) -> Repr {
+            assert!(
+                core::mem::align_of::<ReasonablePosixTimeZone>()
+                    >= Repr::ALIGN
+            );
+            let posix_tz = Arc::into_raw(posix_tz).cast::<u8>();
+            assert!(posix_tz.addr() % 4 == 0);
+            let ptr = posix_tz.map_addr(|addr| addr | Repr::ARC_POSIX);
+            Repr { ptr }
+        }
+
+        /// Gets the offset representation.
+        ///
+        /// # Safety
+        ///
+        /// Callers must ensure that the pointer tag is `FIXED`.
+        #[inline]
+        pub(super) unsafe fn get_fixed(&self) -> Offset {
+            let addr = self.ptr.addr();
+            // NOTE: Because of sign extension, we need to case to `i32`
+            // before shifting.
+            let seconds = t::SpanZoneOffset::new_unchecked((addr as i32) >> 4);
+            Offset::from_seconds_ranged(seconds)
+        }
+
+        /// Returns true if and only if this representation corresponds to the
+        /// `Etc/Unknown` time zone.
+        #[inline]
+        pub(super) fn is_unknown(&self) -> bool {
+            self.tag() == Repr::UNKNOWN
+        }
+
+        /// Gets the static TZif representation.
+        ///
+        /// # Safety
+        ///
+        /// Callers must ensure that the pointer tag is `STATIC_TZIF`.
+        #[inline]
+        pub(super) unsafe fn get_static_tzif(&self) -> &'static TzifStatic {
+            let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+            // SAFETY: Getting a `STATIC_TZIF` tag is only possible when
+            // `self.ptr` was constructed from a valid and aligned (to at least
+            // 4 bytes) `&TzifStatic` borrow. Which must be guaranteed by the
+            // caller. We've also removed the tag bits above, so we must now
+            // have the original pointer.
+            unsafe { &*ptr.cast::<TzifStatic>() }
+        }
+
+        /// Gets the `Arc` TZif representation.
+        ///
+        /// # Safety
+        ///
+        /// Callers must ensure that the pointer tag is `ARC_TZIF`.
+        #[cfg(feature = "alloc")]
+        #[inline]
+        pub(super) unsafe fn get_arc_tzif<'a>(&'a self) -> &'a TzifOwned {
+            let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+            // SAFETY: Getting a `ARC_TZIF` tag is only possible when
+            // `self.ptr` was constructed from a valid and aligned
+            // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
+            // the tag bits above, so we must now have the original
+            // pointer.
+            let arc = ManuallyDrop::new(unsafe {
+                Arc::from_raw(ptr.cast::<TzifOwned>())
+            });
+            // SAFETY: The lifetime of the pointer returned is always
+            // valid as long as the strong count on `arc` is at least
+            // 1. Since the lifetime is no longer than `Repr` itself,
+            // and a `Repr` being alive implies there is at least 1
+            // for the strong `Arc` count, it follows that the lifetime
+            // returned here is correct.
+            unsafe { &*Arc::as_ptr(&arc) }
+        }
+
+        /// Gets the `Arc` POSIX time zone representation.
+        ///
+        /// # Safety
+        ///
+        /// Callers must ensure that the pointer tag is `ARC_POSIX`.
+        #[cfg(feature = "alloc")]
+        #[inline]
+        pub(super) unsafe fn get_arc_posix<'a>(
+            &'a self,
+        ) -> &'a ReasonablePosixTimeZone {
+            let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+            // SAFETY: Getting a `ARC_POSIX` tag is only possible when
+            // `self.ptr` was constructed from a valid and aligned (to at least
+            // 4 bytes) `Arc<ReasonablePosixTimeZone>`. We've removed the tag
+            // bits above, so we must now have the original pointer.
+            let arc = ManuallyDrop::new(unsafe {
+                Arc::from_raw(ptr.cast::<ReasonablePosixTimeZone>())
+            });
+            // SAFETY: The lifetime of the pointer returned is always
+            // valid as long as the strong count on `arc` is at least
+            // 1. Since the lifetime is no longer than `Repr` itself,
+            // and a `Repr` being alive implies there is at least 1
+            // for the strong `Arc` count, it follows that the lifetime
+            // returned here is correct.
+            unsafe { &*Arc::as_ptr(&arc) }
+        }
+
+        /// Returns the tag on the representation's pointer.
+        ///
+        /// The value is guaranteed to be one of the constant tag values.
+        #[inline]
+        pub(super) fn tag(&self) -> usize {
+            self.ptr.addr() & Repr::BITS
+        }
+
+        /// Returns a dumb copy of this representation.
+        ///
+        /// # Safety
+        ///
+        /// Callers must ensure that this representation's tag is UTC,
+        /// UNKNOWN, FIXED or STATIC_TZIF.
+        ///
+        /// Namely, this specifically does not increment the ref count for
+        /// the `Arc` pointers when the tag is `ARC_TZIF` or `ARC_POSIX`.
+        /// This means that incorrect usage of this routine can lead to
+        /// use-after-free.
+        ///
+        /// NOTE: It would be nice if we could make this `copy` routine safe,
+        /// or at least panic if it's misused. But to do that, you need to know
+        /// the time zone variant. And to know the time zone variant, you need
+        /// to "look" at the tag in the pointer. And looking at the address of
+        /// a pointer in a `const` context is precarious.
+        #[inline]
+        pub(super) const unsafe fn copy(&self) -> Repr {
+            Repr { ptr: self.ptr }
+        }
+    }
+
+    // SAFETY: We use automic reference counting.
+    unsafe impl Send for Repr {}
+    // SAFETY: We don't use an interior mutability and otherwise don't permit
+    // any kind of mutation (other than for an `Arc` managing its ref counts)
+    // of a `Repr`.
+    unsafe impl Sync for Repr {}
+
+    impl core::fmt::Debug for Repr {
+        fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+            each! {
+                self,
+                UTC => write!(f, "UTC"),
+                UNKNOWN => write!(f, "Etc/Unknown"),
+                FIXED(offset) => write!(f, "{offset:?}"),
+                STATIC_TZIF(tzif) => {
+                    // The full debug output is a bit much, so constrain it.
+                    let field = tzif.name().unwrap_or("Local");
+                    f.debug_tuple("TZif").field(&field).finish()
+                },
+                ARC_TZIF(tzif) => {
+                    // The full debug output is a bit much, so constrain it.
+                    let field = tzif.name().unwrap_or("Local");
+                    f.debug_tuple("TZif").field(&field).finish()
+                },
+                ARC_POSIX(posix) => write!(f, "Posix({posix})"),
+            }
+        }
+    }
+
+    impl Clone for Repr {
+        #[inline]
+        fn clone(&self) -> Repr {
+            // This `match` is written in an exhaustive fashion so that if
+            // a new tag is added, it should be explicitly considered here.
+            match self.tag() {
+                // These are all `Copy` and can just be memcpy'd as-is.
+                Repr::UTC
+                | Repr::UNKNOWN
+                | Repr::FIXED
+                | Repr::STATIC_TZIF => Repr { ptr: self.ptr },
+                #[cfg(feature = "alloc")]
+                Repr::ARC_TZIF => {
+                    let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+                    // SAFETY: Getting a `ARC_TZIF` tag is only possible when
+                    // `self.ptr` was constructed from a valid and aligned
+                    // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
+                    // the tag bits above, so we must now have the original
+                    // pointer.
+                    unsafe {
+                        Arc::increment_strong_count(ptr.cast::<TzifOwned>());
+                    }
+                    Repr { ptr: self.ptr }
+                }
+                #[cfg(feature = "alloc")]
+                Repr::ARC_POSIX => {
+                    let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+                    // SAFETY: Getting a `ARC_POSIX` tag is only possible when
+                    // `self.ptr` was constructed from a valid and aligned (to
+                    // at least 4 bytes) `Arc<ReasonablePosixTimeZone>`. We've
+                    // removed the tag bits above, so we must now have the
+                    // original pointer.
+                    unsafe {
+                        Arc::increment_strong_count(
+                            ptr.cast::<ReasonablePosixTimeZone>(),
+                        );
+                    }
+                    Repr { ptr: self.ptr }
+                }
+                _ => {
+                    debug_assert!(false, "clone: invalid time zone repr tag!");
+                    // SAFETY: The constructors for `Repr` guarantee that the
+                    // tag is always one of the values matched above.
+                    unsafe {
+                        core::hint::unreachable_unchecked();
+                    }
+                }
+            }
+        }
+    }
+
+    impl Drop for Repr {
+        #[inline]
+        fn drop(&mut self) {
+            // This `match` is written in an exhaustive fashion so that if
+            // a new tag is added, it should be explicitly considered here.
+            match self.tag() {
+                // These are all `Copy` and have no destructor.
+                Repr::UTC
+                | Repr::UNKNOWN
+                | Repr::FIXED
+                | Repr::STATIC_TZIF => {}
+                #[cfg(feature = "alloc")]
+                Repr::ARC_TZIF => {
+                    let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+                    // SAFETY: Getting a `ARC_TZIF` tag is only possible when
+                    // `self.ptr` was constructed from a valid and aligned
+                    // (to at least 4 bytes) `Arc<TzifOwned>`. We've removed
+                    // the tag bits above, so we must now have the original
+                    // pointer.
+                    unsafe {
+                        Arc::decrement_strong_count(ptr.cast::<TzifOwned>());
+                    }
+                }
+                #[cfg(feature = "alloc")]
+                Repr::ARC_POSIX => {
+                    let ptr = self.ptr.map_addr(|addr| addr & !Repr::BITS);
+                    // SAFETY: Getting a `ARC_POSIX` tag is only possible when
+                    // `self.ptr` was constructed from a valid and aligned (to
+                    // at least 4 bytes) `Arc<ReasonablePosixTimeZone>`. We've
+                    // removed the tag bits above, so we must now have the
+                    // original pointer.
+                    unsafe {
+                        Arc::decrement_strong_count(
+                            ptr.cast::<ReasonablePosixTimeZone>(),
+                        );
+                    }
+                }
+                _ => {
+                    debug_assert!(false, "drop: invalid time zone repr tag!");
+                    // SAFETY: The constructors for `Repr` guarantee that the
+                    // tag is always one of the values matched above.
+                    unsafe {
+                        core::hint::unreachable_unchecked();
+                    }
+                }
+            }
+        }
+    }
+
+    impl Eq for Repr {}
+
+    impl PartialEq for Repr {
+        fn eq(&self, other: &Repr) -> bool {
+            if self.tag() != other.tag() {
+                return false;
+            }
+            each! {
+                self,
+                UTC => true,
+                UNKNOWN => true,
+                // SAFETY: OK, because we know the tags are equivalent and
+                // `self` has a `FIXED` tag.
+                FIXED(offset) => offset == unsafe { other.get_fixed() },
+                // SAFETY: OK, because we know the tags are equivalent and
+                // `self` has a `STATIC_TZIF` tag.
+                STATIC_TZIF(tzif) => tzif == unsafe { other.get_static_tzif() },
+                // SAFETY: OK, because we know the tags are equivalent and
+                // `self` has an `ARC_TZIF` tag.
+                ARC_TZIF(tzif) => tzif == unsafe { other.get_arc_tzif() },
+                // SAFETY: OK, because we know the tags are equivalent and
+                // `self` has an `ARC_POSIX` tag.
+                ARC_POSIX(posix) => posix == unsafe { other.get_arc_posix() },
+            }
+        }
+    }
+
+    /// This is a polyfill for a small subset of std's strict provenance APIs.
+    ///
+    /// The strict provenance APIs in `core` were stabilized in Rust 1.84,
+    /// but it will likely be a while before Jiff can use them. (At time of
+    /// writing, 2025-02-24, Jiff's MSRV is Rust 1.70.)
+    ///
+    /// The `const` requirement is also why these are non-generic free
+    /// functions and not defined via an extension trait. It's also why we
+    /// don't have the useful `map_addr` routine (which is directly relevant to
+    /// our pointer tagging use case).
+    mod polyfill {
+        pub(super) const fn without_provenance(addr: usize) -> *const u8 {
+            // SAFETY: Every valid `usize` is also a valid pointer (but not
+            // necessarily legal to dereference).
+            //
+            // MSRV(1.84): We *really* ought to be using
+            // `core::ptr::without_provenance` here, but Jiff's MSRV prevents
+            // us.
+            unsafe { core::mem::transmute(addr) }
+        }
+
+        pub(super) trait StrictProvenancePolyfill:
+            Sized + Clone + Copy
+        {
+            fn addr(&self) -> usize;
+            fn with_addr(&self, addr: usize) -> Self;
+            fn map_addr(&self, map: impl FnOnce(usize) -> usize) -> Self {
+                self.with_addr(map(self.addr()))
+            }
+        }
+
+        impl StrictProvenancePolyfill for *const u8 {
+            fn addr(&self) -> usize {
+                // SAFETY: Pointer-to-integer transmutes are valid (if you are
+                // okay with losing the provenance).
+                //
+                // The implementation in std says that this isn't guaranteed to
+                // be sound outside of std, but I'm not sure how else to do it.
+                // In practice, this seems likely fine?
+                unsafe { core::mem::transmute(self.cast::<()>()) }
+            }
+
+            fn with_addr(&self, address: usize) -> Self {
+                let self_addr = self.addr() as isize;
+                let dest_addr = address as isize;
+                let offset = dest_addr.wrapping_sub(self_addr);
+                self.wrapping_offset(offset)
+            }
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     #[cfg(feature = "alloc")]
@@ -3368,7 +3734,7 @@ mod tests {
         {
             #[cfg(debug_assertions)]
             {
-                assert_eq!(16, core::mem::size_of::<TimeZone>());
+                assert_eq!(8, core::mem::size_of::<TimeZone>());
             }
             #[cfg(not(debug_assertions))]
             {
diff --git a/src/tz/tzif.rs b/src/tz/tzif.rs
index dc023337..5c187c94 100644
--- a/src/tz/tzif.rs
+++ b/src/tz/tzif.rs
@@ -10,24 +10,28 @@ These binary files are the ones commonly found in Unix distributions in the
 
 use core::ops::Range;
 
-use alloc::{string::String, vec, vec::Vec};
+#[cfg(feature = "alloc")]
+use alloc::{string::String, vec::Vec};
 
 use crate::{
     civil::DateTime,
-    error::{err, Error, ErrorContext},
+    error::Error,
+    shared,
     timestamp::Timestamp,
     tz::{
-        posix::{IanaTz, ReasonablePosixTimeZone},
-        timezone::TimeZoneAbbreviation,
+        posix::ReasonablePosixTimeZone, timezone::TimeZoneAbbreviation,
         AmbiguousOffset, Dst, Offset, TimeZoneOffsetInfo, TimeZoneTransition,
     },
-    util::{
-        crc32,
-        escape::{Byte, Bytes},
-        t::UnixSeconds,
-    },
 };
 
+/// The owned variant of `Tzif`.
+#[cfg(feature = "alloc")]
+pub(crate) type TzifOwned = Tzif<String, Vec<LocalTimeType>, Vec<Transition>>;
+
+/// The static variant of `Tzif`.
+pub(crate) type TzifStatic =
+    Tzif<&'static str, &'static [LocalTimeType], &'static [Transition]>;
+
 /// A time zone based on IANA TZif formatted data.
 ///
 /// TZif is a binary format described by RFC 8536. Its typical structure is to
@@ -40,8 +44,17 @@ use crate::{
 /// contents of TZif formatted data in memory, and turning it into a data type
 /// that can be used as a time zone.
 #[derive(Debug)]
-pub(crate) struct Tzif {
-    name: Option<String>,
+// not part of Jiff's public API
+#[doc(hidden)]
+// This ensures the alignment of this type is always *at least* 8 bytes. This
+// is required for the pointer tagging inside of `TimeZone` to be sound. At
+// time of writing (2024-02-24), this explicit `repr` isn't required on 64-bit
+// systems since the type definition is such that it will have an alignment of
+// at least 8 bytes anyway. But this *is* required for 32-bit systems, where
+// the type definition at present only has an alignment of 4 bytes.
+#[repr(align(8))]
+pub struct Tzif<STRING, TYPES, TRANS> {
+    name: Option<STRING>,
     /// An ASCII byte corresponding to the version number. So, 0x50 is '2'.
     ///
     /// This is unused. It's only used in `test` compilation for emitting
@@ -50,14 +63,93 @@ pub(crate) struct Tzif {
     #[allow(dead_code)]
     version: u8,
     checksum: u32,
-    transitions: Vec<Transition>,
-    types: Vec<LocalTimeType>,
-    designations: String,
-    leap_seconds: Vec<LeapSecond>,
+    designations: STRING,
     posix_tz: Option<ReasonablePosixTimeZone>,
+    types: TYPES,
+    transitions: TRANS,
 }
 
-impl Tzif {
+impl TzifStatic {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    ///
+    /// This specifically works in a `const` context. And it requires that
+    /// caller to pass in the parsed `Tzif` in its fixed form along with the
+    /// variable length local time types and transitions. (Technically, the
+    /// TZ identifier and the designations are also variable length despite
+    /// being parsed of `TzifFixed`, but in practice they can be handled just
+    /// fine via `&'static str`.)
+    ///
+    /// Notice that the `types` and `transitions` are *not* from the `shared`
+    /// API, but rather, from the types defined in this module. They have to
+    /// be this way because there's a conversion step that occurs. In practice,
+    /// this sort of thing is embedded as a literal in source code via a proc
+    /// macro. Like this:
+    ///
+    /// ```text
+    /// static TZIF: Tzif<&str, &[LocalTimeType], &[Transition]> =
+    ///     Tzif::from_shared_const(
+    ///         shared::TzifFixed {
+    ///             name: Some("America/New_York"),
+    ///             version: b'3',
+    ///             checksum: 0xDEADBEEF,
+    ///             designations: "ESTEDT",
+    ///             posix_tz: None,
+    ///         },
+    ///         &[
+    ///             shared::TzifLocalTimeType {
+    ///                 offset: -5 * 60 * 60,
+    ///                 is_dst: false,
+    ///                 designation: 0..3,
+    ///                 indicator: shared::TzifIndicator::LocalWall,
+    ///             }.to_jiff(),
+    ///         ],
+    ///         &[
+    ///             shared::TzifTransition {
+    ///                 timestamp: 123456789,
+    ///                 type_index: 0,
+    ///             }.to_jiff(-5, -5),
+    ///         ],
+    ///     );
+    /// ```
+    ///
+    /// Or something like that anyway. The point is, our `static` slices are
+    /// variable length and they need to be the right types. At least, I
+    /// couldn't see a simpler way to arrange this.
+    pub(crate) const fn from_shared_const(
+        sh: &shared::TzifFixed<&'static str>,
+        types: &'static [LocalTimeType],
+        transitions: &'static [Transition],
+    ) -> TzifStatic {
+        let name = sh.name;
+        let version = sh.version;
+        let checksum = sh.checksum;
+        let designations = sh.designations;
+        let posix_tz = match sh.posix_tz {
+            None => None,
+            Some(ref tz) => Some(tz.to_jiff()),
+        };
+        // Unlike in the owned case, we specifically do not check that the
+        // POSIX time zone is consistent with the last transition. This is
+        // because it would require making a lot of code in Jiff `const`
+        // that is difficult to make `const`. Moreover, this constructor is
+        // generally only used with the `jiff-static` proc-macros, and those
+        // go through the owned APIs at compile time. And thus, it is already
+        // validated that the POSIX time zone is consistent with the last
+        // transition.
+        Tzif {
+            name,
+            version,
+            checksum,
+            designations,
+            posix_tz,
+            types,
+            transitions,
+        }
+    }
+}
+
+#[cfg(feature = "alloc")]
+impl TzifOwned {
     /// Parses the given data as a TZif formatted file.
     ///
     /// The name given is attached to the `Tzif` value returned, but is
@@ -75,28 +167,63 @@ impl Tzif {
     pub(crate) fn parse(
         name: Option<String>,
         bytes: &[u8],
-    ) -> Result<Tzif, Error> {
-        let original = bytes;
-        let name = name.into();
-        let (header32, rest) = Header::parse(4, bytes)
-            .map_err(|e| e.context("failed to parse 32-bit header"))?;
-        let (mut tzif, rest) = if header32.version == 0 {
-            Tzif::parse32(name, header32, rest)?
-        } else {
-            Tzif::parse64(name, header32, rest)?
-        };
-        // Compute the checksum using the entire contents of the TZif data.
-        let tzif_raw_len = (rest.as_ptr() as usize)
-            .checked_sub(original.as_ptr() as usize)
-            .unwrap();
-        let tzif_raw_bytes = &original[..tzif_raw_len];
-        tzif.checksum = crc32::sum(tzif_raw_bytes);
+    ) -> Result<Self, Error> {
+        let sh =
+            shared::TzifOwned::parse(name, bytes).map_err(Error::adhoc)?;
+        let tzif = TzifOwned::from_shared_owned(&sh);
         Ok(tzif)
     }
 
+    /// Converts from the shared-but-internal API for use in proc macros.
+    ///
+    /// This is not `const` since it accepts owned `String` and `Vec` values
+    /// for variable length data inside `Tzif`.
+    pub(crate) fn from_shared_owned(sh: &shared::TzifOwned) -> TzifOwned {
+        let name = sh.fixed.name.clone();
+        let version = sh.fixed.version;
+        let checksum = sh.fixed.checksum;
+        let designations = sh.fixed.designations.clone();
+        let posix_tz = match sh.fixed.posix_tz {
+            None => None,
+            Some(ref tz) => {
+                let tz =
+                    crate::tz::posix::PosixTimeZone::from_shared_owned(tz);
+                // OK because `shared::tzif` returns an error otherwise.
+                Some(tz.reasonable().unwrap())
+            }
+        };
+        let types: Vec<LocalTimeType> =
+            sh.types.iter().map(shared::TzifLocalTimeType::to_jiff).collect();
+        let mut transitions = Vec::with_capacity(sh.transitions.len());
+        for (i, this) in sh.transitions.iter().enumerate() {
+            let prev = &sh.transitions[i.saturating_sub(1)];
+            let prev_offset = sh.types[usize::from(prev.type_index)].offset;
+            let this_offset = sh.types[usize::from(this.type_index)].offset;
+            transitions.push(this.to_jiff(prev_offset, this_offset));
+        }
+        // TODO: We need to add back the POSIX time zone consistency check,
+        // and make this routine falllible.
+        Tzif {
+            name,
+            version,
+            checksum,
+            designations,
+            posix_tz,
+            types,
+            transitions,
+        }
+    }
+}
+
+impl<
+        STRING: AsRef<str>,
+        TYPES: AsRef<[LocalTimeType]>,
+        TRANS: AsRef<[Transition]>,
+    > Tzif<STRING, TYPES, TRANS>
+{
     /// Returns the name given to this TZif data in its constructor.
     pub(crate) fn name(&self) -> Option<&str> {
-        self.name.as_deref()
+        self.name.as_ref().map(|n| n.as_ref())
     }
 
     /// Returns the appropriate time zone offset to use for the given
@@ -148,12 +275,13 @@ impl Tzif {
         //
         // The result of the dummy transition is that the code below is simpler
         // with fewer special cases.
-        assert!(!self.transitions.is_empty(), "transitions is non-empty");
-        let index = if timestamp > self.transitions.last().unwrap().timestamp {
-            self.transitions.len() - 1
+        assert!(!self.transitions().is_empty(), "transitions is non-empty");
+        let index = if timestamp > self.transitions().last().unwrap().timestamp
+        {
+            self.transitions().len() - 1
         } else {
             let search = self
-                .transitions
+                .transitions()
                 // It is an optimization to compare only by the second instead
                 // of the second and the nanosecond. This works for two
                 // reasons. Firstly, the timestamps in TZif are limited to
@@ -183,16 +311,16 @@ impl Tzif {
         // binary search returns an Err(len) for a time greater than the
         // maximum transition. But we account for that above by converting
         // Err(len) to Err(len-1).
-        assert!(index < self.transitions.len());
+        assert!(index < self.transitions().len());
         // RFC 8536 says: "Local time for timestamps on or after the last
         // transition is specified by the TZ string in the footer (Section 3.3)
         // if present and nonempty; otherwise, it is unspecified."
         //
         // Subtracting 1 is OK because we know self.transitions is not empty.
-        let t = if index < self.transitions.len() - 1 {
+        let t = if index < self.transitions().len() - 1 {
             // This is the typical case in "fat" TZif files: we found a
             // matching transition.
-            &self.transitions[index]
+            &self.transitions()[index]
         } else {
             match self.posix_tz.as_ref() {
                 // This is the typical case in "slim" TZif files, where the
@@ -209,7 +337,7 @@ impl Tzif {
                 // This case is technically unspecified, but I think the
                 // typical thing to do is to just use the last transition.
                 // I'm not 100% sure on this one.
-                None => &self.transitions[index],
+                None => &self.transitions()[index],
             }
         };
         Ok(self.local_time_type(t))
@@ -233,17 +361,17 @@ impl Tzif {
         // of timestamps. And in particular, each transition begins with a
         // possibly ambiguous range of wall clock times corresponding to either
         // a "gap" or "fold" in time.
-        assert!(!self.transitions.is_empty(), "transitions is non-empty");
+        assert!(!self.transitions().is_empty(), "transitions is non-empty");
         let search =
-            self.transitions.binary_search_by_key(&dt, |t| t.wall.start());
+            self.transitions().binary_search_by_key(&dt, |t| t.wall.start());
         let this_index = match search {
             Err(0) => unreachable!("impossible to come before DateTime::MIN"),
             Ok(i) => i,
             Err(i) => i.checked_sub(1).expect("i is non-zero"),
         };
-        assert!(this_index < self.transitions.len());
+        assert!(this_index < self.transitions().len());
 
-        let this = &self.transitions[this_index];
+        let this = &self.transitions()[this_index];
         let this_offset = self.local_time_type(this).offset;
         // This is a little tricky, but we need to check for ambiguous civil
         // datetimes before possibly using the POSIX TZ string. Namely, a
@@ -256,7 +384,7 @@ impl Tzif {
                 // A gap/fold can only appear when there exists a previous
                 // transition.
                 let prev_index = this_index.checked_sub(1).unwrap();
-                let prev = &self.transitions[prev_index];
+                let prev = &self.transitions()[prev_index];
                 let prev_offset = self.local_time_type(prev).offset;
                 return AmbiguousOffset::Gap {
                     before: prev_offset,
@@ -267,7 +395,7 @@ impl Tzif {
                 // A gap/fold can only appear when there exists a previous
                 // transition.
                 let prev_index = this_index.checked_sub(1).unwrap();
-                let prev = &self.transitions[prev_index];
+                let prev = &self.transitions()[prev_index];
                 let prev_offset = self.local_time_type(prev).offset;
                 return AmbiguousOffset::Fold {
                     before: prev_offset,
@@ -280,7 +408,7 @@ impl Tzif {
         // transitions in the TZif data. But, if we matched at or after the
         // last transition, then we need to use the POSIX TZ string (which
         // could still return an ambiguous offset).
-        if this_index == self.transitions.len() - 1 {
+        if this_index == self.transitions().len() - 1 {
             if let Some(tz) = self.posix_tz.as_ref() {
                 return tz.to_ambiguous_kind(dt);
             }
@@ -302,9 +430,9 @@ impl Tzif {
         &self,
         ts: Timestamp,
     ) -> Option<TimeZoneTransition> {
-        assert!(!self.transitions.is_empty(), "transitions is non-empty");
+        assert!(!self.transitions().is_empty(), "transitions is non-empty");
         let search =
-            self.transitions.binary_search_by_key(&ts, |t| t.timestamp);
+            self.transitions().binary_search_by_key(&ts, |t| t.timestamp);
         let index = match search {
             Ok(i) | Err(i) => i.checked_sub(1)?,
         };
@@ -312,7 +440,7 @@ impl Tzif {
             // The first transition is a dummy that we insert, so if we land on
             // it here, treat it as if it doesn't exist.
             return None;
-        } else if index == self.transitions.len() - 1 {
+        } else if index == self.transitions().len() - 1 {
             if let Some(ref posix_tz) = self.posix_tz {
                 // Since the POSIX TZ must be consistent with the last
                 // transition, it must be the case that tzif_last <=
@@ -324,11 +452,11 @@ impl Tzif {
                 // of the TZif format if it does.
                 return posix_tz.previous_transition(ts);
             }
-            &self.transitions[index]
+            &self.transitions()[index]
         } else {
-            &self.transitions[index]
+            &self.transitions()[index]
         };
-        let typ = &self.types[usize::from(trans.type_index)];
+        let typ = &self.types()[usize::from(trans.type_index)];
         Some(TimeZoneTransition {
             timestamp: trans.timestamp,
             offset: typ.offset,
@@ -343,9 +471,9 @@ impl Tzif {
         &self,
         ts: Timestamp,
     ) -> Option<TimeZoneTransition> {
-        assert!(!self.transitions.is_empty(), "transitions is non-empty");
+        assert!(!self.transitions().is_empty(), "transitions is non-empty");
         let search =
-            self.transitions.binary_search_by_key(&ts, |t| t.timestamp);
+            self.transitions().binary_search_by_key(&ts, |t| t.timestamp);
         let index = match search {
             Ok(i) => i.checked_add(1)?,
             Err(i) => i,
@@ -354,7 +482,7 @@ impl Tzif {
             // The first transition is a dummy that we insert, so if we land on
             // it here, treat it as if it doesn't exist.
             return None;
-        } else if index >= self.transitions.len() - 1 {
+        } else if index >= self.transitions().len() - 1 {
             if let Some(ref posix_tz) = self.posix_tz {
                 // Since the POSIX TZ must be consistent with the last
                 // transition, it must be the case that next.timestamp <=
@@ -366,11 +494,11 @@ impl Tzif {
                 // of the TZif format if it does.
                 return posix_tz.next_transition(ts);
             }
-            self.transitions.last().expect("last transition")
+            self.transitions().last().expect("last transition")
         } else {
-            &self.transitions[index]
+            &self.transitions()[index]
         };
-        let typ = &self.types[usize::from(trans.type_index)];
+        let typ = &self.types()[usize::from(trans.type_index)];
         Some(TimeZoneTransition {
             timestamp: trans.timestamp,
             offset: typ.offset,
@@ -382,540 +510,44 @@ impl Tzif {
     fn designation(&self, typ: &LocalTimeType) -> &str {
         // OK because we verify that the designation range on every local
         // time type is a valid range into `self.designations`.
-        &self.designations[typ.designation()]
+        &self.designations()[typ.designation()]
     }
 
     fn local_time_type(&self, transition: &Transition) -> &LocalTimeType {
         // OK because we require that `type_index` always points to a valid
         // local time type.
-        &self.types[usize::from(transition.type_index)]
-    }
-
-    fn first_transition(&self) -> &Transition {
-        // OK because we know we have at least one transition. This isn't
-        // true generally of the TZif format, since it does actually permit 0
-        // transitions. But as part of parsing, we always add a "dummy" first
-        // transition corresponding to the minimum possible Jiff timestamp.
-        // This makes some logic for transition lookups a little simpler by
-        // reducing special cases.
-        self.transitions.first().unwrap()
-    }
-
-    fn parse32<'b>(
-        name: Option<String>,
-        header32: Header,
-        bytes: &'b [u8],
-    ) -> Result<(Tzif, &'b [u8]), Error> {
-        let mut tzif = Tzif {
-            name,
-            version: header32.version,
-            // filled in later
-            checksum: 0,
-            transitions: vec![],
-            types: vec![],
-            designations: String::new(),
-            leap_seconds: vec![],
-            posix_tz: None,
-        };
-        let rest = tzif.parse_transitions(&header32, bytes)?;
-        let rest = tzif.parse_transition_types(&header32, rest)?;
-        let rest = tzif.parse_local_time_types(&header32, rest)?;
-        let rest = tzif.parse_time_zone_designations(&header32, rest)?;
-        let rest = tzif.parse_leap_seconds(&header32, rest)?;
-        let rest = tzif.parse_indicators(&header32, rest)?;
-        tzif.set_wall_datetimes();
-        Ok((tzif, rest))
-    }
-
-    fn parse64<'b>(
-        name: Option<String>,
-        header32: Header,
-        bytes: &'b [u8],
-    ) -> Result<(Tzif, &'b [u8]), Error> {
-        let (_, rest) = try_split_at(
-            "V1 TZif data block",
-            bytes,
-            header32.data_block_len()?,
-        )?;
-        let (header64, rest) = Header::parse(8, rest)
-            .map_err(|e| e.context("failed to parse 64-bit header"))?;
-        let mut tzif = Tzif {
-            name,
-            version: header64.version,
-            // filled in later
-            checksum: 0,
-            transitions: vec![],
-            types: vec![],
-            designations: String::new(),
-            leap_seconds: vec![],
-            posix_tz: None,
-        };
-        let rest = tzif.parse_transitions(&header64, rest)?;
-        let rest = tzif.parse_transition_types(&header64, rest)?;
-        let rest = tzif.parse_local_time_types(&header64, rest)?;
-        let rest = tzif.parse_time_zone_designations(&header64, rest)?;
-        let rest = tzif.parse_leap_seconds(&header64, rest)?;
-        let rest = tzif.parse_indicators(&header64, rest)?;
-        let rest = tzif.parse_footer(&header64, rest)?;
-        // Validates that the POSIX TZ string we parsed (if one exists) is
-        // consistent with the last transition in this time zone. This is
-        // required by RFC 8536.
-        //
-        // RFC 8536 says, "If the string is nonempty and one or more
-        // transitions appear in the version 2+ data, the string MUST be
-        // consistent with the last version 2+ transition."
-        //
-        // We need to be a little careful, since we always have at least one
-        // transition (accounting for the dummy `Timestamp::MIN` transition).
-        // So if we only have 1 transition and a POSIX TZ string, then we
-        // should not validate it since it's equivalent to the case of 0
-        // transitions and a POSIX TZ string.
-        if tzif.transitions.len() > 1 {
-            if let Some(ref tz) = tzif.posix_tz {
-                let last = tzif.transitions.last().expect("last transition");
-                let typ = tzif.local_time_type(last);
-                let info = tz.to_offset_info(last.timestamp);
-                if info.offset() != typ.offset {
-                    return Err(err!(
-                        "expected last transition to have DST offset \
-                         of {}, but got {} according to POSIX TZ \
-                         string {}",
-                        typ.offset,
-                        info.offset(),
-                        tz,
-                    ));
-                }
-                if info.dst() != typ.is_dst {
-                    return Err(err!(
-                        "expected last transition to have is_dst={}, \
-                         but got is_dst={} according to POSIX TZ \
-                         string {}",
-                        typ.is_dst.is_dst(),
-                        info.dst().is_dst(),
-                        tz,
-                    ));
-                }
-                if info.abbreviation() != tzif.designation(&typ) {
-                    return Err(err!(
-                        "expected last transition to have \
-                         designation={}, \
-                         but got designation={} according to POSIX TZ \
-                         string {}",
-                        info.abbreviation(),
-                        tzif.designation(&typ),
-                        tz,
-                    ));
-                }
-            }
-        }
-        tzif.set_wall_datetimes();
-        // N.B. We don't check that the TZif data is fully valid. It
-        // is possible for it to contain superfluous information. For
-        // example, a non-zero local time type that is never referenced
-        // by a transition.
-        Ok((tzif, rest))
-    }
-
-    fn parse_transitions<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (bytes, rest) = try_split_at(
-            "transition times data block",
-            bytes,
-            header.transition_times_len()?,
-        )?;
-        let mut it = bytes.chunks_exact(header.time_size);
-        // RFC 8536 says: "If there are no transitions, local time for all
-        // timestamps is specified by the TZ string in the footer if present
-        // and nonempty; otherwise, it is specified by time type 0."
-        //
-        // RFC 8536 also says: "Local time for timestamps before the first
-        // transition is specified by the first time type (time type
-        // 0)."
-        //
-        // So if there are no transitions, pushing this dummy one will result
-        // in the desired behavior even when it's the only transition.
-        // Similarly, since this is the minimum timestamp value, it will
-        // trigger for any times before the first transition found in the TZif
-        // data.
-        self.transitions.push(Transition {
-            timestamp: Timestamp::MIN,
-            wall: TransitionWall::Unambiguous { start: DateTime::MIN },
-            type_index: 0,
-        });
-        while let Some(chunk) = it.next() {
-            let seconds = if header.is_32bit() {
-                i64::from(from_be_bytes_i32(chunk))
-            } else {
-                from_be_bytes_i64(chunk)
-            };
-            let timestamp =
-                Timestamp::from_second(seconds).unwrap_or_else(|_| {
-                    // We really shouldn't error here just because the Unix
-                    // timestamp is outside what Jiff supports. Since what Jiff
-                    // supports is _somewhat_ arbitrary. But Jiff's supported
-                    // range is good enough for all realistic purposes, so we
-                    // just clamp an out-of-range Unix timestamp to the Jiff
-                    // min or max value.
-                    //
-                    // This can't result in the sorting order being wrong, but
-                    // it can result in a transition that is duplicative with
-                    // the dummy transition we inserted above. This should be
-                    // fine.
-                    let clamped = seconds
-                        .clamp(UnixSeconds::MIN_REPR, UnixSeconds::MAX_REPR);
-                    warn!(
-                        "found Unix timestamp {seconds} that is outside \
-                         Jiff's supported range, clamping to {clamped}",
-                    );
-                    // Guaranteed to succeed since we clamped `seconds` such
-                    // that it is in the supported range of `Timestamp`.
-                    Timestamp::from_second(clamped).unwrap()
-                });
-            self.transitions.push(Transition {
-                timestamp,
-                // We can't compute the wall clock times until we know the
-                // actual offset for the transition prior to this one. We don't
-                // know that until we parse the local time types.
-                wall: TransitionWall::Unambiguous {
-                    start: DateTime::default(),
-                },
-                // We can't fill in the type index either. We fill this in
-                // later when we parse the transition types.
-                type_index: 0,
-            });
-        }
-        assert!(it.remainder().is_empty());
-        Ok(rest)
+        &self.types()[usize::from(transition.type_index)]
     }
 
-    fn parse_transition_types<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (bytes, rest) = try_split_at(
-            "transition types data block",
-            bytes,
-            header.transition_types_len()?,
-        )?;
-        // We start our transition indices at 1 because we always insert a
-        // dummy first transition corresponding to `Timestamp::MIN`. Its type
-        // index is always 0, so there's no need to change it here.
-        for (transition_index, &type_index) in (1..).zip(bytes) {
-            if usize::from(type_index) >= header.tzh_typecnt {
-                return Err(err!(
-                    "found transition type index {type_index},
-                     but there are only {} local time types",
-                    header.tzh_typecnt,
-                ));
-            }
-            self.transitions[transition_index].type_index = type_index;
-        }
-        Ok(rest)
+    fn designations(&self) -> &str {
+        self.designations.as_ref()
     }
 
-    fn parse_local_time_types<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (bytes, rest) = try_split_at(
-            "local time types data block",
-            bytes,
-            header.local_time_types_len()?,
-        )?;
-        let mut it = bytes.chunks_exact(6);
-        while let Some(chunk) = it.next() {
-            let offset_seconds = from_be_bytes_i32(&chunk[..4]);
-            let offset =
-                Offset::from_seconds(offset_seconds).map_err(|e| {
-                    err!(
-                        "found local time type with out-of-bounds offset: {e}"
-                    )
-                })?;
-            let is_dst = Dst::from(chunk[4] == 1);
-            let designation = chunk[5]..chunk[5];
-            self.types.push(LocalTimeType {
-                offset,
-                is_dst,
-                designation,
-                indicator: Indicator::LocalWall,
-            });
-        }
-        assert!(it.remainder().is_empty());
-        Ok(rest)
+    fn types(&self) -> &[LocalTimeType] {
+        self.types.as_ref()
     }
 
-    fn parse_time_zone_designations<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (bytes, rest) = try_split_at(
-            "time zone designations data block",
-            bytes,
-            header.time_zone_designations_len()?,
-        )?;
-        self.designations =
-            String::from_utf8(bytes.to_vec()).map_err(|_| {
-                err!(
-                    "time zone designations are not valid UTF-8: {:?}",
-                    Bytes(bytes),
-                )
-            })?;
-        // Holy hell, this is brutal. The boundary conditions are crazy.
-        for (i, typ) in self.types.iter_mut().enumerate() {
-            let start = usize::from(typ.designation.start);
-            let Some(suffix) = self.designations.get(start..) else {
-                return Err(err!(
-                    "local time type {i} has designation index of {start}, \
-                     but cannot be more than {}",
-                    self.designations.len(),
-                ));
-            };
-            let Some(len) = suffix.find('\x00') else {
-                return Err(err!(
-                    "local time type {i} has designation index of {start}, \
-                     but could not find NUL terminator after it in \
-                     designations: {:?}",
-                    self.designations,
-                ));
-            };
-            let Some(end) = start.checked_add(len) else {
-                return Err(err!(
-                    "local time type {i} has designation index of {start}, \
-                     but its length {len} is too big",
-                ));
-            };
-            typ.designation.end = u8::try_from(end).map_err(|_| {
-                err!(
-                    "local time type {i} has designation range of \
-                     {start}..{end}, but end is too big",
-                )
-            })?;
-        }
-        Ok(rest)
-    }
-
-    fn parse_leap_seconds<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (bytes, rest) = try_split_at(
-            "leap seconds data block",
-            bytes,
-            header.leap_second_len()?,
-        )?;
-        let chunk_len = header
-            .time_size
-            .checked_add(4)
-            .expect("time_size plus 4 fits in usize");
-        let mut it = bytes.chunks_exact(chunk_len);
-        while let Some(chunk) = it.next() {
-            let (occur_bytes, corr_bytes) = chunk.split_at(header.time_size);
-            let occur_seconds = if header.is_32bit() {
-                i64::from(from_be_bytes_i32(occur_bytes))
-            } else {
-                from_be_bytes_i64(occur_bytes)
-            };
-            let occurrence =
-                Timestamp::from_second(occur_seconds).map_err(|e| {
-                    err!(
-                        "leap second occurrence {occur_seconds} \
-                         is out of range: {e}"
-                    )
-                })?;
-            let correction = from_be_bytes_i32(corr_bytes);
-            self.leap_seconds.push(LeapSecond { occurrence, correction });
-        }
-        assert!(it.remainder().is_empty());
-        Ok(rest)
-    }
-
-    fn parse_indicators<'b>(
-        &mut self,
-        header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        let (std_wall_bytes, rest) = try_split_at(
-            "standard/wall indicators data block",
-            bytes,
-            header.standard_wall_len()?,
-        )?;
-        let (ut_local_bytes, rest) = try_split_at(
-            "UT/local indicators data block",
-            rest,
-            header.ut_local_len()?,
-        )?;
-        if std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
-            // This is a weird case, but technically possible only if all
-            // UT/local indicators are 0. If any are 1, then it's an error,
-            // because it would require the corresponding std/wall indicator
-            // to be 1 too. Which it can't be, because there aren't any. So
-            // we just check that they're all zeros.
-            for (i, &byte) in ut_local_bytes.iter().enumerate() {
-                if byte != 0 {
-                    return Err(err!(
-                        "found UT/local indicator '{byte}' for local time \
-                         type {i}, but it must be 0 since all std/wall \
-                         indicators are 0",
-                    ));
-                }
-            }
-        } else if !std_wall_bytes.is_empty() && ut_local_bytes.is_empty() {
-            for (i, &byte) in std_wall_bytes.iter().enumerate() {
-                // Indexing is OK because Header guarantees that the number of
-                // indicators is 0 or equal to the number of types.
-                self.types[i].indicator = if byte == 0 {
-                    Indicator::LocalWall
-                } else if byte == 1 {
-                    Indicator::LocalStandard
-                } else {
-                    return Err(err!(
-                        "found invalid std/wall indicator '{byte}' for \
-                         local time type {i}, it must be 0 or 1",
-                    ));
-                };
-            }
-        } else if !std_wall_bytes.is_empty() && !ut_local_bytes.is_empty() {
-            assert_eq!(std_wall_bytes.len(), ut_local_bytes.len());
-            let it = std_wall_bytes.iter().zip(ut_local_bytes);
-            for (i, (&stdwall, &utlocal)) in it.enumerate() {
-                // Indexing is OK because Header guarantees that the number of
-                // indicators is 0 or equal to the number of types.
-                self.types[i].indicator = match (stdwall, utlocal) {
-                    (0, 0) => Indicator::LocalWall,
-                    (1, 0) => Indicator::LocalStandard,
-                    (1, 1) => Indicator::UTStandard,
-                    (0, 1) => {
-                        return Err(err!(
-                            "found illegal ut-wall combination for \
-                         local time type {i}, only local-wall, local-standard \
-                         and ut-standard are allowed",
-                        ))
-                    }
-                    _ => {
-                        return Err(err!(
-                            "found illegal std/wall or ut/local value for \
-                         local time type {i}, each must be 0 or 1",
-                        ))
-                    }
-                };
-            }
-        } else {
-            // If they're both empty then we don't need to do anything. Every
-            // local time type record already has the correct default for this
-            // case set.
-            debug_assert!(std_wall_bytes.is_empty());
-            debug_assert!(ut_local_bytes.is_empty());
-        }
-        Ok(rest)
-    }
-
-    fn parse_footer<'b>(
-        &mut self,
-        _header: &Header,
-        bytes: &'b [u8],
-    ) -> Result<&'b [u8], Error> {
-        if bytes.is_empty() {
-            return Err(err!(
-                "invalid V2+ TZif footer, expected \\n, \
-                 but found unexpected end of data",
-            ));
-        }
-        if bytes[0] != b'\n' {
-            return Err(err!(
-                "invalid V2+ TZif footer, expected {:?}, but found {:?}",
-                Byte(b'\n'),
-                Byte(bytes[0]),
-            ));
-        }
-        let bytes = &bytes[1..];
-        // Only scan up to 1KB for a NUL terminator in case we somehow got
-        // passed a huge block of bytes.
-        let toscan = &bytes[..bytes.len().min(1024)];
-        let Some(nlat) = toscan.iter().position(|&b| b == b'\n') else {
-            return Err(err!(
-                "invalid V2 TZif footer, could not find {:?} \
-                 terminator in: {:?}",
-                Byte(b'\n'),
-                Bytes(toscan),
-            ));
-        };
-        let (bytes, rest) = bytes.split_at(nlat);
-        if !bytes.is_empty() {
-            // We could in theory limit TZ strings to their strict POSIX
-            // definition here for TZif V2, but I don't think there is any
-            // harm in allowing the extensions in V2 formatted TZif data. Note
-            // that the GNU tooling allow it via the `TZ` environment variable
-            // even though POSIX doesn't specify it. This all seems okay to me
-            // because the V3+ extension is a strict superset of functionality.
-            let iana_tz = IanaTz::parse_v3plus(bytes)?;
-            self.posix_tz = Some(iana_tz.into_tz());
-        }
-        Ok(&rest[1..])
-    }
-
-    /// This sets the wall clock times for each transition.
-    ///
-    /// The wall clock time corresponds to time on the clock that the
-    /// transition begins. That is, it is the time offset by the previous
-    /// transition's offset.
-    ///
-    /// This also computes whether there is a gap or fold or neither between
-    /// each transition. This is used to resolve ambiguous timestamps when
-    /// given a civil datetime.
-    fn set_wall_datetimes(&mut self) {
-        let mut prev = self.local_time_type(self.first_transition()).offset;
-        // We iterate over indices instead of `transitions.iter_mut()` because
-        // of the borrow checker breaking composition.
-        for i in 0..self.transitions.len() {
-            let this = self.local_time_type(&self.transitions[i]).offset;
-            let t = &mut self.transitions[i];
-            t.wall = if prev == this {
-                // Equivalent offsets means there can never be any ambiguity.
-                let start = prev.to_datetime(t.timestamp);
-                TransitionWall::Unambiguous { start }
-            } else if prev < this {
-                // When the offset of the previous transition is less, that
-                // means there is some non-zero amount of time that is
-                // "skipped" when moving to the next transition. Thus, we have
-                // a gap. The start of the gap is the offset which gets us the
-                // earliest time, i.e., the smaller of the two offsets.
-                let start = prev.to_datetime(t.timestamp);
-                let end = this.to_datetime(t.timestamp);
-                TransitionWall::Gap { start, end }
-            } else {
-                // When the offset of the previous transition is greater, that
-                // means there is some non-zero amount of time that will be
-                // replayed on a wall clock in this time zone. Thus, we have
-                // a fold. The start of the gold is the offset which gets us
-                // the earliest time, i.e., the smaller of the two offsets.
-                assert!(prev > this);
-                let start = this.to_datetime(t.timestamp);
-                let end = prev.to_datetime(t.timestamp);
-                TransitionWall::Fold { start, end }
-            };
-            prev = this;
-        }
+    fn transitions(&self) -> &[Transition] {
+        self.transitions.as_ref()
     }
 }
 
-impl Eq for Tzif {}
+impl<STRING: AsRef<str>, TYPES, TRANS> Eq for Tzif<STRING, TYPES, TRANS> {}
 
-impl PartialEq for Tzif {
-    fn eq(&self, rhs: &Tzif) -> bool {
-        self.name == rhs.name && self.checksum == rhs.checksum
+impl<STRING: AsRef<str>, TYPES, TRANS> PartialEq
+    for Tzif<STRING, TYPES, TRANS>
+{
+    fn eq(&self, rhs: &Self) -> bool {
+        self.name.as_ref().map(|n| n.as_ref())
+            == rhs.name.as_ref().map(|n| n.as_ref())
+            && self.checksum == rhs.checksum
     }
 }
 
 /// A transition to a different offset.
 #[derive(Clone, Debug, Eq, PartialEq)]
-struct Transition {
+#[doc(hidden)] // not part of Jiff's public API
+pub struct Transition {
     /// The UNIX leap time at which the transition starts. The transition
     /// continues up to and _not_ including the next transition.
     timestamp: Timestamp,
@@ -929,6 +561,20 @@ struct Transition {
     type_index: u8,
 }
 
+impl Transition {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    pub(crate) const fn from_shared(
+        sh: &shared::TzifTransition,
+        prev_offset: i32,
+        this_offset: i32,
+    ) -> Transition {
+        let timestamp = Timestamp::constant(sh.timestamp, 0);
+        let wall = TransitionWall::new(sh.timestamp, prev_offset, this_offset);
+        let type_index = sh.type_index;
+        Transition { timestamp, wall, type_index }
+    }
+}
+
 /// The wall clock time for when a transition begins.
 ///
 /// This explicitly represents ambiguous wall clock times that occur at the
@@ -1019,6 +665,54 @@ enum TransitionWall {
 }
 
 impl TransitionWall {
+    /// Creates transition data based on wall-clock time.
+    ///
+    /// This data isn't directly part of TZif, but can be derived from it.
+    /// It is principally done so that TZ lookups for civil datetime are
+    /// faster. That is, we pre-compute whatever we can here.
+    ///
+    /// `timestamp` corresponds to the timestamp of the respective transition.
+    /// `this_offset` is the offset associated with that transition (via the
+    /// corresponding local time type), and `prev_offset` is the offset of the
+    /// previous transition (also through its corresponding local time type).
+    const fn new(
+        timestamp: i64,
+        prev_offset: i32,
+        this_offset: i32,
+    ) -> TransitionWall {
+        const fn to_datetime(timestamp: i64, offset: i32) -> DateTime {
+            use crate::util::common::timestamp_to_datetime_zulu;
+            let (y, mo, d, h, m, s, n) =
+                timestamp_to_datetime_zulu(timestamp, 0, offset);
+            DateTime::constant(y, mo, d, h, m, s, n)
+        }
+
+        if prev_offset == this_offset {
+            // Equivalent offsets means there can never be any ambiguity.
+            let start = to_datetime(timestamp, prev_offset);
+            TransitionWall::Unambiguous { start }
+        } else if prev_offset < this_offset {
+            // When the offset of the previous transition is less, that
+            // means there is some non-zero amount of time that is
+            // "skipped" when moving to the next transition. Thus, we have
+            // a gap. The start of the gap is the offset which gets us the
+            // earliest time, i.e., the smaller of the two offsets.
+            let start = to_datetime(timestamp, prev_offset);
+            let end = to_datetime(timestamp, this_offset);
+            TransitionWall::Gap { start, end }
+        } else {
+            // When the offset of the previous transition is greater, that
+            // means there is some non-zero amount of time that will be
+            // replayed on a wall clock in this time zone. Thus, we have
+            // a fold. The start of the gold is the offset which gets us
+            // the earliest time, i.e., the smaller of the two offsets.
+            assert!(prev_offset > this_offset);
+            let start = to_datetime(timestamp, this_offset);
+            let end = to_datetime(timestamp, prev_offset);
+            TransitionWall::Fold { start, end }
+        }
+    }
+
     fn start(&self) -> DateTime {
         match *self {
             TransitionWall::Unambiguous { start } => start,
@@ -1035,7 +729,8 @@ impl TransitionWall {
 /// abbreviation. (There is also an "indicator," but I have no clue what it
 /// means. See the `Indicator` type for a rant.)
 #[derive(Clone, Debug, Eq, PartialEq)]
-struct LocalTimeType {
+#[doc(hidden)] // not part of Jiff's public API
+pub struct LocalTimeType {
     offset: Offset,
     is_dst: Dst,
     designation: Range<u8>,
@@ -1043,6 +738,17 @@ struct LocalTimeType {
 }
 
 impl LocalTimeType {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    pub(crate) const fn from_shared(
+        sh: &shared::TzifLocalTimeType,
+    ) -> LocalTimeType {
+        let offset = Offset::constant_seconds(sh.offset);
+        let is_dst = if sh.is_dst { Dst::Yes } else { Dst::No };
+        let designation = sh.designation.0..sh.designation.1;
+        let indicator = Indicator::from_shared(&sh.indicator);
+        LocalTimeType { offset, is_dst, designation, indicator }
+    }
+
     fn designation(&self) -> Range<usize> {
         usize::from(self.designation.start)..usize::from(self.designation.end)
     }
@@ -1113,6 +819,17 @@ enum Indicator {
     UTStandard,
 }
 
+impl Indicator {
+    /// Converts from the shared-but-internal API for use in proc macros.
+    pub(crate) const fn from_shared(sh: &shared::TzifIndicator) -> Indicator {
+        match *sh {
+            shared::TzifIndicator::LocalWall => Indicator::LocalWall,
+            shared::TzifIndicator::LocalStandard => Indicator::LocalStandard,
+            shared::TzifIndicator::UTStandard => Indicator::UTStandard,
+        }
+    }
+}
+
 impl core::fmt::Display for Indicator {
     fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
         match *self {
@@ -1123,219 +840,6 @@ impl core::fmt::Display for Indicator {
     }
 }
 
-/// A leap second "correction" record.
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct LeapSecond {
-    /// The Unix leap time at which the leap second occurred.
-    occurrence: Timestamp,
-    /// The leap second offset. Usually +1 or -1.
-    correction: i32,
-}
-
-/// The header for a TZif formatted file.
-///
-/// V2+ TZif format have two headers: one for V1 data, and then a second
-/// following the V1 data block that describes another data block which uses
-/// 64-bit timestamps. The two headers both have the same format and both
-/// use 32-bit big-endian encoded integers.
-#[derive(Debug)]
-struct Header {
-    /// The size of the timestamps encoded in the data block.
-    ///
-    /// This is guaranteed to be either 4 (for V1) or 8 (for the 64-bit header
-    /// block in V2+).
-    time_size: usize,
-    /// The file format version.
-    ///
-    /// Note that this is either a NUL byte (for version 1), or an ASCII byte
-    /// corresponding to the version number. That is, `0x32` for `2`, `0x33`
-    /// for `3` or `0x34` for `4`. Note also that just because zoneinfo might
-    /// have been recently generated does not mean it uses the latest format
-    /// version. It seems like newer versions are only compiled by `zic` when
-    /// they are needed. For example, `America/New_York` on my system (as of
-    /// `2024-03-25`) has version `0x32`, but `Asia/Jerusalem` has version
-    /// `0x33`.
-    version: u8,
-    /// Number of UT/local indicators stored in the file.
-    ///
-    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
-    tzh_ttisutcnt: usize,
-    /// The number of standard/wall indicators stored in the file.
-    ///
-    /// This is checked to be either equal to `0` or equal to `tzh_typecnt`.
-    tzh_ttisstdcnt: usize,
-    /// The number of leap seconds for which data entries are stored in the
-    /// file.
-    tzh_leapcnt: usize,
-    /// The number of transition times for which data entries are stored in
-    /// the file.
-    tzh_timecnt: usize,
-    /// The number of local time types for which data entries are stored in the
-    /// file.
-    ///
-    /// This is checked to be at least `1`.
-    tzh_typecnt: usize,
-    /// The number of bytes of time zone abbreviation strings stored in the
-    /// file.
-    ///
-    /// This is checked to be at least `1`.
-    tzh_charcnt: usize,
-}
-
-impl Header {
-    /// Parse the header record from the given bytes.
-    ///
-    /// Upon success, return the header and all bytes after the header.
-    ///
-    /// The given `time_size` must be 4 or 8, corresponding to either the
-    /// V1 header block or the V2+ header block, respectively.
-    fn parse(
-        time_size: usize,
-        bytes: &[u8],
-    ) -> Result<(Header, &[u8]), Error> {
-        assert!(time_size == 4 || time_size == 8, "time size must be 4 or 8");
-        if bytes.len() < 44 {
-            return Err(err!("invalid header: too short"));
-        }
-        let (magic, rest) = bytes.split_at(4);
-        if magic != b"TZif" {
-            return Err(err!("invalid header: magic bytes mismatch"));
-        }
-        let (version, rest) = rest.split_at(1);
-        let (_reserved, rest) = rest.split_at(15);
-
-        let (tzh_ttisutcnt_bytes, rest) = rest.split_at(4);
-        let (tzh_ttisstdcnt_bytes, rest) = rest.split_at(4);
-        let (tzh_leapcnt_bytes, rest) = rest.split_at(4);
-        let (tzh_timecnt_bytes, rest) = rest.split_at(4);
-        let (tzh_typecnt_bytes, rest) = rest.split_at(4);
-        let (tzh_charcnt_bytes, rest) = rest.split_at(4);
-
-        let tzh_ttisutcnt = from_be_bytes_u32_to_usize(tzh_ttisutcnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_ttisutcnt"))?;
-        let tzh_ttisstdcnt = from_be_bytes_u32_to_usize(tzh_ttisstdcnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_ttisstdcnt"))?;
-        let tzh_leapcnt = from_be_bytes_u32_to_usize(tzh_leapcnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_leapcnt"))?;
-        let tzh_timecnt = from_be_bytes_u32_to_usize(tzh_timecnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_timecnt"))?;
-        let tzh_typecnt = from_be_bytes_u32_to_usize(tzh_typecnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_typecnt"))?;
-        let tzh_charcnt = from_be_bytes_u32_to_usize(tzh_charcnt_bytes)
-            .map_err(|e| e.context("failed to parse tzh_charcnt"))?;
-
-        if tzh_ttisutcnt != 0 && tzh_ttisutcnt != tzh_typecnt {
-            return Err(err!(
-                "expected tzh_ttisutcnt={tzh_ttisutcnt} to be zero \
-                 or equal to tzh_typecnt={tzh_typecnt}",
-            ));
-        }
-        if tzh_ttisstdcnt != 0 && tzh_ttisstdcnt != tzh_typecnt {
-            return Err(err!(
-                "expected tzh_ttisstdcnt={tzh_ttisstdcnt} to be zero \
-                 or equal to tzh_typecnt={tzh_typecnt}",
-            ));
-        }
-        if tzh_typecnt < 1 {
-            return Err(err!(
-                "expected tzh_typecnt={tzh_typecnt} to be at least 1",
-            ));
-        }
-        if tzh_charcnt < 1 {
-            return Err(err!(
-                "expected tzh_charcnt={tzh_charcnt} to be at least 1",
-            ));
-        }
-
-        let header = Header {
-            time_size,
-            version: version[0],
-            tzh_ttisutcnt,
-            tzh_ttisstdcnt,
-            tzh_leapcnt,
-            tzh_timecnt,
-            tzh_typecnt,
-            tzh_charcnt,
-        };
-        Ok((header, rest))
-    }
-
-    /// Returns true if this header is for a 32-bit data block.
-    ///
-    /// When false, it is guaranteed that this header is for a 64-bit data
-    /// block.
-    fn is_32bit(&self) -> bool {
-        self.time_size == 4
-    }
-
-    /// Returns the size of the data block, in bytes, for this header.
-    ///
-    /// This returns an error if the arithmetic required to compute the
-    /// length would overflow.
-    ///
-    /// This is useful for, e.g., skipping over the 32-bit V1 data block in
-    /// V2+ TZif formatted files.
-    fn data_block_len(&self) -> Result<usize, Error> {
-        let a = self.transition_times_len()?;
-        let b = self.transition_types_len()?;
-        let c = self.local_time_types_len()?;
-        let d = self.time_zone_designations_len()?;
-        let e = self.leap_second_len()?;
-        let f = self.standard_wall_len()?;
-        let g = self.ut_local_len()?;
-        a.checked_add(b)
-            .and_then(|z| z.checked_add(c))
-            .and_then(|z| z.checked_add(d))
-            .and_then(|z| z.checked_add(e))
-            .and_then(|z| z.checked_add(f))
-            .and_then(|z| z.checked_add(g))
-            .ok_or_else(|| {
-                err!(
-                    "length of data block in V{} tzfile is too big",
-                    self.version
-                )
-            })
-    }
-
-    fn transition_times_len(&self) -> Result<usize, Error> {
-        self.tzh_timecnt.checked_mul(self.time_size).ok_or_else(|| {
-            err!("tzh_timecnt value {} is too big", self.tzh_timecnt)
-        })
-    }
-
-    fn transition_types_len(&self) -> Result<usize, Error> {
-        Ok(self.tzh_timecnt)
-    }
-
-    fn local_time_types_len(&self) -> Result<usize, Error> {
-        self.tzh_typecnt.checked_mul(6).ok_or_else(|| {
-            err!("tzh_typecnt value {} is too big", self.tzh_typecnt)
-        })
-    }
-
-    fn time_zone_designations_len(&self) -> Result<usize, Error> {
-        Ok(self.tzh_charcnt)
-    }
-
-    fn leap_second_len(&self) -> Result<usize, Error> {
-        let record_len = self
-            .time_size
-            .checked_add(4)
-            .expect("4-or-8 plus 4 always fits in usize");
-        self.tzh_leapcnt.checked_mul(record_len).ok_or_else(|| {
-            err!("tzh_leapcnt value {} is too big", self.tzh_leapcnt)
-        })
-    }
-
-    fn standard_wall_len(&self) -> Result<usize, Error> {
-        Ok(self.tzh_ttisstdcnt)
-    }
-
-    fn ut_local_len(&self) -> Result<usize, Error> {
-        Ok(self.tzh_ttisutcnt)
-    }
-}
-
 /// Does a quick check that returns true if the data might be in TZif format.
 ///
 /// It is possible that this returns true even if the given data is not in TZif
@@ -1347,82 +851,11 @@ pub(crate) fn is_possibly_tzif(data: &[u8]) -> bool {
     data.starts_with(b"TZif")
 }
 
-/// Interprets the given slice as an unsigned 32-bit big endian integer,
-/// attempts to convert it to a `usize` and returns it.
-///
-/// # Panics
-///
-/// When `bytes.len() != 4`.
-///
-/// # Errors
-///
-/// This errors if the `u32` parsed from the given bytes cannot fit in a
-/// `usize`.
-fn from_be_bytes_u32_to_usize(bytes: &[u8]) -> Result<usize, Error> {
-    let n = from_be_bytes_u32(bytes);
-    usize::try_from(n).map_err(|_| {
-        err!(
-            "failed to parse integer {n} (too big, max allowed is {}",
-            usize::MAX
-        )
-    })
-}
-
-/// Interprets the given slice as an unsigned 32-bit big endian integer and
-/// returns it.
-///
-/// # Panics
-///
-/// When `bytes.len() != 4`.
-fn from_be_bytes_u32(bytes: &[u8]) -> u32 {
-    u32::from_be_bytes(bytes.try_into().unwrap())
-}
-
-/// Interprets the given slice as a signed 32-bit big endian integer and
-/// returns it.
-///
-/// # Panics
-///
-/// When `bytes.len() != 4`.
-fn from_be_bytes_i32(bytes: &[u8]) -> i32 {
-    i32::from_be_bytes(bytes.try_into().unwrap())
-}
-
-/// Interprets the given slice as a signed 64-bit big endian integer and
-/// returns it.
-///
-/// # Panics
-///
-/// When `bytes.len() != 8`.
-fn from_be_bytes_i64(bytes: &[u8]) -> i64 {
-    i64::from_be_bytes(bytes.try_into().unwrap())
-}
-
-/// Splits the given slice of bytes at the index given.
-///
-/// If the index is out of range (greater than `bytes.len()`) then an error is
-/// returned. The error message will include the `what` string given, which is
-/// meant to describe the thing being split.
-fn try_split_at<'b>(
-    what: &'static str,
-    bytes: &'b [u8],
-    at: usize,
-) -> Result<(&'b [u8], &'b [u8]), Error> {
-    if at > bytes.len() {
-        Err(err!(
-            "expected at least {at} bytes for {what}, \
-             but found only {} bytes",
-            bytes.len(),
-        ))
-    } else {
-        Ok(bytes.split_at(at))
-    }
-}
-
-#[cfg(test)]
+#[cfg(all(test, feature = "alloc"))]
 mod tests {
-    use alloc::string::ToString;
+    use alloc::{string::ToString, vec};
 
+    #[cfg(not(miri))]
     use crate::tz::testdata::TZIF_TEST_FILES;
 
     use super::*;
@@ -1436,7 +869,7 @@ mod tests {
     ///
     /// For this to work, we make sure everything in a `Tzif` value is
     /// represented in some way in this output.
-    fn tzif_to_human_readable(tzif: &Tzif) -> String {
+    fn tzif_to_human_readable(tzif: &TzifOwned) -> String {
         use std::io::Write;
 
         let mut out = tabwriter::TabWriter::new(vec![])
@@ -1494,14 +927,6 @@ mod tests {
                 .unwrap();
             }
         }
-        if !tzif.leap_seconds.is_empty() {
-            writeln!(out, "LEAP SECONDS").unwrap();
-            for ls in tzif.leap_seconds.iter() {
-                let dt = Offset::UTC.to_datetime(ls.occurrence);
-                let c = ls.correction;
-                writeln!(out, "  {dt:?}\tcorrection={c}").unwrap();
-            }
-        }
         if let Some(ref posix_tz) = tzif.posix_tz {
             writeln!(out, "POSIX TIME ZONE STRING").unwrap();
             writeln!(out, "  {}", posix_tz).unwrap();
@@ -1534,6 +959,7 @@ mod tests {
         Ok(())
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn tzif_parse_v2plus() {
         for tzif_test in TZIF_TEST_FILES {
@@ -1544,6 +970,7 @@ mod tests {
         }
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn tzif_parse_v1() {
         for tzif_test in TZIF_TEST_FILES {
@@ -1559,6 +986,7 @@ mod tests {
     /// do much with it other than to ensure we don't panic or return an error.
     /// That is, we check that we can parse each file, but not that we do so
     /// correctly.
+    #[cfg(not(miri))]
     #[cfg(feature = "tzdb-zoneinfo")]
     #[cfg(target_os = "linux")]
     #[test]
diff --git a/src/tz/zic.rs b/src/tz/zic.rs
index 2da27db2..090c3418 100644
--- a/src/tz/zic.rs
+++ b/src/tz/zic.rs
@@ -1592,6 +1592,7 @@ mod tests {
         assert_eq!(wall, date(2024, 3, 10).at(2, 0, 0, 0));
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn parse_zic_man1() {
         // An example from `man zic`.
@@ -1608,6 +1609,7 @@ mod tests {
         insta::assert_debug_snapshot!(zic);
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn parse_zic_man2() {
         // An example from `man zic`.
@@ -1690,6 +1692,7 @@ mod tests {
         assert!(ZicP::default().parse(data).is_err());
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn parse_rule_ok() {
         let rule: RuleP = RuleP::parse(&[
@@ -1749,6 +1752,7 @@ mod tests {
         .is_err());
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn parse_zone_first_ok() {
         let zone: ZoneFirstP =
@@ -1830,6 +1834,7 @@ mod tests {
         .is_err());
     }
 
+    #[cfg(not(miri))]
     #[test]
     fn parse_zone_continuation_ok() {
         let zone: ZoneContinuationP =
diff --git a/src/util/array_str.rs b/src/util/array_str.rs
index fab7be6a..d564a165 100644
--- a/src/util/array_str.rs
+++ b/src/util/array_str.rs
@@ -28,27 +28,22 @@ impl<const N: usize> ArrayStr<N> {
     ///
     /// If the given string exceeds `N` bytes, then this returns
     /// `None`.
-    pub(crate) fn new(s: &str) -> Option<ArrayStr<N>> {
+    pub(crate) const fn new(s: &str) -> Option<ArrayStr<N>> {
         let len = s.len();
         if len > N {
             return None;
         }
         let mut bytes = [0; N];
-        bytes[..len].copy_from_slice(s.as_bytes());
+        let mut i = 0;
+        while i < s.as_bytes().len() {
+            bytes[i] = s.as_bytes()[i];
+            i += 1;
+        }
         // OK because we don't ever use anything bigger than u8::MAX for `N`.
         // And we probably shouldn't, because that would be a pretty chunky
         // array. If such a thing is needed, please file an issue to discuss.
-        debug_assert!(
-            N <= usize::from(u8::MAX),
-            "size of ArrayStr is too big"
-        );
-        let len = u8::try_from(len).unwrap();
-        Some(ArrayStr { bytes, len })
-    }
-
-    /// Returns the capacity of this fixed string.
-    pub(crate) const fn capacity() -> usize {
-        N
+        debug_assert!(N <= u8::MAX as usize, "size of ArrayStr is too big");
+        Some(ArrayStr { bytes, len: len as u8 })
     }
 
     /// Append the bytes given to the end of this string.
diff --git a/src/util/common.rs b/src/util/common.rs
index 3c130f1f..60693c66 100644
--- a/src/util/common.rs
+++ b/src/util/common.rs
@@ -55,7 +55,7 @@ pub(crate) const fn days_in_month(year: i16, month: i8) -> i8 {
 /// Ref: https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L83
 #[inline(always)]
 #[allow(non_upper_case_globals, non_snake_case)]
-pub(crate) fn to_unix_epoch_day(year: i16, month: i8, day: i8) -> i32 {
+pub(crate) const fn to_unix_epoch_day(year: i16, month: i8, day: i8) -> i32 {
     const s: u32 = 82;
     const K: u32 = 719468 + 146097 * s;
     const L: u32 = 400 * s;
@@ -85,7 +85,7 @@ pub(crate) fn to_unix_epoch_day(year: i16, month: i8, day: i8) -> i32 {
 /// Ref: <https://github.com/cassioneri/eaf/blob/684d3cc32d14eee371d0abe4f683d6d6a49ed5c1/algorithms/neri_schneider.hpp#L40C3-L40C34>
 #[inline(always)]
 #[allow(non_upper_case_globals, non_snake_case)]
-pub(crate) fn from_unix_epoch_day(days: i32) -> (i16, i8, i8) {
+pub(crate) const fn from_unix_epoch_day(days: i32) -> (i16, i8, i8) {
     const s: u32 = 82;
     const K: u32 = 719468 + 146097 * s;
     const L: u32 = 400 * s;
@@ -98,7 +98,7 @@ pub(crate) fn from_unix_epoch_day(days: i32) -> (i16, i8, i8) {
     let N_C = (N_1 % 146097) / 4;
 
     let N_2 = 4 * N_C + 3;
-    let P_2 = 2939745 * u64::from(N_2);
+    let P_2 = 2939745 * (N_2 as u64);
     let Z = (P_2 / 4294967296) as u32;
     let N_Y = (P_2 % 4294967296) as u32 / 2939745 / 4;
     let Y = 100 * C + Z;
@@ -114,6 +114,31 @@ pub(crate) fn from_unix_epoch_day(days: i32) -> (i16, i8, i8) {
     (year, month, day)
 }
 
+/// Converts `HH:MM:SS` to a second in a single civil day.
+#[inline(always)]
+pub(crate) fn to_day_second(hour: i8, minute: i8, second: i8) -> i32 {
+    let mut seconds: i32 = 0;
+    seconds += i32::from(hour) * (t::SECONDS_PER_HOUR.value() as i32);
+    seconds += i32::from(minute) * (t::SECONDS_PER_MINUTE.value() as i32);
+    seconds += i32::from(second);
+    seconds
+}
+
+/// Converts a second in a single civil day to `HH:MM::SS`.
+#[inline(always)]
+pub(crate) fn from_day_second(mut seconds: i32) -> (i8, i8, i8) {
+    let (mut hour, mut minute, mut second) = (0, 0, 0);
+    if seconds != 0 {
+        hour = (seconds / t::SECONDS_PER_HOUR.value() as i32) as i8;
+        seconds %= t::SECONDS_PER_HOUR.value() as i32;
+        if seconds != 0 {
+            minute = (seconds / t::SECONDS_PER_MINUTE.value() as i32) as i8;
+            second = (seconds % t::SECONDS_PER_MINUTE.value() as i32) as i8;
+        }
+    }
+    (hour, minute, second)
+}
+
 /// Converts `HH:MM:SS.nnnnnnnnn` to a nanosecond in a single civil day.
 #[inline(always)]
 pub(crate) fn to_day_nanosecond(
@@ -151,12 +176,12 @@ pub(crate) fn from_day_nanosecond(mut nanos: i64) -> (i8, i8, i8, i32) {
 
 /// Converts a Unix timestamp with an offset to a Gregorian datetime.
 #[inline(always)]
-pub(crate) fn timestamp_to_datetime_zulu(
+pub(crate) const fn timestamp_to_datetime_zulu(
     mut secs: i64,
     mut subsec: i32,
     offset: i32,
 ) -> (i16, i8, i8, i8, i8, i8, i32) {
-    secs += i64::from(offset);
+    secs += offset as i64;
     let mut days = secs.div_euclid(86_400) as i32;
     secs = secs.rem_euclid(86_400);
     if subsec < 0 {
@@ -180,7 +205,7 @@ pub(crate) fn timestamp_to_datetime_zulu(
 
 /// Converts a Gregorian datetime and its offset to a Unix timestamp.
 #[inline(always)]
-pub(crate) fn datetime_zulu_to_timestamp(
+pub(crate) const fn datetime_zulu_to_timestamp(
     year: i16,
     month: i8,
     day: i8,
@@ -191,11 +216,11 @@ pub(crate) fn datetime_zulu_to_timestamp(
     offset: i32,
 ) -> (i64, i32) {
     let day = to_unix_epoch_day(year, month, day);
-    let mut secs = i64::from(day) * t::SECONDS_PER_CIVIL_DAY.value();
-    secs += i64::from(hour) * t::SECONDS_PER_HOUR.value();
-    secs += i64::from(minute) * t::SECONDS_PER_MINUTE.value();
-    secs += i64::from(second);
-    secs -= i64::from(offset);
+    let mut secs = (day as i64) * t::SECONDS_PER_CIVIL_DAY.value();
+    secs += (hour as i64) * t::SECONDS_PER_HOUR.value();
+    secs += (minute as i64) * t::SECONDS_PER_MINUTE.value();
+    secs += second as i64;
+    secs -= offset as i64;
     if day < 0 && subsec != 0 {
         secs += 1;
         subsec -= 1_000_000_000;
diff --git a/src/util/constant.rs b/src/util/constant.rs
new file mode 100644
index 00000000..c9164fbc
--- /dev/null
+++ b/src/util/constant.rs
@@ -0,0 +1,13 @@
+/// Unwrap an `Option<T>` in a `const` context.
+///
+/// If it fails, panics with the given message.
+macro_rules! unwrap {
+    ($val:expr, $msg:expr$(,)?) => {
+        match $val {
+            Some(val) => val,
+            None => panic!($msg),
+        }
+    };
+}
+
+pub(crate) use unwrap;
diff --git a/src/util/escape.rs b/src/util/escape.rs
index 2c498732..89e27d64 100644
--- a/src/util/escape.rs
+++ b/src/util/escape.rs
@@ -4,91 +4,8 @@ Provides convenience routines for escaping raw bytes.
 This was copied from `regex-automata` with a few light edits.
 */
 
-use crate::util::utf8;
-
-/// Provides a convenient `Debug` implementation for a `u8`.
-///
-/// The `Debug` impl treats the byte as an ASCII, and emits a human readable
-/// representation of it. If the byte isn't ASCII, then it's emitted as a hex
-/// escape sequence.
-#[derive(Clone, Copy)]
-pub struct Byte(pub u8);
-
-impl core::fmt::Display for Byte {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        if self.0 == b' ' {
-            return write!(f, " ");
-        }
-        // 10 bytes is enough to cover any output from ascii::escape_default.
-        let mut bytes = [0u8; 10];
-        let mut len = 0;
-        for (i, mut b) in core::ascii::escape_default(self.0).enumerate() {
-            // capitalize \xab to \xAB
-            if i >= 2 && b'a' <= b && b <= b'f' {
-                b -= 32;
-            }
-            bytes[len] = b;
-            len += 1;
-        }
-        write!(f, "{}", core::str::from_utf8(&bytes[..len]).unwrap())
-    }
-}
-
-impl core::fmt::Debug for Byte {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        write!(f, "\"")?;
-        core::fmt::Display::fmt(self, f)?;
-        write!(f, "\"")?;
-        Ok(())
-    }
-}
-
-/// Provides a convenient `Debug` implementation for `&[u8]`.
-///
-/// This generally works best when the bytes are presumed to be mostly UTF-8,
-/// but will work for anything. For any bytes that aren't UTF-8, they are
-/// emitted as hex escape sequences.
-#[derive(Clone, Copy)]
-pub struct Bytes<'a>(pub &'a [u8]);
-
-impl<'a> core::fmt::Display for Bytes<'a> {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        // This is a sad re-implementation of a similar impl found in bstr.
-        let mut bytes = self.0;
-        while let Some(result) = utf8::decode(bytes) {
-            let ch = match result {
-                Ok(ch) => ch,
-                Err(byte) => {
-                    write!(f, r"\x{:02x}", byte)?;
-                    bytes = &bytes[1..];
-                    continue;
-                }
-            };
-            bytes = &bytes[ch.len_utf8()..];
-            match ch {
-                '\0' => write!(f, "\\0")?,
-                // ASCII control characters except \0, \n, \r, \t
-                '\x01'..='\x08'
-                | '\x0b'
-                | '\x0c'
-                | '\x0e'..='\x19'
-                | '\x7f' => {
-                    write!(f, "\\x{:02x}", u32::from(ch))?;
-                }
-                '\n' | '\r' | '\t' | _ => {
-                    write!(f, "{}", ch.escape_debug())?;
-                }
-            }
-        }
-        Ok(())
-    }
-}
-
-impl<'a> core::fmt::Debug for Bytes<'a> {
-    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
-        write!(f, "\"")?;
-        core::fmt::Display::fmt(self, f)?;
-        write!(f, "\"")?;
-        Ok(())
-    }
-}
+// These were originally defined here, but they got moved to
+// shared since they're needed there. We re-export them here
+// because this is really where they should live, but they're
+// in shared because `jiff-tzdb-static` needs it.
+pub(crate) use crate::shared::util::{Byte, Bytes};
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 6f8d320f..b360f1fb 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -7,7 +7,7 @@ pub(crate) mod borrow;
 ))]
 pub(crate) mod cache;
 pub(crate) mod common;
-pub(crate) mod crc32;
+pub(crate) mod constant;
 pub(crate) mod escape;
 #[cfg(feature = "std")]
 pub(crate) mod fs;
diff --git a/src/util/rangeint.rs b/src/util/rangeint.rs
index 406a1142..e43a23d4 100644
--- a/src/util/rangeint.rs
+++ b/src/util/rangeint.rs
@@ -138,6 +138,26 @@ macro_rules! define_ranged {
                 }
             }
 
+            /// Like `new`, but monomorphic and works in a `const` context.
+            #[inline]
+            pub(crate) const fn new_const(val: $repr) -> Option<Self> {
+                if !Self::contains(val) {
+                    return None;
+                }
+                #[cfg(not(debug_assertions))]
+                {
+                    Some(Self { val })
+                }
+                #[cfg(debug_assertions)]
+                {
+                    Some(Self {
+                        val,
+                        min: Self::MIN_REPR,
+                        max: Self::MAX_REPR,
+                    })
+                }
+            }
+
             #[inline]
             pub(crate) fn try_new(
                 what: &'static str,
diff --git a/src/util/t.rs b/src/util/t.rs
index e74c8c11..41b639e7 100644
--- a/src/util/t.rs
+++ b/src/util/t.rs
@@ -175,6 +175,10 @@ pub(crate) type Nanosecond = ri16<0, 999>;
 /// The range of possible nanosecond values.
 pub(crate) type SubsecNanosecond = ri32<0, { NANOS_PER_SECOND.bound() - 1 }>;
 
+/// A range representing each possible second in a single civil day.
+pub(crate) type CivilDaySecond =
+    ri32<0, { SECONDS_PER_CIVIL_DAY.bound() - 1 }>;
+
 /// A range representing each possible nanosecond in a single civil day.
 pub(crate) type CivilDayNanosecond =
     ri64<0, { NANOS_PER_CIVIL_DAY.bound() - 1 }>;
diff --git a/src/util/utf8.rs b/src/util/utf8.rs
index b1dad7bc..e39731a9 100644
--- a/src/util/utf8.rs
+++ b/src/util/utf8.rs
@@ -13,19 +13,7 @@ use core::cmp::Ordering;
 /// fast UTF-8 decoder, this is not it. If you feel like you need one in this
 /// crate, then please file an issue and discuss your use case.
 pub(crate) fn decode(bytes: &[u8]) -> Option<Result<char, u8>> {
-    if bytes.is_empty() {
-        return None;
-    }
-    let len = match utf8_len(bytes[0]) {
-        None => return Some(Err(bytes[0])),
-        Some(len) if len > bytes.len() => return Some(Err(bytes[0])),
-        Some(1) => return Some(Ok(char::from(bytes[0]))),
-        Some(len) => len,
-    };
-    match core::str::from_utf8(&bytes[..len]) {
-        Ok(s) => Some(Ok(s.chars().next().unwrap())),
-        Err(_) => Some(Err(bytes[0])),
-    }
+    crate::shared::util::utf8_decode(bytes)
 }
 
 /// Like std's `eq_ignore_ascii_case`, but returns a full `Ordering`.
@@ -58,24 +46,3 @@ pub(crate) fn cmp_ignore_ascii_case_bytes(s1: &[u8], s2: &[u8]) -> Ordering {
         }
     }
 }
-
-/// Given a UTF-8 leading byte, this returns the total number of code units
-/// in the following encoded codepoint.
-///
-/// If the given byte is not a valid UTF-8 leading byte, then this returns
-/// `None`.
-fn utf8_len(byte: u8) -> Option<usize> {
-    if byte <= 0x7F {
-        return Some(1);
-    } else if byte & 0b1100_0000 == 0b1000_0000 {
-        return None;
-    } else if byte <= 0b1101_1111 {
-        Some(2)
-    } else if byte <= 0b1110_1111 {
-        Some(3)
-    } else if byte <= 0b1111_0111 {
-        Some(4)
-    } else {
-        None
-    }
-}
diff --git a/src/zoned.rs b/src/zoned.rs
index f2e30d6f..6baf0f9a 100644
--- a/src/zoned.rs
+++ b/src/zoned.rs
@@ -5421,7 +5421,7 @@ mod tests {
             }
             #[cfg(all(target_pointer_width = "64", not(feature = "alloc")))]
             {
-                assert_eq!(104, core::mem::size_of::<Zoned>());
+                assert_eq!(96, core::mem::size_of::<Zoned>());
             }
         }
         #[cfg(not(debug_assertions))]


Test patch:
diff --git a/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v1.snap b/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v1.snap
index cfb2b3a2..4a61aa8b 100644
Binary files a/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v1.snap and b/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v1.snap differ
diff --git a/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v2+.snap b/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v2+.snap
index e80cbde5..ca4f6c5a 100644
--- a/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v2+.snap
+++ b/src/tz/snapshots/jiff__tz__tzif__tests__right__America__New_York_v2+.snap
@@ -251,33 +251,5 @@ TRANSITIONS
   0234:  2036-11-02T06:00:27Z     unix=2109218427     wall=2036-11-02T01:00:27     fold-until(2036-11-02T02:00:27)  type=2  -05        EST  
   0235:  2037-03-08T07:00:27Z     unix=2120108427     wall=2037-03-08T02:00:27      gap-until(2037-03-08T03:00:27)  type=1  -04        EDT  dst
   0236:  2037-11-01T06:00:27Z     unix=2140668027     wall=2037-11-01T01:00:27     fold-until(2037-11-01T02:00:27)  type=2  -05        EST  
-LEAP SECONDS
-  1972-07-01T00:00:00  correction=1
-  1973-01-01T00:00:01  correction=2
-  1974-01-01T00:00:02  correction=3
-  1975-01-01T00:00:03  correction=4
-  1976-01-01T00:00:04  correction=5
-  1977-01-01T00:00:05  correction=6
-  1978-01-01T00:00:06  correction=7
-  1979-01-01T00:00:07  correction=8
-  1980-01-01T00:00:08  correction=9
-  1981-07-01T00:00:09  correction=10
-  1982-07-01T00:00:10  correction=11
-  1983-07-01T00:00:11  correction=12
-  1985-07-01T00:00:12  correction=13
-  1988-01-01T00:00:13  correction=14
-  1990-01-01T00:00:14  correction=15
-  1991-01-01T00:00:15  correction=16
-  1992-07-01T00:00:16  correction=17
-  1993-07-01T00:00:17  correction=18
-  1994-07-01T00:00:18  correction=19
-  1996-01-01T00:00:19  correction=20
-  1997-07-01T00:00:20  correction=21
-  1999-01-01T00:00:21  correction=22
-  2006-01-01T00:00:22  correction=23
-  2009-01-01T00:00:23  correction=24
-  2012-07-01T00:00:24  correction=25
-  2015-07-01T00:00:25  correction=26
-  2017-01-01T00:00:26  correction=27
 POSIX TIME ZONE STRING
   EST5EDT,M3.2.0,M11.1.0
diff --git a/src/tz/testdata.rs b/src/tz/testdata.rs
index f3978a40..d0f64a86 100644
--- a/src/tz/testdata.rs
+++ b/src/tz/testdata.rs
@@ -1,6 +1,5 @@
-use alloc::string::ToString;
-
-use crate::tz::tzif::Tzif;
+#[cfg(not(miri))]
+use crate::tz::tzif::TzifOwned;
 
 /// A concatenated list of TZif data with a header and an index block.
 ///
@@ -93,19 +92,25 @@ impl TzifTestFile {
     }
 
     /// Parse this test TZif data into a structured representation.
-    pub(crate) fn parse(self) -> Tzif {
+    #[cfg(not(miri))]
+    pub(crate) fn parse(self) -> TzifOwned {
+        use alloc::string::ToString;
+
         let name = Some(self.name.to_string());
-        Tzif::parse(name, self.data).unwrap_or_else(|err| {
+        TzifOwned::parse(name, self.data).unwrap_or_else(|err| {
             panic!("failed to parse TZif test file for {:?}: {err}", self.name)
         })
     }
 
     /// Parse this test TZif data as if it were V1.
-    pub(crate) fn parse_v1(self) -> Tzif {
+    #[cfg(not(miri))]
+    pub(crate) fn parse_v1(self) -> TzifOwned {
+        use alloc::string::ToString;
+
         let name = Some(self.name.to_string());
         let mut data = self.data.to_vec();
         data[4] = 0;
-        Tzif::parse(name, &data).unwrap_or_else(|err| {
+        TzifOwned::parse(name, &data).unwrap_or_else(|err| {
             panic!(
                 "failed to parse V1 TZif test file for {:?}: {err}",
                 self.name
diff --git a/test b/test
index 73e5aafc..4d5ba063 100755
--- a/test
+++ b/test
@@ -1,5 +1,12 @@
 #!/bin/bash
 
+# This script is used to run a more exhaustive set of tests than what just
+# `cargo test` or even `cargo test --all` will do. Specifically, we run tests
+# under a number of different feature configurations.
+#
+# This is practically necessary because it is very easy to accidentally break
+# a particular feature combination, especially `--no-default-features`.
+
 set -e
 
 # cd to the directory containing this crate's Cargo.toml so that we don't need
@@ -10,7 +17,10 @@ cd "$(dirname "$0")"
 # features. We don't test the complete space, since the complete space is quite
 # large.
 echo "===== DEFAULT FEATURES ====="
-cargo test
+cargo test --all
+
+echo "===== DEFAULT FEATURES WITH STATIC ====="
+cargo test --features static
 
 # This one is useful because sometimes the bundled time zone database can
 # behave differently than the system time zone database depending on the
@@ -41,6 +51,7 @@ features=(
     "serde"
     "logging"
     "serde logging"
+    "static"
 )
 for f in "${features[@]}"; do
     echo "===== COREONLY PLUS '$f' ====="
@@ -49,6 +60,7 @@ for f in "${features[@]}"; do
     INSTA_FORCE_PASS=1 INSTA_UPDATE=no cargo test --test integration --no-default-features --features "$f"
 done
 
+exit 0
 features=(
     "alloc"
     "alloc tzdb-bundle-always"
diff --git a/tests/lib.rs b/tests/lib.rs
index 863845fc..78ec5fd3 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,4 +1,5 @@
 // See: https://github.com/rust-lang/rust/pull/121364
 #![allow(unknown_lints, ambiguous_negative_literals)]
 
+mod procmacro;
 mod tc39_262;
diff --git a/tests/procmacro/mod.rs b/tests/procmacro/mod.rs
new file mode 100644
index 00000000..8ffa20d6
--- /dev/null
+++ b/tests/procmacro/mod.rs
@@ -0,0 +1,518 @@
+use jiff::{
+    civil::date,
+    tz::{self, AmbiguousOffset, Offset, TimeZone},
+    Timestamp,
+};
+
+/// Tests Jiff's "static" time zone support via the proc macro.
+///
+/// This test was copied from `src/tz/timezone.rs`, and modified to use the
+/// proc macro. This test isn't in Jiff proper since it seems like unit tests
+/// are compiled with a different copy of Jiff, and the `TimeZone` type created
+/// by the proc macro doesn't match the one used by the unit tests.
+#[test]
+fn time_zone_static_tzif_to_ambiguous_timestamp() {
+    let tests: &[(TimeZone, &[_])] = &[
+        (
+            tz::get!("America/New_York"),
+            &[
+                ((1969, 12, 31, 19, 0, 0, 0), unambiguous(-5)),
+                ((2024, 3, 10, 1, 59, 59, 999_999_999), unambiguous(-5)),
+                ((2024, 3, 10, 2, 0, 0, 0), gap(-5, -4)),
+                ((2024, 3, 10, 2, 59, 59, 999_999_999), gap(-5, -4)),
+                ((2024, 3, 10, 3, 0, 0, 0), unambiguous(-4)),
+                ((2024, 11, 3, 0, 59, 59, 999_999_999), unambiguous(-4)),
+                ((2024, 11, 3, 1, 0, 0, 0), fold(-4, -5)),
+                ((2024, 11, 3, 1, 59, 59, 999_999_999), fold(-4, -5)),
+                ((2024, 11, 3, 2, 0, 0, 0), unambiguous(-5)),
+            ],
+        ),
+        (
+            tz::get!("Europe/Dublin"),
+            &[
+                ((1970, 1, 1, 0, 0, 0, 0), unambiguous(1)),
+                ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
+                ((2024, 3, 31, 1, 0, 0, 0), gap(0, 1)),
+                ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 1)),
+                ((2024, 3, 31, 2, 0, 0, 0), unambiguous(1)),
+                ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(1)),
+                ((2024, 10, 27, 1, 0, 0, 0), fold(1, 0)),
+                ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(1, 0)),
+                ((2024, 10, 27, 2, 0, 0, 0), unambiguous(0)),
+            ],
+        ),
+        (
+            tz::get!("Australia/Tasmania"),
+            &[
+                ((1970, 1, 1, 11, 0, 0, 0), unambiguous(11)),
+                ((2024, 4, 7, 1, 59, 59, 999_999_999), unambiguous(11)),
+                ((2024, 4, 7, 2, 0, 0, 0), fold(11, 10)),
+                ((2024, 4, 7, 2, 59, 59, 999_999_999), fold(11, 10)),
+                ((2024, 4, 7, 3, 0, 0, 0), unambiguous(10)),
+                ((2024, 10, 6, 1, 59, 59, 999_999_999), unambiguous(10)),
+                ((2024, 10, 6, 2, 0, 0, 0), gap(10, 11)),
+                ((2024, 10, 6, 2, 59, 59, 999_999_999), gap(10, 11)),
+                ((2024, 10, 6, 3, 0, 0, 0), unambiguous(11)),
+            ],
+        ),
+        (
+            tz::get!("Antarctica/Troll"),
+            &[
+                ((1970, 1, 1, 0, 0, 0, 0), unambiguous(0)),
+                // test the gap
+                ((2024, 3, 31, 0, 59, 59, 999_999_999), unambiguous(0)),
+                ((2024, 3, 31, 1, 0, 0, 0), gap(0, 2)),
+                ((2024, 3, 31, 1, 59, 59, 999_999_999), gap(0, 2)),
+                // still in the gap!
+                ((2024, 3, 31, 2, 0, 0, 0), gap(0, 2)),
+                ((2024, 3, 31, 2, 59, 59, 999_999_999), gap(0, 2)),
+                // finally out
+                ((2024, 3, 31, 3, 0, 0, 0), unambiguous(2)),
+                // test the fold
+                ((2024, 10, 27, 0, 59, 59, 999_999_999), unambiguous(2)),
+                ((2024, 10, 27, 1, 0, 0, 0), fold(2, 0)),
+                ((2024, 10, 27, 1, 59, 59, 999_999_999), fold(2, 0)),
+                // still in the fold!
+                ((2024, 10, 27, 2, 0, 0, 0), fold(2, 0)),
+                ((2024, 10, 27, 2, 59, 59, 999_999_999), fold(2, 0)),
+                // finally out
+                ((2024, 10, 27, 3, 0, 0, 0), unambiguous(0)),
+            ],
+        ),
+        (
+            tz::get!("America/St_Johns"),
+            &[
+                ((1969, 12, 31, 20, 30, 0, 0), o_unambiguous(-hms(3, 30, 0))),
+                (
+                    (2024, 3, 10, 1, 59, 59, 999_999_999),
+                    o_unambiguous(-hms(3, 30, 0)),
+                ),
+                (
+                    (2024, 3, 10, 2, 0, 0, 0),
+                    o_gap(-hms(3, 30, 0), -hms(2, 30, 0)),
+                ),
+                (
+                    (2024, 3, 10, 2, 59, 59, 999_999_999),
+                    o_gap(-hms(3, 30, 0), -hms(2, 30, 0)),
+                ),
+                ((2024, 3, 10, 3, 0, 0, 0), o_unambiguous(-hms(2, 30, 0))),
+                (
+                    (2024, 11, 3, 0, 59, 59, 999_999_999),
+                    o_unambiguous(-hms(2, 30, 0)),
+                ),
+                (
+                    (2024, 11, 3, 1, 0, 0, 0),
+                    o_fold(-hms(2, 30, 0), -hms(3, 30, 0)),
+                ),
+                (
+                    (2024, 11, 3, 1, 59, 59, 999_999_999),
+                    o_fold(-hms(2, 30, 0), -hms(3, 30, 0)),
+                ),
+                ((2024, 11, 3, 2, 0, 0, 0), o_unambiguous(-hms(3, 30, 0))),
+            ],
+        ),
+        // This time zone has an interesting transition where it jumps
+        // backwards a full day at 1867-10-19T15:30:00.
+        (
+            tz::get!("America/Sitka"),
+            &[
+                ((1969, 12, 31, 16, 0, 0, 0), unambiguous(-8)),
+                ((-9999, 1, 2, 16, 58, 46, 0), o_unambiguous(hms(14, 58, 47))),
+                (
+                    (1867, 10, 18, 15, 29, 59, 0),
+                    o_unambiguous(hms(14, 58, 47)),
+                ),
+                (
+                    (1867, 10, 18, 15, 30, 0, 0),
+                    // A fold of 24 hours!!!
+                    o_fold(hms(14, 58, 47), -hms(9, 1, 13)),
+                ),
+                (
+                    (1867, 10, 19, 15, 29, 59, 999_999_999),
+                    // Still in the fold...
+                    o_fold(hms(14, 58, 47), -hms(9, 1, 13)),
+                ),
+                (
+                    (1867, 10, 19, 15, 30, 0, 0),
+                    // Finally out.
+                    o_unambiguous(-hms(9, 1, 13)),
+                ),
+            ],
+        ),
+        // As with to_datetime, we test every possible transition
+        // point here since this time zone has a small number of them.
+        (
+            tz::get!("Pacific/Honolulu"),
+            &[
+                (
+                    (1896, 1, 13, 11, 59, 59, 0),
+                    o_unambiguous(-hms(10, 31, 26)),
+                ),
+                (
+                    (1896, 1, 13, 12, 0, 0, 0),
+                    o_gap(-hms(10, 31, 26), -hms(10, 30, 0)),
+                ),
+                (
+                    (1896, 1, 13, 12, 1, 25, 0),
+                    o_gap(-hms(10, 31, 26), -hms(10, 30, 0)),
+                ),
+                ((1896, 1, 13, 12, 1, 26, 0), o_unambiguous(-hms(10, 30, 0))),
+                ((1933, 4, 30, 1, 59, 59, 0), o_unambiguous(-hms(10, 30, 0))),
+                (
+                    (1933, 4, 30, 2, 0, 0, 0),
+                    o_gap(-hms(10, 30, 0), -hms(9, 30, 0)),
+                ),
+                (
+                    (1933, 4, 30, 2, 59, 59, 0),
+                    o_gap(-hms(10, 30, 0), -hms(9, 30, 0)),
+                ),
+                ((1933, 4, 30, 3, 0, 0, 0), o_unambiguous(-hms(9, 30, 0))),
+                ((1933, 5, 21, 10, 59, 59, 0), o_unambiguous(-hms(9, 30, 0))),
+                (
+                    (1933, 5, 21, 11, 0, 0, 0),
+                    o_fold(-hms(9, 30, 0), -hms(10, 30, 0)),
+                ),
+                (
+                    (1933, 5, 21, 11, 59, 59, 0),
+                    o_fold(-hms(9, 30, 0), -hms(10, 30, 0)),
+                ),
+                ((1933, 5, 21, 12, 0, 0, 0), o_unambiguous(-hms(10, 30, 0))),
+                ((1942, 2, 9, 1, 59, 59, 0), o_unambiguous(-hms(10, 30, 0))),
+                (
+                    (1942, 2, 9, 2, 0, 0, 0),
+                    o_gap(-hms(10, 30, 0), -hms(9, 30, 0)),
+                ),
+                (
+                    (1942, 2, 9, 2, 59, 59, 0),
+                    o_gap(-hms(10, 30, 0), -hms(9, 30, 0)),
+                ),
+                ((1942, 2, 9, 3, 0, 0, 0), o_unambiguous(-hms(9, 30, 0))),
+                ((1945, 8, 14, 13, 29, 59, 0), o_unambiguous(-hms(9, 30, 0))),
+                ((1945, 8, 14, 13, 30, 0, 0), o_unambiguous(-hms(9, 30, 0))),
+                ((1945, 8, 14, 13, 30, 1, 0), o_unambiguous(-hms(9, 30, 0))),
+                ((1945, 9, 30, 0, 59, 59, 0), o_unambiguous(-hms(9, 30, 0))),
+                (
+                    (1945, 9, 30, 1, 0, 0, 0),
+                    o_fold(-hms(9, 30, 0), -hms(10, 30, 0)),
+                ),
+                (
+                    (1945, 9, 30, 1, 59, 59, 0),
+                    o_fold(-hms(9, 30, 0), -hms(10, 30, 0)),
+                ),
+                ((1945, 9, 30, 2, 0, 0, 0), o_unambiguous(-hms(10, 30, 0))),
+                ((1947, 6, 8, 1, 59, 59, 0), o_unambiguous(-hms(10, 30, 0))),
+                (
+                    (1947, 6, 8, 2, 0, 0, 0),
+                    o_gap(-hms(10, 30, 0), -tz::offset(10)),
+                ),
+                (
+                    (1947, 6, 8, 2, 29, 59, 0),
+                    o_gap(-hms(10, 30, 0), -tz::offset(10)),
+                ),
+                ((1947, 6, 8, 2, 30, 0, 0), unambiguous(-10)),
+            ],
+        ),
+    ];
+    for &(ref tz, datetimes_to_ambiguous) in tests {
+        let tzname = tz.iana_name().unwrap();
+        // let test_file = TzifTestFile::get(tzname);
+        // let tz = TimeZone::tzif(test_file.name, test_file.data).unwrap();
+        for &(datetime, ambiguous_kind) in datetimes_to_ambiguous {
+            let (year, month, day, hour, min, sec, nano) = datetime;
+            let dt = date(year, month, day).at(hour, min, sec, nano);
+            let got = tz.to_ambiguous_zoned(dt);
+            assert_eq!(
+                got.offset(),
+                ambiguous_kind,
+                "\nTZ: {tzname}\ndatetime: \
+                     {year:04}-{month:02}-{day:02}T\
+                     {hour:02}:{min:02}:{sec:02}.{nano:09}",
+            );
+        }
+    }
+}
+
+#[test]
+fn time_zone_static_tzif_to_datetime() {
+    let o = |hours| tz::offset(hours);
+    let tests: &[(TimeZone, &[_])] = &[
+        (
+            tz::get!("America/New_York"),
+            &[
+                ((0, 0), o(-5), "EST", (1969, 12, 31, 19, 0, 0, 0)),
+                ((1710052200, 0), o(-5), "EST", (2024, 3, 10, 1, 30, 0, 0)),
+                (
+                    (1710053999, 999_999_999),
+                    o(-5),
+                    "EST",
+                    (2024, 3, 10, 1, 59, 59, 999_999_999),
+                ),
+                ((1710054000, 0), o(-4), "EDT", (2024, 3, 10, 3, 0, 0, 0)),
+                ((1710055800, 0), o(-4), "EDT", (2024, 3, 10, 3, 30, 0, 0)),
+                ((1730610000, 0), o(-4), "EDT", (2024, 11, 3, 1, 0, 0, 0)),
+                ((1730611800, 0), o(-4), "EDT", (2024, 11, 3, 1, 30, 0, 0)),
+                (
+                    (1730613599, 999_999_999),
+                    o(-4),
+                    "EDT",
+                    (2024, 11, 3, 1, 59, 59, 999_999_999),
+                ),
+                ((1730613600, 0), o(-5), "EST", (2024, 11, 3, 1, 0, 0, 0)),
+                ((1730615400, 0), o(-5), "EST", (2024, 11, 3, 1, 30, 0, 0)),
+            ],
+        ),
+        (
+            tz::get!("Australia/Tasmania"),
+            &[
+                ((0, 0), o(11), "AEDT", (1970, 1, 1, 11, 0, 0, 0)),
+                ((1728142200, 0), o(10), "AEST", (2024, 10, 6, 1, 30, 0, 0)),
+                (
+                    (1728143999, 999_999_999),
+                    o(10),
+                    "AEST",
+                    (2024, 10, 6, 1, 59, 59, 999_999_999),
+                ),
+                ((1728144000, 0), o(11), "AEDT", (2024, 10, 6, 3, 0, 0, 0)),
+                ((1728145800, 0), o(11), "AEDT", (2024, 10, 6, 3, 30, 0, 0)),
+                ((1712415600, 0), o(11), "AEDT", (2024, 4, 7, 2, 0, 0, 0)),
+                ((1712417400, 0), o(11), "AEDT", (2024, 4, 7, 2, 30, 0, 0)),
+                (
+                    (1712419199, 999_999_999),
+                    o(11),
+                    "AEDT",
+                    (2024, 4, 7, 2, 59, 59, 999_999_999),
+                ),
+                ((1712419200, 0), o(10), "AEST", (2024, 4, 7, 2, 0, 0, 0)),
+                ((1712421000, 0), o(10), "AEST", (2024, 4, 7, 2, 30, 0, 0)),
+            ],
+        ),
+        // Pacific/Honolulu is small eough that we just test every
+        // possible instant before, at and after each transition.
+        (
+            tz::get!("Pacific/Honolulu"),
+            &[
+                (
+                    (-2334101315, 0),
+                    -hms(10, 31, 26),
+                    "LMT",
+                    (1896, 1, 13, 11, 59, 59, 0),
+                ),
+                (
+                    (-2334101314, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1896, 1, 13, 12, 1, 26, 0),
+                ),
+                (
+                    (-2334101313, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1896, 1, 13, 12, 1, 27, 0),
+                ),
+                (
+                    (-1157283001, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1933, 4, 30, 1, 59, 59, 0),
+                ),
+                (
+                    (-1157283000, 0),
+                    -hms(9, 30, 0),
+                    "HDT",
+                    (1933, 4, 30, 3, 0, 0, 0),
+                ),
+                (
+                    (-1157282999, 0),
+                    -hms(9, 30, 0),
+                    "HDT",
+                    (1933, 4, 30, 3, 0, 1, 0),
+                ),
+                (
+                    (-1155436201, 0),
+                    -hms(9, 30, 0),
+                    "HDT",
+                    (1933, 5, 21, 11, 59, 59, 0),
+                ),
+                (
+                    (-1155436200, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1933, 5, 21, 11, 0, 0, 0),
+                ),
+                (
+                    (-1155436199, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1933, 5, 21, 11, 0, 1, 0),
+                ),
+                (
+                    (-880198201, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1942, 2, 9, 1, 59, 59, 0),
+                ),
+                (
+                    (-880198200, 0),
+                    -hms(9, 30, 0),
+                    "HWT",
+                    (1942, 2, 9, 3, 0, 0, 0),
+                ),
+                (
+                    (-880198199, 0),
+                    -hms(9, 30, 0),
+                    "HWT",
+                    (1942, 2, 9, 3, 0, 1, 0),
+                ),
+                (
+                    (-769395601, 0),
+                    -hms(9, 30, 0),
+                    "HWT",
+                    (1945, 8, 14, 13, 29, 59, 0),
+                ),
+                (
+                    (-769395600, 0),
+                    -hms(9, 30, 0),
+                    "HPT",
+                    (1945, 8, 14, 13, 30, 0, 0),
+                ),
+                (
+                    (-769395599, 0),
+                    -hms(9, 30, 0),
+                    "HPT",
+                    (1945, 8, 14, 13, 30, 1, 0),
+                ),
+                (
+                    (-765376201, 0),
+                    -hms(9, 30, 0),
+                    "HPT",
+                    (1945, 9, 30, 1, 59, 59, 0),
+                ),
+                (
+                    (-765376200, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1945, 9, 30, 1, 0, 0, 0),
+                ),
+                (
+                    (-765376199, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1945, 9, 30, 1, 0, 1, 0),
+                ),
+                (
+                    (-712150201, 0),
+                    -hms(10, 30, 0),
+                    "HST",
+                    (1947, 6, 8, 1, 59, 59, 0),
+                ),
+                // At this point, we hit the last transition and the POSIX
+                // TZ string takes over.
+                (
+                    (-712150200, 0),
+                    -hms(10, 0, 0),
+                    "HST",
+                    (1947, 6, 8, 2, 30, 0, 0),
+                ),
+                (
+                    (-712150199, 0),
+                    -hms(10, 0, 0),
+                    "HST",
+                    (1947, 6, 8, 2, 30, 1, 0),
+                ),
+            ],
+        ),
+        // This time zone has an interesting transition where it jumps
+        // backwards a full day at 1867-10-19T15:30:00.
+        (
+            tz::get!("America/Sitka"),
+            &[
+                ((0, 0), o(-8), "PST", (1969, 12, 31, 16, 0, 0, 0)),
+                (
+                    (-377705023201, 0),
+                    hms(14, 58, 47),
+                    "LMT",
+                    (-9999, 1, 2, 16, 58, 46, 0),
+                ),
+                (
+                    (-3225223728, 0),
+                    hms(14, 58, 47),
+                    "LMT",
+                    (1867, 10, 19, 15, 29, 59, 0),
+                ),
+                // Notice the 24 hour time jump backwards a whole day!
+                (
+                    (-3225223727, 0),
+                    -hms(9, 1, 13),
+                    "LMT",
+                    (1867, 10, 18, 15, 30, 0, 0),
+                ),
+                (
+                    (-3225223726, 0),
+                    -hms(9, 1, 13),
+                    "LMT",
+                    (1867, 10, 18, 15, 30, 1, 0),
+                ),
+            ],
+        ),
+    ];
+    for &(ref tz, timestamps_to_datetimes) in tests {
+        let tzname = tz.iana_name().unwrap();
+        for &((unix_sec, unix_nano), offset, abbrev, datetime) in
+            timestamps_to_datetimes
+        {
+            let (year, month, day, hour, min, sec, nano) = datetime;
+            let timestamp = Timestamp::new(unix_sec, unix_nano).unwrap();
+            let info = tz.to_offset_info(timestamp);
+            assert_eq!(
+                info.offset(),
+                offset,
+                "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
+            );
+            assert_eq!(
+                info.abbreviation(),
+                abbrev,
+                "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
+            );
+            assert_eq!(
+                info.offset().to_datetime(timestamp),
+                date(year, month, day).at(hour, min, sec, nano),
+                "\nTZ={tzname}, timestamp({unix_sec}, {unix_nano})",
+            );
+        }
+    }
+}
+
+fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
+    let seconds =
+        (hours as i32 * 60 * 60) + (minutes as i32 * 60) + (seconds as i32);
+    Offset::from_seconds(seconds).unwrap()
+}
+
+fn unambiguous(offset_hours: i8) -> AmbiguousOffset {
+    let offset = tz::offset(offset_hours);
+    o_unambiguous(offset)
+}
+
+fn gap(earlier_offset_hours: i8, later_offset_hours: i8) -> AmbiguousOffset {
+    let earlier = tz::offset(earlier_offset_hours);
+    let later = tz::offset(later_offset_hours);
+    o_gap(earlier, later)
+}
+
+fn fold(earlier_offset_hours: i8, later_offset_hours: i8) -> AmbiguousOffset {
+    let earlier = tz::offset(earlier_offset_hours);
+    let later = tz::offset(later_offset_hours);
+    o_fold(earlier, later)
+}
+
+fn o_unambiguous(offset: Offset) -> AmbiguousOffset {
+    AmbiguousOffset::Unambiguous { offset }
+}
+
+fn o_gap(earlier: Offset, later: Offset) -> AmbiguousOffset {
+    AmbiguousOffset::Gap { before: earlier, after: later }
+}
+
+fn o_fold(earlier: Offset, later: Offset) -> AmbiguousOffset {
+    AmbiguousOffset::Fold { before: earlier, after: later }
+}