master ohmu/fastfetch-openbsd / src / common / android / zip.c
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}