{"record":{"id":"5a8ffaf4d34eb8c4","repo":"Tencent/tinker","slug":"spanned-archives-not-supported","errorCode":null,"errorMessage":"Spanned archives not supported","messagePattern":"Spanned archives not supported","errorType":"exception","errorClass":"ZipException","httpStatus":null,"severity":"error","filePath":"third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java","lineNumber":224,"sourceCode":"        if (isZip64) {\n            numEntries = -1;\n            centralDirOffset = -1;\n            // If we have a zip64 end of central directory record, we skip through the regular\n            // end of central directory record and use the information from the zip64 eocd record.\n            // We're still forced to read the comment length (below) since it isn't present in the\n            // zip64 eocd record.\n            it.skip(16);\n        } else {\n            // If we don't have a zip64 eocd record, we read values from the \"regular\"\n            // eocd record.\n            int diskNumber = it.readShort() & 0xffff;\n            int diskWithCentralDir = it.readShort() & 0xffff;\n            numEntries = it.readShort() & 0xffff;\n            int totalNumEntries = it.readShort() & 0xffff;\n            it.skip(4); // Ignore centralDirSize.\n            centralDirOffset = ((long) it.readInt()) & 0xffffffffL;\n            if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {\n                throw new ZipException(\"Spanned archives not supported\");\n            }\n        }\n        final int commentLength = it.readShort() & 0xffff;\n        return new EocdRecord(numEntries, centralDirOffset, commentLength);\n    }\n\n    static void throwZipException(String filename, long fileSize, String entryName, long localHeaderRelOffset, String msg, int magic) throws ZipException {\n        final String hexString = Integer.toHexString(magic);\n        throw new ZipException(\"file name:\" + filename\n                               + \", file size\" + fileSize\n                               + \", entry name:\" + entryName\n                               + \", entry localHeaderRelOffset:\" + localHeaderRelOffset\n                               + \", \"\n                               + msg + \" signature not found; was \" + hexString);\n    }\n\n    /**\n     * Closes this zip file. This method is idempotent. This method may cause I/O if the","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/Tencent/tinker/blob/1b7ea02c239840f563ea64fb5bd286eb98d4011e/third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java#L206-L242","documentation":"While parsing the End Of Central Directory record (non-zip64 path), TinkerZipFile checks that the entry count on this disk equals the total entry count and that both disk-number fields are zero. Any mismatch means the archive is split across multiple disks/volumes, which this reader does not support, so it throws ZipException('Spanned archives not supported'). This is the same restriction as java.util.zip.ZipFile.","triggerScenarios":"Opening a multi-part zip (created with 'zip -s', WinRAR split, or hjsplit) with new TinkerZipFile(...): the EOCD then reports diskNumber != 0, diskWithCentralDir != 0, or per-disk numEntries != totalNumEntries.","commonSituations":"Android patch/OTA files distributed as split archives; a download tool that stored parts without rejoining; a truncated single-disk archive whose EOCD fields were corrupted so they resemble a spanned set.","solutions":["Rejoin all parts into a single file before opening (e.g. 'zip -s 0 archive.zip --out full.zip' or 'zip -F').","If the file should be single-part, treat this as corruption: re-download or regenerate the archive.","Verify with 'unzip -l' or 'zipinfo' that the archive is a self-contained single-volume zip before handing it to TinkerZipFile."],"exampleFix":"// before\nTinkerZipFile zf = new TinkerZipFile(new File(\"patch-part1.zip\")); // spanned archive\n\n// after\n// merge parts first: zip -s 0 patch.zip --out patch-full.zip\nTinkerZipFile zf = new TinkerZipFile(new File(\"patch-full.zip\"));","handlingStrategy":"validation","validationCode":"// cheap pre-check: EOCD disk fields must be zero for a single-volume zip\nstatic boolean isSingleVolume(File f) throws IOException {\n    try (RandomAccessFile r = new RandomAccessFile(f, \"r\")) {\n        long len = r.length();\n        if (len < 22) return false;\n        for (long off = len - 22; off >= Math.max(0, len - 22 - 65536); off--) {\n            r.seek(off);\n            if (r.readInt() == 0x06054b50) { // ENDSIG little-endian after reverseBytes\n                r.seek(off + 4);\n                return (r.readShort() | r.readShort()) == 0; // diskNumber & diskWithCentralDir\n            }\n        }\n    }\n    return false;\n}","typeGuard":null,"tryCatchPattern":"try {\n    zf = new TinkerZipFile(file);\n} catch (ZipException e) {\n    if (e.getMessage().contains(\"Spanned archives\")) {\n        // actionable: merge parts, then reopen\n        throw new IllegalArgumentException(\"Multi-part archive not supported: merge parts first\", e);\n    }\n    throw e;\n}","preventionTips":["Reject multi-part archives at upload/ingestion with a dedicated check and message.","Standardize on single-volume zips in your distribution pipeline.","Validate with 'zipinfo' in CI for any archive you feed to TinkerZipFile."],"tags":["zip","archive-format","spanned","tinker"],"backgroundTag":null,"analyzedSha":"1b7ea02c239840f563ea64fb5bd286eb98d4011e","analyzedAt":"2026-08-14T15:16:52.110Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}