gedcomkit 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. gedcomkit-0.1.5/CHANGELOG.md +126 -0
  2. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/README.md +3 -3
  6. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/bindings/gedcomkit-py/Cargo.lock +2 -2
  7. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/bindings/gedcomkit-py/Cargo.toml +1 -1
  8. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/bindings/gedcomkit-py/src/lib.rs +76 -0
  9. gedcomkit-0.1.5/src/gedzip.rs +2344 -0
  10. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/requirement_corpus.rs +3 -2
  11. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/version_conversion.rs +3 -2
  12. gedcomkit-0.1.4/CHANGELOG.md +0 -56
  13. gedcomkit-0.1.4/src/gedzip.rs +0 -965
  14. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/LICENSE +0 -0
  15. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/bindings/gedcomkit-py/LICENSE +0 -0
  16. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/bindings/gedcomkit-py/README.md +0 -0
  17. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  18. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/README.md +0 -0
  19. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  20. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  21. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  22. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  23. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  24. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  25. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  26. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  27. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  28. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  29. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  30. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  31. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  32. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  33. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  34. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  35. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  36. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  37. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  38. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  39. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  40. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  41. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  42. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  43. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  44. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  45. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  46. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  47. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  48. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  49. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  50. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  51. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  52. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  53. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  54. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  55. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  56. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  57. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  58. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  59. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  60. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  61. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  62. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  63. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  64. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  65. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  66. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  67. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  68. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  69. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  70. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  71. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  72. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  73. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  74. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  75. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  76. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  77. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  78. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  79. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/zip-interop/README.md +0 -0
  80. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  81. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  82. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/pyproject.toml +0 -0
  83. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/convert.rs +0 -0
  84. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/dates.rs +0 -0
  85. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/decode.rs +0 -0
  86. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/family.rs +0 -0
  87. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/lib.rs +0 -0
  88. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/names.rs +0 -0
  89. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/schema.rs +0 -0
  90. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/synthetic.rs +0 -0
  91. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/tags.rs +0 -0
  92. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/validate.rs +0 -0
  93. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/src/view.rs +0 -0
  94. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/endurance.rs +0 -0
  95. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/external_charset.rs +0 -0
  96. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/properties.rs +0 -0
  97. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/spec_conformance.rs +0 -0
  98. {gedcomkit-0.1.4 → gedcomkit-0.1.5}/tests/vendored_corpus.rs +0 -0
@@ -0,0 +1,126 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. The
4
+ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
+ the project uses semantic versioning within the usual pre-1.0 compatibility
6
+ rules.
7
+
8
+ ## [0.1.5] - 2026-09-18
9
+
10
+ ### Added
11
+
12
+ - `gedzip::ArchiveWriter`, a streaming GEDZIP writer: entries are added one
13
+ at a time and written straight to a sink, so peak memory is a small
14
+ multiple of the largest single entry plus the growing central directory,
15
+ never the archive's total. Measured, one `add` runs from roughly 2x the
16
+ entry (already-compressed media, stored) to a little over 3.5x (highly
17
+ compressible text, which briefly holds the source, the deflate output and
18
+ the encoder's state together) — never a function of how many entries came
19
+ before. A hash set of every lowercased path added so far is resident for
20
+ the writer's lifetime too, and at the default 100,000-entry limit the
21
+ central directory is bounded but not small.
22
+ It produces Zip64 structures once the entry count, an entry's size or the
23
+ running offset needs them, so it has no archive-size ceiling; the local
24
+ header's Zip64 extra carries both the original and compressed size
25
+ together whenever either overflows, as APPNOTE 4.5.3 requires for readers
26
+ that resolve local headers while streaming. Entry names carry
27
+ general-purpose bit 11 (UTF-8). A write failure to the sink poisons the
28
+ writer, so no entry is ever written next to bytes a failed write left
29
+ truncated, and `finish` flushes the sink before returning it rather than
30
+ leaving the last bytes to a `Drop` that cannot report failure.
31
+ `ArchiveWriter` implements `Debug` — entry count, bytes written, the
32
+ active limits and whether it is poisoned, never the sink or the directory
33
+ buffer's contents.
34
+ - `gedzip::Archive::total_size`, the sum of an opened archive's entries'
35
+ claimed decompressed sizes, read from the central directory alone — so a
36
+ caller can ask what extracting will cost before decompressing anything.
37
+ - Python and JavaScript/WASM bindings for both: `ArchiveWriter` (`new`,
38
+ `add`, `finish`) and `archive_total_size`. Both bindings write to an
39
+ in-memory `Vec<u8>` sink and copy it again when `finish` hands the bytes
40
+ to the host language, so — unlike the Rust API given a real sink such as
41
+ a file — they do not get the streaming memory benefit yet; their
42
+ docstrings say so.
43
+
44
+ ### Fixed
45
+
46
+ Both entries below change `gedzip::write_archive`, which has shipped since
47
+ 0.1.0. Everything under **Added** is new in this release and never shipped
48
+ broken; these two are what moves under an existing caller.
49
+
50
+ - **`write_archive` now sets general-purpose bit 11 (UTF-8 names)**, so a
51
+ non-ASCII path round-trips as itself instead of being read back as IBM
52
+ code page 437 mush (confirmed against Python 3.12 `zipfile` and Java 17
53
+ `ZipFile`; .NET's `ZipArchive` happened to decode the unflagged form
54
+ correctly, and 7-Zip guesses). Directory entries also now carry the MS-DOS
55
+ directory external attribute (`0x10`) instead of `0`. **This changes the
56
+ bytes `write_archive` emits**: on the same five-entry input, exactly
57
+ eleven bytes differ from 0.1.4 — the flags field on every local and
58
+ central record, and the external-attribute byte on each directory entry.
59
+ Versions, timestamps, CRCs, sizes, offsets and names are unchanged. No
60
+ fixture or test pinned the previous bytes.
61
+ - **`write_archive` now refuses one value earlier**, at the Zip64 sentinel
62
+ rather than past it. It rejected a count or size only once it *exceeded*
63
+ what a plain ZIP field holds, so it accepted a count of exactly 65,535
64
+ entries, or a size, compressed size, directory offset or directory size of
65
+ exactly 4,294,967,295 — but those values *are* the sentinels a reader
66
+ takes as "see the Zip64 record", which `write_archive` never writes. It
67
+ emitted the bare sentinel, and this crate's own `Archive::open` then
68
+ refused the result ("the archive needs a Zip64 directory and has none")
69
+ while Python, Java, 7-Zip and `bsdtar` accepted the broken archive.
70
+ **An input that produced a malformed archive in 0.1.4 is now an error**,
71
+ so a caller writing exactly 65,535 entries sees a refusal where it
72
+ previously saw a file. The design is unchanged — `write_archive` still
73
+ refuses rather than emit Zip64 — only the boundary moved from `>` the
74
+ sentinel to `>=` it, for the entry count and every 32-bit size and offset
75
+ field alike. Confirmed at 65,535 (refused), 65,534 and 4,294,967,294
76
+ (accepted and read back) against this crate's reader, Python 3.12, Java 17
77
+ `ZipFile` and `ZipInputStream`, 7-Zip 26.02, `bsdtar` and .NET's
78
+ `ZipArchive`.
79
+
80
+ ## [0.1.4] - 2026-09-16
81
+
82
+ ### Changed
83
+
84
+ - Migrated Cargo release publishing to crates.io Trusted Publishing through
85
+ GitLab OIDC, retaining the API token only as a transition fallback.
86
+
87
+ ## [0.1.3] - 2026-09-16
88
+
89
+ ### Changed
90
+
91
+ - Constrained PyPI Trusted Publishing to the protected `release` environment.
92
+ - Recorded the completed npm OIDC migration and the legacy token removal.
93
+
94
+ ## [0.1.2] - 2026-09-16
95
+
96
+ ### Changed
97
+
98
+ - Switched npm release publishing to GitLab OIDC Trusted Publishing.
99
+ - Documented the synchronized multi-registry release checklist.
100
+
101
+ ## [0.1.1] - 2026-09-16
102
+
103
+ ### Changed
104
+
105
+ - Synchronized the Rust core, Python binding, and JavaScript/WASM binding at
106
+ version 0.1.1 for the next registry release.
107
+ - Added protected-tag publishing for Cargo, PyPI, and npm, with clean
108
+ registry-install verification documented for all three packages.
109
+
110
+ ## [0.1.0] - 2026-09-08
111
+
112
+ ### Added
113
+
114
+ - Archival GEDCOM 5.5 through 7.x parsing, decoding, views, validation, and
115
+ reported version conversion.
116
+ - Bounded GEDZIP reading and writing.
117
+ - Python and JavaScript/WASM bindings over the same Rust core.
118
+ - Byte-exact vendored corpus, conformance, property, interoperability, and
119
+ adversarial test suites.
120
+
121
+ ### Security
122
+
123
+ - Refuse traversal, platform-special, duplicate, and central/local-mismatched
124
+ GEDZIP paths before returning entry bytes.
125
+ - Stage and checksum external fixture downloads before making them visible.
126
+ - Reject line injection through programmatically constructed GEDCOM fields.
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.4"
38
+ version = "0.1.5"
39
39
  dependencies = [
40
40
  "flate2",
41
41
  "serde",
@@ -3,7 +3,7 @@
3
3
  # was checked on crates.io, npm, and PyPI before the initial release.
4
4
  [package]
5
5
  name = "gedcomkit"
6
- version = "0.1.4"
6
+ version = "0.1.5"
7
7
  description = "A byte-preserving GEDCOM document model: decoding, parsing, readings, version conversion, plausibility checks, and the GEDZIP container, for GEDCOM 5.5 through 7.x."
8
8
  edition = "2024"
9
9
  # The true minimum, not the workspace's toolchain: a parser library is a leaf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gedcomkit
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -203,9 +203,9 @@ than dropped.
203
203
 
204
204
  ## AI disclosure
205
205
 
206
- This project was created with the help of AI — specifically OpenAI's
207
- [Codex](https://openai.com/codex/). That includes the library code, bindings,
208
- tests, CI configuration, and documentation, with design decisions and
206
+ This project was created with the help of AI — specifically Anthropic's
207
+ [Claude](https://claude.com/claude-code). That includes the library code,
208
+ bindings, tests, CI configuration, and documentation, with design decisions and
209
209
  validation made by a human. See [`AGENTS.md`](AGENTS.md) for the conventions AI
210
210
  contributors are expected to follow.
211
211
 
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.4"
38
+ version = "0.1.5"
39
39
  dependencies = [
40
40
  "flate2",
41
41
  "serde",
@@ -43,7 +43,7 @@ dependencies = [
43
43
 
44
44
  [[package]]
45
45
  name = "gedcomkit-py"
46
- version = "0.1.4"
46
+ version = "0.1.5"
47
47
  dependencies = [
48
48
  "gedcomkit",
49
49
  "pyo3",
@@ -2,7 +2,7 @@
2
2
  # abi3 keeps it to one wheel per platform across Python versions.
3
3
  [package]
4
4
  name = "gedcomkit-py"
5
- version = "0.1.4"
5
+ version = "0.1.5"
6
6
  description = "Python bindings for gedcomkit: an archival-grade GEDCOM library — byte-preserving round-trip, encoding detection, GEDCOM 5.5 through 7.x, GEDZIP."
7
7
  edition = "2024"
8
8
  rust-version = "1.88"
@@ -7,11 +7,13 @@
7
7
  //! record and structure path. Everything else stays in Rust.
8
8
 
9
9
  use gedcomkit::family::FamilyGraph;
10
+ use gedcomkit::gedzip::{self, Archive};
10
11
  use gedcomkit::view::{FamilyView, IndividualView, RecordView};
11
12
  use gedcomkit::{Document, EncodingReport};
12
13
  use pyo3::exceptions::{PyKeyError, PyValueError};
13
14
  use pyo3::prelude::*;
14
15
  use pyo3::types::{PyBytes, PyModule};
16
+ use pyo3::wrap_pyfunction;
15
17
  use serde::Serialize;
16
18
 
17
19
  /// A parsed GEDCOM document and the report of how its bytes were read.
@@ -219,6 +221,78 @@ impl Gedcom {
219
221
  }
220
222
  }
221
223
 
224
+ /// Builds a GEDZIP archive one entry at a time. Peak memory per `add` call
225
+ /// is a small multiple of the entry just added, not the archive's total —
226
+ /// roughly 2x for already-compressed media (stored rather than deflated) up
227
+ /// to a little over 3.5x for highly compressible text, plus the growing
228
+ /// central directory; see `gedzip::ArchiveWriter`'s own documentation for
229
+ /// the full accounting. This binding's sink is an in-memory buffer rather
230
+ /// than a real file, though, so `finish` copies that whole buffer again to
231
+ /// hand the bytes to Python — the streaming benefit a real file sink gets
232
+ /// in Rust does not yet extend past this binding's boundary.
233
+ /// `write_archive`-equivalent behavior for a whole list already in memory
234
+ /// is not bound separately — build one of these and call `add` in a loop
235
+ /// instead.
236
+ #[pyclass]
237
+ struct ArchiveWriter {
238
+ /// `None` once `finish` has been called.
239
+ inner: Option<gedzip::ArchiveWriter<Vec<u8>>>,
240
+ }
241
+
242
+ #[pymethods]
243
+ impl ArchiveWriter {
244
+ /// Starts a new archive under the default bounds
245
+ /// (`gedcomkit.gedzip.ArchiveLimits` has no Python binding yet; this
246
+ /// uses the library's defaults).
247
+ #[staticmethod]
248
+ fn new() -> Self {
249
+ Self {
250
+ inner: Some(gedzip::ArchiveWriter::new(Vec::new())),
251
+ }
252
+ }
253
+
254
+ /// Adds one entry. Raises `ValueError` for an unsafe or repeated path,
255
+ /// one that exceeds the writer's per-entry limit, or a call after
256
+ /// `finish`.
257
+ fn add(&mut self, name: &str, data: &[u8]) -> PyResult<()> {
258
+ let writer = self
259
+ .inner
260
+ .as_mut()
261
+ .ok_or_else(|| PyValueError::new_err("finish() was already called"))?;
262
+ writer
263
+ .add(name, data)
264
+ .map_err(|error| PyValueError::new_err(error.to_string()))
265
+ }
266
+
267
+ /// Writes the central directory and returns the finished archive's
268
+ /// bytes. Valid to call once; raises `ValueError` on a second call.
269
+ fn finish<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
270
+ let writer = self
271
+ .inner
272
+ .take()
273
+ .ok_or_else(|| PyValueError::new_err("finish() was already called"))?;
274
+ let bytes = writer
275
+ .finish()
276
+ .map_err(|error| PyValueError::new_err(error.to_string()))?;
277
+ Ok(PyBytes::new(py, &bytes))
278
+ }
279
+ }
280
+
281
+ impl Default for ArchiveWriter {
282
+ fn default() -> Self {
283
+ Self::new()
284
+ }
285
+ }
286
+
287
+ /// The sum of a GEDZIP archive's entries' claimed decompressed sizes, read
288
+ /// from its central directory alone — nothing is decompressed to answer
289
+ /// this. Raises `ValueError` for bytes that do not open as an archive.
290
+ #[pyfunction]
291
+ fn archive_total_size(data: &[u8]) -> PyResult<u64> {
292
+ let archive = Archive::open(data).map_err(|error| PyValueError::new_err(error.to_string()))?;
293
+ Ok(archive.total_size())
294
+ }
295
+
222
296
  /// An archival-grade GEDCOM library: byte-preserving round-trip, encoding
223
297
  /// detection and reporting, GEDCOM 5.5 through 7.x.
224
298
  ///
@@ -229,6 +303,8 @@ impl Gedcom {
229
303
  #[pyo3(name = "gedcomkit")]
230
304
  fn gedcomkit_py(module: &Bound<'_, PyModule>) -> PyResult<()> {
231
305
  module.add_class::<Gedcom>()?;
306
+ module.add_class::<ArchiveWriter>()?;
307
+ module.add_function(wrap_pyfunction!(archive_total_size, module)?)?;
232
308
  module.add("__version__", env!("CARGO_PKG_VERSION"))?;
233
309
  Ok(())
234
310
  }