A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at records-reader 561 lines 16 kB view raw
1#include "ZipFile.h" 2 3#include <HalStorage.h> 4#include <InflateReader.h> 5#include <Logging.h> 6 7#include <algorithm> 8 9struct ZipInflateCtx { 10 InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx* 11 FsFile* file = nullptr; 12 size_t fileRemaining = 0; 13 uint8_t* readBuf = nullptr; 14 size_t readBufSize = 0; 15}; 16 17namespace { 18constexpr uint16_t ZIP_METHOD_STORED = 0; 19constexpr uint16_t ZIP_METHOD_DEFLATED = 8; 20 21// RAII zip: opens the zip if not already open, closes on destruction only if 22// it performed the open. Removes the wasOpen/close boilerplate from every method. 23class ScopedOpenClose final { 24 public: 25 [[nodiscard]] explicit ScopedOpenClose(ZipFile& zf) : zf(zf), needsClose(!zf.isOpen()) { 26 if (needsClose) ok = zf.open(); 27 } 28 ~ScopedOpenClose() { 29 if (needsClose && ok) zf.close(); 30 } 31 ScopedOpenClose(const ScopedOpenClose&) = delete; 32 ScopedOpenClose& operator=(const ScopedOpenClose&) = delete; 33 ScopedOpenClose(ScopedOpenClose&&) = delete; 34 ScopedOpenClose& operator=(ScopedOpenClose&&) = delete; 35 explicit operator bool() const { return ok || !needsClose; } 36 37 private: 38 ZipFile& zf; 39 bool needsClose = false; 40 bool ok = true; // true when zip was already open (no open() call needed) 41}; 42 43int zipReadCallback(uzlib_uncomp* uncomp) { 44 auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp); 45 if (ctx->fileRemaining == 0) return -1; 46 47 const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize; 48 const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead); 49 ctx->fileRemaining -= bytesRead; 50 51 if (bytesRead == 0) return -1; 52 53 uncomp->source = ctx->readBuf + 1; 54 uncomp->source_limit = ctx->readBuf + bytesRead; 55 return ctx->readBuf[0]; 56} 57} // namespace 58 59bool ZipFile::loadAllFileStatSlims() { 60 const ScopedOpenClose zip{*this}; 61 if (!zip) return false; 62 63 if (!loadZipDetails()) return false; 64 65 file.seek(zipDetails.centralDirOffset); 66 67 uint32_t sig; 68 char itemName[256]; 69 fileStatSlimCache.clear(); 70 fileStatSlimCache.reserve(zipDetails.totalEntries); 71 72 while (file.available()) { 73 file.read(&sig, 4); 74 if (sig != 0x02014b50) break; // End of list 75 76 FileStatSlim fileStat = {}; 77 78 file.seekCur(6); 79 file.read(&fileStat.method, 2); 80 file.seekCur(8); 81 file.read(&fileStat.compressedSize, 4); 82 file.read(&fileStat.uncompressedSize, 4); 83 uint16_t nameLen, m, k; 84 file.read(&nameLen, 2); 85 file.read(&m, 2); 86 file.read(&k, 2); 87 file.seekCur(8); 88 file.read(&fileStat.localHeaderOffset, 4); 89 90 if (nameLen < sizeof(itemName)) { 91 file.read(itemName, nameLen); 92 itemName[nameLen] = '\0'; 93 fileStatSlimCache.emplace(itemName, fileStat); 94 } else { 95 // Skip over oversized entry names to avoid writing past fixed buffer. 96 file.seekCur(nameLen); 97 } 98 99 // Skip the rest of this entry (extra field + comment) 100 file.seekCur(m + k); 101 } 102 103 // Set cursor to start of central directory for sequential access 104 lastCentralDirPos = zipDetails.centralDirOffset; 105 lastCentralDirPosValid = true; 106 107 return true; 108} 109 110bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { 111 if (!fileStatSlimCache.empty()) { 112 const auto it = fileStatSlimCache.find(filename); 113 if (it != fileStatSlimCache.end()) { 114 *fileStat = it->second; 115 return true; 116 } 117 return false; 118 } 119 120 const ScopedOpenClose zip{*this}; 121 if (!zip) return false; 122 123 if (!loadZipDetails()) return false; 124 125 // Phase 1: Try scanning from cursor position first 126 uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset; 127 bool wrapped = false; 128 bool found = false; 129 130 file.seek(startPos); 131 132 uint32_t sig; 133 char itemName[256]; 134 135 while (true) { 136 uint32_t entryStart = file.position(); 137 138 if (file.read(&sig, 4) != 4 || sig != 0x02014b50) { 139 // End of central directory 140 if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) { 141 // Wrap around to beginning 142 file.seek(zipDetails.centralDirOffset); 143 wrapped = true; 144 continue; 145 } 146 break; 147 } 148 149 // If we've wrapped and reached our start position, stop 150 if (wrapped && entryStart >= startPos) { 151 break; 152 } 153 154 file.seekCur(6); 155 file.read(&fileStat->method, 2); 156 file.seekCur(8); 157 file.read(&fileStat->compressedSize, 4); 158 file.read(&fileStat->uncompressedSize, 4); 159 uint16_t nameLen, m, k; 160 file.read(&nameLen, 2); 161 file.read(&m, 2); 162 file.read(&k, 2); 163 file.seekCur(8); 164 file.read(&fileStat->localHeaderOffset, 4); 165 166 if (nameLen < 256) { 167 file.read(itemName, nameLen); 168 itemName[nameLen] = '\0'; 169 170 if (strcmp(itemName, filename) == 0) { 171 // Found it! Update cursor to next entry 172 file.seekCur(m + k); 173 lastCentralDirPos = file.position(); 174 lastCentralDirPosValid = true; 175 found = true; 176 break; 177 } 178 } else { 179 // Name too long, skip it 180 file.seekCur(nameLen); 181 } 182 183 // Skip extra field + comment 184 file.seekCur(m + k); 185 } 186 187 return found; 188} 189 190long ZipFile::getDataOffset(const FileStatSlim& fileStat) { 191 const ScopedOpenClose zip{*this}; 192 if (!zip) return -1; 193 194 constexpr auto localHeaderSize = 30; 195 196 uint8_t pLocalHeader[localHeaderSize]; 197 const uint64_t fileOffset = fileStat.localHeaderOffset; 198 199 file.seek(fileOffset); 200 const size_t read = file.read(pLocalHeader, localHeaderSize); 201 202 if (read != localHeaderSize) { 203 LOG_ERR("ZIP", "Something went wrong reading the local header"); 204 return -1; 205 } 206 207 if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) != 208 0x04034b50 /* ZIP local file header signature */) { 209 LOG_ERR("ZIP", "Not a valid zip file header"); 210 return -1; 211 } 212 213 const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8); 214 const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8); 215 return fileOffset + localHeaderSize + filenameLength + extraOffset; 216} 217 218bool ZipFile::loadZipDetails() { 219 if (zipDetails.isSet) { 220 return true; 221 } 222 223 const ScopedOpenClose zip{*this}; 224 if (!zip) return false; 225 226 const size_t fileSize = file.size(); 227 if (fileSize < 22) { 228 LOG_ERR("ZIP", "File too small to be a valid zip"); 229 return false; // Minimum EOCD size is 22 bytes 230 } 231 232 // We scan the last 1KB (or the whole file if smaller) for the EOCD signature 233 // 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian 234 const int scanRange = fileSize > 1024 ? 1024 : fileSize; 235 const auto buffer = static_cast<uint8_t*>(malloc(scanRange)); 236 if (!buffer) { 237 LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer"); 238 return false; 239 } 240 241 file.seek(fileSize - scanRange); 242 file.read(buffer, scanRange); 243 244 // Scan backwards for the signature 245 int foundOffset = -1; 246 for (int i = scanRange - 22; i >= 0; i--) { 247 constexpr uint32_t signature = 0x06054b50; 248 if (*reinterpret_cast<uint32_t*>(&buffer[i]) == signature) { 249 foundOffset = i; 250 break; 251 } 252 } 253 254 if (foundOffset == -1) { 255 LOG_ERR("ZIP", "EOCD signature not found in zip file"); 256 free(buffer); 257 return false; 258 } 259 260 // Now extract the values we need from the EOCD record 261 // Relative positions within EOCD: 262 // Offset 10: Total number of entries (2 bytes) 263 // Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes) 264 zipDetails.totalEntries = *reinterpret_cast<uint16_t*>(&buffer[foundOffset + 10]); 265 zipDetails.centralDirOffset = *reinterpret_cast<uint32_t*>(&buffer[foundOffset + 16]); 266 zipDetails.isSet = true; 267 268 free(buffer); 269 return true; 270} 271 272bool ZipFile::open() { 273 if (!Storage.openFileForRead("ZIP", filePath, file)) { 274 return false; 275 } 276 return true; 277} 278 279bool ZipFile::close() { 280 if (file) { 281 // Explicit close() required: member variable persists beyond function scope 282 file.close(); 283 } 284 lastCentralDirPos = 0; 285 lastCentralDirPosValid = false; 286 return true; 287} 288 289bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) { 290 FileStatSlim fileStat = {}; 291 if (!loadFileStatSlim(filename, &fileStat)) { 292 return false; 293 } 294 295 *size = static_cast<size_t>(fileStat.uncompressedSize); 296 return true; 297} 298 299int ZipFile::fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes) { 300 if (targets.empty()) { 301 return 0; 302 } 303 304 const ScopedOpenClose zip{*this}; 305 if (!zip) return 0; 306 307 if (!loadZipDetails()) return 0; 308 309 file.seek(zipDetails.centralDirOffset); 310 311 int matched = 0; 312 const int targetCount = static_cast<int>(targets.size()); 313 uint32_t sig; 314 char itemName[256]; 315 316 while (file.available()) { 317 file.read(&sig, 4); 318 if (sig != 0x02014b50) break; 319 320 file.seekCur(6); 321 uint16_t method; 322 file.read(&method, 2); 323 file.seekCur(8); 324 uint32_t compressedSize, uncompressedSize; 325 file.read(&compressedSize, 4); 326 file.read(&uncompressedSize, 4); 327 uint16_t nameLen, m, k; 328 file.read(&nameLen, 2); 329 file.read(&m, 2); 330 file.read(&k, 2); 331 file.seekCur(8); 332 uint32_t localHeaderOffset; 333 file.read(&localHeaderOffset, 4); 334 335 if (nameLen < 256) { 336 file.read(itemName, nameLen); 337 itemName[nameLen] = '\0'; 338 339 uint64_t hash = fnvHash64(itemName, nameLen); 340 SizeTarget key = {hash, nameLen, 0}; 341 342 auto it = std::lower_bound(targets.begin(), targets.end(), key, [](const SizeTarget& a, const SizeTarget& b) { 343 return a.hash < b.hash || (a.hash == b.hash && a.len < b.len); 344 }); 345 346 while (it != targets.end() && it->hash == hash && it->len == nameLen) { 347 if (it->index < sizes.size()) { 348 sizes[it->index] = uncompressedSize; 349 matched++; 350 } 351 ++it; 352 } 353 354 if (matched >= targetCount) { 355 break; 356 } 357 } else { 358 file.seekCur(nameLen); 359 } 360 361 file.seekCur(m + k); 362 } 363 364 return matched; 365} 366 367uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) { 368 const ScopedOpenClose zip{*this}; 369 if (!zip) return nullptr; 370 371 FileStatSlim fileStat = {}; 372 if (!loadFileStatSlim(filename, &fileStat)) return nullptr; 373 374 const long fileOffset = getDataOffset(fileStat); 375 if (fileOffset < 0) return nullptr; 376 377 file.seek(fileOffset); 378 379 const auto deflatedDataSize = fileStat.compressedSize; 380 const auto inflatedDataSize = fileStat.uncompressedSize; 381 const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize; 382 const auto data = static_cast<uint8_t*>(malloc(dataSize)); 383 if (data == nullptr) { 384 LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize); 385 return nullptr; 386 } 387 388 if (fileStat.method == ZIP_METHOD_STORED) { 389 // no deflation, just read content 390 const size_t dataRead = file.read(data, inflatedDataSize); 391 392 if (dataRead != inflatedDataSize) { 393 LOG_ERR("ZIP", "Failed to read data"); 394 free(data); 395 return nullptr; 396 } 397 398 // Continue out of block with data set 399 } else if (fileStat.method == ZIP_METHOD_DEFLATED) { 400 // Read out deflated content from file 401 const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize)); 402 if (deflatedData == nullptr) { 403 LOG_ERR("ZIP", "Failed to allocate memory for decompression buffer"); 404 free(data); 405 return nullptr; 406 } 407 408 const size_t dataRead = file.read(deflatedData, deflatedDataSize); 409 410 if (dataRead != deflatedDataSize) { 411 LOG_ERR("ZIP", "Failed to read data, expected %d got %d", deflatedDataSize, dataRead); 412 free(deflatedData); 413 free(data); 414 return nullptr; 415 } 416 417 bool success = false; 418 { 419 InflateReader r; 420 r.init(false); 421 r.setSource(deflatedData, deflatedDataSize); 422 success = r.read(data, inflatedDataSize); 423 } 424 free(deflatedData); 425 426 if (!success) { 427 LOG_ERR("ZIP", "Failed to inflate file"); 428 free(data); 429 return nullptr; 430 } 431 432 // Continue out of block with data set 433 } else { 434 LOG_ERR("ZIP", "Unsupported compression method"); 435 free(data); 436 return nullptr; 437 } 438 439 if (trailingNullByte) data[inflatedDataSize] = '\0'; 440 if (size) *size = inflatedDataSize; 441 return data; 442} 443 444bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) { 445 const ScopedOpenClose zip{*this}; 446 if (!zip) return false; 447 448 FileStatSlim fileStat = {}; 449 if (!loadFileStatSlim(filename, &fileStat)) return false; 450 451 const long fileOffset = getDataOffset(fileStat); 452 if (fileOffset < 0) return false; 453 454 file.seek(fileOffset); 455 const auto deflatedDataSize = fileStat.compressedSize; 456 const auto inflatedDataSize = fileStat.uncompressedSize; 457 458 if (fileStat.method == ZIP_METHOD_STORED) { 459 // no deflation, just read content 460 const auto buffer = static_cast<uint8_t*>(malloc(chunkSize)); 461 if (!buffer) { 462 LOG_ERR("ZIP", "Failed to allocate memory for buffer"); 463 return false; 464 } 465 466 size_t remaining = inflatedDataSize; 467 while (remaining > 0) { 468 const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize); 469 if (dataRead == 0) { 470 LOG_ERR("ZIP", "Could not read more bytes"); 471 free(buffer); 472 return false; 473 } 474 475 if (out.write(buffer, dataRead) != dataRead) { 476 LOG_ERR("ZIP", "Failed to write all output bytes to stream"); 477 free(buffer); 478 return false; 479 } 480 remaining -= dataRead; 481 } 482 483 free(buffer); 484 return true; 485 } 486 487 if (fileStat.method == ZIP_METHOD_DEFLATED) { 488 auto* fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize)); 489 if (!fileReadBuffer) { 490 LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer"); 491 return false; 492 } 493 494 auto* outputBuffer = static_cast<uint8_t*>(malloc(chunkSize)); 495 if (!outputBuffer) { 496 LOG_ERR("ZIP", "Failed to allocate memory for output buffer"); 497 free(fileReadBuffer); 498 return false; 499 } 500 501 ZipInflateCtx ctx; 502 ctx.file = &file; 503 ctx.fileRemaining = deflatedDataSize; 504 ctx.readBuf = fileReadBuffer; 505 ctx.readBufSize = chunkSize; 506 507 if (!ctx.reader.init(true)) { 508 LOG_ERR("ZIP", "Failed to init inflate reader"); 509 free(outputBuffer); 510 free(fileReadBuffer); 511 return false; 512 } 513 ctx.reader.setReadCallback(zipReadCallback); 514 515 bool success = false; 516 size_t totalProduced = 0; 517 518 while (true) { 519 size_t produced; 520 const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced); 521 522 totalProduced += produced; 523 if (totalProduced > static_cast<size_t>(inflatedDataSize)) { 524 LOG_ERR("ZIP", "Decompressed size exceeds expected (%zu > %zu)", totalProduced, 525 static_cast<size_t>(inflatedDataSize)); 526 break; 527 } 528 529 if (produced > 0) { 530 if (out.write(outputBuffer, produced) != produced) { 531 LOG_ERR("ZIP", "Failed to write all output bytes to stream"); 532 break; 533 } 534 } 535 536 if (status == InflateStatus::Done) { 537 if (totalProduced != static_cast<size_t>(inflatedDataSize)) { 538 LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize), 539 totalProduced); 540 break; 541 } 542 LOG_DBG("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize); 543 success = true; 544 break; 545 } 546 547 if (status == InflateStatus::Error) { 548 LOG_ERR("ZIP", "Decompression failed"); 549 break; 550 } 551 // InflateStatus::Ok: output buffer full, continue 552 } 553 554 free(outputBuffer); 555 free(fileReadBuffer); 556 return success; // ctx.reader destructor frees the ring buffer 557 } 558 559 LOG_ERR("ZIP", "Unsupported compression method"); 560 return false; 561}