2025-12-03 22:00:29 +11:00
|
|
|
#include "ZipFile.h"
|
|
|
|
|
|
|
|
|
|
#include <HardwareSerial.h>
|
2025-12-30 15:09:30 +10:00
|
|
|
#include <SDCardManager.h>
|
2025-12-03 22:00:29 +11:00
|
|
|
#include <miniz.h>
|
|
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) {
|
|
|
|
|
// Setup inflator
|
|
|
|
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
|
|
|
|
if (!inflator) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
memset(inflator, 0, sizeof(tinfl_decompressor));
|
|
|
|
|
tinfl_init(inflator);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
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);
|
|
|
|
|
free(inflator);
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
if (status != TINFL_STATUS_DONE) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
return true;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
bool ZipFile::loadAllFileStatSlims() {
|
|
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!loadZipDetails()) {
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
file.seek(zipDetails.centralDirOffset);
|
|
|
|
|
|
|
|
|
|
uint32_t sig;
|
|
|
|
|
char itemName[256];
|
|
|
|
|
fileStatSlimCache.clear();
|
|
|
|
|
fileStatSlimCache.reserve(zipDetails.totalEntries);
|
|
|
|
|
|
|
|
|
|
while (file.available()) {
|
2025-12-30 15:09:30 +10:00
|
|
|
file.read(&sig, 4);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (sig != 0x02014b50) break; // End of list
|
|
|
|
|
|
|
|
|
|
FileStatSlim fileStat = {};
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seekCur(6);
|
|
|
|
|
file.read(&fileStat.method, 2);
|
|
|
|
|
file.seekCur(8);
|
|
|
|
|
file.read(&fileStat.compressedSize, 4);
|
|
|
|
|
file.read(&fileStat.uncompressedSize, 4);
|
2025-12-29 20:17:29 +10:00
|
|
|
uint16_t nameLen, m, k;
|
2025-12-30 15:09:30 +10:00
|
|
|
file.read(&nameLen, 2);
|
|
|
|
|
file.read(&m, 2);
|
|
|
|
|
file.read(&k, 2);
|
|
|
|
|
file.seekCur(8);
|
|
|
|
|
file.read(&fileStat.localHeaderOffset, 4);
|
|
|
|
|
file.read(itemName, nameLen);
|
2025-12-29 20:17:29 +10:00
|
|
|
itemName[nameLen] = '\0';
|
|
|
|
|
|
|
|
|
|
fileStatSlimCache.emplace(itemName, fileStat);
|
|
|
|
|
|
|
|
|
|
// Skip the rest of this entry (extra field + comment)
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seekCur(m + k);
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-29 20:17:29 +10:00
|
|
|
|
2026-01-20 23:35:54 -08:00
|
|
|
// Set cursor to start of central directory for sequential access
|
|
|
|
|
lastCentralDirPos = zipDetails.centralDirOffset;
|
|
|
|
|
lastCentralDirPosValid = true;
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2025-12-21 04:38:51 +01:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
|
|
|
|
if (!fileStatSlimCache.empty()) {
|
|
|
|
|
const auto it = fileStatSlimCache.find(filename);
|
|
|
|
|
if (it != fileStatSlimCache.end()) {
|
|
|
|
|
*fileStat = it->second;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-29 20:17:29 +10:00
|
|
|
|
|
|
|
|
if (!loadZipDetails()) {
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 23:35:54 -08:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
file.seek(startPos);
|
2025-12-29 20:17:29 +10:00
|
|
|
|
|
|
|
|
uint32_t sig;
|
|
|
|
|
char itemName[256];
|
|
|
|
|
|
2026-01-20 23:35:54 -08:00
|
|
|
while (true) {
|
|
|
|
|
uint32_t entryStart = file.position();
|
|
|
|
|
|
|
|
|
|
if (file.read(&sig, 4) != 4 || sig != 0x02014b50) {
|
|
|
|
|
// End of central directory
|
|
|
|
|
if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) {
|
|
|
|
|
// Wrap around to beginning
|
|
|
|
|
file.seek(zipDetails.centralDirOffset);
|
|
|
|
|
wrapped = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we've wrapped and reached our start position, stop
|
|
|
|
|
if (wrapped && entryStart >= startPos) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-12-29 20:17:29 +10:00
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seekCur(6);
|
|
|
|
|
file.read(&fileStat->method, 2);
|
|
|
|
|
file.seekCur(8);
|
|
|
|
|
file.read(&fileStat->compressedSize, 4);
|
|
|
|
|
file.read(&fileStat->uncompressedSize, 4);
|
2025-12-29 20:17:29 +10:00
|
|
|
uint16_t nameLen, m, k;
|
2025-12-30 15:09:30 +10:00
|
|
|
file.read(&nameLen, 2);
|
|
|
|
|
file.read(&m, 2);
|
|
|
|
|
file.read(&k, 2);
|
|
|
|
|
file.seekCur(8);
|
|
|
|
|
file.read(&fileStat->localHeaderOffset, 4);
|
2025-12-29 20:17:29 +10:00
|
|
|
|
2026-01-20 23:35:54 -08:00
|
|
|
if (nameLen < 256) {
|
|
|
|
|
file.read(itemName, nameLen);
|
|
|
|
|
itemName[nameLen] = '\0';
|
|
|
|
|
|
|
|
|
|
if (strcmp(itemName, filename) == 0) {
|
|
|
|
|
// Found it! Update cursor to next entry
|
|
|
|
|
file.seekCur(m + k);
|
|
|
|
|
lastCentralDirPos = file.position();
|
|
|
|
|
lastCentralDirPosValid = true;
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Name too long, skip it
|
|
|
|
|
file.seekCur(nameLen);
|
2025-12-29 20:17:29 +10:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 23:35:54 -08:00
|
|
|
// Skip extra field + comment
|
2025-12-30 15:09:30 +10:00
|
|
|
file.seekCur(m + k);
|
2025-12-29 20:17:29 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return found;
|
2025-12-08 00:39:17 +11:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
|
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
constexpr auto localHeaderSize = 30;
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
uint8_t pLocalHeader[localHeaderSize];
|
2025-12-29 20:17:29 +10:00
|
|
|
const uint64_t fileOffset = fileStat.localHeaderOffset;
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
file.seek(fileOffset);
|
|
|
|
|
const size_t read = file.read(pLocalHeader, localHeaderSize);
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
2025-12-19 13:28:43 +01:00
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
if (read != localHeaderSize) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
return -1;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
|
|
|
|
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
return -1;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8);
|
|
|
|
|
const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8);
|
2025-12-08 00:39:17 +11:00
|
|
|
return fileOffset + localHeaderSize + filenameLength + extraOffset;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
bool ZipFile::loadZipDetails() {
|
|
|
|
|
if (zipDetails.isSet) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t fileSize = file.size();
|
|
|
|
|
if (fileSize < 22) {
|
|
|
|
|
Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis());
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return false; // Minimum EOCD size is 22 bytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We scan the last 1KB (or the whole file if smaller) for the EOCD signature
|
|
|
|
|
// 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian
|
|
|
|
|
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
|
|
|
|
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
|
|
|
|
if (!buffer) {
|
|
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis());
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.seek(fileSize - scanRange);
|
|
|
|
|
file.read(buffer, scanRange);
|
|
|
|
|
|
|
|
|
|
// Scan backwards for the signature
|
|
|
|
|
int foundOffset = -1;
|
|
|
|
|
for (int i = scanRange - 22; i >= 0; i--) {
|
|
|
|
|
constexpr uint32_t signature = 0x06054b50;
|
|
|
|
|
if (*reinterpret_cast<uint32_t*>(&buffer[i]) == signature) {
|
|
|
|
|
foundOffset = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (foundOffset == -1) {
|
|
|
|
|
Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis());
|
|
|
|
|
free(buffer);
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-13 19:36:01 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
// Now extract the values we need from the EOCD record
|
|
|
|
|
// Relative positions within EOCD:
|
|
|
|
|
// Offset 10: Total number of entries (2 bytes)
|
|
|
|
|
// Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes)
|
|
|
|
|
zipDetails.totalEntries = *reinterpret_cast<uint16_t*>(&buffer[foundOffset + 10]);
|
|
|
|
|
zipDetails.centralDirOffset = *reinterpret_cast<uint32_t*>(&buffer[foundOffset + 16]);
|
|
|
|
|
zipDetails.isSet = true;
|
|
|
|
|
|
|
|
|
|
free(buffer);
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ZipFile::open() {
|
2025-12-30 15:09:30 +10:00
|
|
|
if (!SdMan.openFileForRead("ZIP", filePath, file)) {
|
2025-12-29 20:17:29 +10:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ZipFile::close() {
|
|
|
|
|
if (file) {
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
2026-01-20 23:35:54 -08:00
|
|
|
lastCentralDirPos = 0;
|
|
|
|
|
lastCentralDirPosValid = false;
|
2025-12-29 20:17:29 +10:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
|
|
|
|
|
FileStatSlim fileStat = {};
|
|
|
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*size = static_cast<size_t>(fileStat.uncompressedSize);
|
2025-12-13 19:36:01 +11:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
|
|
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
2025-12-08 00:39:17 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
FileStatSlim fileStat = {};
|
|
|
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
const long fileOffset = getDataOffset(fileStat);
|
|
|
|
|
if (fileOffset < 0) {
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-19 13:28:43 +01:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
file.seek(fileOffset);
|
|
|
|
|
|
|
|
|
|
const auto deflatedDataSize = fileStat.compressedSize;
|
|
|
|
|
const auto inflatedDataSize = fileStat.uncompressedSize;
|
2025-12-03 22:00:29 +11:00
|
|
|
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
|
|
|
|
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
2025-12-21 03:35:37 +01:00
|
|
|
if (data == nullptr) {
|
|
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-21 03:35:37 +01:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (fileStat.method == MZ_NO_COMPRESSION) {
|
2025-12-03 22:00:29 +11:00
|
|
|
// no deflation, just read content
|
2025-12-29 20:17:29 +10:00
|
|
|
const size_t dataRead = file.read(data, inflatedDataSize);
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
if (dataRead != inflatedDataSize) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to read data\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
free(data);
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
|
|
|
|
// Continue out of block with data set
|
2025-12-29 20:17:29 +10:00
|
|
|
} else if (fileStat.method == MZ_DEFLATED) {
|
2025-12-03 22:00:29 +11:00
|
|
|
// Read out deflated content from file
|
|
|
|
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
|
|
|
|
if (deflatedData == nullptr) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
const size_t dataRead = file.read(deflatedData, deflatedDataSize);
|
|
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-03 22:00:29 +11:00
|
|
|
if (dataRead != deflatedDataSize) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead);
|
2025-12-03 22:00:29 +11:00
|
|
|
free(deflatedData);
|
2025-12-08 00:39:17 +11:00
|
|
|
free(data);
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
const bool success = inflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
|
2025-12-03 22:00:29 +11:00
|
|
|
free(deflatedData);
|
2025-12-08 00:39:17 +11:00
|
|
|
|
|
|
|
|
if (!success) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
free(data);
|
2025-12-03 22:00:29 +11:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
|
|
|
|
// Continue out of block with data set
|
|
|
|
|
} else {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return nullptr;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 00:39:17 +11:00
|
|
|
if (trailingNullByte) data[inflatedDataSize] = '\0';
|
|
|
|
|
if (size) *size = inflatedDataSize;
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) {
|
|
|
|
|
const bool wasOpen = isOpen();
|
|
|
|
|
if (!wasOpen && !open()) {
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
FileStatSlim fileStat = {};
|
|
|
|
|
if (!loadFileStatSlim(filename, &fileStat)) {
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
const long fileOffset = getDataOffset(fileStat);
|
|
|
|
|
if (fileOffset < 0) {
|
2025-12-19 13:28:43 +01:00
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
file.seek(fileOffset);
|
|
|
|
|
const auto deflatedDataSize = fileStat.compressedSize;
|
|
|
|
|
const auto inflatedDataSize = fileStat.uncompressedSize;
|
2025-12-08 00:39:17 +11:00
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (fileStat.method == MZ_NO_COMPRESSION) {
|
2025-12-08 00:39:17 +11:00
|
|
|
// no deflation, just read content
|
|
|
|
|
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
|
|
|
|
if (!buffer) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t remaining = inflatedDataSize;
|
|
|
|
|
while (remaining > 0) {
|
2025-12-29 20:17:29 +10:00
|
|
|
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
2025-12-08 00:39:17 +11:00
|
|
|
if (dataRead == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
free(buffer);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out.write(buffer, dataRead);
|
|
|
|
|
remaining -= dataRead;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
free(buffer);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (fileStat.method == MZ_DEFLATED) {
|
2025-12-08 00:39:17 +11:00
|
|
|
// Setup inflator
|
|
|
|
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
|
|
|
|
if (!inflator) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
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) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
free(inflator);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
|
|
|
|
if (!outputBuffer) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
free(inflator);
|
|
|
|
|
free(fileReadBuffer);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
|
// Load more compressed bytes when needed
|
|
|
|
|
if (fileReadBufferCursor >= fileReadBufferFilledBytes) {
|
|
|
|
|
if (fileRemainingBytes == 0) {
|
|
|
|
|
// Should not be hit, but a safe protection
|
|
|
|
|
break; // EOF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileReadBufferFilledBytes =
|
2025-12-29 20:17:29 +10:00
|
|
|
file.read(fileReadBuffer, fileRemainingBytes < chunkSize ? fileRemainingBytes : chunkSize);
|
2025-12-08 00:39:17 +11:00
|
|
|
fileRemainingBytes -= fileReadBufferFilledBytes;
|
|
|
|
|
fileReadBufferCursor = 0;
|
|
|
|
|
|
|
|
|
|
if (fileReadBufferFilledBytes == 0) {
|
|
|
|
|
// Bad read
|
|
|
|
|
break; // EOF
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
2025-12-13 19:36:01 +11:00
|
|
|
if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) {
|
|
|
|
|
Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-13 19:36:01 +11:00
|
|
|
free(outputBuffer);
|
|
|
|
|
free(fileReadBuffer);
|
|
|
|
|
free(inflator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
// Update output position in buffer (with wraparound)
|
|
|
|
|
outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status < 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
free(outputBuffer);
|
|
|
|
|
free(fileReadBuffer);
|
|
|
|
|
free(inflator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status == TINFL_STATUS_DONE) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize,
|
|
|
|
|
inflatedDataSize);
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
free(inflator);
|
|
|
|
|
free(fileReadBuffer);
|
|
|
|
|
free(outputBuffer);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we get here, EOF reached without TINFL_STATUS_DONE
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis());
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
2025-12-08 00:39:17 +11:00
|
|
|
free(outputBuffer);
|
|
|
|
|
free(fileReadBuffer);
|
|
|
|
|
free(inflator);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:17:29 +10:00
|
|
|
if (!wasOpen) {
|
|
|
|
|
close();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis());
|
2025-12-08 00:39:17 +11:00
|
|
|
return false;
|
2025-12-03 22:00:29 +11:00
|
|
|
}
|