fix(sdk): DSPX-4589 zip64 EOCD sentinels, truncated archive detection, and UTF-8 entry names - #398
Draft
dmihalcik-virtru wants to merge 1 commit into
Draft
Conversation
…, UTF-8 entry names Three zip container conformance fixes found while auditing the TDF zip container against PKWARE APPNOTE.TXT. 1. ZipWriter only set the zip64 flag on the end of central directory record when the entry count exceeded 0xFF or the central directory offset/size exceeded 0xFFFF. Those masks do not match the field widths: the entry count is 2 bytes and the offset and size are 4 bytes each. Archives with between 256 and 65534 entries were needlessly promoted to zip64, and the offset/size checks now go through needsZip64 so they honor the same 2 GiB ceiling as the per-entry fields. 2. ZipReader treated a short read while scanning backwards for the end of central directory signature as a signature match, so a truncated archive could fall out of the scan loop and parse whatever followed as an end of central directory record. It now only breaks on a real match and throws InvalidZipException otherwise, and rejects an archive too small to hold the zip64 locator it claims to have. 3. ZipWriter computed the central directory filename length from String.length() rather than from the UTF-8 encoded byte count. The value was assigned to a field that write() never read, so the bytes on the wire were already correct, but the dead field is removed, the name is encoded once instead of twice, and a name too long for the 2 byte length field is now rejected instead of silently truncated.
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Jira: https://virtru.atlassian.net/browse/DSPX-4589
Stack — this is 2 of 2. Split out of #396.
main)main)Three zip container conformance findings from the audit against PKWARE APPNOTE.TXT. Each was written against pre-#393
mainand re-verified against currentmain(98c839e2) before being changed; the honest status of each is below. None of the three has field impact — the one that did is #397.Finding 1 — end of central directory sentinel thresholds (fixed)
ZipWriter.finish()decided whether the archive needed a zip64 end of central directory record with:None of those masks match the real field widths. The entry count field is 2 bytes, not 1, and the central directory offset and size fields are 4 bytes each, not 2. The practical effect was that any archive with 256 or more entries was needlessly promoted to zip64, and the offset/size checks fired three orders of magnitude too early.
Now:
The offset and size go through
needsZip64, so they honor the same deliberate 2 GiB (Integer.MAX_VALUE) ceiling as the per-entry fields — released readers widen these unsigned wire fields with signed reads, so we never write a value that would come back negative. UsingneedsZip64also keeps theZipWriter(out, maxNonZip64Value)test seam that #393 added usable for these two fields.Not touched by #393 — verified with
jj diff -r 98c839e2.Finding 2 — truncated archive detection (fixed, but the branch was latent)
ZipReader.readEndOfCentralDirectory()scanned backwards for the EOCD signature and treated anullfromreadInteger()(a short read) as if it were a signature match, so a truncated archive could fall out of the loop and parse whatever bytes followed as an EOCD record. It now only breaks on a genuine match and throwsInvalidZipExceptionif the scan runs off the front, and separately rejects an archive too small to hold the zip64 locator its EOCD claims to have.Stated plainly: with the
SeekableInMemoryByteChannelthe SDK actually uses, the scan position is always<= size - 22, soreadInteger()always had four bytes available and thenullbranch was unreachable. This is a correctness/robustness fix (aFileChannelcould in principle short-read), not a bug anyone was hitting. The new tests still pass on unpatched code for the plain-truncation cases; they are there to lock the behavior in.Finding 3 — UTF-8 filename length (already correct on the wire; dead code removed)
The ticket says the central directory filename length was computed from
String.length(). That assignment did exist:but
CDFileHeader.write()never read the field — it wrote(short) filename.lengthfrom the already-encoded byte array. The bytes on the wire were already correct. No archive was ever mis-written.What changed: the misleading dead
filenameLengthfield is removed from bothLocalFileHeaderandCDFileHeader, the name is encoded to UTF-8 once instead of twice, and a newencodeFilenamehelper rejects a name whose encoded length exceeds0xFFFFwith anSDKExceptionrather than silently truncating it into the 2-byte field.Tests
7 new tests:
ZipWriterTest— one test per EOCD sentinel driver: entry count (0xFFFE non-zip64 vs 0xFFFF zip64, round-tripped throughZipReader), central directory offset, and central directory size, each isolated so only the EOCD is zip64 and no entry is; plusfilenameLengthIsMeasuredInUtf8Bytes("🔒両.txt",String.length()7 vs 11 UTF-8 bytes, asserted at both header offsets) andrejectsAnEntryNameTooLongToDescribe.ZipReaderTest—testTruncatedArchiveIsRejected(four truncation shapes) andtestArchiveTooShortForTheZip64LocatorIsRejected, both assertingInvalidZipException.The sentinel tests were confirmed to be genuine regression tests by reverting the EOCD mask fix and watching them fail.
End-to-end validation
None of these three findings has an xtest cell of its own — they are unit-tested only. What the e2e runs give this PR is a no-regression signal: xtest against this branch (which is also #396's head, i.e. the whole stack) is green, including the
chunkycells that #397 fixes.opentdf/tests run 34357326964 ✅ —
java-ref=DSPX-4589-02-zip-format,force-supports=chunky, on theDSPX-4592-02-chunkybranch. Java job:82 passed, 22 skipped, no failures. All five javatest_chunky_roundtrippairs PASSED, none skipped.The fix-vs-control comparison that proves #397 actually does something lives in #397.