# gedcomkit
[](https://gitlab.com/timrs2998/gedcomkit/-/pipelines)
[](https://crates.io/crates/gedcomkit)
[](https://docs.rs/gedcomkit)
**An archival-grade GEDCOM library: nothing dropped, nothing rewritten, every
anomaly reported, safe on hostile input.** A byte-preserving document model
for GEDCOM 5.5 through 7.x, with encoding detection, read-only view
projections, plausibility checks, explicit version conversion, and the GEDZIP
container. Rust core; Python and JavaScript/WASM bindings in the same
repository.
## Why another GEDCOM library
Every existing library makes the same trade: parse into a typed model,
write canonical GEDCOM back out. That is a fine trade for an application —
and the wrong one for an archive. A family history file is often the only
machine-readable record of decades of work, full of constructs its next
program will not recognize, and a round trip that normalizes it quietly
discards evidence. gedcomkit trades the other way:
- **A node no caller touched is written back byte for byte.** Preservation
is a property of the representation, not a feature per construct: every
line keeps its source text whenever re-rendering the parsed pieces would
differ from what arrived, and the only way to change a node is through
setters that surrender exactly that node's kept line. Golden round-trip
tests over a 112-file corpus keep this true.
- **Nothing is dropped for being unrecognized.** Unknown tags, vendor
extensions, and odd constructs are read, kept, rendered in the views, and
written back.
- **Nothing is decided in silence.** Encoding disagreements — a file that
declares UTF-8 and contains Windows-1252, ANSEL with no declaration at
all — are reported, not guessed away. Validation reports contradictions
with the numbers they were drawn from and never refuses a file. Version
conversion names every construct it touched and keeps what the target
cannot hold.
- **Hostile input is bounded.** Decoding, parsing, and the GEDZIP reader run
under explicit `Limits` — input size, line length, nesting depth, record
and structure counts — so a malicious file is rejected rather than
exhausting memory. A 30-minute, 3.3-million-case mutation-fuzz campaign
and libFuzzer targets stand behind the claim.
## Quickstart
```rust
use gedcomkit::view::IndividualView;
let bytes = std::fs::read("family.ged")?;
let (mut document, report) = gedcomkit::Document::from_bytes(&bytes)?;
println!("{}", report.summary()); // "Read as ANSEL, declared ANSEL"
let graph = gedcomkit::family::FamilyGraph::new(&document); // CHIL∪FAMC merged
for record in document.records_of("INDI") {
let person = IndividualView::from_node(record);
println!("{} {}", person.display_name(), person.life_years().unwrap_or_default());
}
document
.record_mut("@I1@")
.and_then(|record| record.first_mut("NAME"))
.expect("name line")
.set_logical_value("Ada /Lovelace/");
std::fs::write("family.ged", document.to_bytes())?; // CHAR now tells the truth
```
From Python or JavaScript, the same library through the same corpus-proven
core (see [`bindings/`](bindings/README.md)):
```python
import gedcomkit
doc = gedcomkit.Gedcom.parse(open("family.ged", "rb").read())
print(doc.encoding, doc.version, len(doc))
print(doc.parents_of("@I3@")) # both directions of the family link
```
## The pieces
| `decode` | Bytes to text: BOMs, UTF-8/16, ANSEL, Windows-1252, and disagreements between the declared and the actual encoding, all reported |
| `Document` / `Node` | The document model: parse, address, edit, render — with the byte-preservation invariant enforced by the API, not by convention |
| `dates`, `names`, `tags`, `view` | Read-only projections for display, sorting, and search — never a rewrite of the payload. Dates accept varied real-world shapes (BCE included) and can be *written* via `DatePoint::to_gedcom` |
| `family` | The family graph: parents, children, partners merged from both directions of the link, because real files are often one-sided |
| `validate` | Plausibility findings: contradictions and missing citations, reported with the numbers they were drawn from, never enforced |
| `convert` | Explicit 5.5.1 ↔ 7.0 conversion that reports every construct it touched and keeps what the target cannot hold |
| `schema` | GEDCOM 7 `HEAD.SCHMA` extension declarations |
| `gedzip` | The GEDZIP container, read and written under the same bounds (default-on feature; turn it off for a zero-dependency parser core) |
## How it compares
`ged_io` is a good, actively maintained Rust choice when you want a
*normalizing* model — parse into typed structs, write canonical GEDCOM out —
and don't need the original bytes to survive.
| Byte-preserving round-trip | guaranteed | no (normalizes) | read-only | no (normalizes) |
| GEDCOM 7 + GEDZIP | yes | yes | 5.5.1 only | 5.5.1 only |
| Encoding-mismatch reporting | yes | silent choice | silent choice | silent choice |
| Hostile-input bounds | size, depth, counts | file size | — | — |
| Version conversion | reported per construct | — | — | — |
| Python + browser bindings | yes | — | JS only | — |
| License | AGPL-3.0-or-later | MIT | MIT | MIT |
## How it is tested
- The [gedcom7code/test-files](https://github.com/gedcom7code/test-files)
corpus, vendored and run on every `cargo test`, byte-for-byte.
- The FamilySearch GEDCOM specification's own machine-readable tables
(ABNF and generated TSVs, vendored at a pinned tag), driving conformance
tests so a spec patch release becomes a failing diff — the first run
caught twelve vocabulary gaps.
- **Differential validation**: every corpus file's verdict compared against
GED-inline and GedValidate. Zero files this reader refuses that either
oracle reads clean.
- Archive interop against ZIPs written by an independent implementation,
Zip64 included; deterministic property suites; corpus-seeded mutation
fuzzing (`tests/endurance.rs`) and libFuzzer targets (`fuzz/`).
- Fetch-gated suites over the gedcom.io samples, the 5.5.1 torture files,
the Eichmann character-set challenges, and sixteen real producer exports
from Family Tree Maker, Ancestry, RootsMagic, Legacy, PAF, and others.
## Extension tags
The library refuses to hard-code anyone's extension vocabulary as *meaning*.
`tags::vendor_label` offers display labels for the vendor tags real files
carry (`_APID`, `_FSFTID`, `_FREL`, …), grounded in a fourteen-producer
corpus — but they stay badged as extensions, are never rewritten, and never
validated. Callers label their own tags on top of `tags`, and pass their
`(tag, documentation URI)` pairs to `convert::to_version_7_with` so version 7
output declares them; foreign extensions are declared as undocumented rather
than dropped.
## Features
- `gedzip` — the GEDZIP container (on by default). With
`default-features = false` the parser core has **no dependencies at all**.
- `serde` — `Serialize` on the views and reports, for callers that send them
to a webview or over a wire. Off by default.
- `fixture` — a deterministic, corpus-shaped synthetic tree generator of any
size, for performance and stress tests. Off by default.
## AI disclosure
This project was created with the help of AI — specifically OpenAI's
[Codex](https://openai.com/codex/). That includes the library code, bindings,
tests, CI configuration, and documentation, with design decisions and
validation made by a human. See [`AGENTS.md`](AGENTS.md) for the conventions AI
contributors are expected to follow.
## License
**AGPL-3.0-or-later** (see [LICENSE](LICENSE)). In short: use, study, and
change it freely; if you distribute it — or run a modified copy as a network
service — pass the same freedoms on, source included. The copyright holder
can grant separate terms; open an issue to ask.
The library's code is original to this project. What it learned from
elsewhere, it learned as published fact — the GEDCOM specifications' tag and
date grammars, ANSEL's Appendix-D mapping, Windows-1252 — not as copied
expression.
## Acknowledgements
- [gedcom7code/test-files](https://github.com/gedcom7code/test-files)
(Unlicense) — the vendored parser corpus this crate is proven against.
- The [FamilySearch GEDCOM specification](https://gedcom.io/) and its
machine-readable `extracted-files` (Apache-2.0), vendored under
`fixtures/vendored/gedcom-spec/` with their license text; conformance
tests are generated from them, not hand-restated.
- Heiner Eichmann's GEDCOM 5.5 torture tests and character-set challenges,
and the gedcom.io sample files — used at test time only, fetched by
script, and never redistributed here (their terms differ from this
project's license).
- [GED-inline](https://github.com/nigel-parker/gedinline) (Nigel Munro
Parker, MIT) and
[GedValidate](https://github.com/ArmidaleSoftware/gedcom7) (Armidale
Software, MIT) — the outside validators in the differential test gate.
GEDCOM is a trademark of Intellectual Reserve, Inc. This project is not
affiliated with or endorsed by FamilySearch.