fix(sdk): DSPX-4590 zip64 conformance - #3981
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe ZIP stream implementation now improves ZIP64 detection and extra-field parsing, widens central-directory offset arithmetic, adds configurable ZIP64 thresholds, rejects field overflow, and adds conformance and fuzz coverage. ChangesZIP64 support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SegmentWriter
participant CentralDirectory
participant ZIP64Records
SegmentWriter->>CentralDirectory: finalize entries with configured threshold
CentralDirectory->>CentralDirectory: check sizes and offsets
CentralDirectory-->>SegmentWriter: ZIP32 or ZIP64 decision
SegmentWriter->>ZIP64Records: write ZIP64 records when required
Suggested reviewers: Merge Risk: ⚪ Minimal · up to The ZIP64 conformance fixes appear internally consistent and covered by focused tests; no actionable merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit reads each line, Comment |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
f57c73b to
01d6d43
Compare
X-Test Failure Reportopentdf |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
01d6d43 to
eaf68f8
Compare
X-Test Failure Report |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
eaf68f8 to
2e0d5ec
Compare
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
2e0d5ec to
67a7320
Compare
X-Test Failure Report |
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
80e1538 to
fa3a416
Compare
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
fa3a416 to
7a4ca54
Compare
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
7a4ca54 to
dc98348
Compare
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
go-sdk's zip layer disagrees with the other SDKs in a handful of places. This addresses findings 1-6 from the DSPX-4590 investigation (finding 7, per-segment size defaults, is the parent PR this one is stacked on). ## Finding 1 (interop): writer switched to ZIP64 at 4 GiB instead of 2 GiB `Finalize` compared against `^uint32(0)`, so a payload between 2 GiB and 4 GiB was written as a zip32 archive with a value in the top half of the unsigned 32-bit range. java-sdk widens those central-directory fields *signed*, so deployed Java clients read the size/offset back as a negative number and cannot open the container. web-sdk always writes ZIP64, java-sdk (since java-sdk#393) switches at `Integer.MAX_VALUE`. - New `maxNonZip64Value = math.MaxInt32` in `zip_primitives.go`, mirroring java-sdk's `MAX_NON_ZIP64_VALUE`. - The rule is applied to the uncompressed size, the compressed size **and** `entry.Offset` (the local-header offset), which was previously not checked at all -- an archive under 2 GiB of payload could still place a later entry's header past the boundary. - The threshold is injectable: `Config.MaxNonZip64Value` plus a `WithMaxNonZip64Value` option (clamped to `(0, maxNonZip64Value]`, so it can only ever make the writer *more* eager to use ZIP64). This lets the tests drive the ZIP64 path with a 1 KiB threshold instead of allocating gigabytes. ## Finding 2: ZIP64 extra field parsed positionally The reader assumed the ZIP64 extended-information field was the first entry in the extra-field area and that all three values were always present. Per APPNOTE 4.5.3 the values appear in the order *original size, compressed size, local header offset*, and each is present **only** when its central-directory counterpart holds the `0xFFFFFFFF` sentinel. A container whose extra area leads with, say, an extended-timestamp field (tag `0x5455`) was misparsed. `parseZip64ExtraField` now walks the whole extra area, skips foreign tags, reads values in spec order gated on the sentinels, and rejects a field that claims to run past the end of the area. ## Finding 3: ZIP64 detected from the CD offset alone `NewReader` only looked at `CentralDirectoryOffset == 0xFFFFFFFF`. An archive that overflows the entry count (`0xFFFF`) or the central-directory size but not the offset was read as zip32. `eocdNeedsZip64` now checks all three EOCD sentinel fields. ## Finding 4: per-entry ZIP64 extra field ignored without a ZIP64 EOCD A writer may put a ZIP64 extra field on an individual entry while leaving the EOCD in zip32 form. The reader now consults the extra field whenever the central-directory header carries a sentinel, independent of the EOCD form. ## Finding 5: central-directory cursor could wrap at uint16 `nextCD` was advanced with uint16 arithmetic and did not include the file comment. A 65000-byte filename plus a 600-byte extra field wraps to 110 and the reader walks into the middle of a header. The advance is now done in uint64 and includes `FileCommentLength`. ## Finding 6: silent truncation when a value does not fit 32 bits The zip32 paths narrowed with a bare cast. `checkFitsInCentralDirectory` now returns a new `ErrFieldOverflow` for the compressed size, uncompressed size, local-header offset, central-directory size/offset and entry count instead of writing a corrupt archive. (With finding 1 in place this is unreachable in normal operation; it is a backstop against future callers.) ## Tests `sdk/internal/zipstream/zip64_conformance_test.go` -- hand-assembles raw zip archives (`buildRawZip`) so the reader can be pointed at containers no Go writer would produce: extra field not first, differing compressed and uncompressed sizes so the APPNOTE 4.5.3 ordering is actually asserted (a fixture with equal sizes passes either way), per-entry ZIP64 under a zip32 EOCD, a central-directory file comment, the 65646-byte name+extra case that wraps to 110 at uint16, ZIP64 implied by the entry count, and a malformed extra field. Writer side: `TestWriterSwitchesToZip64AtInjectedThreshold` uses the injected 1 KiB threshold, `TestEntryNeedsZip64AtTwoGiB` pins `maxNonZip64Value == math.MaxInt32` and covers size / compressed size / offset, `TestCentralDirectoryNarrowingGuard` covers finding 6. `sdk/internal/zipstream/fuzz_test.go` -- two new seeds for the `nextCD` overflow and the file-comment case. ## Follow-up required in opentdf/tests (NOT covered by this PR) > The xtest cell `test_tdfs.py::test_chunky_roundtrip` currently **SKIPS** for > go, because `xtest/sdk/go/cli.sh` answers no to `supports chunky`. That shim > lives in the `opentdf/tests` repo, so merging this PR does **not** flip it -- > the go column will stay skipped and the interop regression will stay > invisible in CI. When this fix ships in a release, someone needs to > version-gate the `chunky)` case in `xtest/sdk/go/cli.sh` so it reports > support at or above that version. Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
dc98348 to
38472ff
Compare
Invalidated by push of 38472ff
Benchmark results, click to expandBenchmark authorization.GetDecisions Results:
Benchmark authorization.v2.GetMultiResourceDecision Results:
Benchmark Statistics
Bulk Benchmark Results
TDF3 Benchmark Results:
|
|
Jira: https://virtru.atlassian.net/browse/DSPX-4590
Part 3 of a 3-PR stack: #3933 (base) <- #3979 <- this PR.
Based on #3979 (default per-segment sizes when a writer omits them), which
is itself based on #3933. This PR carries findings 1-6 of the DSPX-4590
investigation (go-sdk's ZIP64/APPNOTE conformance issues); finding 7
(per-segment size defaults) is fixed in #3979 below it and is unrelated to
these changes -- the two were originally one PR (#3967) and are split here
so each can be reviewed independently.
go-sdk's zip layer disagrees with the other SDKs in a handful of places,
and this addresses all six.
Finding 1 (interop): writer switched to ZIP64 at 4 GiB instead of 2 GiB
Finalizecompared against^uint32(0), so a payload between 2 GiB and 4 GiBwas written as a zip32 archive with a value in the top half of the unsigned
32-bit range. java-sdk widens those central-directory fields signed, so
deployed Java clients read the size/offset back as a negative number and cannot
open the container. web-sdk always writes ZIP64, java-sdk (since java-sdk#393)
switches at
Integer.MAX_VALUE.maxNonZip64Value = math.MaxInt32inzip_primitives.go, mirroringjava-sdk's
MAX_NON_ZIP64_VALUE.entry.Offset(the local-header offset), which was previously not checkedat all -- an archive under 2 GiB of payload could still place a later
entry's header past the boundary.
Config.MaxNonZip64Valueplus aWithMaxNonZip64Valueoption (clamped to(0, maxNonZip64Value]), so thetests can drive the ZIP64 path with a 1 KiB threshold instead of
allocating gigabytes.
Finding 2: ZIP64 extra field parsed positionally
The reader assumed the ZIP64 extended-information field was first in the
extra-field area and that all three values were always present. Per APPNOTE
4.5.3 the values appear in the order original size, compressed size, local
header offset, and each is present only when its central-directory
counterpart holds the
0xFFFFFFFFsentinel.parseZip64ExtraFieldnow walksthe whole extra area, skips foreign tags, reads values in spec order gated on
the sentinels, and rejects a field that claims to run past the end of the
area.
Finding 3: ZIP64 detected from the CD offset alone
NewReaderonly looked atCentralDirectoryOffset == 0xFFFFFFFF. An archivethat overflows the entry count (
0xFFFF) or the central-directory size butnot the offset was read as zip32.
eocdNeedsZip64now checks all three EOCDsentinel fields.
Finding 4: per-entry ZIP64 extra field ignored without a ZIP64 EOCD
A writer may put a ZIP64 extra field on an individual entry while leaving
the EOCD in zip32 form. The reader now consults the extra field whenever the
central-directory header carries a sentinel, independent of the EOCD form.
Finding 5: central-directory cursor could wrap at uint16
nextCDwas advanced with uint16 arithmetic and did not include the filecomment. A 65000-byte filename plus a 600-byte extra field wraps to 110 and
the reader walks into the middle of a header. The advance is now done in
uint64 and includes
FileCommentLength.Finding 6: silent truncation when a value does not fit 32 bits
The zip32 paths narrowed with a bare cast.
checkFitsInCentralDirectorynowreturns a new
ErrFieldOverflowfor the compressed size, uncompressed size,local-header offset, central-directory size/offset and entry count instead
of writing a corrupt archive. (With finding 1 in place this is unreachable
in normal operation; it is a backstop against future callers.)
Tests
sdk/internal/zipstream/zip64_conformance_test.go-- hand-assembles raw ziparchives (
buildRawZip) so the reader can be pointed at containers no Gowriter would produce: extra field not first, differing compressed and
uncompressed sizes so the APPNOTE 4.5.3 ordering is actually asserted, per-
entry ZIP64 under a zip32 EOCD, a central-directory file comment, the
65646-byte name+extra case that wraps to 110 at uint16, ZIP64 implied by the
entry count, and a malformed extra field. Writer side:
TestWriterSwitchesToZip64AtInjectedThresholduses the injected 1 KiBthreshold,
TestEntryNeedsZip64AtTwoGiBpinsmaxNonZip64Value == math.MaxInt32and covers size / compressed size / offset,TestCentralDirectoryNarrowingGuardcovers finding 6.sdk/internal/zipstream/fuzz_test.go-- two new seeds for thenextCDoverflow and the file-comment case.
Follow-up required in opentdf/tests (NOT covered by this PR)
Testing
cd sdk && go test ./... -racemake fmt,make lint(0 new issues)Supersedes the zip64 portion of #3967, which is left open, unmodified, for
reference.
Summary by CodeRabbit
Bug Fixes
New Features