ZipFile.cpp
1 #include "ZipFile.h" 2 3 #include <HardwareSerial.h> 4 #include <SDCardManager.h> 5 #include <miniz.h> 6 7 #include <algorithm> 8 9 bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) { 10 // Setup inflator 11 const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor))); 12 if (!inflator) { 13 Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis()); 14 return false; 15 } 16 memset(inflator, 0, sizeof(tinfl_decompressor)); 17 tinfl_init(inflator); 18 19 size_t inBytes = deflatedSize; 20 size_t outBytes = inflatedSize; 21 const tinfl_status status = tinfl_decompress(inflator, inputBuf, &inBytes, nullptr, outputBuf, &outBytes, 22 TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF); 23 free(inflator); 24 25 if (status != TINFL_STATUS_DONE) { 26 Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status); 27 return false; 28 } 29 30 return true; 31 } 32 33 bool ZipFile::loadAllFileStatSlims() { 34 const bool wasOpen = isOpen(); 35 if (!wasOpen && !open()) { 36 return false; 37 } 38 39 if (!loadZipDetails()) { 40 if (!wasOpen) { 41 close(); 42 } 43 return false; 44 } 45 46 file.seek(zipDetails.centralDirOffset); 47 48 uint32_t sig; 49 char itemName[256]; 50 fileStatSlimCache.clear(); 51 fileStatSlimCache.reserve(zipDetails.totalEntries); 52 53 while (file.available()) { 54 file.read(&sig, 4); 55 if (sig != 0x02014b50) break; // End of list 56 57 FileStatSlim fileStat = {}; 58 59 file.seekCur(6); 60 file.read(&fileStat.method, 2); 61 file.seekCur(8); 62 file.read(&fileStat.compressedSize, 4); 63 file.read(&fileStat.uncompressedSize, 4); 64 uint16_t nameLen, m, k; 65 file.read(&nameLen, 2); 66 file.read(&m, 2); 67 file.read(&k, 2); 68 file.seekCur(8); 69 file.read(&fileStat.localHeaderOffset, 4); 70 71 // Bounds check to prevent buffer overflow 72 if (nameLen >= 255) { 73 file.seekCur(nameLen + m + k); // Skip this entry entirely 74 continue; 75 } 76 77 file.read(itemName, nameLen); 78 itemName[nameLen] = '\0'; 79 80 fileStatSlimCache.emplace(itemName, fileStat); 81 82 // Skip the rest of this entry (extra field + comment) 83 file.seekCur(m + k); 84 } 85 86 if (!wasOpen) { 87 close(); 88 } 89 return true; 90 } 91 92 bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { 93 if (!fileStatSlimCache.empty()) { 94 const auto it = fileStatSlimCache.find(filename); 95 if (it != fileStatSlimCache.end()) { 96 *fileStat = it->second; 97 return true; 98 } 99 return false; 100 } 101 102 const bool wasOpen = isOpen(); 103 if (!wasOpen && !open()) { 104 return false; 105 } 106 107 if (!loadZipDetails()) { 108 if (!wasOpen) { 109 close(); 110 } 111 return false; 112 } 113 114 file.seek(zipDetails.centralDirOffset); 115 116 uint32_t sig; 117 char itemName[256]; 118 bool found = false; 119 120 while (file.available()) { 121 file.read(&sig, 4); 122 if (sig != 0x02014b50) break; // End of list 123 124 file.seekCur(6); 125 file.read(&fileStat->method, 2); 126 file.seekCur(8); 127 file.read(&fileStat->compressedSize, 4); 128 file.read(&fileStat->uncompressedSize, 4); 129 uint16_t nameLen, m, k; 130 file.read(&nameLen, 2); 131 file.read(&m, 2); 132 file.read(&k, 2); 133 file.seekCur(8); 134 file.read(&fileStat->localHeaderOffset, 4); 135 136 // Bounds check to prevent buffer overflow 137 if (nameLen >= 255) { 138 file.seekCur(nameLen + m + k); // Skip this entry entirely 139 continue; 140 } 141 142 file.read(itemName, nameLen); 143 itemName[nameLen] = '\0'; 144 145 if (strcmp(itemName, filename) == 0) { 146 found = true; 147 break; 148 } 149 150 // Skip the rest of this entry (extra field + comment) 151 file.seekCur(m + k); 152 } 153 154 if (!wasOpen) { 155 close(); 156 } 157 return found; 158 } 159 160 long ZipFile::getDataOffset(const FileStatSlim& fileStat) { 161 const bool wasOpen = isOpen(); 162 if (!wasOpen && !open()) { 163 return -1; 164 } 165 166 constexpr auto localHeaderSize = 30; 167 168 uint8_t pLocalHeader[localHeaderSize]; 169 const uint64_t fileOffset = fileStat.localHeaderOffset; 170 171 file.seek(fileOffset); 172 const size_t read = file.read(pLocalHeader, localHeaderSize); 173 if (!wasOpen) { 174 close(); 175 } 176 177 if (read != localHeaderSize) { 178 Serial.printf("[%lu] [ZIP] Something went wrong reading the local header\n", millis()); 179 return -1; 180 } 181 182 if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) != 183 0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) { 184 Serial.printf("[%lu] [ZIP] Not a valid zip file header\n", millis()); 185 return -1; 186 } 187 188 const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8); 189 const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8); 190 return fileOffset + localHeaderSize + filenameLength + extraOffset; 191 } 192 193 bool ZipFile::loadZipDetails() { 194 if (zipDetails.isSet) { 195 return true; 196 } 197 198 const bool wasOpen = isOpen(); 199 if (!wasOpen && !open()) { 200 return false; 201 } 202 203 const size_t fileSize = file.size(); 204 if (fileSize < 22) { 205 Serial.printf("[%lu] [ZIP] File too small to be a valid zip\n", millis()); 206 if (!wasOpen) { 207 close(); 208 } 209 return false; // Minimum EOCD size is 22 bytes 210 } 211 212 // We scan the last 1KB (or the whole file if smaller) for the EOCD signature 213 // 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian 214 const int scanRange = fileSize > 1024 ? 1024 : fileSize; 215 const auto buffer = static_cast<uint8_t*>(malloc(scanRange)); 216 if (!buffer) { 217 Serial.printf("[%lu] [ZIP] Failed to allocate memory for EOCD scan buffer\n", millis()); 218 if (!wasOpen) { 219 close(); 220 } 221 return false; 222 } 223 224 file.seek(fileSize - scanRange); 225 file.read(buffer, scanRange); 226 227 // Scan backwards for the signature 228 int foundOffset = -1; 229 for (int i = scanRange - 22; i >= 0; i--) { 230 constexpr uint8_t signature[4] = {0x50, 0x4b, 0x05, 0x06}; // Little-endian EOCD signature 231 if (memcmp(&buffer[i], signature, 4) == 0) { 232 foundOffset = i; 233 break; 234 } 235 } 236 237 if (foundOffset == -1) { 238 Serial.printf("[%lu] [ZIP] EOCD signature not found in zip file\n", millis()); 239 free(buffer); 240 if (!wasOpen) { 241 close(); 242 } 243 return false; 244 } 245 246 // Now extract the values we need from the EOCD record 247 // Relative positions within EOCD: 248 // Offset 10: Total number of entries (2 bytes) 249 // Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes) 250 memcpy(&zipDetails.totalEntries, &buffer[foundOffset + 10], sizeof(zipDetails.totalEntries)); 251 memcpy(&zipDetails.centralDirOffset, &buffer[foundOffset + 16], sizeof(zipDetails.centralDirOffset)); 252 zipDetails.isSet = true; 253 254 free(buffer); 255 if (!wasOpen) { 256 close(); 257 } 258 return true; 259 } 260 261 uint16_t ZipFile::getTotalEntries() { 262 if (!zipDetails.isSet) { 263 loadZipDetails(); 264 } 265 return zipDetails.totalEntries; 266 } 267 268 bool ZipFile::open() { 269 if (!SdMan.openFileForRead("ZIP", filePath, file)) { 270 return false; 271 } 272 return true; 273 } 274 275 bool ZipFile::close() { 276 if (file) { 277 file.close(); 278 } 279 return true; 280 } 281 282 bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) { 283 FileStatSlim fileStat = {}; 284 if (!loadFileStatSlim(filename, &fileStat)) { 285 return false; 286 } 287 288 *size = static_cast<size_t>(fileStat.uncompressedSize); 289 return true; 290 } 291 292 int ZipFile::fillUncompressedSizes(std::vector<SizeTarget>& targets, std::vector<uint32_t>& sizes) { 293 if (targets.empty()) { 294 return 0; 295 } 296 297 const bool wasOpen = isOpen(); 298 if (!wasOpen && !open()) { 299 return 0; 300 } 301 302 if (!loadZipDetails()) { 303 if (!wasOpen) { 304 close(); 305 } 306 return 0; 307 } 308 309 file.seek(zipDetails.centralDirOffset); 310 311 uint32_t sig; 312 char itemName[256]; 313 int matched = 0; 314 315 while (file.available()) { 316 if (file.read(&sig, 4) != 4) break; 317 if (sig != 0x02014b50) break; // End of central directory 318 319 // Skip: version made by (2), version needed (2), flags (2), method (2), time (2), date (2), crc32 (4) 320 file.seekCur(16); 321 // Skip compressedSize (4), read uncompressedSize (4) 322 file.seekCur(4); 323 uint32_t uncompressedSize; 324 if (file.read(&uncompressedSize, 4) != 4) break; 325 uint16_t nameLen, m, k; 326 if (file.read(&nameLen, 2) != 2) break; 327 if (file.read(&m, 2) != 2) break; 328 if (file.read(&k, 2) != 2) break; 329 // Skip: comment len already read in k, disk# (2), internal attr (2), external attr (4), local header offset (4) 330 file.seekCur(12); 331 332 // Bounds check to prevent buffer overflow 333 if (nameLen >= 255) { 334 file.seekCur(nameLen + m + k); // Skip this entry entirely 335 continue; 336 } 337 338 if (file.read(itemName, nameLen) != nameLen) break; 339 itemName[nameLen] = '\0'; 340 341 // Compute hash on-the-fly from filename 342 const uint64_t entryHash = fnvHash64(itemName, nameLen); 343 344 // Binary search for matching target 345 SizeTarget key = {entryHash, nameLen, 0}; 346 auto it = std::lower_bound(targets.begin(), targets.end(), key); 347 348 // Check for match (hash and len must match) 349 if (it != targets.end() && it->hash == entryHash && it->len == nameLen) { 350 // Bounds check before write 351 if (it->index < sizes.size()) { 352 sizes[it->index] = uncompressedSize; 353 matched++; 354 } 355 } 356 357 // Skip the rest of this entry (extra field + comment) 358 file.seekCur(m + k); 359 } 360 361 if (!wasOpen) { 362 close(); 363 } 364 return matched; 365 } 366 367 int ZipFile::findFirstExisting(const char* const* paths, int pathCount) { 368 if (!paths || pathCount <= 0 || pathCount > 65535) { 369 return -1; 370 } 371 372 const bool wasOpen = isOpen(); 373 if (!wasOpen && !open()) { 374 return -1; 375 } 376 377 if (!loadZipDetails()) { 378 if (!wasOpen) { 379 close(); 380 } 381 return -1; 382 } 383 384 // Build sorted vector of targets with hashes for binary search 385 std::vector<SizeTarget> targets; 386 for (int i = 0; i < pathCount; i++) { 387 const char* path = paths[i]; 388 if (!path) continue; 389 const size_t len = strlen(path); 390 if (len > 255) continue; // Skip paths that are too long for itemName[256] 391 targets.push_back({fnvHash64(path, len), static_cast<uint16_t>(len), static_cast<uint16_t>(i)}); 392 } 393 std::sort(targets.begin(), targets.end()); 394 395 file.seek(zipDetails.centralDirOffset); 396 397 uint32_t sig; 398 char itemName[256]; 399 int foundIndex = -1; 400 int lowestPriority = pathCount; // Lower index = higher priority 401 402 while (file.available()) { 403 if (file.read(&sig, 4) != 4) break; 404 if (sig != 0x02014b50) break; // End of central directory 405 406 // Skip to name length (skip 24 bytes from after signature) 407 if (!file.seekCur(24)) break; 408 uint16_t nameLen, m, k; 409 if (file.read(&nameLen, 2) != 2) break; 410 if (file.read(&m, 2) != 2) break; 411 if (file.read(&k, 2) != 2) break; 412 // Skip remaining header (12 bytes) 413 if (!file.seekCur(12)) break; 414 415 // Bounds check to prevent buffer overflow 416 if (nameLen > 255) { 417 file.seekCur(nameLen + m + k); 418 continue; 419 } 420 421 if (file.read(itemName, nameLen) != nameLen) break; 422 itemName[nameLen] = '\0'; 423 424 // Compute hash on-the-fly from filename 425 const uint64_t entryHash = fnvHash64(itemName, nameLen); 426 427 // Binary search for matching target 428 SizeTarget key = {entryHash, nameLen, 0}; 429 auto it = std::lower_bound(targets.begin(), targets.end(), key); 430 431 // Check for match (hash and len must match) 432 if (it != targets.end() && it->hash == entryHash && it->len == nameLen) { 433 // Verify string match (hash collision protection) with bounds check 434 if (it->index < pathCount && strcmp(itemName, paths[it->index]) == 0) { 435 // Keep track of lowest index (highest priority) 436 if (it->index < lowestPriority) { 437 lowestPriority = it->index; 438 foundIndex = it->index; 439 if (lowestPriority == 0) break; // Can't find higher priority 440 } 441 } 442 } 443 444 // Skip the rest of this entry (extra field + comment) 445 file.seekCur(m + k); 446 } 447 448 if (!wasOpen) { 449 close(); 450 } 451 return foundIndex; 452 } 453 454 uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) { 455 const bool wasOpen = isOpen(); 456 if (!wasOpen && !open()) { 457 return nullptr; 458 } 459 460 FileStatSlim fileStat = {}; 461 if (!loadFileStatSlim(filename, &fileStat)) { 462 if (!wasOpen) { 463 close(); 464 } 465 return nullptr; 466 } 467 468 const long fileOffset = getDataOffset(fileStat); 469 if (fileOffset < 0) { 470 if (!wasOpen) { 471 close(); 472 } 473 return nullptr; 474 } 475 476 file.seek(fileOffset); 477 478 const auto deflatedDataSize = fileStat.compressedSize; 479 const auto inflatedDataSize = fileStat.uncompressedSize; 480 const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize; 481 const auto data = static_cast<uint8_t*>(malloc(dataSize)); 482 if (data == nullptr) { 483 Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize); 484 if (!wasOpen) { 485 close(); 486 } 487 return nullptr; 488 } 489 490 if (fileStat.method == 0) { // MZ_NO_COMPRESSION = 0 491 // no deflation, just read content 492 const size_t dataRead = file.read(data, inflatedDataSize); 493 if (!wasOpen) { 494 close(); 495 } 496 497 if (dataRead != inflatedDataSize) { 498 Serial.printf("[%lu] [ZIP] Failed to read data\n", millis()); 499 free(data); 500 return nullptr; 501 } 502 503 // Continue out of block with data set 504 } else if (fileStat.method == MZ_DEFLATED) { 505 // Read out deflated content from file 506 const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize)); 507 if (deflatedData == nullptr) { 508 Serial.printf("[%lu] [ZIP] Failed to allocate memory for decompression buffer\n", millis()); 509 if (!wasOpen) { 510 close(); 511 } 512 return nullptr; 513 } 514 515 const size_t dataRead = file.read(deflatedData, deflatedDataSize); 516 if (!wasOpen) { 517 close(); 518 } 519 520 if (dataRead != deflatedDataSize) { 521 Serial.printf("[%lu] [ZIP] Failed to read data, expected %d got %d\n", millis(), deflatedDataSize, dataRead); 522 free(deflatedData); 523 free(data); 524 return nullptr; 525 } 526 527 const bool success = inflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize); 528 free(deflatedData); 529 530 if (!success) { 531 Serial.printf("[%lu] [ZIP] Failed to inflate file\n", millis()); 532 free(data); 533 if (!wasOpen) close(); 534 return nullptr; 535 } 536 537 // Continue out of block with data set 538 } else { 539 Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis()); 540 if (!wasOpen) { 541 close(); 542 } 543 return nullptr; 544 } 545 546 if (trailingNullByte) data[inflatedDataSize] = '\0'; 547 if (size) *size = inflatedDataSize; 548 return data; 549 } 550 551 bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) { 552 const bool wasOpen = isOpen(); 553 if (!wasOpen && !open()) { 554 return false; 555 } 556 557 FileStatSlim fileStat = {}; 558 if (!loadFileStatSlim(filename, &fileStat)) { 559 if (!wasOpen) close(); 560 return false; 561 } 562 563 const long fileOffset = getDataOffset(fileStat); 564 if (fileOffset < 0) { 565 if (!wasOpen) close(); 566 return false; 567 } 568 569 file.seek(fileOffset); 570 const auto deflatedDataSize = fileStat.compressedSize; 571 const auto inflatedDataSize = fileStat.uncompressedSize; 572 573 if (fileStat.method == 0) { // MZ_NO_COMPRESSION = 0 574 // no deflation, just read content 575 const auto buffer = static_cast<uint8_t*>(malloc(chunkSize)); 576 if (!buffer) { 577 Serial.printf("[%lu] [ZIP] Failed to allocate memory for buffer\n", millis()); 578 if (!wasOpen) { 579 close(); 580 } 581 return false; 582 } 583 584 size_t remaining = inflatedDataSize; 585 while (remaining > 0) { 586 const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize); 587 if (dataRead == 0) { 588 Serial.printf("[%lu] [ZIP] Could not read more bytes\n", millis()); 589 free(buffer); 590 if (!wasOpen) { 591 close(); 592 } 593 return false; 594 } 595 596 out.write(buffer, dataRead); 597 remaining -= dataRead; 598 } 599 600 if (!wasOpen) { 601 close(); 602 } 603 free(buffer); 604 return true; 605 } 606 607 if (fileStat.method == MZ_DEFLATED) { 608 // Setup inflator 609 const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor))); 610 if (!inflator) { 611 Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis()); 612 if (!wasOpen) { 613 close(); 614 } 615 return false; 616 } 617 memset(inflator, 0, sizeof(tinfl_decompressor)); 618 tinfl_init(inflator); 619 620 // Setup file read buffer 621 const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize)); 622 if (!fileReadBuffer) { 623 Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis()); 624 free(inflator); 625 if (!wasOpen) { 626 close(); 627 } 628 return false; 629 } 630 631 const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE)); 632 if (!outputBuffer) { 633 Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis()); 634 free(inflator); 635 free(fileReadBuffer); 636 if (!wasOpen) { 637 close(); 638 } 639 return false; 640 } 641 memset(outputBuffer, 0, TINFL_LZ_DICT_SIZE); 642 643 size_t fileRemainingBytes = deflatedDataSize; 644 size_t processedOutputBytes = 0; 645 size_t fileReadBufferFilledBytes = 0; 646 size_t fileReadBufferCursor = 0; 647 size_t outputCursor = 0; // Current offset in the circular dictionary 648 649 while (true) { 650 // Load more compressed bytes when needed 651 if (fileReadBufferCursor >= fileReadBufferFilledBytes) { 652 if (fileRemainingBytes == 0) { 653 // Should not be hit, but a safe protection 654 break; // EOF 655 } 656 657 fileReadBufferFilledBytes = 658 file.read(fileReadBuffer, fileRemainingBytes < chunkSize ? fileRemainingBytes : chunkSize); 659 fileRemainingBytes -= fileReadBufferFilledBytes; 660 fileReadBufferCursor = 0; 661 662 if (fileReadBufferFilledBytes == 0) { 663 // Bad read 664 break; // EOF 665 } 666 } 667 668 // Available bytes in fileReadBuffer to process 669 size_t inBytes = fileReadBufferFilledBytes - fileReadBufferCursor; 670 // Space remaining in outputBuffer 671 size_t outBytes = TINFL_LZ_DICT_SIZE - outputCursor; 672 673 const tinfl_status status = tinfl_decompress(inflator, fileReadBuffer + fileReadBufferCursor, &inBytes, 674 outputBuffer, outputBuffer + outputCursor, &outBytes, 675 fileRemainingBytes > 0 ? TINFL_FLAG_HAS_MORE_INPUT : 0); 676 677 // Update input position 678 fileReadBufferCursor += inBytes; 679 680 // Write output chunk 681 if (outBytes > 0) { 682 processedOutputBytes += outBytes; 683 if (out.write(outputBuffer + outputCursor, outBytes) != outBytes) { 684 Serial.printf("[%lu] [ZIP] Failed to write all output bytes to stream\n", millis()); 685 if (!wasOpen) { 686 close(); 687 } 688 free(outputBuffer); 689 free(fileReadBuffer); 690 free(inflator); 691 return false; 692 } 693 // Update output position in buffer (with wraparound) 694 outputCursor = (outputCursor + outBytes) & (TINFL_LZ_DICT_SIZE - 1); 695 } 696 697 if (status < 0) { 698 Serial.printf("[%lu] [ZIP] tinfl_decompress() failed with status %d\n", millis(), status); 699 if (!wasOpen) { 700 close(); 701 } 702 free(outputBuffer); 703 free(fileReadBuffer); 704 free(inflator); 705 return false; 706 } 707 708 if (status == TINFL_STATUS_DONE) { 709 Serial.printf("[%lu] [ZIP] Decompressed %d bytes into %d bytes\n", millis(), deflatedDataSize, 710 inflatedDataSize); 711 if (!wasOpen) { 712 close(); 713 } 714 free(inflator); 715 free(fileReadBuffer); 716 free(outputBuffer); 717 return true; 718 } 719 } 720 721 // If we get here, EOF reached without TINFL_STATUS_DONE 722 Serial.printf("[%lu] [ZIP] Unexpected EOF\n", millis()); 723 if (!wasOpen) { 724 close(); 725 } 726 free(outputBuffer); 727 free(fileReadBuffer); 728 free(inflator); 729 return false; 730 } 731 732 if (!wasOpen) { 733 close(); 734 } 735 736 Serial.printf("[%lu] [ZIP] Unsupported compression method\n", millis()); 737 return false; 738 }