fix: detect empty ZIP archives by their EOCD magic bytes - #181
Conversation
📝 WalkthroughWalkthroughThe ZIP and ZIPX catalog rules now detect empty archives through the EOCD signature. Magic-number tests cover normal ZIP files, empty ZIP records, and spanned ZIP markers. ChangesEmpty ZIP detection
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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. Comment |
|
Thanks for the PR. Just had a first look. This is an area where we need to be cautious. First of all the bug is valid in the sense that the 2 4-byte sequences are invalid. But actually because I made a typo there at the 3rd byte. instead of This was a copy-paste error from my side. However, We need test archives that cover all of those magic bytes use cases to be added to the test archives repo. Then some tests to prove that this is correct. |
…ned marker Add 50 4B 05 06 (End-of-Central-Directory) to the zip/zipx magic tests so a lone-EOCD empty ZIP is recognized. The same signature is the zip-spanned split marker, so add MagicNumberTests covering all cases: normal ZIP (50 4B 03 04), empty ZIP (EOCD, disk field 0), empty-not-flagged-as-spanned, and a spanned terminal volume (EOCD, nonzero disk) kept as spanned. 392 CoreTests pass.
db63ee9 to
c6aad86
Compare
|
Addressed your review in
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Modules/Tests/CoreTests/MagicNumberTests.swift`:
- Around line 50-61: Add fixture-backed detect(for:) tests covering empty ZIP,
non-empty ZIP, and spanned ZIP archives, using the project’s established
test-archive fixtures rather than synthetic bytes or temporary .bin files. Add
corresponding ZIPX coverage; if ZIPX detection is intentionally extension-only
and limited to .zipx, remove its shared ZIP magic entry from Catalog.json
instead.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 832f8876-9401-45ea-977f-b1a0ec12f523
📒 Files selected for processing (1)
Modules/Tests/CoreTests/MagicNumberTests.swift
| // MARK: - ZIP magic-bytes coverage (EOCD vs spanned-marker collision) | ||
| // | ||
| // A normal ZIP begins with the local-file-header signature `50 4B 03 04`, | ||
| // but an EMPTY zip is just a lone 22-byte End-of-Central-Directory record | ||
| // (`50 4B 05 06` + 18 zero bytes). That same `50 4B 05 06` signature is | ||
| // also the zip-spanned split marker in Catalog.json — these tests prove the | ||
| // two uses do not collide: an empty EOCD is matched as plain `zip`, while a | ||
| // spanned terminal volume (EOCD with a non-zero disk field) still wins the | ||
| // spanned classification. Each case uses in-memory bytes + a temp file | ||
| // (same shape as the fixture-less EdgeCaseTests), since the real | ||
| // `empty.zip` / spanned fixtures live in the MacPacker-TestArchives | ||
| // submodule (a maintainer follow-up to add there). |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect the ZIP, ZIPX, and split-volume catalog rules.
rg -n -C 12 \
'50 4B 05 06|50 4B 07 08|"zip"|"zipx"' \
Modules/Sources/Core/Formats/Catalog.json
# Locate real archive fixtures.
fd -HI -t f . | rg -i \
'(^|/)(empty|spanned|defaultArchive|.*\.zipx?)($|[./])' || true
# Check for ZIPX and public-detector assertions.
rg -n -C 4 \
'magicNumberEmptyZip|emptyZipEocd|spannedZip|zipx|detect\(for:' \
Modules/Tests Modules/Sources || trueRepository: sarensw/MacPacker
Length of output: 24941
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Inspect the relevant test and implementation slices without executing repository code.
sed -n '1,150p' Modules/Tests/CoreTests/MagicNumberTests.swift
printf '\n--- detector outline candidates ---\n'
fd -HI -t f 'ArchiveType.*' Modules/Sources Modules/Tests | sort
for f in $(fd -HI -t f 'ArchiveType.*' Modules/Sources Modules/Tests | sort); do
echo "--- $f ---"
wc -l "$f"
ast-grep outline "$f" 2>/dev/null | sed -n '1,220p' || true
done
printf '\n--- ZIPX references ---\n'
rg -n -C 3 '\bzipx\b|\.zipx\b|zipx' Modules/Tests Modules/Sources
printf '\n--- fixtures around Catalog.json format definitions and split rules ---\n'
sed -n '513,600p' Modules/Sources/Core/Formats/Catalog.jsonRepository: sarensw/MacPacker
Length of output: 13811
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Read the detector source sections that determine extension lookup order,
# detection source, and how split markers override detection results.
sed -n '1,235p' Modules/Sources/Core/ArchiveSupport/ArchiveTypeDetector.swift
printf '\n--- ZIPX fixture/detection coverage ---\n'
rg -n -C 4 'defaultArchive\.zipx|\.zipx|zipx' Modules/Tests/CoreTests Modules/Sources/Core/Formats/Catalog.json Modules/Sources/Core/ArchiveSupport/ArchiveTypeDetector.swiftRepository: sarensw/MacPacker
Length of output: 12346
Add fixture-backed ZIP detection coverage.
The new cases use synthetic bytes and temp .bin files. The spanned case only changes one EOCD field, and the comment defers empty.zip and spanned fixture support. Add fixture-backed detect(for:) coverage for empty, non-empty, and spanned ZIP archives. Also add ZIPX coverage, or remove ZIPX’s shared ZIP magic from Catalog.json if ZIPX detection is extension-only and constrained to .zipx.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Modules/Tests/CoreTests/MagicNumberTests.swift` around lines 50 - 61, Add
fixture-backed detect(for:) tests covering empty ZIP, non-empty ZIP, and spanned
ZIP archives, using the project’s established test-archive fixtures rather than
synthetic bytes or temporary .bin files. Add corresponding ZIPX coverage; if
ZIPX detection is intentionally extension-only and limited to .zipx, remove its
shared ZIP magic entry from Catalog.json instead.
|
I'm with CodeRabbit here. Creating synthesized files that look exactly what we try to proof is no valid test. We have several test archives in the TestArchives submodule. Including standard zip files ( |
Worker report — w-top-macpacker-1
Task: Find ONE real NEW functional bug in MacPacker, verify at HEAD, fix minimally with a regression test, run the gate, COMMIT (no push).
Repo:
sarensw/MacPacker(forkYuriNachos/MacPacker), defaultmain.Base (upstream/main):
3de1e7c—fix: extract a selected folder as one folder, not its loose contents (#177).Worktree HEAD after fix:
db63ee9—fix: detect empty ZIP archives by their EOCD magic bytes(1 commit on top of base; not pushed).1. Dup-check (done first)
YuriNachos, all states): fix: stop under-counting the XAD total uncompressed size by one byte per directory #176 (XAD uncompressed-size per-directory undercount), fix: strip split-volume suffix when naming the extract-to folder #171 (split-volume suffix in extract-to-folder name), fix: strip uppercase archive extensions when deriving the extract-to-folder name #170 (uppercase ext in extract-to-folder name), lang: localize the Edited marker in the window subtitle #168 (localize "Edited" subtitle). → Avoided: XAD size math, extract-to-folder naming, localization.pincetgore/fix/finder-toolbar-localization) — none covers ZIP magic detection.fix/swc-lz4-extract-key(commit4fcd3c9"fix: LZ4 extract-to-folder left the destination empty") already fixes the ArchiveSwcEngine result-key bug ([UUID(): …]→[items.first!.id: …]). The whole SWC-extract area was therefore treated as touched and left alone, including a separate (real, non-duplicate) bug there:ArchiveSwcEngine.extract(_:to:)swallows LZ4 decompression errors and reports success. Not fixed here to stay out of the in-flight area.2. The bug
File:
Modules/Sources/Core/Formats/Catalog.json—zip(lines 522–524) andzipx(lines 542–544) format magic rules.Both listed three offset-0 signatures:
The End-of-Central-Directory signature
50 4B 05 06(0x06054b50) — which is the first bytes of an empty ZIP archive — was not listed (it appears only as a splitmarkerat line 590, never as an offset-0 format signature).Concrete reproducer: A valid empty ZIP is a lone 22-byte EOCD record (
50 4B 05 06+ 18 zero bytes; no entries, no comment). Given a file like that with no recognized extension,ArchiveTypeDetector.detectByMagicNumber(for:)matched none of03 04/03 06/03 08, returnednil, andArchiveLoader.loadEntriesthrewinvalidArchive("… is not a recognised archive type.")— MacPacker refused to open a valid (empty) ZIP. The two bogus entries could also only ever cause harm: a non-archive file whose first four bytes happen to be50 4B 03 06/50 4B 03 08would be misdetected as ZIP.Verified at HEAD
3de1e7cwith a regression test (see §5): before the fix the test fails withresult → nil.3. The fix (minimal)
Replace the two invalid signatures with the EOCD signature, in both
zipandzipx:Non-empty ZIPs still match
50 4B 03 04(so all existing detection is unchanged — confirmed: all 18 cases inMagicNumberTestsstill pass). Empty ZIPs now match50 4B 05 06. ZIP-record signatures are 32-bit little-endian;50 4B 05 06is the spec-defined EOCD (0x06054b50), so the fix is correct by the ZIP spec, not a guess.4. Files changed (only the fix + test)
No other modules, formatting, deps, or strings touched. (No changelog entry added — see §7.)
5. Regression test (red → green)
Modules/Tests/CoreTests/MagicNumberTests.swift→ new@Test func magicNumberEmptyZip():builds a 22-byte EOCD-only empty ZIP in the temp dir with a
.binextension (forces magic detection), then assertsdetectByMagicNumberreturnstype.id == "zip",source == .magic.3de1e7c, before Catalog.json edit):magicNumberEmptyZip() failed … Expectation failed: (result → nil) != nil(3 issues). All 18 existing magic cases passed.db63ee9):magicNumberEmptyZip() passed; all 18 existing magic cases still passed.The test synthesizes the EOCD bytes (spec-exact, 22 bytes), consistent with how
EdgeCaseTestsalready synthesizes tiny byte files forArchiveTypeDetector. It does not add an archive fixture to the submodule (none is needed for a magic-byte detection test).6. Gate (both green)
git submodule update --init --recursivewas run first (required for the build;CSevenZip/vendor/7zip+MacPacker-TestArchives).7. Notes for the owner at push time
Do not push / open a PR was honored: commit
db63ee9is local on branchYuriNachos/w-top-macpacker-1-2only.Changelog entry deferred.
Config/products/macpacker.jsonchangelogrequiresissuesto be non-empty, and the only honest value is this PR's own number — which does not exist until the PR is open (the repo's own rule: "add it in a second push. Never guess one."). There is no matching open issue. Suggested entry to add with the PR number at push time (newest block is0.20.0, not tagged):{ "type": "fix", "title": { "en": "Open empty ZIP archives", /* …translate per existing languages… */ }, "issues": ["<this PR's number>"] }Catalog.json is a
.copyresource of theCoretarget, so the edit takes effect after a rebuild (verified by the green test run).8. PR-body draft (for the later push)
Summary
Empty ZIP archives (a lone End-of-Central-Directory record) were not recognized by content and were rejected as "not a recognised archive type" when the file had no recognized extension. The zip/zipx magic rules listed two non-existent ZIP signatures and omitted the EOCD signature.
Root cause
Catalog.jsonlisted50 4B 03 06and50 4B 03 08as offset-0 ZIP signatures. Neither is a real ZIP record signature (ZIP signatures are 32-bit little-endian; no record decodes to those bytes). The valid first-bytes of an empty ZIP — the EOCD signature50 4B 05 06— was missing, so an EOCD-only file matched no rule.Changes
Modules/Sources/Core/Formats/Catalog.json: zip and zipx offset-0 signatures are now50 4B 03 04(local file header) and50 4B 05 06(EOCD). Non-empty ZIPs are unaffected.Modules/Tests/CoreTests/MagicNumberTests.swift:magicNumberEmptyZip()builds a 22-byte EOCD-only ZIP and asserts it is detected aszipby magic (fails before, passes after).Test plan
cd Modules && swift test→ 389 tests pass (includes the new test and all 18 existing magic-number cases).xcodebuild -scheme MacPacker -configuration Debug build→ BUILD SUCCEEDED.Summary by CodeRabbit
Bug Fixes
Tests