gedcomkit 0.1.11

A byte-preserving GEDCOM document model: decoding, parsing, readings, version conversion, plausibility checks, and the GEDZIP container, for GEDCOM 5.5 through 7.x.
Documentation
//! H. Eichmann's character-set challenge files: one tree written as ANSEL and
//! as seven UTF-16 permutations (endianness × byte-order mark × line ending),
//! each inside a plain ZIP.
//!
//! Fetched, never committed — the terms are non-commercial-only — by
//! `scripts/fetch-fixtures.ps1`; the test skips with a message when the files
//! are absent, matching the repository's other fetch-gated suites. The ZIP
//! containers are opened by this crate's own archive reader, which accepts a
//! single root `.ged` under any name.

#![cfg(feature = "gedzip")]

use std::path::{Path, PathBuf};

fn charset_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/external/torture/charset")
}

const ARCHIVES: &[&str] = &[
    "ansel.zip",
    "uhlbomcl.zip",
    "uhlcl.zip",
    "ulhbomcl.zip",
    "ulhc.zip",
    "ulhcl.zip",
    "ulhl.zip",
    "ulhlc.zip",
];

#[test]
fn every_charset_permutation_of_the_same_tree_reads_to_the_same_records() {
    let root = charset_root();
    if !root.exists() {
        eprintln!(
            "skipped: {} is absent; run scripts/fetch-fixtures.ps1",
            root.display()
        );
        return;
    }

    let mut record_counts = Vec::new();
    for name in ARCHIVES {
        let bytes = match std::fs::read(root.join(name)) {
            Ok(bytes) => bytes,
            Err(error) => {
                eprintln!("skipped {name}: {error}; run scripts/fetch-fixtures.ps1");
                continue;
            }
        };
        let archive = gedcomkit::gedzip::Archive::open(&bytes)
            .unwrap_or_else(|error| panic!("{name} did not open as an archive: {error}"));
        let document_name = archive
            .document_name()
            .unwrap_or_else(|error| panic!("{name}: {error}"))
            .to_owned();
        let inner = archive
            .read(&document_name)
            .unwrap_or_else(|error| panic!("{name}/{document_name}: {error}"));

        let (text, report) = gedcomkit::decode_gedcom(&inner)
            .unwrap_or_else(|error| panic!("{name} did not decode: {error}"));
        assert_eq!(
            report.undecodable_bytes,
            0,
            "{name} had undecodable bytes: {}",
            report.summary()
        );
        let document = gedcomkit::Document::parse(&text)
            .unwrap_or_else(|error| panic!("{name} did not parse: {error}"));
        record_counts.push((*name, document.records.len(), document.content_records()));

        // Byte-for-byte preservation is defined over LF line endings; these
        // files exist to permute CR, LF, CRLF, and even LF+CR, so the honest
        // assertion is the fixed point: what was written parses back to
        // exactly what was written.
        let written = document.to_text();
        let again = gedcomkit::Document::parse(&written)
            .unwrap_or_else(|error| panic!("{name} output did not re-parse: {error}"));
        assert_eq!(
            again.to_text(),
            written,
            "{name} did not reach a round-trip fixed point"
        );
    }

    if record_counts.is_empty() {
        return;
    }
    let (first_name, first_records, first_content) = record_counts[0];
    for (name, records, content) in &record_counts {
        assert_eq!(
            (*records, *content),
            (first_records, first_content),
            "{name} read a different tree than {first_name}"
        );
    }
}