gedcomkit 0.1.9__tar.gz → 0.1.10__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.9 → gedcomkit-0.1.10}/CHANGELOG.md +36 -0
  2. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/Cargo.lock +1 -1
  3. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/Cargo.toml +1 -1
  4. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/PKG-INFO +1 -1
  5. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/Cargo.lock +2 -2
  6. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/Cargo.toml +1 -1
  7. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/src/lib.rs +20 -0
  8. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/build.rs +426 -0
  9. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/lib.rs +109 -5
  10. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/LICENSE +0 -0
  11. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/README.md +0 -0
  12. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/LICENSE +0 -0
  13. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/README.md +0 -0
  14. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/bindings/gedcomkit-py/gedcomkit.pyi +0 -0
  15. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/LICENSE +0 -0
  16. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/README.md +0 -0
  17. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/cardinalities.tsv +0 -0
  18. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/enumerations.tsv +0 -0
  19. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/enumerationsets.tsv +0 -0
  20. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/grammar.abnf +0 -0
  21. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/grammar.gedstruct +0 -0
  22. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/payloads.tsv +0 -0
  23. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom-spec/substructures.tsv +0 -0
  24. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/age-invalid.ged +0 -0
  25. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/age-valid.ged +0 -0
  26. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/atsign.ged +0 -0
  27. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_ascii_1.ged +0 -0
  28. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_ascii_2.ged +0 -0
  29. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf16be-1.ged +0 -0
  30. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf16be-2.ged +0 -0
  31. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf16le-1.ged +0 -0
  32. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf16le-2.ged +0 -0
  33. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf8-1.ged +0 -0
  34. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf8-2.ged +0 -0
  35. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/char_utf8-3.ged +0 -0
  36. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/date-all.ged +0 -0
  37. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/date-dual-invalid.ged +0 -0
  38. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/date-dual-valid.ged +0 -0
  39. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/enum-ext.ged +0 -0
  40. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/filename-1.ged +0 -0
  41. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/lang-all.ged +0 -0
  42. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/notes-1.ged +0 -0
  43. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/obje-1.ged +0 -0
  44. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/obsolete-1.ged +0 -0
  45. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/pedi-1.ged +0 -0
  46. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/rela_1.ged +0 -0
  47. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/sour-1.ged +0 -0
  48. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/tiny-1.ged +0 -0
  49. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/5/xref-case.ged +0 -0
  50. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/age-invalid.ged +0 -0
  51. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/age-valid.ged +0 -0
  52. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/atsign.ged +0 -0
  53. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_ascii_1.ged +0 -0
  54. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_ascii_2.ged +0 -0
  55. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf16be-1.ged +0 -0
  56. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf16be-2.ged +0 -0
  57. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf16le-1.ged +0 -0
  58. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf16le-2.ged +0 -0
  59. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf8-1.ged +0 -0
  60. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf8-2.ged +0 -0
  61. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/char_utf8-3.ged +0 -0
  62. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/date-all.ged +0 -0
  63. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/date-dual-invalid.ged +0 -0
  64. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/enum-ext.ged +0 -0
  65. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/filename-1.ged +0 -0
  66. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/lang-all.ged +0 -0
  67. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/notes-1.ged +0 -0
  68. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/obje-1.ged +0 -0
  69. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/obsolete-1.ged +0 -0
  70. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/pedi-1.ged +0 -0
  71. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/rela_1.ged +0 -0
  72. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/sour-1.ged +0 -0
  73. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/tiny-1.ged +0 -0
  74. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/7/xref-case.ged +0 -0
  75. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/LICENSE +0 -0
  76. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/gedcom7code/UPSTREAM-README.md +0 -0
  77. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/zip-interop/README.md +0 -0
  78. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/zip-interop/python-zip64.gdz +0 -0
  79. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/fixtures/vendored/zip-interop/python-zipfile.gdz +0 -0
  80. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/pyproject.toml +0 -0
  81. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/convert.rs +0 -0
  82. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/dates.rs +0 -0
  83. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/decode.rs +0 -0
  84. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/family.rs +0 -0
  85. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/gedzip.rs +0 -0
  86. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/names.rs +0 -0
  87. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/schema.rs +0 -0
  88. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/synthetic.rs +0 -0
  89. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/tags.rs +0 -0
  90. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/validate.rs +0 -0
  91. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/src/view.rs +0 -0
  92. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/endurance.rs +0 -0
  93. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/external_charset.rs +0 -0
  94. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/properties.rs +0 -0
  95. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/requirement_corpus.rs +0 -0
  96. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/spec_conformance.rs +0 -0
  97. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/vendored_corpus.rs +0 -0
  98. {gedcomkit-0.1.9 → gedcomkit-0.1.10}/tests/version_conversion.rs +0 -0
@@ -5,6 +5,42 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  the project uses semantic versioning within the usual pre-1.0 compatibility
6
6
  rules.
7
7
 
8
+ ## [0.1.10] - 2026-09-27
9
+
10
+ ### Added
11
+
12
+ - `Node::fit_line_limit`, `Document::fit_line_limit` (the same, addressed by
13
+ a record's cross-reference identifier), and the `MAX_LINE_LENGTH` constant
14
+ for GEDCOM 5.5.1's 255-byte line, for a caller to fit an edited record to
15
+ a pre-7.0 document's line limit just before serializing:
16
+ `set_logical_value` only ever splits a payload across `CONT` at the
17
+ newlines it is given, so a long edited line was otherwise written over
18
+ length. Only nodes with no verbatim source line are touched, checked
19
+ recursively, so a node no caller reached into keeps its exact bytes; a
20
+ pointer payload is never split either. The line is measured with a
21
+ 2-byte terminator reserve — wider than `wrap_long_lines`' 1 byte — because
22
+ this is meant for a caller whose own pipeline may keep a source file's
23
+ `\r\n` terminator rather than always writing `\n`. Bound in both language
24
+ bindings as `fit_line_limit` (Python) and `fitLineLimit` (WASM), mirroring
25
+ `wrap_long_lines`/`wrapLongLines`.
26
+
27
+ ### Fixed
28
+
29
+ - `Document::next_xref` matched its prefix case-sensitively, so in a file
30
+ using lowercase identifiers (`@i1@`) `next_xref("I")` minted `@I1@` —
31
+ which `Document::record`'s case-insensitive lookup resolves to the
32
+ existing `@i1@`, silently colliding with it. The prefix match is now
33
+ case-insensitive, and the returned identifier is checked against every
34
+ existing xref case-insensitively and advanced past any collision. Its
35
+ doc comment claimed "the lowest unused" identifier while returning one
36
+ past the highest; the comment now says that.
37
+ - The same method minted `highest + 1` with unchecked arithmetic: a hostile
38
+ or fuzzed file carrying `@I18446744073709551615@` (`u64::MAX`) made it
39
+ panic in a debug build and wrap around to `@I0@` in release. Arithmetic
40
+ is now checked throughout, and once the series above the high-water mark
41
+ is exhausted, `next_xref` falls back to the lowest unused number instead
42
+ of overflowing.
43
+
8
44
  ## [0.1.9] - 2026-09-27
9
45
 
10
46
  ### Fixed
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.9"
38
+ version = "0.1.10"
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.9"
6
+ version = "0.1.10"
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.9
3
+ Version: 0.1.10
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -35,7 +35,7 @@ dependencies = [
35
35
 
36
36
  [[package]]
37
37
  name = "gedcomkit"
38
- version = "0.1.9"
38
+ version = "0.1.10"
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.9"
46
+ version = "0.1.10"
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.9"
5
+ version = "0.1.10"
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"
@@ -167,6 +167,26 @@ impl Gedcom {
167
167
  wrapped.iter().map(|line| to_py(py, line)).collect()
168
168
  }
169
169
 
170
+ /// Fits one record inside a 5.5.x document's line limit
171
+ /// (`max_line_bytes`, terminator included; 255 is GEDCOM 5.5.1's limit)
172
+ /// by inserting `CONC` lines, addressed by cross-reference identifier.
173
+ /// Call this on a record just edited with `set_logical_value`, since
174
+ /// that only ever splits a payload at the newlines it is given. Returns
175
+ /// whether anything changed; a node still carrying its original,
176
+ /// unedited line is never touched. Raises `KeyError` when no record
177
+ /// answers to `xref`.
178
+ #[pyo3(signature = (xref, max_line_bytes = 255))]
179
+ fn fit_line_limit(&mut self, xref: &str, max_line_bytes: usize) -> PyResult<bool> {
180
+ let changed = self
181
+ .document
182
+ .fit_line_limit(xref, max_line_bytes)
183
+ .map_err(|_| PyKeyError::new_err(xref.to_owned()))?;
184
+ if changed {
185
+ self.graph = None;
186
+ }
187
+ Ok(changed)
188
+ }
189
+
170
190
  /// What decoding decided, one line — show it before accepting an import.
171
191
  #[getter]
172
192
  fn encoding(&self) -> String {
@@ -186,6 +186,222 @@ pub struct WrappedLine {
186
186
  /// The fewest bytes a line limit may leave for level, tag, and payload.
187
187
  const SMALLEST_LINE_LIMIT: usize = 32;
188
188
 
189
+ /// GEDCOM 5.5.1's line length limit, in bytes.
190
+ ///
191
+ /// The specification: "The total length of a GEDCOM line, including level
192
+ /// number, cross-reference number, tag, value, delimiters, and terminator,
193
+ /// must not exceed 255 (wide) characters." GEDCOM 7.0 removed `CONC` and
194
+ /// every length limit, so this only matters for a document that declares
195
+ /// 5.5.x. Pass it to [`Node::fit_line_limit`] or
196
+ /// [`Document::wrap_long_lines`] — the two reserve a different number of
197
+ /// bytes for the line terminator; see [`Node::fit_line_limit`]'s own
198
+ /// documentation for why.
199
+ pub const MAX_LINE_LENGTH: usize = 255;
200
+
201
+ /// Bytes [`Node::fit_line_limit`] reserves for the line terminator when
202
+ /// measuring whether a line fits: exactly 2, for a `\r\n` (CRLF)
203
+ /// terminator. [`Document::wrap_long_lines`] reserves only 1, for the
204
+ /// `\n` this crate's own writer emits; `fit_line_limit` reserves the wider
205
+ /// CRLF instead because callers of this function may hand the result to a
206
+ /// pipeline that preserves the source file's own terminator — CRLF
207
+ /// included — rather than always writing LF. A line this leaves alone is
208
+ /// therefore safe under either terminator convention.
209
+ const TERMINATOR_RESERVE: usize = 2;
210
+
211
+ /// The bytes a line spends before its payload, payload delimiter included:
212
+ /// level, a space, the identifier and a space when there is one, the tag,
213
+ /// and the space that introduces the payload.
214
+ fn segment_prefix_bytes(level: usize, xref: Option<&str>, tag: &str) -> usize {
215
+ level.to_string().len() + 1 + xref.map_or(0, |xref| xref.len() + 1) + tag.len() + 1
216
+ }
217
+
218
+ /// Fits one payload segment — a node's own line plus the `CONC` lines that
219
+ /// continue it without a newline — inside `max_line` bytes, the way
220
+ /// [`Node::fit_line_limit`] documents. `old_concs` is that segment's
221
+ /// existing trailing `CONC` children (already removed from the tree by the
222
+ /// caller); the return value is what should replace them.
223
+ ///
224
+ /// Left alone, unconditionally: a segment whose head carries a verbatim
225
+ /// source line (its line is fixed, not ours to move text out of), a
226
+ /// segment with a verbatim `CONC` in it (same reason), a pointer payload
227
+ /// (pointers are short; splitting one would corrupt it), and a segment that
228
+ /// already fits. Otherwise every byte of the segment's folded text — the
229
+ /// head's value followed by each old `CONC`'s value, exactly as
230
+ /// [`Node::logical_value`] would fold them — is recut with [`cut`] and
231
+ /// redistributed, which is what guarantees the fold-after-split round trip.
232
+ fn resplit_segment(
233
+ level: usize,
234
+ xref: Option<&str>,
235
+ tag: &str,
236
+ verbatim: Option<&str>,
237
+ value: &mut Option<String>,
238
+ old_concs: Vec<Node>,
239
+ max_line: usize,
240
+ ) -> (Vec<Node>, bool) {
241
+ if verbatim.is_some() || old_concs.iter().any(|conc| conc.verbatim.is_some()) {
242
+ return (old_concs, false);
243
+ }
244
+ if value.as_deref().is_some_and(is_xref) {
245
+ return (old_concs, false);
246
+ }
247
+
248
+ let head_budget =
249
+ max_line.checked_sub(TERMINATOR_RESERVE + segment_prefix_bytes(level, xref, tag));
250
+ let conc_budget =
251
+ max_line.checked_sub(TERMINATOR_RESERVE + segment_prefix_bytes(level + 1, None, "CONC"));
252
+ // Four bytes is the widest UTF-8 character: less and a cut may not be
253
+ // able to make progress, so the line is left as it is.
254
+ let (Some(head_budget), Some(conc_budget)) = (head_budget, conc_budget) else {
255
+ return (old_concs, false);
256
+ };
257
+ if head_budget < 4 || conc_budget < 4 {
258
+ return (old_concs, false);
259
+ }
260
+
261
+ let head_fits = value.as_deref().map_or(0, str::len) <= head_budget;
262
+ let concs_fit = old_concs
263
+ .iter()
264
+ .all(|conc| conc.value.as_deref().map_or(0, str::len) <= conc_budget);
265
+ if head_fits && concs_fit {
266
+ return (old_concs, false);
267
+ }
268
+
269
+ let mut text = value.clone().unwrap_or_default();
270
+ for conc in &old_concs {
271
+ text.push_str(conc.value.as_deref().unwrap_or_default());
272
+ }
273
+
274
+ let mut pieces = cut(&text, head_budget, conc_budget).into_iter();
275
+ let head_piece = pieces.next().unwrap_or_default();
276
+ *value = (!head_piece.is_empty()).then(|| head_piece.to_owned());
277
+ let new_concs = pieces
278
+ .map(|piece| Node::with_value("CONC", piece))
279
+ .collect();
280
+ (new_concs, true)
281
+ }
282
+
283
+ impl Node {
284
+ /// Fits this node and its substructures inside a 5.5.x document's line
285
+ /// limit — [`MAX_LINE_LENGTH`] for the specification's own 255 bytes —
286
+ /// by inserting `CONC` lines. Returns whether anything changed.
287
+ ///
288
+ /// Call this on the records a caller edited in a pre-7.0 document, just
289
+ /// before serializing: [`Node::set_logical_value`] only ever splits a
290
+ /// payload across `CONT` at the newlines it is given, so an edited note
291
+ /// with one long paragraph is otherwise written as a single over-long
292
+ /// line. `level` is the nesting level this node is rendered at — the
293
+ /// same number [`Node::at`]'s caller already tracks, since a node does
294
+ /// not store its own level (see the type's own documentation).
295
+ ///
296
+ /// Only nodes with no verbatim source line are touched, checked at
297
+ /// every node this walks into, recursively: a node still carrying its
298
+ /// verbatim line — meaning a caller never reached into it — is left
299
+ /// exactly as it was, whether or not its rendered line is over length.
300
+ /// That is what keeps this additive next to the byte-preservation
301
+ /// guarantee: it only ever rewrites a node through the same setters an
302
+ /// editing caller already surrendered the node's verbatim line to use.
303
+ /// A payload read as a pointer ([`Node::pointer`]) is never split
304
+ /// either — pointers are well under any real line limit, and cutting
305
+ /// one would corrupt it.
306
+ ///
307
+ /// A too-long line becomes its payload's head plus `CONC` children
308
+ /// inserted right after whatever `CONC` lines already continued that
309
+ /// same line (so an existing, already-fitting split is preserved in
310
+ /// place rather than reordered); a too-long `CONT` line is split the
311
+ /// same way, into that `CONT` followed by its own new `CONC` siblings.
312
+ /// Every cut lands on a UTF-8 character boundary and, whenever the
313
+ /// payload has one within the limit, on a byte with a non-space
314
+ /// character on each side, since GEDCOM 5.5.1 warns that a line broken
315
+ /// beside a space loses that space on re-joining. Failing that — a run
316
+ /// of spaces wider than the limit, say — the cut falls back to the
317
+ /// latest legal byte boundary instead, which is documented behavior of
318
+ /// [`cut`] this call reuses, not a special case here.
319
+ ///
320
+ /// The rendered line is measured in UTF-8 bytes, including level,
321
+ /// optional cross-reference, tag, delimiters, and a terminator reserved
322
+ /// at 2 bytes — a `\r\n` (CRLF) terminator, not the single-byte `\n`
323
+ /// this crate's own writer emits. That is deliberately wider than
324
+ /// [`Document::wrap_long_lines`]'s 1-byte reserve: this function exists
325
+ /// for a caller that fits one edited record before handing the document
326
+ /// on to something that may write it with the source file's own
327
+ /// terminator, CRLF included, so a line this leaves alone is safe under
328
+ /// either convention.
329
+ ///
330
+ /// ```
331
+ /// use gedcomkit::{Node, build::MAX_LINE_LENGTH};
332
+ ///
333
+ /// let mut note = Node::record("@N1@", "NOTE");
334
+ /// note.set_logical_value(&"word ".repeat(80));
335
+ /// let changed = note.fit_line_limit(0, MAX_LINE_LENGTH);
336
+ /// assert!(changed);
337
+ /// assert_eq!(note.logical_value(), "word ".repeat(80));
338
+ /// ```
339
+ pub fn fit_line_limit(&mut self, level: usize, max_line: usize) -> bool {
340
+ fit_node(self, level, max_line)
341
+ }
342
+ }
343
+
344
+ fn fit_node(node: &mut Node, level: usize, max_line: usize) -> bool {
345
+ let run_len = node
346
+ .children
347
+ .iter()
348
+ .take_while(|child| matches!(child.tag.as_str(), "CONT" | "CONC"))
349
+ .count();
350
+ let mut run: Vec<Node> = node.children.drain(0..run_len).collect();
351
+ let self_conc_end = run
352
+ .iter()
353
+ .position(|child| child.tag == "CONT")
354
+ .unwrap_or(run.len());
355
+ let self_concs: Vec<Node> = run.drain(0..self_conc_end).collect();
356
+
357
+ let xref = node.xref.clone();
358
+ let tag = node.tag.clone();
359
+ let verbatim = node.verbatim.clone();
360
+ let (mut output, mut changed) = resplit_segment(
361
+ level,
362
+ xref.as_deref(),
363
+ &tag,
364
+ verbatim.as_deref(),
365
+ &mut node.value,
366
+ self_concs,
367
+ max_line,
368
+ );
369
+ if changed {
370
+ node.verbatim = None;
371
+ }
372
+
373
+ let mut segments = run.into_iter().peekable();
374
+ while let Some(mut cont) = segments.next() {
375
+ let mut concs = Vec::new();
376
+ while segments.peek().is_some_and(|next| next.tag == "CONC") {
377
+ concs.push(segments.next().expect("peeked"));
378
+ }
379
+ let cont_verbatim = cont.verbatim.clone();
380
+ let (new_concs, seg_changed) = resplit_segment(
381
+ level + 1,
382
+ None,
383
+ "CONT",
384
+ cont_verbatim.as_deref(),
385
+ &mut cont.value,
386
+ concs,
387
+ max_line,
388
+ );
389
+ if seg_changed {
390
+ cont.verbatim = None;
391
+ }
392
+ changed |= seg_changed;
393
+ output.push(cont);
394
+ output.extend(new_concs);
395
+ }
396
+
397
+ let inserted = output.len();
398
+ node.children.splice(0..0, output);
399
+ for child in node.children.iter_mut().skip(inserted) {
400
+ changed |= fit_node(child, level + 1, max_line);
401
+ }
402
+ changed
403
+ }
404
+
189
405
  impl Document {
190
406
  /// Adds substructures after the existing ones of the structure at `path`
191
407
  /// in `record` — a header's `DATE` and `SUBM`, say, which
@@ -301,6 +517,14 @@ impl Document {
301
517
  /// specification's warning, since some readers strip the space at the
302
518
  /// end of a line and lose it from the joined value.
303
519
  ///
520
+ /// The terminator is reserved at 1 byte here, matching the single `\n`
521
+ /// this crate's own writer emits ([`Document::to_text`],
522
+ /// [`Document::to_bytes`]) — this function measures the document it is
523
+ /// about to write out itself. [`Node::fit_line_limit`] reserves 2 bytes
524
+ /// instead, for a caller that fits one record ahead of writing the
525
+ /// document through something else that may keep the source file's own
526
+ /// terminator, CRLF included.
527
+ ///
304
528
  /// This rewrites what it touches, so it is an explicit operation with a
305
529
  /// report rather than something writing does: a structure that fits is
306
530
  /// left exactly as it is, and one that does not has its payload's
@@ -354,6 +578,37 @@ impl Document {
354
578
  }
355
579
  Ok(wrapped)
356
580
  }
581
+
582
+ /// [`Node::fit_line_limit`] for one record, addressed by cross-reference
583
+ /// identifier — the `Document`-level entry point for it, the way
584
+ /// [`Document::record_mut`] is for reaching a record to edit it in the
585
+ /// first place. Records sit at level 0, so unlike [`Node::fit_line_limit`]
586
+ /// there is no level for the caller to track. Returns whether anything
587
+ /// changed.
588
+ ///
589
+ /// ```
590
+ /// use gedcomkit::{Document, Node, build::MAX_LINE_LENGTH};
591
+ ///
592
+ /// let mut document = Document::new_v551("EXAMPLE");
593
+ /// let mut note = Node::record("@N1@", "NOTE");
594
+ /// note.set_logical_value(&"word ".repeat(80));
595
+ /// document.append_records(vec![note])?;
596
+ ///
597
+ /// assert!(document.fit_line_limit("@N1@", MAX_LINE_LENGTH)?);
598
+ /// assert!(document.to_text().lines().all(|line| line.len() < MAX_LINE_LENGTH));
599
+ /// # Ok::<(), gedcomkit::GedcomError>(())
600
+ /// ```
601
+ ///
602
+ /// # Errors
603
+ ///
604
+ /// A [`GedcomErrorKind::Structure`] error when no record answers to
605
+ /// `xref`.
606
+ pub fn fit_line_limit(&mut self, xref: &str, max_line: usize) -> Result<bool, GedcomError> {
607
+ let record = self
608
+ .record_mut(xref)
609
+ .ok_or_else(|| fault(0, GedcomErrorKind::Structure, format!("no record {xref}")))?;
610
+ Ok(record.fit_line_limit(0, max_line))
611
+ }
357
612
  }
358
613
 
359
614
  fn wrap_node(
@@ -708,4 +963,175 @@ mod tests {
708
963
  assert!(!piece.ends_with(' '), "{piece:?}");
709
964
  }
710
965
  }
966
+
967
+ fn fits(node: &Node, max_line: usize) -> bool {
968
+ Document {
969
+ records: vec![node.clone()],
970
+ }
971
+ .to_text()
972
+ .lines()
973
+ .all(|line| line.len() < max_line)
974
+ }
975
+
976
+ #[test]
977
+ fn fit_line_limit_splits_a_long_edited_ascii_payload() {
978
+ let mut note = Node::record("@N1@", "NOTE");
979
+ let text = "word ".repeat(80);
980
+ note.set_logical_value(&text);
981
+ assert!(note.verbatim().is_none(), "an edited node has no verbatim");
982
+
983
+ assert!(note.fit_line_limit(0, MAX_LINE_LENGTH));
984
+ assert_eq!(note.logical_value(), text, "fold(split(x)) == x");
985
+ assert!(fits(&note, MAX_LINE_LENGTH));
986
+ assert!(
987
+ note.all("CONC").count() > 0,
988
+ "the overflow became CONC children"
989
+ );
990
+ // A second pass has nothing left to do.
991
+ assert!(!note.fit_line_limit(0, MAX_LINE_LENGTH));
992
+ }
993
+
994
+ #[test]
995
+ fn fit_line_limit_splits_multibyte_text_on_character_boundaries() {
996
+ let mut note = Node::record("@N1@", "NOTE");
997
+ let text = "Pointe-Claire, Île de Montréal, Québec ".repeat(12);
998
+ note.set_logical_value(text.trim_end());
999
+
1000
+ assert!(note.fit_line_limit(0, 80));
1001
+ assert_eq!(note.logical_value(), text.trim_end());
1002
+ assert!(fits(&note, 80));
1003
+ assert!(note.all("CONC").count() > 0);
1004
+ }
1005
+
1006
+ #[test]
1007
+ fn fit_line_limit_handles_a_space_at_every_candidate_boundary() {
1008
+ let mut note = Node::record("@N1@", "NOTE");
1009
+ // Every legal cut point in `x x x ...` has a space on at least one
1010
+ // side of it, exercising the fallback in `best_cut`.
1011
+ let text = "x ".repeat(200);
1012
+ note.set_logical_value(text.trim_end());
1013
+
1014
+ assert!(note.fit_line_limit(0, 40));
1015
+ assert_eq!(note.logical_value(), text.trim_end());
1016
+ assert!(fits(&note, 40));
1017
+ }
1018
+
1019
+ #[test]
1020
+ fn fit_line_limit_splits_an_over_long_cont_line_in_place() {
1021
+ let mut note = Node::record("@N1@", "NOTE");
1022
+ let long_second_line = "b".repeat(80);
1023
+ let logical = format!("short first line\n{long_second_line}\nlast line");
1024
+ note.set_logical_value(&logical);
1025
+ note.push(Node::with_value("SOUR", "@S1@"));
1026
+
1027
+ assert!(note.fit_line_limit(0, 40));
1028
+ assert_eq!(note.logical_value(), logical);
1029
+ assert!(fits(&note, 40));
1030
+ // The CONT run still comes before the untouched substructure that
1031
+ // followed it.
1032
+ assert_eq!(note.children.last().map(Node::tag), Some("SOUR"));
1033
+ let tags: Vec<&str> = note.children.iter().map(Node::tag).collect();
1034
+ assert_eq!(tags.last(), Some(&"SOUR"));
1035
+ assert_eq!(tags.iter().filter(|tag| **tag == "CONT").count(), 2);
1036
+ assert!(tags.contains(&"CONC"));
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn fit_line_limit_never_splits_a_pointer_payload() {
1041
+ let long_pointer = format!("@{}@", "I".repeat(300));
1042
+ let mut record = Node::record("@F1@", "FAM");
1043
+ record.push(Node::with_value("HUSB", long_pointer.clone()));
1044
+
1045
+ let husb = record.first_mut("HUSB").expect("HUSB");
1046
+ assert!(husb.pointer().is_some());
1047
+ assert!(!husb.fit_line_limit(1, MAX_LINE_LENGTH));
1048
+ assert_eq!(husb.value(), Some(long_pointer.as_str()));
1049
+ assert!(husb.all("CONC").next().is_none());
1050
+ }
1051
+
1052
+ #[test]
1053
+ fn fit_line_limit_never_touches_a_node_still_carrying_its_verbatim_line() {
1054
+ // The double space between the level and the xref is not what
1055
+ // canonical rendering would produce, so the parser keeps this
1056
+ // line's exact bytes — the mark of a node no caller has reached
1057
+ // into yet.
1058
+ let long = "y".repeat(300);
1059
+ let input = format!("0 HEAD\n0 @N1@ NOTE {long}\n0 TRLR\n");
1060
+ let mut document = Document::parse(&input).expect("parses");
1061
+ let note = document.record_mut("@N1@").expect("note");
1062
+ assert!(note.verbatim().is_some(), "kept because of the odd spacing");
1063
+
1064
+ let before = note.clone();
1065
+ assert!(!note.fit_line_limit(0, MAX_LINE_LENGTH));
1066
+ assert_eq!(*note, before, "an untouched node is not rewritten");
1067
+ assert_eq!(document.to_text(), input);
1068
+ }
1069
+
1070
+ #[test]
1071
+ fn fit_line_limit_recurses_past_an_untouched_verbatim_child() {
1072
+ // The record itself has no verbatim (it was appended fresh), but
1073
+ // its NOTE child is a kept verbatim line the caller never reached
1074
+ // into — that child must be left exactly as it was even though the
1075
+ // record around it is being fitted.
1076
+ let long = "z".repeat(300);
1077
+ let odd_note = format!("0 NOTE {long}\n");
1078
+ let reparsed = Document::parse(&format!("0 HEAD\n{odd_note}0 TRLR\n"))
1079
+ .expect("parses")
1080
+ .records
1081
+ .remove(1);
1082
+ assert!(reparsed.verbatim().is_some());
1083
+
1084
+ let mut record = Node::record("@I1@", "INDI");
1085
+ record.push(reparsed.clone());
1086
+ let mut edited_name = Node::with_value("NAME", "x".repeat(300));
1087
+ edited_name.set_logical_value(&"x".repeat(300));
1088
+ record.push(edited_name);
1089
+
1090
+ assert!(record.fit_line_limit(0, MAX_LINE_LENGTH));
1091
+ assert_eq!(
1092
+ record.children[0], reparsed,
1093
+ "the verbatim child is untouched"
1094
+ );
1095
+ assert!(
1096
+ record.children[1].all("CONC").count() > 0,
1097
+ "the edited sibling was still fitted"
1098
+ );
1099
+ }
1100
+
1101
+ #[test]
1102
+ fn fit_line_limit_covers_a_record_with_an_xref() {
1103
+ let mut record = Node::record("@I123456@", "INDI");
1104
+ record.push(Node::with_value("NOTE", "n".repeat(260)));
1105
+
1106
+ assert!(record.fit_line_limit(0, MAX_LINE_LENGTH));
1107
+ assert!(fits(&record, MAX_LINE_LENGTH));
1108
+ let note = record.first("NOTE").expect("note");
1109
+ assert_eq!(note.logical_value(), "n".repeat(260));
1110
+ }
1111
+
1112
+ #[test]
1113
+ fn document_fit_line_limit_reaches_one_record_by_xref() {
1114
+ let mut document = Document::new_v551("TEST");
1115
+ let mut note = Node::record("@N1@", "NOTE");
1116
+ note.set_logical_value(&"word ".repeat(80));
1117
+ document.append_records(vec![note]).expect("appended");
1118
+
1119
+ assert!(
1120
+ document
1121
+ .fit_line_limit("@N1@", MAX_LINE_LENGTH)
1122
+ .expect("record exists")
1123
+ );
1124
+ assert!(
1125
+ document
1126
+ .to_text()
1127
+ .lines()
1128
+ .all(|line| line.len() < MAX_LINE_LENGTH)
1129
+ );
1130
+
1131
+ let missing = document.fit_line_limit("@N9@", MAX_LINE_LENGTH);
1132
+ assert_eq!(
1133
+ missing.expect_err("no such record").kind(),
1134
+ GedcomErrorKind::Structure
1135
+ );
1136
+ }
711
1137
  }
@@ -884,22 +884,64 @@ impl Document {
884
884
  .is_some_and(|major| major >= 7)
885
885
  }
886
886
 
887
- /// The lowest unused cross-reference identifier with this prefix, in the
888
- /// `@I123@` style every producer uses.
887
+ /// One past the highest existing cross-reference identifier with this
888
+ /// prefix, in the `@I123@` style every producer uses — or, when that
889
+ /// would overflow `u64` (a hostile or fuzzed file can carry
890
+ /// `@I18446744073709551615@`, `u64::MAX`), the lowest unused number
891
+ /// instead. Arithmetic is checked throughout, so a file at or near the
892
+ /// top of the `u64` range is handled, never panics, and never wraps.
893
+ ///
894
+ /// The prefix match is case-insensitive, the same way [`Document::record`]
895
+ /// resolves identifiers: a file numbering its records `@i1@`, `@i2@` would
896
+ /// otherwise let `next_xref("I")` mint `@I1@`, which `record` treats as
897
+ /// the same identifier as the existing `@i1@`. The returned identifier is
898
+ /// also checked against every existing xref case-insensitively and
899
+ /// advanced past any collision, so the result is always genuinely unused
900
+ /// even when the file mixes padding or case within one prefix (an
901
+ /// `@I01@` alongside an `@I1@`, say).
889
902
  #[must_use]
890
903
  pub fn next_xref(&self, prefix: &str) -> String {
904
+ let prefix_upper = prefix.to_ascii_uppercase();
891
905
  let mut highest = 0u64;
892
906
  for record in &self.records {
893
907
  let Some(xref) = &record.xref else { continue };
894
- let inner = xref.trim_matches('@');
895
- let Some(digits) = inner.strip_prefix(prefix) else {
908
+ let inner = xref.trim_matches('@').to_ascii_uppercase();
909
+ let Some(digits) = inner.strip_prefix(prefix_upper.as_str()) else {
896
910
  continue;
897
911
  };
898
912
  if let Ok(number) = digits.parse::<u64>() {
899
913
  highest = highest.max(number);
900
914
  }
901
915
  }
902
- format!("@{prefix}{}@", highest + 1)
916
+ // Every existing identifier, uppercased, built once: a collision
917
+ // check through `Document::record` is a scan of the whole document
918
+ // per candidate, which a hostile file can turn quadratic.
919
+ let taken: HashSet<String> = self
920
+ .records
921
+ .iter()
922
+ .filter_map(|record| record.xref.as_ref())
923
+ .map(|xref| xref.to_ascii_uppercase())
924
+ .collect();
925
+ let unused = |number: u64| {
926
+ let xref = format!("@{prefix}{number}@");
927
+ (!taken.contains(&xref.to_ascii_uppercase())).then_some(xref)
928
+ };
929
+ // Above the high-water mark, skipping any case-insensitive collision.
930
+ // At most `taken.len()` candidates can collide, so this ends.
931
+ let mut candidate = highest.checked_add(1);
932
+ while let Some(number) = candidate {
933
+ if let Some(xref) = unused(number) {
934
+ return xref;
935
+ }
936
+ candidate = number.checked_add(1);
937
+ }
938
+ // The series above the high-water mark is exhausted — `highest` was
939
+ // `u64::MAX`, or every candidate up to it collided. The lowest unused
940
+ // number exists among the first `taken.len() + 1`, so this finds it
941
+ // without overflowing.
942
+ (1..=u64::MAX)
943
+ .find_map(unused)
944
+ .unwrap_or_else(|| format!("@{prefix}0@"))
903
945
  }
904
946
 
905
947
  /// Every pointer in the document whose target is not a record, reported as
@@ -1256,6 +1298,68 @@ mod tests {
1256
1298
  assert_eq!(document.next_xref("S"), "@S1@");
1257
1299
  }
1258
1300
 
1301
+ #[test]
1302
+ fn next_xref_matches_a_lowercase_files_prefix_case_insensitively() {
1303
+ let document = Document::parse("0 HEAD\n0 @i1@ INDI\n0 TRLR\n").expect("parse");
1304
+
1305
+ let minted = document.next_xref("I");
1306
+ assert_eq!(minted, "@I2@");
1307
+ assert!(
1308
+ document.record(&minted).is_none(),
1309
+ "a freshly minted xref must not already resolve to a record"
1310
+ );
1311
+ }
1312
+
1313
+ #[test]
1314
+ fn next_xref_treats_mixed_case_prefixes_as_the_same_series() {
1315
+ let document = Document::parse("0 HEAD\n0 @I1@ INDI\n0 @i5@ INDI\n0 @I3@ INDI\n0 TRLR\n")
1316
+ .expect("parse");
1317
+
1318
+ assert_eq!(document.next_xref("I"), "@I6@");
1319
+ assert_eq!(document.next_xref("i"), "@i6@");
1320
+ }
1321
+
1322
+ #[test]
1323
+ fn next_xref_skips_past_a_case_insensitive_collision() {
1324
+ // @I1@ and @i1@ both exist as distinct source lines (a malformed but
1325
+ // real-world file), so the numeric high-water mark alone is not
1326
+ // enough: candidate @I1@ would resolve back to the existing @i1@ via
1327
+ // `Document::record`'s case-insensitive lookup, and @i1@ likewise.
1328
+ let document = Document::parse(
1329
+ "0 HEAD\n0 @I1@ INDI\n1 NAME First\n0 @i1@ INDI\n1 NAME Second\n0 TRLR\n",
1330
+ )
1331
+ .expect("parse");
1332
+
1333
+ let minted = document.next_xref("I");
1334
+ assert!(document.record(&minted).is_none());
1335
+ assert_eq!(minted, "@I2@");
1336
+ }
1337
+
1338
+ #[test]
1339
+ fn next_xref_falls_back_to_the_lowest_unused_number_past_u64_max() {
1340
+ // A hostile or fuzzed file can carry `@I{u64::MAX}@`; `highest + 1`
1341
+ // would panic in debug and wrap in release. next_xref must do
1342
+ // neither, and must still return a genuinely unused identifier.
1343
+ let document =
1344
+ Document::parse(&format!("0 HEAD\n0 @I{}@ INDI\n0 TRLR\n", u64::MAX)).expect("parse");
1345
+
1346
+ let minted = document.next_xref("I");
1347
+ assert_eq!(minted, "@I1@", "falls back to the lowest unused number");
1348
+ assert!(document.record(&minted).is_none());
1349
+ }
1350
+
1351
+ #[test]
1352
+ fn next_xref_at_u64_max_minus_one_advances_to_u64_max_without_overflow() {
1353
+ // One below the top of the range: `highest + 1` is exactly
1354
+ // `u64::MAX`, which must not overflow computing it or checking it.
1355
+ let document = Document::parse(&format!("0 HEAD\n0 @I{}@ INDI\n0 TRLR\n", u64::MAX - 1))
1356
+ .expect("parse");
1357
+
1358
+ let minted = document.next_xref("I");
1359
+ assert_eq!(minted, format!("@I{}@", u64::MAX));
1360
+ assert!(document.record(&minted).is_none());
1361
+ }
1362
+
1259
1363
  #[test]
1260
1364
  fn version_is_read_from_the_header() {
1261
1365
  let five = Document::parse("0 HEAD\n1 GEDC\n2 VERS 5.5.1\n0 TRLR\n").expect("parse 5.5.1");
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes