Skip to content

fix: detect empty ZIP archives by their EOCD magic bytes - #181

Open
YuriNachos wants to merge 1 commit into
sarensw:mainfrom
YuriNachos:YuriNachos/w-top-macpacker-1-2
Open

fix: detect empty ZIP archives by their EOCD magic bytes#181
YuriNachos wants to merge 1 commit into
sarensw:mainfrom
YuriNachos:YuriNachos/w-top-macpacker-1-2

Conversation

@YuriNachos

@YuriNachos YuriNachos commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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 (fork YuriNachos/MacPacker), default main.
Base (upstream/main): 3de1e7cfix: extract a selected folder as one folder, not its loose contents (#177).
Worktree HEAD after fix: db63ee9fix: detect empty ZIP archives by their EOCD magic bytes (1 commit on top of base; not pushed).


1. Dup-check (done first)

2. The bug

File: Modules/Sources/Core/Formats/Catalog.jsonzip (lines 522–524) and zipx (lines 542–544) format magic rules.

Both listed three offset-0 signatures:

50 4B 03 04   ← local file header (valid)
50 4B 03 06   ← NOT a ZIP signature (0x06034b50 — no such record)
50 4B 03 08   ← NOT a ZIP signature (0x08034b50 — no such record)

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 split marker at 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 of 03 04/03 06/03 08, returned nil, and ArchiveLoader.loadEntries threw invalidArchive("… 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 be 50 4B 03 06/50 4B 03 08 would be misdetected as ZIP.

Verified at HEAD 3de1e7c with a regression test (see §5): before the fix the test fails with result → nil.

3. The fix (minimal)

Replace the two invalid signatures with the EOCD signature, in both zip and zipx:

50 4B 03 04   ← local file header (non-empty ZIPs — unchanged)
50 4B 05 06   ← EOCD (empty ZIPs — added)

Non-empty ZIPs still match 50 4B 03 04 (so all existing detection is unchanged — confirmed: all 18 cases in MagicNumberTests still pass). Empty ZIPs now match 50 4B 05 06. ZIP-record signatures are 32-bit little-endian; 50 4B 05 06 is the spec-defined EOCD (0x06054b50), so the fix is correct by the ZIP spec, not a guess.

4. Files changed (only the fix + test)

Modules/Sources/Core/Formats/Catalog.json      |  6 ++----   (zip + zipx: 03 06/03 08 → 05 06)
Modules/Tests/CoreTests/MagicNumberTests.swift  | 24 ++++++++++++++++   (+ magicNumberEmptyZip)
2 files changed, 26 insertions(+), 4 deletions(-)

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 .bin extension (forces magic detection), then asserts detectByMagicNumber returns type.id == "zip", source == .magic.

  • RED on buggy HEAD (3de1e7c, before Catalog.json edit):
    magicNumberEmptyZip() failed … Expectation failed: (result → nil) != nil (3 issues). All 18 existing magic cases passed.
  • GREEN after fix (db63ee9): magicNumberEmptyZip() passed; all 18 existing magic cases still passed.

The test synthesizes the EOCD bytes (spec-exact, 22 bytes), consistent with how EdgeCaseTests already synthesizes tiny byte files for ArchiveTypeDetector. It does not add an archive fixture to the submodule (none is needed for a magic-byte detection test).

6. Gate (both green)

cd Modules && swift test
# ✔ Test run with 389 tests in 117 suites passed after 14.989 seconds

xcodebuild -scheme MacPacker -configuration Debug build
# ** BUILD SUCCEEDED **  (ad-hoc codesigning; no CODE_SIGNING_ALLOWED workaround needed)

git submodule update --init --recursive was 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 db63ee9 is local on branch YuriNachos/w-top-macpacker-1-2 only.

  • Changelog entry deferred. Config/products/macpacker.json changelog requires issues to 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 is 0.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 .copy resource of the Core target, 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.json listed 50 4B 03 06 and 50 4B 03 08 as 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 signature 50 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 now 50 4B 03 04 (local file header) and 50 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 as zip by 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

    • Improved ZIP and ZIPX detection for empty archives.
    • Removed outdated signatures that could incorrectly identify certain files.
    • Improved distinction between empty and spanned ZIP archives.
  • Tests

    • Added coverage for standard ZIP files, empty ZIP archives, and spanned ZIP markers.
    • Verified detection using ZIP magic numbers and end-of-central-directory records.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Empty ZIP detection

Layer / File(s) Summary
ZIP and ZIPX signature rules
Modules/Sources/Core/Formats/Catalog.json
ZIP and ZIPX detection now use 50 4B 05 06 for empty archives.
ZIP magic-number validation
Modules/Tests/CoreTests/MagicNumberTests.swift
Tests create temporary ZIP data and verify normal ZIP detection, empty ZIP detection, and correct handling of spanned ZIP records.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: detecting empty ZIP archives through their EOCD magic bytes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sarensw

sarensw commented Aug 9, 2026

Copy link
Copy Markdown
Owner

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.

50 4B 03 04
50 4B 05 06 (empty archive)
50 4B 07 08 (spanned archive)

instead of

50 4B 03 04
50 4B 03 06 (empty archive)
50 4B 03 08 (spanned archive)

This was a copy-paste error from my side.

However, 50 4B 05 06 is already defined for split zip files (using spanned method) in the catalog. If this is changed now, the I'm not sure of the consequences.

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.
@YuriNachos
YuriNachos force-pushed the YuriNachos/w-top-macpacker-1-2 branch from db63ee9 to c6aad86 Compare August 9, 2026 22:05
@YuriNachos

Copy link
Copy Markdown
Contributor Author

Addressed your review in c6aad86. Added MagicNumberTests covering all ZIP magic-bytes cases so the empty-ZIP detection is provably non-colliding with the spanned marker:

  • Normal ZIP (50 4B 03 04 local file header) → detected zip by magic.
  • Empty ZIP (lone 22-byte EOCD 50 4B 05 06, disk/entry fields 0) → detected zip by magic.
  • Empty EOCD is NOT flagged as spanned — its disk field is 0, so the zip-spanned marker (which requires nonzero) does not match.
  • Spanned terminal volume (EOCD with a nonzero disk field) → still classified spanned (split.scheme == "spanned"), i.e. the plain-zip magic does not swallow it.

cd Modules && swift test392 tests pass. The real empty.zip / spanned fixtures belong in the MacPacker-TestArchives submodule (a separate repo I cannot push to); the tests use in-memory bytes + temp files in the same fixture-less style as EdgeCaseTests. Happy to leave the submodule fixtures as a maintainer follow-up if you prefer them there too.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between db63ee9 and c6aad86.

📒 Files selected for processing (1)
  • Modules/Tests/CoreTests/MagicNumberTests.swift

Comment on lines +50 to +61
// 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 || true

Repository: 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.json

Repository: 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.swift

Repository: 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.

@sarensw

sarensw commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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 (50 4B 03 04), spanned zip files in both PK (50 4B 07 08 50 4B 03 04) and split versions (50 4B 03 04). I have now also added a valid (not synthesized) empty.zip file that starts with 50 4B 05 06. Pull the latest commit there and adjust the tests to use real zip files, instead of synthesized ones please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants