| Document | draft-fossil-artifact-format-01 |
| Section | 4.7.3, B Card (a delta manifest whose baseline is itself a delta) |
| Type | Technical (the named delta's delete markers are not covered; Fossil's handling beyond the file listing is not recorded) |
| Status | Applied in draft-fossil-artifact-format-01 (2026-10-14) |
| Reported | 2026-10-14 |
| Submitter | Dan Mestas |
| Found via | A review of the drafts from libfossil; confirmed by running Fossil 2.28 and reading its source |
| Amends | artifact-format-00-s4.7.3-delta-of-delta.md (2026-10-11) |
1. Summary
The earlier erratum made §4.7.3 say that Fossil accepts a delta manifest
whose B card names another delta manifest, computes its file set one level
deep, and reports no error. That is what Fossil's file listing does. Two
things are missing.
- The named delta may carry delete markers (
Fcards with no hash). In the one-level merge they arrive on the baseline side of the walk, where §8.2's steps never expect a hashless entry. §8.2 does require consumers to filter entries without a hash, but the text only names the delta's own tail. - Fossil is not consistent on such an artifact beyond the listing. Its archive commands crash on the named delta's delete markers, and the file-change rows it records when crosslinking the check-in do not describe the change from the parent. The draft's "no error is reported" reads as if Fossil handled the artifact cleanly.
2. The text in error
Page 22 (§4.7.3):
Fossil does not enforce this on read: it parses and cross-links a delta manifest whose baseline is itself a delta and computes its file set from that delta's own F cards and the named artifact's own F cards only. Files the named artifact inherits from its own baseline are absent from the result, and no error is reported. A reader MUST accept such an artifact and MUST compute its file set in the same way (Section 8.2); rejecting it, or resolving it recursively, diverges from Fossil.
3. What Fossil implements
manifest_file_next() in src/manifest.c merges a delta's F cards with
its baseline's. Entries taken from the baseline side are returned as they
are, with no test for a missing hash, because a baseline manifest never
carries a delete marker. When the baseline is itself a delta, its delete
markers come back as file entries with a null hash. fossil ls skips them.
zip.c:811 and the matching loops in tar.c and the sqlar writer pass the
null hash to uuid_to_rid() and crash.
Checked with Fossil 2.28 [52445a27f1]. Each repository was built by
committing check-ins with fossil commit, adding one hand-written check-in
C2 (B and P both naming the delta check-in C1, one F card for b),
and running fossil reconstruct. fossil rebuild gives the same rows.
Case 1: A holds a b c; C1 is a delta that edits a.
| Command on C2 | Result |
|---|---|
fossil ls -r C2 |
a b (one level: c is dropped) |
fossil zip, tarball |
archive of a b, exit 0 |
| mlink rows for C2 | b changed; a changed from itself to itself; no row for c |
Case 2: A holds a b c; C1 is a delta that deletes c (F c, no hash).
| Command on C2 | Result |
|---|---|
fossil ls -r C2 |
b |
fossil zip, tarball, sqlar |
segmentation fault, exit 139 |
| mlink rows for C2 | b changed; c changed from nothing to nothing; no row for a |
In both cases the parent's file set differs from C2's by files that the mlink rows do not mention, and the rows that are written record no change.
zit trunk, given the same repositories through import-fossil, lists
the same file sets, writes the archives with the same files, and does not
crash, because it filters hashless entries as §8.2 requires.
4. Corrected text
Replace the paragraph quoted in Section 2 with:
Fossil does not enforce this on read: it parses and cross-links a delta manifest whose baseline is itself a delta and computes its file set from that delta's own F cards and the named artifact's own F cards only. Files the named artifact inherits from its own baseline are absent from the result, and the file listing reports no error. A reader MUST accept such an artifact and MUST compute its file set in the same way (Section 8.2); rejecting it, or resolving it recursively, diverges from Fossil. The named artifact's delete markers enter that walk on the baseline side, and like the delta's own they add no file: a consumer MUST filter out every merged entry without a hash.
This paragraph is informative. Fossil 2.28 is not consistent on such an artifact beyond its file listing. Its zip, tarball, and sqlar commands do not filter the named artifact's delete markers and terminate with a segmentation fault when it carries one. The file-change records it writes when it cross-links the check-in do not describe the difference between the parent's file set and the check-in's: files the one-level rule drops get no record, while unchanged files and delete markers can be recorded as changes. This document does not define those records for such a check-in.
5. Observable consequence of the uncorrected text
A reader that applies §8.2's steps literally to the baseline side can treat a hashless baseline entry as a file and fail to look it up, which is Fossil's crash. A reader that compares itself against Fossil's file-change history for such a check-in will find differences the draft does not explain.
6. Implementations
zit already filters hashless entries from both sides of the merge. The change for an implementation that does not is to drop every merged entry without a hash before using the file set.