Skip to content

feat: add secure streaming archive extraction#2

Merged
mozharovsky merged 6 commits into
mainfrom
codex/secure-streaming-extraction
Jun 24, 2026
Merged

feat: add secure streaming archive extraction#2
mozharovsky merged 6 commits into
mainfrom
codex/secure-streaming-extraction

Conversation

@mozharovsky

@mozharovsky mozharovsky commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

  • add incremental stored and DEFLATE reads through ZipStreamingCompression
  • stream extraction output instead of buffering complete entries in memory
  • preflight normalized paths, local/central header consistency, destination symlinks, and file-directory conflicts
  • add configurable entry-count and uncompressed-size limits
  • reject symbolic-link entries by default, with an explicit regular-file compatibility policy
  • preserve the existing extract(to:password:) API while adding policy-aware extraction options

Extraction flow

flowchart LR
    A[Read central directory incrementally] --> B[Validate limits and normalized paths]
    B --> C[Reject conflicts and symbolic links]
    C --> D[Verify local header matches central metadata]
    D --> E[Inflate bounded chunks]
    E --> F[Write with exclusive create and no-follow checks]
    F --> G[Validate size and CRC]
Loading

Validation

  • swift test (82 tests)
  • swift build -c release
  • swift build --swift-sdk swift-6.3.2-RELEASE_wasm
  • swift package diagnose-api-breaking-changes f43a4db
  • strict swift format lint
  • public symbol graph generation

Summary by CodeRabbit

  • New Features
    • Added streaming decompression with bounded chunking and a streaming-capable compression interface.
    • Added options-driven extraction with configurable buffer sizing, symbolic-link policy, and entry/per-entry/total uncompressed limits.
  • Bug Fixes
    • Strengthened encryption handling, CRC32 validation, and buffered/streaming size/feasibility checks.
    • Hardened extraction safety: validates all archive paths before writing, prevents duplicates/conflicts, never overwrites existing files, and cleans up on failure.
    • Improved archive path handling in the writer and rejects non-portable entry paths (backslashes).
  • Documentation
    • Updated extraction and password-protected zip examples for the current APIs.
  • Tests
    • Added comprehensive streaming, extraction safety/limit, and writer-path validation coverage.

@mozharovsky

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44d87d44-01b8-4610-b6a2-2ef8000147c4

📥 Commits

Reviewing files that changed from the base of the PR and between 6628488 and 9a76d87.

📒 Files selected for processing (3)
  • README.md
  • Sources/ZipArchive/ZipArchiveReader+extract.swift
  • Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift
  • Sources/ZipArchive/ZipArchiveReader+extract.swift

📝 Walkthrough

Walkthrough

The PR adds streaming and buffered zip reads, extraction options with path and limit validation, path-aware writing, platform file-removal handling, and expanded tests and documentation.

Changes

Chunked reads and extraction validation

Layer / File(s) Summary
Streaming compression contract
Sources/ZipArchive/ZipCompression.swift
Adds ZipStreamingCompression and chunked inflate implementations for stored and zlib-compressed entries.
Buffered and streaming read paths
Sources/ZipArchive/ZipArchiveReader.swift, Sources/ZipArchive/crc32.swift
Updates buffered reads, adds chunked reading, reconciles header metadata, extends the read-path error surface, and adjusts CRC32 input handling.
Extraction options and path validation
Sources/ZipArchive/ZipArchiveReader+extract.swift, Sources/ZipArchive/ZipTypes.swift
Adds extraction options, symbolic-link classification, archive-path normalization, prevalidation, and safer extraction-time filesystem checks.
Path-aware archive writing
Sources/ZipArchive/ZipArchiveWriter.swift
Threads archive-relative paths through writer overloads and header emission so stored filenames and directory entries use the normalized archive path.
Windows filesystem support
Sources/ZipArchive/System+directory.swift
Adds a Windows-only Foundation import and routes file removal through FileManager on Windows while keeping the existing non-Windows path.
Tests and documentation
Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift, README.md, Sources/ZipArchive/Docs.docc/index.md
Adds extraction and streaming tests, plus README and DocC updates for the new APIs and behaviors.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90 minutes

Possibly related PRs

  • rorkai/swift-zip-archive#1: Both PRs touch Sources/ZipArchive/crc32.swift to change how the running CRC is passed/computed via the zlib-backed implementation.

Poem

🐇 I hopped through zips with care and glee,
Streamed bytes in chunks as small as tea.
Paths stayed tidy, links stayed shy,
And windows learned to say goodbye.
CRCs hummed a tidy tune,
While I munched docs by afternoon.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: secure streaming archive extraction.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/secure-streaming-extraction

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

@mozharovsky

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mozharovsky

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 5

🤖 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 `@Sources/ZipArchive/ZipArchiveReader.swift`:
- Around line 133-142: The buffered inflate path in ZipArchiveReader’s
entry-reading logic only validates CRC32 after calling
compressor.inflate(from:uncompressedSize:) and can accept a size-mismatched
stored entry; add an explicit uncompressed byte count check against
preparedEntry.uncompressedSize before returning uncompressedBytes, mirroring the
streaming path’s validation, and keep the CRC32 check in place for the existing
integrity guard.
- Around line 102-123: The encrypted-entry read path in ZipArchiveReader should
fail before loading the payload when no password is available. In
ZipArchiveReader.readEntry, after detecting
preparedEntry.localHeader.flags.contains(.encrypted) and consuming the 12-byte
encryption header, check for the missing password state immediately and throw
the same password-required error before calling storage.readBytes for the
compressed data. Keep the fix localized around the encryptionKeys handling and
the subsequent fileBytes read so encrypted entries never buffer the full payload
unnecessarily.

In `@Sources/ZipArchive/ZipArchiveReader`+extract.swift:
- Around line 319-324: The validation in validatedExtractionPath(_:) is running
after entry.filename has already been converted into FilePath, which can
normalize separators and incorrectly reject valid nested ZIP entries. Move the
safety checks to the original filename string/bytes before constructing
FilePath, then let validatedExtractionPath(_:) operate on the already-approved
path; use the entry.filename handling in ZipArchiveReader+extract as the place
to apply the raw-name policy.

In `@Sources/ZipArchive/ZipCompression.swift`:
- Around line 215-218: In the inflate loop inside ZipCompression, the
Z_STREAM_END path currently accepts decompression even if zlib leaves unread
input in stream.avail_in. Update the status handling around the switch on status
so that CZIPARCHIVE_Z_STREAM_END is only accepted when no compressed bytes
remain buffered; if stream.avail_in is still greater than zero, treat the entry
as malformed and fail the read. Make the check in the same flow that updates
reachedEnd and remainingCompressedSize so junk bytes after a valid deflate
stream cannot pass validation.

In `@Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift`:
- Around line 204-209: The test assertions in ZipArchiveExtractionTests are
using String(decoding:as:), which SwiftLint flags because it can silently
replace invalid UTF-8. Update the affected assertions in the extraction tests to
use failable UTF-8 decoding via String(bytes:encoding:) instead, and assert on
the optional result so invalid data does not pass unnoticed. Apply this change
in the Current symlink/content checks and the other matching assertions in
ZipArchiveExtractionTests.
🪄 Autofix (Beta)

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

Run ID: 24213043-8c9c-4090-acf4-7f4c82e8ea7a

📥 Commits

Reviewing files that changed from the base of the PR and between f43a4db and 1e5b5dc.

📒 Files selected for processing (6)
  • README.md
  • Sources/ZipArchive/ZipArchiveReader+extract.swift
  • Sources/ZipArchive/ZipArchiveReader.swift
  • Sources/ZipArchive/ZipCompression.swift
  • Sources/ZipArchive/ZipTypes.swift
  • Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift

Comment thread Sources/ZipArchive/ZipArchiveReader.swift
Comment thread Sources/ZipArchive/ZipArchiveReader.swift
Comment thread Sources/ZipArchive/ZipArchiveReader+extract.swift Outdated
Comment thread Sources/ZipArchive/ZipCompression.swift
Comment thread Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift Outdated
@mozharovsky

Copy link
Copy Markdown
Member Author

@coderabbitai review

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/ZipArchive/ZipArchiveReader.swift (1)

187-190: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return invalidFileHeader for truncated encrypted entries.

Line 188 conflates missing passwords with compressedSize < 12; with a password supplied, a malformed encrypted entry is reported as encryptedFilesRequirePassword, unlike the buffered path.

🐛 Proposed fix
         var cryptKey: CryptKey?
         if preparedEntry.localHeader.flags.contains(.encrypted) {
-            guard let password, compressedSize >= 12 else {
+            guard compressedSize >= 12 else {
+                throw ZipArchiveReaderError.invalidFileHeader
+            }
+            guard let password else {
                 throw ZipArchiveReaderError.encryptedFilesRequirePassword
             }
             var key = CryptKey(password: password)
🤖 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 `@Sources/ZipArchive/ZipArchiveReader.swift` around lines 187 - 190, The
encrypted-entry check in ZipArchiveReader should distinguish between a missing
password and a truncated header: in the branch using
preparedEntry.localHeader.flags.contains(.encrypted), keep
encryptedFilesRequirePassword only for the no-password case, but when a password
is present and compressedSize is less than the required encrypted header size,
throw invalidFileHeader instead. Update the logic around preparedEntry and the
encryptedFilesRequirePassword guard so it matches the buffered path’s behavior
for malformed encrypted entries.
Sources/ZipArchive/ZipTypes.swift (1)

208-213: 🔒 Security & Privacy | 🟠 Major

Accept Darwin host values in isSymbolicLink. The current .unix-only check lets OS X/Darwin symlink entries bypass symbolicLinkPolicy and get extracted as regular files. Add a Unix-like host helper or a Darwin case here.

🤖 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 `@Sources/ZipArchive/ZipTypes.swift` around lines 208 - 213, The isSymbolicLink
check in ZipEntry currently only accepts versionMadeBy.system == .unix, which
causes Darwin-created symlink entries to be missed and handled like regular
files. Update ZipEntry.isSymbolicLink to also treat Darwin/OS X host values as
valid Unix-like hosts, either by adding a dedicated helper or extending the
system check, while still using
externalAttributes.unixAttributes.contains(.isSymbolicLink) for the actual
symlink detection.
🤖 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 `@Sources/ZipArchive/System`+directory.swift:
- Around line 91-97: The failed-extraction cleanup on Windows is using the wrong
delete path; update the Windows branch in the directory removal helper to
perform a file-only delete for the extracted file rather than calling the shared
FileManager-based removal. Fix the specific cleanup path by hardening the try?
FileDescriptor.remove(fullFilePath) usage in the relevant extraction/removal
flow, and keep DirectoryDescriptor.recursiveDelete(_:) and
FileDescriptor.remove(_:) behavior unchanged so directory cleanup still works
correctly.

In `@Sources/ZipArchive/ZipArchiveWriter.swift`:
- Around line 140-143: The String-based archive write path in
ZipArchiveWriter.writeFile is forwarding filename directly into pathInArchive,
which bypasses the same archive-path validation used by the FilePath overloads.
Update the String overload(s) to either normalize the incoming filename through
the existing FilePath archive-path rules or explicitly reject
unsafe/non-portable separators such as backslashes before calling writeFile, so
the paths created here remain extractable by the reader.

In `@Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift`:
- Around line 970-973: The `read(_:)` helper in the test double is counting
requested bytes instead of actual bytes read, so `bytesRead` can be inflated by
failed or short reads. Update `read(_ count: Int)` to increment `bytesRead` only
after `storage.read(count)` succeeds, using the returned slice’s length rather
than `count`. Keep the fix localized to the `read(_:)` method so the read-bound
assertions remain accurate.

---

Outside diff comments:
In `@Sources/ZipArchive/ZipArchiveReader.swift`:
- Around line 187-190: The encrypted-entry check in ZipArchiveReader should
distinguish between a missing password and a truncated header: in the branch
using preparedEntry.localHeader.flags.contains(.encrypted), keep
encryptedFilesRequirePassword only for the no-password case, but when a password
is present and compressedSize is less than the required encrypted header size,
throw invalidFileHeader instead. Update the logic around preparedEntry and the
encryptedFilesRequirePassword guard so it matches the buffered path’s behavior
for malformed encrypted entries.

In `@Sources/ZipArchive/ZipTypes.swift`:
- Around line 208-213: The isSymbolicLink check in ZipEntry currently only
accepts versionMadeBy.system == .unix, which causes Darwin-created symlink
entries to be missed and handled like regular files. Update
ZipEntry.isSymbolicLink to also treat Darwin/OS X host values as valid Unix-like
hosts, either by adding a dedicated helper or extending the system check, while
still using externalAttributes.unixAttributes.contains(.isSymbolicLink) for the
actual symlink detection.
🪄 Autofix (Beta)

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

Run ID: 5d36a111-84bf-49a7-bca7-1b6e622e9d73

📥 Commits

Reviewing files that changed from the base of the PR and between 9e9603f and ed82573.

📒 Files selected for processing (7)
  • Sources/ZipArchive/System+directory.swift
  • Sources/ZipArchive/ZipArchiveReader+extract.swift
  • Sources/ZipArchive/ZipArchiveReader.swift
  • Sources/ZipArchive/ZipArchiveWriter.swift
  • Sources/ZipArchive/ZipCompression.swift
  • Sources/ZipArchive/ZipTypes.swift
  • Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/ZipArchive/ZipCompression.swift
  • Sources/ZipArchive/ZipArchiveReader+extract.swift

Comment thread Sources/ZipArchive/System+directory.swift
Comment thread Sources/ZipArchive/ZipArchiveWriter.swift
Comment thread Tests/ZipArchiveTests/ZipArchiveExtractionTests.swift
@mozharovsky mozharovsky self-assigned this Jun 24, 2026
@mozharovsky

Copy link
Copy Markdown
Member Author

Addressed all five findings in 6628488:

  • use file-only cleanup for failed extraction while preserving recursive directory removal
  • reject non-portable backslashes in both String writer overloads
  • count actual successful reads in ReadTrackingStorage
  • classify truncated encrypted streaming headers as invalidFileHeader
  • treat PKWARE host system 19 (OS X/Darwin) as Unix-compatible for directory and symlink attributes

Validated with 91 tests, strict swift-format, release and WASI builds, and the API breakage check. Windows CI will confirm the _wunlink branch.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

@mozharovsky Thanks for the update — I’ve noted the fixes and the validation results. I’m re-reviewing the latest changes now.


✏️ Learnings added
Learnt from: mozharovsky
URL: https://github.com/rorkai/swift-zip-archive/pull/2

Timestamp: 2026-06-24T21:33:54.617Z
Learning: In the Swift `swift-zip-archive` codebase, the String-based `ZipArchiveWriter.writeFile` overloads should reject backslashes in archive paths as non-portable ZIP entry separators.

Learnt from: mozharovsky
URL: https://github.com/rorkai/swift-zip-archive/pull/2

Timestamp: 2026-06-24T21:33:54.617Z
Learning: In the Swift `swift-zip-archive` codebase, PKWARE host system 19 (OS X/Darwin) should be treated as Unix-compatible when interpreting ZIP external attributes for directory and symbolic-link detection.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mozharovsky

Copy link
Copy Markdown
Member Author

Restored compatibility for existing extraction calls: symbolic-link entries now default to materializing their stored target as regular file contents, matching the package's historical behavior without creating filesystem links. Strict rejection remains available through symbolicLinkPolicy: .reject.

Validated with the full 91-test suite, release and WASI builds, strict Swift formatting, and diagnose-api-breaking-changes against origin/main (no breaking changes detected).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

@mozharovsky Thanks for the update — I’ve noted the restored backward-compatible symbolic-link extraction behavior and the validation results. I’m reviewing the latest changes now.


✏️ Learnings added
Learnt from: mozharovsky
URL: https://github.com/rorkai/swift-zip-archive/pull/2

Timestamp: 2026-06-24T22:54:24.503Z
Learning: In the Swift `swift-zip-archive` codebase, the default extraction behavior for symbolic-link ZIP entries should preserve historical compatibility by materializing the stored link target as regular file contents rather than creating a filesystem symlink; strict rejection is available via `ZipArchiveExtractionOptions.SymbolicLinkPolicy.reject` in `Sources/ZipArchive/ZipArchiveReader+extract.swift`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mozharovsky
mozharovsky merged commit a611fb9 into main Jun 24, 2026
16 checks passed
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.

1 participant