Test iOS ZIP extraction paths and quotas on a cloud Mac
A user imports a ZIP, the app reports success, yet files end up outside the import directory. The problem usually lies not in the compression algorithm, but in using archive entry names directly to build output paths. When moving this workflow to a cloud Mac, test the path policy first, then integrate the project’s extractor. Do not use real user data to create malicious test samples, and do not let tests write into the source directory.
Define the import boundary first
This policy accepts only regular files and directories; symbolic links and unknown entry types are rejected. Create a separate staging directory for each import, and move its contents into the application’s data directory only after all validation succeeds. If any entry fails, delete the entire staged import rather than keeping “the parts that already succeeded.”
Set three engineering quotas: at most one thousand entries, twenty megabytes per file, and one hundred megabytes of total extracted data. These are example policy choices, not system limits; teams should adjust them to their needs. Preflight checks use the sizes declared in the archive directory, while the write stage separately counts actual bytes. Neither check can replace the other.
The sandbox is not the extractor’s security boundary. Even if an entry cannot escape the app container, it can still overwrite a database or configuration file inside it.
Write an executable path preflight check
Save the following code as zip-path-check.swift and run xcrun swift zip-path-check.swift on the cloud Mac. It validates entry names only; it does not read ZIP files. Your project must apply the same policy to the entry names and types supplied by its extraction library. A successful command run does not establish that the entire import workflow is safe.
import Foundation
enum PathError: Error {
case invalid
}
func safeComponents(_ name: String) throws -> [String] {
guard !name.isEmpty,
!name.hasPrefix("/"),
!name.contains("\\"),
!name.contains(":"),
!name.unicodeScalars.contains(where: {
CharacterSet.controlCharacters.contains($0)
}) else {
throw PathError.invalid
}
let body = name.hasSuffix("/") ? String(name.dropLast()) : name
let parts = body.split(
separator: "/",
omittingEmptySubsequences: false
).map(String.init)
guard !parts.isEmpty,
parts.allSatisfy({
!$0.isEmpty && $0 != "." && $0 != ".."
}) else {
throw PathError.invalid
}
return parts
}
let cases: [(String, Bool)] = [
("docs/readme.txt", true),
("docs/", true),
("report..txt", true),
("../outside.txt", false),
("docs/../../outside.txt", false),
("/tmp/outside.txt", false),
("docs//readme.txt", false),
("docs/./readme.txt", false),
("docs\\..\\outside.txt", false),
("C:/outside.txt", false),
("docs/\u{0000}bad.txt", false)
]
for (name, expected) in cases {
let accepted = (try? safeComponents(name)) != nil
precondition(accepted == expected, "Path policy mismatch")
}
print("Path checks passed")
This is a deliberately restrictive import policy: it accepts only relative paths separated by forward slashes, with a single trailing slash allowed for directories. Colons and control characters are also rejected to avoid differences in interpretation across file systems. If your application genuinely needs these characters, add explicit test cases rather than relaxing all the checks.
Check links and conflicting destinations
A valid path still requires an entry-type check. Do not extract a link and then follow it when writing the next entry, and do not infer entry types from filename extensions. Read the types and attributes exposed by the extraction library; reject any type that cannot be reliably identified.
Index each destination path by its components. In addition to exact duplicates, reject parent–child conflicts such as writing a file named docs and then writing docs/a.txt. The reverse entry order must fail as well, or the outcome will depend on how entries are ordered in the archive.
Case differences and Unicode-equivalent names need an additional policy. If the imported data does not need to distinguish these names, you can apply canonical composition normalization and case folding to the components for conflict detection while preserving the original names on disk. Do not infer iOS write behavior from the case sensitivity of the cloud Mac’s current volume. Prepare at least two sets of samples—case collisions and combining-character cases—and retest them in the target simulator.
Verify quotas against actual writes
The adapter layer must receive decompressed data chunks before they are written to a file, check the accumulated size first, and only then write. Compare each chunk against the remaining allowance rather than adding sizes directly, which could cause integer overflow. If the library can only decompress everything into memory and offers no way to enforce limits during extraction, switch interfaces or impose separate, strict constraints on the input.
Define an expected outcome for every test archive: a valid directory structure passes, paths escaping the allowed directory are rejected, duplicate destinations are rejected, and exceeding any quota causes rejection. Add corrupted compressed data and verify that a read error is not treated as a normal end of input. Even for valid archives, check the integrity-verification result reported by the library rather than merely checking that the final files exist.
After a write failure, assert that the staging directory has been deleted, the application’s data directory is unchanged, and a sentinel file placed beforehand next to the import directory still has its original contents. These checks are more valuable than simply asserting that a function throws: an error may occur after an out-of-bounds file has already been written.
Add a regression gate and preserve the boundary
In the pipeline, run the path script first, followed by application tests that invoke the real extractor. Record the sample ID, rejection reason, and actual number of bytes written, but not user file contents. Create test directories with random names and clean them up afterward. Never use the system temporary directory itself as the extraction root.
A directory can still be replaced between preflight validation and writing. Use an app-private staging directory that other tasks cannot modify, and refuse to follow links during writes. If a shared desktop script allows other processes to modify directories, a string-prefix check does not eliminate this risk.
Finally, confirm three things: failed imports leave no partial data, successful imports create only the expected files, and quotas are enforced using actual byte counts. The path function is the first gate; assertions about writes from real archives are the final acceptance check.
Frequently asked questions
Is rejecting every filename containing two dots enough?
No. Reject a standalone .. path component, absolute paths and unsupported separators. Also check links, duplicate destinations and write-time directory boundaries. A filename such as report..txt is not traversal.
Can the size declared in a ZIP directory prevent decompression bombs?
Not by itself. Use declared sizes for early rejection, but count actual decompressed bytes while writing. Limit each entry, the total bytes and the entry count; abort and remove staging data when a limit is reached.
Hand Off Build Queues to Your Dedicated Cloud Mac
Choose a physical node, Mac model, and billing term to run Xcode builds and automation in your own macOS environment. Check the console for real-time availability.