ohmu
·
2026-10-15
1#include "common/android/zip.h"
2#include "common/debug.h"
3
4#include <string.h>
5
6static uint16_t zipU16(const uint8_t* p) {
7 return (uint16_t) ((uint16_t) p[0] | (uint16_t) ((uint16_t) p[1] << 8));
8}
9
10static uint32_t zipU32(const uint8_t* p) {
11 return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
12}
13
14const char* ffAndroidZipFindEntry(const uint8_t* archive, size_t archiveSize, const char* entry, FFAndroidZipEntry* result) {
15 const size_t entryLength = strlen(entry);
16 if (archiveSize < FF_ZIP_EOCD_SIZE) {
17 return "The archive is too small to be a zip";
18 }
19
20 // The record is the last thing in the file, so it is searched for backwards -- over at most the
21 // 64 KiB a trailing comment can take, not over the whole archive.
22 const size_t newest = archiveSize - FF_ZIP_EOCD_SIZE;
23 const size_t oldest = archiveSize > FF_ZIP_EOCD_MAX_COMMENT + FF_ZIP_EOCD_SIZE
24 ? archiveSize - FF_ZIP_EOCD_MAX_COMMENT - FF_ZIP_EOCD_SIZE
25 : 0;
26 size_t eocd = SIZE_MAX;
27 for (size_t i = newest;; --i) {
28 if (memcmp(archive + i, FF_ZIP_EOCD_MAGIC, sizeof(FF_ZIP_EOCD_MAGIC) - 1) == 0) {
29 eocd = i;
30 break;
31 }
32 if (i == oldest) {
33 // The walk stops here rather than at zero: a `size_t` countdown that has to reach zero
34 // before it stops would wrap and read the whole file backwards, which is the very scan
35 // this is here to avoid.
36 break;
37 }
38 }
39 if (eocd == SIZE_MAX) {
40 FF_DEBUG("No end-of-central-directory record in the last %zu bytes of a %zu byte archive",
41 newest - oldest + FF_ZIP_EOCD_SIZE, archiveSize);
42 return "The archive has no zip directory";
43 }
44
45 const uint32_t directorySize = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY_SIZE);
46 const uint32_t directoryOffset = zipU32(archive + eocd + FF_ZIP_OFF_EOCD_DIRECTORY);
47 if (directoryOffset > archiveSize || directorySize > archiveSize - directoryOffset) {
48 return "The zip directory is out of range";
49 }
50
51 // The directory is walked to its end rather than for the entry count the record carries: that
52 // count is a 16-bit field which a 65535-entry archive pins at 0xFFFF and moves into a zip64
53 // record, while the two bounds above describe the directory itself on every archive this small.
54 const size_t directoryEnd = (size_t) directoryOffset + directorySize;
55 for (size_t offset = directoryOffset; offset + FF_ZIP_CENTRAL_SIZE <= directoryEnd;) {
56 if (memcmp(archive + offset, FF_ZIP_CENTRAL_MAGIC, sizeof(FF_ZIP_CENTRAL_MAGIC) - 1) != 0) {
57 return "A zip directory entry is malformed";
58 }
59 const uint16_t nameLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_NAME_LENGTH);
60 const uint16_t extraLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_EXTRA_LENGTH);
61 const uint16_t commentLength = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_COMMENT_LENGTH);
62 const size_t entrySize = FF_ZIP_CENTRAL_SIZE + (size_t) nameLength + (size_t) extraLength + (size_t) commentLength;
63 if (entrySize > directoryEnd - offset) {
64 FF_DEBUG("A zip entry claims %zu bytes but only %zu remain in the directory",
65 entrySize, (size_t) (directoryEnd - offset));
66 return "A zip directory entry runs past the directory";
67 }
68
69 if (nameLength == entryLength && memcmp(archive + offset + FF_ZIP_CENTRAL_SIZE, entry, entryLength) == 0) {
70 const uint32_t compressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_COMPRESSED_SIZE);
71 const uint32_t uncompressedSize = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_UNCOMPRESSED_SIZE);
72 const uint32_t localOffset = zipU32(archive + offset + FF_ZIP_OFF_CENTRAL_LOCAL_OFFSET);
73 if (compressedSize == FF_ZIP_ZIP64_SENTINEL || uncompressedSize == FF_ZIP_ZIP64_SENTINEL || localOffset == FF_ZIP_ZIP64_SENTINEL) {
74 FF_DEBUG("The zip entry \"%s\" is zip64: compressed=%u, uncompressed=%u, localOffset=%u",
75 entry, compressedSize, uncompressedSize, localOffset);
76 return "The zip entry is stored as zip64, which is not supported";
77 }
78 if (localOffset > archiveSize || FF_ZIP_LOCAL_HEADER_SIZE > archiveSize - localOffset) {
79 return "A zip local header is out of range";
80 }
81
82 const uint16_t localNameLength = zipU16(archive + localOffset + FF_ZIP_OFF_NAME_LENGTH);
83 const uint16_t localExtraLength = zipU16(archive + localOffset + FF_ZIP_OFF_EXTRA_LENGTH);
84 const size_t headerSize = FF_ZIP_LOCAL_HEADER_SIZE + (size_t) localNameLength + (size_t) localExtraLength;
85 if (headerSize > archiveSize - localOffset || compressedSize > archiveSize - localOffset - headerSize) {
86 return "The zip entry bounds are out of range";
87 }
88
89 result->method = zipU16(archive + offset + FF_ZIP_OFF_CENTRAL_METHOD);
90 result->uncompressedSize = uncompressedSize;
91 result->payload = archive + localOffset + headerSize;
92 result->payloadSize = compressedSize;
93 return nullptr;
94 }
95
96 offset += entrySize;
97 }
98 FF_DEBUG("No entry in the %zu byte archive is named \"%s\"", archiveSize, entry);
99 return "The archive has no entry with that name";
100}