embedded-tz 0.5.2

embedded/no_std chrono::TimeZone implementation for TZif data
Documentation
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

fn main() {
    println!("cargo:rerun-if-changed=zoneinfo");
    println!("cargo:rerun-if-changed=tzdb/version");
    println!("cargo:rerun-if-env-changed=CARGO_FEATURE_BUNDLED_TZDB");

    if env::var_os("CARGO_FEATURE_BUNDLED_TZDB").is_none() {
        return;
    }

    if let Err(err) = generate_bundled_tzdb() {
        panic!("failed generating bundled tzdb: {}", err);
    }
}

fn generate_bundled_tzdb() -> io::Result<()> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let zoneinfo_root = manifest_dir.join("zoneinfo");
    if !zoneinfo_root.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::NotFound,
            format!(
                "missing {} (run scripts/build-tzdb.sh first)",
                zoneinfo_root.display()
            ),
        ));
    }

    let mut paths = Vec::new();
    collect_regular_files(&zoneinfo_root, &mut paths)?;
    paths.sort();

    let out_file = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bundled_tzdb.rs");
    let mut out = fs::File::create(out_file)?;
    let tzdb_version = read_tzdb_version(&manifest_dir)?;

    writeln!(out, "pub const TZ_DB_VERSION: &str = {tzdb_version:?};")?;

    writeln!(out, "pub static BUNDLED_TZDB: &[(&str, &[u8])] = &[")?;

    for path in paths {
        let bytes = fs::read(&path)?;
        if !is_tzif(&bytes) {
            continue;
        }

        let rel = path
            .strip_prefix(&zoneinfo_root)
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "strip_prefix failed"))?;
        let rel_norm = rel.to_string_lossy().replace('\\', "/");
        let include_path = path.to_string_lossy().replace('\\', "/");
        writeln!(out, "    ({rel_norm:?}, include_bytes!({include_path:?})),")?;
    }

    writeln!(out, "];")?;
    Ok(())
}

fn read_tzdb_version(manifest_dir: &Path) -> io::Result<String> {
    let raw = fs::read_to_string(manifest_dir.join("tzdb").join("version"))?;
    let version = raw.trim();
    let cleaned = version.strip_suffix("-dirty").unwrap_or(version);
    if cleaned.is_empty() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "tzdb/version is empty",
        ));
    }
    Ok(cleaned.to_string())
}

fn collect_regular_files(root: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
    for entry in fs::read_dir(root)? {
        let entry = entry?;
        let path = entry.path();
        let ft = entry.file_type()?;
        if ft.is_dir() {
            collect_regular_files(&path, files)?;
        } else if ft.is_file() {
            if path.file_name() == Some(OsStr::new("posixrules")) {
                continue;
            }
            files.push(path);
        }
    }
    Ok(())
}

fn is_tzif(bytes: &[u8]) -> bool {
    bytes.len() >= 4 && &bytes[..4] == b"TZif"
}