chore: Remove miniz and modularise inflation logic (#1073)

## Summary

* Remove miniz and move completely to uzlib
* Move uzlib interfacing to InflateReader to better modularise inflation
code

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? Yes, Claude helped with
the extraction and refactor
This commit is contained in:
Dave Allie
2026-02-22 21:38:03 +11:00
committed by GitHub
parent f28623dacd
commit ecb5b1b4e5
9 changed files with 348 additions and 8626 deletions

View File

@@ -1,14 +1,11 @@
#include "FontDecompressor.h"
#include <Logging.h>
#include <uzlib.h>
#include <cstdlib>
#include <cstring>
bool FontDecompressor::init() {
clearCache();
memset(&decomp, 0, sizeof(decomp));
return true;
}
@@ -82,20 +79,10 @@ bool FontDecompressor::decompressGroup(const EpdFontData* fontData, uint16_t gro
return false;
}
// Decompress using uzlib
const uint8_t* inputBuf = &fontData->bitmap[group.compressedOffset];
uzlib_uncompress_init(&decomp, NULL, 0);
decomp.source = inputBuf;
decomp.source_limit = inputBuf + group.compressedSize;
decomp.dest_start = outBuf;
decomp.dest = outBuf;
decomp.dest_limit = outBuf + group.uncompressedSize;
int res = uzlib_uncompress(&decomp);
if (res < 0 || decomp.dest != decomp.dest_limit) {
LOG_ERR("FDC", "Decompression failed for group %u (status %d)", groupIndex, res);
inflateReader.init(false);
inflateReader.setSource(&fontData->bitmap[group.compressedOffset], group.compressedSize);
if (!inflateReader.read(outBuf, group.uncompressedSize)) {
LOG_ERR("FDC", "Decompression failed for group %u", groupIndex);
free(outBuf);
return false;
}

View File

@@ -1,8 +1,6 @@
#pragma once
#include <uzlib.h>
#include <cstdint>
#include <InflateReader.h>
#include "EpdFontData.h"
@@ -30,7 +28,7 @@ class FontDecompressor {
bool valid = false;
};
struct uzlib_uncomp decomp = {};
InflateReader inflateReader;
CacheEntry cache[CACHE_SLOTS] = {};
uint32_t accessCounter = 0;

View File

@@ -0,0 +1,78 @@
#include "InflateReader.h"
#include <cstring>
#include <type_traits>
namespace {
constexpr size_t INFLATE_DICT_SIZE = 32768;
}
// Guarantee the cast pattern in the header comment is valid.
static_assert(std::is_standard_layout<InflateReader>::value,
"InflateReader must be standard-layout for the uzlib callback cast to work");
InflateReader::~InflateReader() { deinit(); }
bool InflateReader::init(const bool streaming) {
deinit(); // free any previously allocated ring buffer and reset state
if (streaming) {
ringBuffer = static_cast<uint8_t*>(malloc(INFLATE_DICT_SIZE));
if (!ringBuffer) return false;
memset(ringBuffer, 0, INFLATE_DICT_SIZE);
}
uzlib_uncompress_init(&decomp, ringBuffer, ringBuffer ? INFLATE_DICT_SIZE : 0);
return true;
}
void InflateReader::deinit() {
if (ringBuffer) {
free(ringBuffer);
ringBuffer = nullptr;
}
memset(&decomp, 0, sizeof(decomp));
}
void InflateReader::setSource(const uint8_t* src, size_t len) {
decomp.source = src;
decomp.source_limit = src + len;
}
void InflateReader::setReadCallback(int (*cb)(struct uzlib_uncomp*)) { decomp.source_read_cb = cb; }
void InflateReader::skipZlibHeader() {
uzlib_get_byte(&decomp);
uzlib_get_byte(&decomp);
}
bool InflateReader::read(uint8_t* dest, size_t len) {
if (!ringBuffer) {
// One-shot mode: back-references use absolute offset from dest_start.
// Valid only when read() is called once with the full output buffer.
decomp.dest_start = dest;
}
decomp.dest = dest;
decomp.dest_limit = dest + len;
const int res = uzlib_uncompress(&decomp);
if (res < 0) return false;
return decomp.dest == decomp.dest_limit;
}
InflateStatus InflateReader::readAtMost(uint8_t* dest, size_t maxLen, size_t* produced) {
if (!ringBuffer) {
// One-shot mode: back-references use absolute offset from dest_start.
// Valid only when readAtMost() is called once with the full output buffer.
decomp.dest_start = dest;
}
decomp.dest = dest;
decomp.dest_limit = dest + maxLen;
const int res = uzlib_uncompress(&decomp);
*produced = static_cast<size_t>(decomp.dest - dest);
if (res == TINF_DONE) return InflateStatus::Done;
if (res < 0) return InflateStatus::Error;
return InflateStatus::Ok;
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include <uzlib.h>
#include <cstddef>
// Return value for readAtMost().
enum class InflateStatus {
Ok, // Output buffer full; more compressed data remains.
Done, // Stream ended cleanly (TINF_DONE). produced may be < maxLen.
Error, // Decompression failed.
};
// Streaming deflate decompressor wrapping uzlib.
//
// Two modes:
// init(false) — one-shot: input is a contiguous buffer, call read() once.
// init(true) — streaming: allocates a 32KB ring buffer for back-references
// across multiple read() / readAtMost() calls.
//
// Streaming callback pattern:
// The uzlib read callback receives a `struct uzlib_uncomp*` with no separate
// context pointer. To attach context, make InflateReader the *first member* of
// your context struct, then cast inside the callback:
//
// struct MyCtx {
// InflateReader reader; // must be first
// FsFile* file;
// // ...
// };
// static int myCb(struct uzlib_uncomp* u) {
// MyCtx* ctx = reinterpret_cast<MyCtx*>(u); // valid: reader.decomp is at offset 0
// // ... fill u->source / u->source_limit, return first byte
// }
// MyCtx ctx;
// ctx.reader.init(true);
// ctx.reader.setReadCallback(myCb);
//
class InflateReader {
public:
InflateReader() = default;
~InflateReader();
InflateReader(const InflateReader&) = delete;
InflateReader& operator=(const InflateReader&) = delete;
// Initialise decompressor. streaming=true allocates a 32KB ring buffer needed
// when read() or readAtMost() will be called multiple times.
// Returns false only in streaming mode if the ring buffer allocation fails.
bool init(bool streaming = false);
// Release the ring buffer and reset internal state.
void deinit();
// Set the entire compressed input as a contiguous memory buffer.
// Used in one-shot mode; not needed when a read callback is set.
void setSource(const uint8_t* src, size_t len);
// Set a uzlib-compatible read callback for streaming input.
// See class-level comment for the expected callback/context struct pattern.
void setReadCallback(int (*cb)(uzlib_uncomp*));
// Consume the 2-byte zlib header (CMF + FLG) from the input stream.
// Call this once before the first read() when input is zlib-wrapped (e.g. PNG IDAT).
void skipZlibHeader();
// Decompress exactly len bytes into dest.
// Returns false if the stream ends before producing len bytes, or on error.
bool read(uint8_t* dest, size_t len);
// Decompress up to maxLen bytes into dest.
// Sets *produced to the number of bytes written.
// Returns Done when the stream ends cleanly, Ok when there is more to read,
// and Error on failure.
InflateStatus readAtMost(uint8_t* dest, size_t maxLen, size_t* produced);
// Returns a pointer to the underlying TINF_DATA.
// Useful for advanced streaming setups where the callback needs access to the
// uzlib struct directly (e.g. updating source/source_limit).
uzlib_uncomp* raw() { return &decomp; }
private:
uzlib_uncomp decomp = {};
uint8_t* ringBuffer = nullptr;
};

View File

@@ -1,8 +1,8 @@
#include "PngToBmpConverter.h"
#include <HalStorage.h>
#include <InflateReader.h>
#include <Logging.h>
#include <miniz.h>
#include <cstdio>
#include <cstring>
@@ -20,36 +20,6 @@ constexpr int TARGET_MAX_WIDTH = 480;
constexpr int TARGET_MAX_HEIGHT = 800;
// ============================================================================
// PNG constants
static constexpr uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
// PNG color types
enum PngColorType : uint8_t {
PNG_COLOR_GRAYSCALE = 0,
PNG_COLOR_RGB = 2,
PNG_COLOR_PALETTE = 3,
PNG_COLOR_GRAYSCALE_ALPHA = 4,
PNG_COLOR_RGBA = 6,
};
// PNG filter types
enum PngFilter : uint8_t {
PNG_FILTER_NONE = 0,
PNG_FILTER_SUB = 1,
PNG_FILTER_UP = 2,
PNG_FILTER_AVERAGE = 3,
PNG_FILTER_PAETH = 4,
};
// Read a big-endian 32-bit value from file
static bool readBE32(FsFile& file, uint32_t& value) {
uint8_t buf[4];
if (file.read(buf, 4) != 4) return false;
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
return true;
}
// BMP writing helpers (same as JpegToBmpConverter)
inline void write16(Print& out, const uint16_t value) {
out.write(value & 0xFF);
@@ -70,7 +40,49 @@ inline void write32Signed(Print& out, const int32_t value) {
out.write((value >> 24) & 0xFF);
}
static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
// Paeth predictor function per PNG spec
inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
int p = static_cast<int>(a) + b - c;
int pa = p > a ? p - a : a - p;
int pb = p > b ? p - b : b - p;
int pc = p > c ? p - c : c - p;
if (pa <= pb && pa <= pc) return a;
if (pb <= pc) return b;
return c;
}
namespace {
// PNG constants
uint8_t PNG_SIGNATURE[8] = {137, 80, 78, 71, 13, 10, 26, 10};
// PNG color types
enum PngColorType : uint8_t {
PNG_COLOR_GRAYSCALE = 0,
PNG_COLOR_RGB = 2,
PNG_COLOR_PALETTE = 3,
PNG_COLOR_GRAYSCALE_ALPHA = 4,
PNG_COLOR_RGBA = 6,
};
// PNG filter types
enum PngFilter : uint8_t {
PNG_FILTER_NONE = 0,
PNG_FILTER_SUB = 1,
PNG_FILTER_UP = 2,
PNG_FILTER_AVERAGE = 3,
PNG_FILTER_PAETH = 4,
};
// Read a big-endian 32-bit value from file
bool readBE32(FsFile& file, uint32_t& value) {
uint8_t buf[4];
if (file.read(buf, 4) != 4) return false;
value = (static_cast<uint32_t>(buf[0]) << 24) | (static_cast<uint32_t>(buf[1]) << 16) |
(static_cast<uint32_t>(buf[2]) << 8) | buf[3];
return true;
}
void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width + 3) / 4 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t paletteSize = 256 * 4;
@@ -102,7 +114,7 @@ static void writeBmpHeader8bit(Print& bmpOut, const int width, const int height)
}
}
static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width + 31) / 32 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 62 + imageSize;
@@ -131,7 +143,7 @@ static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height)
}
}
static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) {
const int bytesPerRow = (width * 2 + 31) / 32 * 4;
const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 70 + imageSize;
@@ -160,21 +172,13 @@ static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height)
bmpOut.write(i);
}
}
// Paeth predictor function per PNG spec
static inline uint8_t paethPredictor(uint8_t a, uint8_t b, uint8_t c) {
int p = static_cast<int>(a) + b - c;
int pa = p > a ? p - a : a - p;
int pb = p > b ? p - b : b - p;
int pc = p > c ? p - c : c - p;
if (pa <= pb && pa <= pc) return a;
if (pb <= pc) return b;
return c;
}
} // namespace
// Context for streaming PNG decompression
// IMPORTANT: reader must be the first field - the uzlib callback casts uzlib_uncomp* to PngDecodeContext*
struct PngDecodeContext {
FsFile& file;
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to PngDecodeContext*
FsFile* file;
// PNG image properties
uint32_t width;
@@ -188,15 +192,11 @@ struct PngDecodeContext {
uint8_t* currentRow; // current defiltered scanline
uint8_t* previousRow; // previous defiltered scanline
// zlib decompression state
mz_stream zstream;
bool zstreamInitialized;
// Chunk reading state
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
bool idatFinished; // no more IDAT chunks
// File read buffer for feeding zlib
// File read buffer for feeding uzlib
uint8_t readBuf[2048];
// Palette for indexed color (type 3)
@@ -209,10 +209,10 @@ struct PngDecodeContext {
static bool findNextIdatChunk(PngDecodeContext& ctx) {
while (true) {
uint32_t chunkLen;
if (!readBE32(ctx.file, chunkLen)) return false;
if (!readBE32(*ctx.file, chunkLen)) return false;
uint8_t chunkType[4];
if (ctx.file.read(chunkType, 4) != 4) return false;
if (ctx.file->read(chunkType, 4) != 4) return false;
if (memcmp(chunkType, "IDAT", 4) == 0) {
ctx.chunkBytesRemaining = chunkLen;
@@ -221,7 +221,7 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
// Skip this chunk's data + 4-byte CRC
// Use seek to skip efficiently
if (!ctx.file.seekCur(chunkLen + 4)) return false;
if (!ctx.file->seekCur(chunkLen + 4)) return false;
// If we hit IEND, there are no more chunks
if (memcmp(chunkType, "IEND", 4) == 0) {
@@ -230,72 +230,50 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
}
}
// Feed compressed data to zlib from IDAT chunks
// Returns number of bytes made available in zstream, or -1 on error
static int feedZlibInput(PngDecodeContext& ctx) {
if (ctx.idatFinished) return 0;
// uzlib callback: reads the next batch of IDAT data from the file
static int pngIdatReadCallback(uzlib_uncomp* uncomp) {
auto* ctx = reinterpret_cast<PngDecodeContext*>(uncomp);
// If current IDAT chunk is exhausted, skip its CRC and find next
while (ctx.chunkBytesRemaining == 0) {
// Skip 4-byte CRC of previous IDAT
if (!ctx.file.seekCur(4)) return -1;
if (ctx->idatFinished) return -1;
if (!findNextIdatChunk(ctx)) {
ctx.idatFinished = true;
return 0;
// Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted
while (ctx->chunkBytesRemaining == 0) {
if (!ctx->file->seekCur(4)) { // skip 4-byte CRC of previous IDAT
ctx->idatFinished = true;
return -1;
}
if (!findNextIdatChunk(*ctx)) {
ctx->idatFinished = true;
return -1;
}
}
// Read from current IDAT chunk
size_t toRead = sizeof(ctx.readBuf);
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining;
// Read from current IDAT chunk into the read buffer
size_t toRead = sizeof(ctx->readBuf);
if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining;
int bytesRead = ctx.file.read(ctx.readBuf, toRead);
if (bytesRead <= 0) return -1;
ctx.chunkBytesRemaining -= bytesRead;
ctx.zstream.next_in = ctx.readBuf;
ctx.zstream.avail_in = bytesRead;
return bytesRead;
}
// Decompress exactly 'needed' bytes into 'dest'
static bool decompressBytes(PngDecodeContext& ctx, uint8_t* dest, size_t needed) {
ctx.zstream.next_out = dest;
ctx.zstream.avail_out = needed;
while (ctx.zstream.avail_out > 0) {
if (ctx.zstream.avail_in == 0) {
int fed = feedZlibInput(ctx);
if (fed < 0) return false;
if (fed == 0) {
// Try one more inflate to flush
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
if (ctx.zstream.avail_out == 0) break;
return false;
}
}
int ret = mz_inflate(&ctx.zstream, MZ_SYNC_FLUSH);
if (ret != MZ_OK && ret != MZ_STREAM_END && ret != MZ_BUF_ERROR) {
LOG_ERR("PNG", "zlib inflate error: %d", ret);
return false;
}
if (ret == MZ_STREAM_END) break;
int bytesRead = ctx->file->read(ctx->readBuf, toRead);
if (bytesRead <= 0) {
ctx->idatFinished = true;
return -1;
}
return ctx.zstream.avail_out == 0;
ctx->chunkBytesRemaining -= bytesRead;
// Give uzlib the buffer (skip first byte since we return it directly)
uncomp->source = ctx->readBuf + 1;
uncomp->source_limit = ctx->readBuf + bytesRead;
return ctx->readBuf[0];
}
// Decode one scanline: decompress filter byte + raw bytes, then unfilter
static bool decodeScanline(PngDecodeContext& ctx) {
// Decompress filter byte
uint8_t filterType;
if (!decompressBytes(ctx, &filterType, 1)) return false;
if (!ctx.reader.read(&filterType, 1)) return false;
// Decompress raw row data into currentRow
if (!decompressBytes(ctx, ctx.currentRow, ctx.rawRowBytes)) return false;
if (!ctx.reader.read(ctx.currentRow, ctx.rawRowBytes)) return false;
// Apply reverse filter
const int bpp = ctx.bytesPerPixel;
@@ -521,22 +499,15 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
}
// Initialize decode context
PngDecodeContext ctx = {.file = pngFile,
.width = width,
.height = height,
.bitDepth = bitDepth,
.colorType = colorType,
.bytesPerPixel = bytesPerPixel,
.rawRowBytes = rawRowBytes,
.currentRow = nullptr,
.previousRow = nullptr,
.zstream = {},
.zstreamInitialized = false,
.chunkBytesRemaining = 0,
.idatFinished = false,
.readBuf = {},
.palette = {},
.paletteSize = 0};
PngDecodeContext ctx = {};
ctx.file = &pngFile;
ctx.width = width;
ctx.height = height;
ctx.bitDepth = bitDepth;
ctx.colorType = colorType;
ctx.bytesPerPixel = bytesPerPixel;
ctx.rawRowBytes = rawRowBytes;
ctx.paletteSize = 0;
// Allocate scanline buffers
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
@@ -585,15 +556,16 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
return false;
}
// Initialize zlib decompression
memset(&ctx.zstream, 0, sizeof(ctx.zstream));
if (mz_inflateInit(&ctx.zstream) != MZ_OK) {
LOG_ERR("PNG", "Failed to initialize zlib");
// Initialize streaming decompressor with 32KB ring buffer for back-reference history
if (!ctx.reader.init(true)) {
LOG_ERR("PNG", "Failed to init inflate reader");
free(ctx.currentRow);
free(ctx.previousRow);
return false;
}
ctx.zstreamInitialized = true;
ctx.reader.setReadCallback(pngIdatReadCallback);
// PNG IDAT data is zlib-wrapped: consume the 2-byte zlib header (CMF + FLG)
ctx.reader.skipZlibHeader();
// Calculate output dimensions (same logic as JpegToBmpConverter)
int outWidth = width;
@@ -618,8 +590,8 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
if (outWidth < 1) outWidth = 1;
if (outHeight < 1) outHeight = 1;
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth;
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight;
scaleX_fp = (width << 16) / outWidth;
scaleY_fp = (height << 16) / outHeight;
needsScaling = true;
LOG_DBG("PNG", "Scaling %ux%u -> %dx%d (target %dx%d)", width, height, outWidth, outHeight, targetWidth,
@@ -643,7 +615,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
if (!rowBuffer) {
LOG_ERR("PNG", "Failed to allocate row buffer");
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);
return false;
@@ -687,7 +658,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
delete fsDitherer;
delete atkinson1BitDitherer;
free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);
return false;
@@ -842,7 +812,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
delete fsDitherer;
delete atkinson1BitDitherer;
free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow);
free(ctx.previousRow);

View File

@@ -1,35 +1,38 @@
#include "ZipFile.h"
#include <HalStorage.h>
#include <InflateReader.h>
#include <Logging.h>
#include <miniz.h>
#include <algorithm>
static bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf,
const size_t inflatedSize) {
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!inflator) {
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
return false;
}
memset(inflator, 0, sizeof(tinfl_decompressor));
tinfl_init(inflator);
struct ZipInflateCtx {
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx*
FsFile* file = nullptr;
size_t fileRemaining = 0;
uint8_t* readBuf = nullptr;
size_t readBufSize = 0;
};
size_t inBytes = deflatedSize;
size_t outBytes = inflatedSize;
const tinfl_status status = tinfl_decompress(inflator, inputBuf, &inBytes, nullptr, outputBuf, &outBytes,
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
namespace {
constexpr uint16_t ZIP_METHOD_STORED = 0;
constexpr uint16_t ZIP_METHOD_DEFLATED = 8;
free(inflator);
int zipReadCallback(uzlib_uncomp* uncomp) {
auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp);
if (ctx->fileRemaining == 0) return -1;
if (status != TINFL_STATUS_DONE) {
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
return false;
}
const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize;
const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead);
ctx->fileRemaining -= bytesRead;
return true;
if (bytesRead == 0) return -1;
uncomp->source = ctx->readBuf + 1;
uncomp->source_limit = ctx->readBuf + bytesRead;
return ctx->readBuf[0];
}
} // namespace
bool ZipFile::loadAllFileStatSlims() {
const bool wasOpen = isOpen();
@@ -111,7 +114,6 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
// Phase 1: Try scanning from cursor position first
uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset;
uint32_t wrapPos = zipDetails.centralDirOffset;
bool wrapped = false;
bool found = false;
@@ -201,7 +203,7 @@ long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
}
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
0x04034b50 /* ZIP local file header signature */) {
LOG_ERR("ZIP", "Not a valid zip file header");
return -1;
}
@@ -415,7 +417,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
return nullptr;
}
if (fileStat.method == MZ_NO_COMPRESSION) {
if (fileStat.method == ZIP_METHOD_STORED) {
// no deflation, just read content
const size_t dataRead = file.read(data, inflatedDataSize);
if (!wasOpen) {
@@ -429,7 +431,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
}
// Continue out of block with data set
} else if (fileStat.method == MZ_DEFLATED) {
} else if (fileStat.method == ZIP_METHOD_DEFLATED) {
// Read out deflated content from file
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
if (deflatedData == nullptr) {
@@ -452,7 +454,13 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
return nullptr;
}
bool success = inflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
bool success = false;
{
InflateReader r;
r.init(false);
r.setSource(deflatedData, deflatedDataSize);
success = r.read(data, inflatedDataSize);
}
free(deflatedData);
if (!success) {
@@ -495,7 +503,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
const auto deflatedDataSize = fileStat.compressedSize;
const auto inflatedDataSize = fileStat.uncompressedSize;
if (fileStat.method == MZ_NO_COMPRESSION) {
if (fileStat.method == ZIP_METHOD_STORED) {
// no deflation, just read content
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
if (!buffer) {
@@ -529,127 +537,88 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
return true;
}
if (fileStat.method == MZ_DEFLATED) {
auto* inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
if (!inflator) {
LOG_ERR("ZIP", "Failed to allocate memory for inflator");
if (!wasOpen) {
close();
}
return false;
}
memset(inflator, 0, sizeof(tinfl_decompressor));
tinfl_init(inflator);
// Setup file read buffer
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
if (fileStat.method == ZIP_METHOD_DEFLATED) {
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
if (!fileReadBuffer) {
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
free(inflator);
if (!wasOpen) {
close();
}
return false;
}
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
auto* outputBuffer = static_cast<uint8_t*>(malloc(chunkSize));
if (!outputBuffer) {
LOG_ERR("ZIP", "Failed to allocate memory for dictionary");
free(inflator);
LOG_ERR("ZIP", "Failed to allocate memory for output buffer");
free(fileReadBuffer);
if (!wasOpen) {
close();
}
return false;
}
memset(outputBuffer, 0, TINFL_LZ_DICT_SIZE);
size_t fileRemainingBytes = deflatedDataSize;
size_t processedOutputBytes = 0;
size_t fileReadBufferFilledBytes = 0;
size_t fileReadBufferCursor = 0;
size_t outputCursor = 0; // Current offset in the circular dictionary
ZipInflateCtx ctx;
ctx.file = &file;
ctx.fileRemaining = deflatedDataSize;
ctx.readBuf = fileReadBuffer;
ctx.readBufSize = chunkSize;
if (!ctx.reader.init(true)) {
LOG_ERR("ZIP", "Failed to init inflate reader");
free(outputBuffer);
free(fileReadBuffer);
if (!wasOpen) {
close();
}
return false;
}
ctx.reader.setReadCallback(zipReadCallback);
bool success = false;
size_t totalProduced = 0;
while (true) {
// Load more compressed bytes when needed
if (fileReadBufferCursor >= fileReadBufferFilledBytes) {
if (fileRemainingBytes == 0) {
// Should not be hit, but a safe protection
break; // EOF
}
size_t produced;
const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced);
fileReadBufferFilledBytes =
file.read(fileReadBuffer, fileRemainingBytes < chunkSize ? fileRemainingBytes : chunkSize);
fileRemainingBytes -= fileReadBufferFilledBytes;
fileReadBufferCursor = 0;
if (fileReadBufferFilledBytes == 0) {
// Bad read
break; // EOF
}
totalProduced += produced;
if (totalProduced > static_cast<size_t>(inflatedDataSize)) {
LOG_ERR("ZIP", "Decompressed size exceeds expected (%zu > %zu)", totalProduced,
static_cast<size_t>(inflatedDataSize));
break;
}
// Available bytes in fileReadBuffer to process
size_t inBytes = fileReadBufferFilledBytes - fileReadBufferCursor;
// Space remaining in outputBuffer
size_t outBytes = TINFL_LZ_DICT_SIZE - outputCursor;
const tinfl_status status = tinfl_decompress(inflator, fileReadBuffer + fileReadBufferCursor, &inBytes,
outputBuffer, outputBuffer + outputCursor, &outBytes,
fileRemainingBytes > 0 ? TINFL_FLAG_HAS_MORE_INPUT : 0);
// Update input position
fileReadBufferCursor += inBytes;
// Write output chunk
if (outBytes > 0) {
processedOutputBytes += outBytes;
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
if (produced > 0) {
if (out.write(outputBuffer, produced) != produced) {
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
if (!wasOpen) {
close();
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
break;
}
// Update output position in buffer (with wraparound)
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
}
if (status < 0) {
LOG_ERR("ZIP", "tinfl_decompress() failed with status %d", status);
if (!wasOpen) {
close();
if (status == InflateStatus::Done) {
if (totalProduced != static_cast<size_t>(inflatedDataSize)) {
LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize),
totalProduced);
break;
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
LOG_DBG("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
success = true;
break;
}
if (status == TINFL_STATUS_DONE) {
LOG_ERR("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
if (!wasOpen) {
close();
}
free(inflator);
free(fileReadBuffer);
free(outputBuffer);
return true;
if (status == InflateStatus::Error) {
LOG_ERR("ZIP", "Decompression failed");
break;
}
// InflateStatus::Ok: output buffer full, continue
}
// If we get here, EOF reached without TINFL_STATUS_DONE
LOG_ERR("ZIP", "Unexpected EOF");
if (!wasOpen) {
close();
}
free(outputBuffer);
free(fileReadBuffer);
free(inflator);
return false;
return success; // ctx.reader destructor frees the ring buffer
}
if (!wasOpen) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,6 @@ board_upload.offset_address = 0x10000
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
-DMINIZ_NO_STDIO=1
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
-DDISABLE_FS_H_WARNING=1
# https://libexpat.github.io/doc/api/latest/#XML_GE