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

View File

@@ -1,8 +1,6 @@
#pragma once #pragma once
#include <uzlib.h> #include <InflateReader.h>
#include <cstdint>
#include "EpdFontData.h" #include "EpdFontData.h"
@@ -30,7 +28,7 @@ class FontDecompressor {
bool valid = false; bool valid = false;
}; };
struct uzlib_uncomp decomp = {}; InflateReader inflateReader;
CacheEntry cache[CACHE_SLOTS] = {}; CacheEntry cache[CACHE_SLOTS] = {};
uint32_t accessCounter = 0; 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 "PngToBmpConverter.h"
#include <HalStorage.h> #include <HalStorage.h>
#include <InflateReader.h>
#include <Logging.h> #include <Logging.h>
#include <miniz.h>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -20,36 +20,6 @@ constexpr int TARGET_MAX_WIDTH = 480;
constexpr int TARGET_MAX_HEIGHT = 800; 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) // BMP writing helpers (same as JpegToBmpConverter)
inline void write16(Print& out, const uint16_t value) { inline void write16(Print& out, const uint16_t value) {
out.write(value & 0xFF); out.write(value & 0xFF);
@@ -70,7 +40,49 @@ inline void write32Signed(Print& out, const int32_t value) {
out.write((value >> 24) & 0xFF); 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 bytesPerRow = (width + 3) / 4 * 4;
const int imageSize = bytesPerRow * height; const int imageSize = bytesPerRow * height;
const uint32_t paletteSize = 256 * 4; 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 bytesPerRow = (width + 31) / 32 * 4;
const int imageSize = bytesPerRow * height; const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 62 + imageSize; 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 bytesPerRow = (width * 2 + 31) / 32 * 4;
const int imageSize = bytesPerRow * height; const int imageSize = bytesPerRow * height;
const uint32_t fileSize = 70 + imageSize; const uint32_t fileSize = 70 + imageSize;
@@ -160,21 +172,13 @@ static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height)
bmpOut.write(i); bmpOut.write(i);
} }
} }
} // namespace
// 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;
}
// Context for streaming PNG decompression // Context for streaming PNG decompression
// IMPORTANT: reader must be the first field - the uzlib callback casts uzlib_uncomp* to PngDecodeContext*
struct PngDecodeContext { struct PngDecodeContext {
FsFile& file; InflateReader reader; // Must be first — callback casts uzlib_uncomp* to PngDecodeContext*
FsFile* file;
// PNG image properties // PNG image properties
uint32_t width; uint32_t width;
@@ -188,15 +192,11 @@ struct PngDecodeContext {
uint8_t* currentRow; // current defiltered scanline uint8_t* currentRow; // current defiltered scanline
uint8_t* previousRow; // previous defiltered scanline uint8_t* previousRow; // previous defiltered scanline
// zlib decompression state
mz_stream zstream;
bool zstreamInitialized;
// Chunk reading state // Chunk reading state
uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk uint32_t chunkBytesRemaining; // bytes left in current IDAT chunk
bool idatFinished; // no more IDAT chunks bool idatFinished; // no more IDAT chunks
// File read buffer for feeding zlib // File read buffer for feeding uzlib
uint8_t readBuf[2048]; uint8_t readBuf[2048];
// Palette for indexed color (type 3) // Palette for indexed color (type 3)
@@ -209,10 +209,10 @@ struct PngDecodeContext {
static bool findNextIdatChunk(PngDecodeContext& ctx) { static bool findNextIdatChunk(PngDecodeContext& ctx) {
while (true) { while (true) {
uint32_t chunkLen; uint32_t chunkLen;
if (!readBE32(ctx.file, chunkLen)) return false; if (!readBE32(*ctx.file, chunkLen)) return false;
uint8_t chunkType[4]; 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) { if (memcmp(chunkType, "IDAT", 4) == 0) {
ctx.chunkBytesRemaining = chunkLen; ctx.chunkBytesRemaining = chunkLen;
@@ -221,7 +221,7 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
// Skip this chunk's data + 4-byte CRC // Skip this chunk's data + 4-byte CRC
// Use seek to skip efficiently // 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 we hit IEND, there are no more chunks
if (memcmp(chunkType, "IEND", 4) == 0) { if (memcmp(chunkType, "IEND", 4) == 0) {
@@ -230,72 +230,50 @@ static bool findNextIdatChunk(PngDecodeContext& ctx) {
} }
} }
// Feed compressed data to zlib from IDAT chunks // uzlib callback: reads the next batch of IDAT data from the file
// Returns number of bytes made available in zstream, or -1 on error static int pngIdatReadCallback(uzlib_uncomp* uncomp) {
static int feedZlibInput(PngDecodeContext& ctx) { auto* ctx = reinterpret_cast<PngDecodeContext*>(uncomp);
if (ctx.idatFinished) return 0;
// If current IDAT chunk is exhausted, skip its CRC and find next if (ctx->idatFinished) return -1;
while (ctx.chunkBytesRemaining == 0) {
// Skip 4-byte CRC of previous IDAT
if (!ctx.file.seekCur(4)) return -1;
if (!findNextIdatChunk(ctx)) { // Skip 4-byte CRC and find next IDAT chunk when current chunk is exhausted
ctx.idatFinished = true; while (ctx->chunkBytesRemaining == 0) {
return 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 // Read from current IDAT chunk into the read buffer
size_t toRead = sizeof(ctx.readBuf); size_t toRead = sizeof(ctx->readBuf);
if (toRead > ctx.chunkBytesRemaining) toRead = ctx.chunkBytesRemaining; if (toRead > ctx->chunkBytesRemaining) toRead = ctx->chunkBytesRemaining;
int bytesRead = ctx.file.read(ctx.readBuf, toRead); int bytesRead = ctx->file->read(ctx->readBuf, toRead);
if (bytesRead <= 0) return -1; if (bytesRead <= 0) {
ctx->idatFinished = true;
ctx.chunkBytesRemaining -= bytesRead; return -1;
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;
} }
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 // Decode one scanline: decompress filter byte + raw bytes, then unfilter
static bool decodeScanline(PngDecodeContext& ctx) { static bool decodeScanline(PngDecodeContext& ctx) {
// Decompress filter byte // Decompress filter byte
uint8_t filterType; uint8_t filterType;
if (!decompressBytes(ctx, &filterType, 1)) return false; if (!ctx.reader.read(&filterType, 1)) return false;
// Decompress raw row data into currentRow // 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 // Apply reverse filter
const int bpp = ctx.bytesPerPixel; const int bpp = ctx.bytesPerPixel;
@@ -521,22 +499,15 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
} }
// Initialize decode context // Initialize decode context
PngDecodeContext ctx = {.file = pngFile, PngDecodeContext ctx = {};
.width = width, ctx.file = &pngFile;
.height = height, ctx.width = width;
.bitDepth = bitDepth, ctx.height = height;
.colorType = colorType, ctx.bitDepth = bitDepth;
.bytesPerPixel = bytesPerPixel, ctx.colorType = colorType;
.rawRowBytes = rawRowBytes, ctx.bytesPerPixel = bytesPerPixel;
.currentRow = nullptr, ctx.rawRowBytes = rawRowBytes;
.previousRow = nullptr, ctx.paletteSize = 0;
.zstream = {},
.zstreamInitialized = false,
.chunkBytesRemaining = 0,
.idatFinished = false,
.readBuf = {},
.palette = {},
.paletteSize = 0};
// Allocate scanline buffers // Allocate scanline buffers
ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes)); ctx.currentRow = static_cast<uint8_t*>(malloc(rawRowBytes));
@@ -585,15 +556,16 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
return false; return false;
} }
// Initialize zlib decompression // Initialize streaming decompressor with 32KB ring buffer for back-reference history
memset(&ctx.zstream, 0, sizeof(ctx.zstream)); if (!ctx.reader.init(true)) {
if (mz_inflateInit(&ctx.zstream) != MZ_OK) { LOG_ERR("PNG", "Failed to init inflate reader");
LOG_ERR("PNG", "Failed to initialize zlib");
free(ctx.currentRow); free(ctx.currentRow);
free(ctx.previousRow); free(ctx.previousRow);
return false; 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) // Calculate output dimensions (same logic as JpegToBmpConverter)
int outWidth = width; int outWidth = width;
@@ -618,8 +590,8 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
if (outWidth < 1) outWidth = 1; if (outWidth < 1) outWidth = 1;
if (outHeight < 1) outHeight = 1; if (outHeight < 1) outHeight = 1;
scaleX_fp = (static_cast<uint32_t>(width) << 16) / outWidth; scaleX_fp = (width << 16) / outWidth;
scaleY_fp = (static_cast<uint32_t>(height) << 16) / outHeight; scaleY_fp = (height << 16) / outHeight;
needsScaling = true; needsScaling = true;
LOG_DBG("PNG", "Scaling %ux%u -> %dx%d (target %dx%d)", width, height, outWidth, outHeight, targetWidth, 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)); auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
if (!rowBuffer) { if (!rowBuffer) {
LOG_ERR("PNG", "Failed to allocate row buffer"); LOG_ERR("PNG", "Failed to allocate row buffer");
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow); free(ctx.currentRow);
free(ctx.previousRow); free(ctx.previousRow);
return false; return false;
@@ -687,7 +658,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
delete fsDitherer; delete fsDitherer;
delete atkinson1BitDitherer; delete atkinson1BitDitherer;
free(rowBuffer); free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow); free(ctx.currentRow);
free(ctx.previousRow); free(ctx.previousRow);
return false; return false;
@@ -842,7 +812,6 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu
delete fsDitherer; delete fsDitherer;
delete atkinson1BitDitherer; delete atkinson1BitDitherer;
free(rowBuffer); free(rowBuffer);
mz_inflateEnd(&ctx.zstream);
free(ctx.currentRow); free(ctx.currentRow);
free(ctx.previousRow); free(ctx.previousRow);

View File

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