Files
Crosspoint/lib/ZipFile/ZipFile.h
T
Justin Mitchell 9d147cade9 Deduplicate identical CSS files in EPUB parsing & probe images for dimensions instead of reading the full file
Some EPUB converters emit byte-identical stylesheets per chapter (100+ entries). Now scan the ZIP central directory once to identify duplicates by CRC32 and compressed size, then skip parsing identical files. This avoids redundant ZIP lookups and SD extraction round-trips while preserving all styles since rules merge into a global set. Also add extractItemToFile helper and allowEarlyStop parameter to readItemContentsToStream.
2026-07-20 17:16:54 -04:00

145 lines
4.4 KiB
C++

#pragma once
#include <HalStorage.h>
#include <deque>
#include <string>
#include <string_view>
#include <unordered_map>
class ZipFile {
public:
struct FileStatSlim {
uint16_t method; // Compression method
uint32_t compressedSize; // Compressed size
uint32_t uncompressedSize; // Uncompressed size
uint32_t localHeaderOffset; // Offset of local file header
};
struct ZipDetails {
uint32_t centralDirOffset;
uint16_t totalEntries;
bool isSet;
};
// Target for batch uncompressed size lookup (sorted by hash, then len)
struct SizeTarget {
uint64_t hash; // FNV-1a 64-bit hash of normalized path
uint16_t len; // Length of path for collision reduction
uint16_t index; // Caller's index (e.g. spine index)
};
// FNV-1a 64-bit hash computed from char buffer (no std::string allocation)
static uint64_t fnvHash64(const char* s, size_t len) {
uint64_t hash = 14695981039346656037ull;
for (size_t i = 0; i < len; i++) {
hash ^= static_cast<uint8_t>(s[i]);
hash *= 1099511628211ull;
}
return hash;
}
private:
const std::string& filePath;
HalFile file;
ZipDetails zipDetails = {0, 0, false};
std::unordered_map<std::string, FileStatSlim> fileStatSlimCache;
// Cursor for sequential central-dir scanning optimization
uint32_t lastCentralDirPos = 0;
bool lastCentralDirPosValid = false;
bool loadFileStatSlim(const char* filename, FileStatSlim* fileStat);
long getDataOffset(const FileStatSlim& fileStat);
bool loadZipDetails();
public:
explicit ZipFile(const std::string& filePath) : filePath(filePath) {}
~ZipFile() = default;
// Zip file can be opened and closed by hand in order to allow for quick calculation of inflated file size
// It is NOT recommended to pre-open it for any kind of inflation due to memory constraints
bool isOpen() const { return !!file; }
bool open();
bool close();
bool loadAllFileStatSlims();
bool getInflatedFileSize(const char* filename, size_t* size);
// Batch lookup: scan ZIP central dir once and fill sizes for matching targets.
// targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize.
// Returns number of targets matched.
int fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes);
// Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple
// These functions will open and close the zip as needed
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
// allowEarlyStop: a short write from `out` is treated as the sink asking to
// stop (returns true) instead of a write failure — used by header probes
// that only need the first bytes of an entry.
bool readFileToStream(const char* filename, Print& out, size_t chunkSize, bool allowEarlyStop = false);
template <typename F>
bool enumerateFilePaths(F&& callback) {
if (!fileStatSlimCache.empty()) {
for (const auto& entry : fileStatSlimCache) {
callback(std::string_view{entry.first});
}
return true;
}
return enumerateFileEntries([&callback](std::string_view path, uint32_t, uint32_t) { callback(path); });
}
// Callback receives (path, crc32, compressedSize) for each central-directory
// entry. Always scans the central directory: the slim-stat cache does not
// hold CRCs.
template <typename F>
bool enumerateFileEntries(F&& callback) {
const bool wasOpen = isOpen();
if (!wasOpen && !open()) {
return false;
}
if (!loadZipDetails()) {
if (!wasOpen) {
close();
}
return false;
}
file.seek(zipDetails.centralDirOffset);
uint32_t sig;
char itemName[256];
while (file.available()) {
file.read(&sig, 4);
if (sig != 0x02014b50) {
break;
}
file.seekCur(12);
uint32_t crc32, compressedSize;
file.read(&crc32, 4);
file.read(&compressedSize, 4);
file.seekCur(4);
uint16_t nameLen, m, k;
file.read(&nameLen, 2);
file.read(&m, 2);
file.read(&k, 2);
file.seekCur(12);
if (nameLen < sizeof(itemName)) {
file.read(itemName, nameLen);
itemName[nameLen] = '\0';
callback(std::string_view{itemName, nameLen}, crc32, compressedSize);
} else {
file.seekCur(nameLen);
}
file.seekCur(m + k);
}
if (!wasOpen) {
close();
}
return true;
}
};