diff --git a/lib/EpdFont/SdCardFontRegistry.cpp b/lib/EpdFont/SdCardFontRegistry.cpp index 099732f2..9b1c6458 100644 --- a/lib/EpdFont/SdCardFontRegistry.cpp +++ b/lib/EpdFont/SdCardFontRegistry.cpp @@ -116,19 +116,16 @@ void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo dir.close(); } -bool SdCardFontRegistry::discover() { - families_.clear(); - families_.reserve(MAX_SD_FAMILIES); - - FsFile root = Storage.open(FONTS_DIR); +void SdCardFontRegistry::scanRoot(const char* rootPath) { + FsFile root = Storage.open(rootPath); if (!root) { - LOG_DBG("SDREG", "Fonts directory not found: %s", FONTS_DIR); - return false; + LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath); + return; } if (!root.isDirectory()) { - LOG_ERR("SDREG", "Fonts path is not a directory: %s", FONTS_DIR); + LOG_ERR("SDREG", "Fonts path is not a directory: %s", rootPath); root.close(); - return false; + return; } char nameBuffer[128]; @@ -143,21 +140,58 @@ bool SdCardFontRegistry::discover() { // Skip hidden/system directories (macOS ._*, .Trashes, etc.) if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue; + // Skip in-flight install/rollback dirs (Bookerly__staging, Bookerly__backup, ...) + // — these may contain valid-looking .cpfont files during a resumable + // download but must not be exposed as installable families. + const size_t nameLen = strlen(nameBuffer); + static constexpr const char* kStagingSuffix = "__staging"; + static constexpr const char* kBackupSuffix = "__backup"; + const size_t stagingLen = strlen(kStagingSuffix); + const size_t backupLen = strlen(kBackupSuffix); + if ((nameLen > stagingLen && strcmp(nameBuffer + nameLen - stagingLen, kStagingSuffix) == 0) || + (nameLen > backupLen && strcmp(nameBuffer + nameLen - backupLen, kBackupSuffix) == 0)) { + continue; + } + + // Primary wins: skip if the family was already discovered in an earlier root. + bool alreadyKnown = false; + for (const auto& f : families_) { + if (f.name == nameBuffer) { + alreadyKnown = true; + break; + } + } + if (alreadyKnown) { + LOG_DBG("SDREG", "Skipping duplicate family %s in %s (primary wins)", nameBuffer, rootPath); + continue; + } + SdCardFontFamilyInfo family; family.name = nameBuffer; - std::string subDirPath = std::string(FONTS_DIR) + "/" + nameBuffer; + std::string subDirPath = std::string(rootPath) + "/" + nameBuffer; scanDirectory(subDirPath.c_str(), family); if (!family.files.empty()) { families_.push_back(std::move(family)); - LOG_DBG("SDREG", "Found family: %s (%d files)", families_.back().name.c_str(), - static_cast(families_.back().files.size())); + LOG_DBG("SDREG", "Found family: %s (%d files) in %s", families_.back().name.c_str(), + static_cast(families_.back().files.size()), rootPath); } } else { entry.close(); } } root.close(); +} + +bool SdCardFontRegistry::discover() { + families_.clear(); + families_.reserve(MAX_SD_FAMILIES); + + // Primary first so it wins on family-name conflicts; the upstream roots are + // read-only fallbacks — installers/deleters always target only the primary. + scanRoot(FONTS_DIR); + scanRoot(FONTS_DIR_UPSTREAM_HIDDEN); + scanRoot(FONTS_DIR_UPSTREAM_VISIBLE); // Sort families alphabetically std::sort(families_.begin(), families_.end(), diff --git a/lib/EpdFont/SdCardFontRegistry.h b/lib/EpdFont/SdCardFontRegistry.h index ee8c34bb..138280a7 100644 --- a/lib/EpdFont/SdCardFontRegistry.h +++ b/lib/EpdFont/SdCardFontRegistry.h @@ -29,7 +29,13 @@ struct SdCardFontFamilyInfo { class SdCardFontRegistry { public: static constexpr int MAX_SD_FAMILIES = 128; + // Primary (writable) location. All installs/downloads/deletes target this root. static constexpr const char* FONTS_DIR = "/.crosspoint/fonts"; + // Read-only fallback roots for upstream crosspoint-reader compatibility. + // Upstream uses "/.fonts" (preferred, hidden) and "/fonts" (visible). Both are + // scanned after the primary; primary wins on family-name conflicts. + static constexpr const char* FONTS_DIR_UPSTREAM_HIDDEN = "/.fonts"; + static constexpr const char* FONTS_DIR_UPSTREAM_VISIBLE = "/fonts"; // Scan SD card, populate families_. Returns true if any families found. bool discover(); @@ -44,4 +50,5 @@ class SdCardFontRegistry { static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style); void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family); + void scanRoot(const char* rootPath); }; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 6310742f..55f94b7d 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -305,6 +305,7 @@ STR_CONNECT: "Connect" STR_OPEN: "Open" STR_DOWNLOAD: "Download" STR_RETRY: "Retry" +STR_RESUME: "Resume" STR_YES: "Yes" STR_NO: "No" STR_SHOW: "Show" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index ba37a442..71d1a1f8 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -244,6 +244,7 @@ STR_CONNECT: "Verbinden" STR_OPEN: "Öffnen" STR_DOWNLOAD: "Herunterladen" STR_RETRY: "Wiederh." +STR_RESUME: "Fortsetzen" STR_YES: "Ja" STR_NO: "Nein" STR_SHOW: "Zeigen" diff --git a/open-x4-sdk b/open-x4-sdk index 4d4c6678..570e9c4e 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 4d4c66787ff655adc93a1f37811610552a73aa1c +Subproject commit 570e9c4ee01c01abba9df52b4b3bda7ff610910b diff --git a/scripts/generate-font-manifest.py b/scripts/generate-font-manifest.py index aff5fde4..cc16f1df 100644 --- a/scripts/generate-font-manifest.py +++ b/scripts/generate-font-manifest.py @@ -22,6 +22,7 @@ import json import os import struct import sys +import zlib from pathlib import Path # --- .cpfont binary format constants --- @@ -96,6 +97,17 @@ def read_cpfont_styles(filepath: Path) -> list[str]: return styles +def compute_crc32(filepath: Path) -> int: + # Matches the on-device esp_rom_crc32_le(0, buf, len) accumulator used by + # FontDownloadActivity::computeFileCrc32. Originally introduced upstream in + # crosspoint-reader PR #1904 ("verify CRC32 checksum for font files"). + crc = 0 + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + crc = zlib.crc32(chunk, crc) + return crc & 0xFFFFFFFF + + def parse_filename(filename: str) -> tuple[str, str] | None: """Parse '_.cpfont' into (family, size_str). @@ -162,6 +174,7 @@ def build_manifest( { "name": filepath.relative_to(input_dir).as_posix(), "size": filepath.stat().st_size, + "crc32": compute_crc32(filepath), } ) @@ -175,7 +188,9 @@ def build_manifest( ) return { - "version": 1, + # v2 adds per-file crc32 (kept optional on the device side so old v1 + # manifests still load — see FontDownloadActivity::fetchAndParseManifest). + "version": 2, "baseUrl": base_url, "families": manifest_families, } diff --git a/src/FontInstaller.cpp b/src/FontInstaller.cpp index f0b9d55d..fa099d24 100644 --- a/src/FontInstaller.cpp +++ b/src/FontInstaller.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -90,6 +91,32 @@ void FontInstaller::buildFontPath(const char* family, const char* filename, char snprintf(outBuf, outBufSize, "%s/%s/%s", SdCardFontRegistry::FONTS_DIR, family, filename); } +void FontInstaller::buildStagingDirPath(const char* family, char* outBuf, size_t outBufSize) { + snprintf(outBuf, outBufSize, "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family); +} + +void FontInstaller::buildBackupDirPath(const char* family, char* outBuf, size_t outBufSize) { + snprintf(outBuf, outBufSize, "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family); +} + +bool FontInstaller::computeFileCrc32(const char* path, uint32_t& outCrc) { + FsFile f; + if (!Storage.openFileForRead("FONT", path, f)) { + return false; + } + constexpr size_t BUF_SIZE = 128; + uint8_t buf[BUF_SIZE]; + uint32_t crc = 0; + while (f.available()) { + const int n = f.read(buf, BUF_SIZE); + if (n <= 0) break; + crc = esp_rom_crc32_le(crc, buf, static_cast(n)); + } + f.close(); + outCrc = crc; + return true; +} + FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) { if (!isValidFamilyName(familyName)) { return Error::INVALID_FAMILY_NAME; diff --git a/src/FontInstaller.h b/src/FontInstaller.h index c331b5d4..bb33fadd 100644 --- a/src/FontInstaller.h +++ b/src/FontInstaller.h @@ -31,10 +31,21 @@ class FontInstaller { /// Validate a .cpfont file on disk (check magic bytes). bool validateCpfontFile(const char* path); + /// Compute CRC32 of a file (esp_rom_crc32_le accumulator, matches + /// zlib.crc32 used by scripts/generate-font-manifest.py). Returns false if + /// the file cannot be opened. Mirrors the upstream PR #1904 approach so + /// our manifests stay binary-compatible. + static bool computeFileCrc32(const char* path, uint32_t& outCrc); + /// Build the full SD path for a font file. /// Writes "/.crosspoint/fonts//" to outBuf. static void buildFontPath(const char* family, const char* filename, char* outBuf, size_t outBufSize); + /// Build the staging-dir path "/__staging". + static void buildStagingDirPath(const char* family, char* outBuf, size_t outBufSize); + /// Build the backup-dir path "/__backup". + static void buildBackupDirPath(const char* family, char* outBuf, size_t outBufSize); + /// Delete a family directory and all .cpfont files in it. /// If the deleted family is the active reader font, clears the setting. Error deleteFamily(const char* familyName); diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index e7cd6818..869e740e 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -100,7 +100,10 @@ bool FontDownloadActivity::fetchAndParseManifest() { } int version = doc["version"] | 0; - if (version != 1) { + // v1 (legacy, no crc32) and v2 (with crc32) are both accepted; crc check is + // skipped per-file when the field is absent. See upstream PR #1904 for the + // CRC32 design we mirror. + if (version != 1 && version != 2) { LOG_ERR("FONT", "Unsupported manifest version: %d", version); errorMessage_ = "Unsupported manifest version"; return false; @@ -126,6 +129,10 @@ bool FontDownloadActivity::fetchAndParseManifest() { ManifestFile file; file.name = fileObj["name"] | ""; file.size = fileObj["size"] | 0; + if (fileObj["crc32"].is()) { + file.crc32 = fileObj["crc32"].as(); + file.hasCrc32 = true; + } family.totalSize += file.size; family.files.push_back(std::move(file)); } @@ -155,6 +162,12 @@ bool FontDownloadActivity::fetchAndParseManifest() { break; } } + } else { + // Surface leftover staging from a previously interrupted download so the + // UI can offer "Resume" instead of restarting from scratch. + char stagingDir[128]; + FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir)); + family.hasResumableDownload = Storage.exists(stagingDir); } families_.push_back(std::move(family)); @@ -259,20 +272,14 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { char stagingDir[128]; char backupDir[128]; snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); - snprintf(stagingDir, sizeof(stagingDir), "%s/%s__staging", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); - snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); + FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir)); + FontInstaller::buildBackupDirPath(family.name.c_str(), backupDir, sizeof(backupDir)); - if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) { - LOG_ERR("FONT", "Failed to clean staging dir: %s", stagingDir); - RenderLock lock(*this); - state_ = ERROR; - pendingErrorAction_ = PendingFontAction::Download; - downloadingFamilyIndex_ = static_cast(&family - families_.data()); - errorMessage_ = "Failed to prepare staging area"; - return; - } - - if (!Storage.mkdir(stagingDir)) { + // Resume-aware staging: if a __staging dir is left over from a previous + // interrupted download, keep it so files already on disk can be reused. + // Files are individually re-verified below (size + CRC) before being + // accepted, so half-written files are caught. + if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) { LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir); RenderLock lock(*this); state_ = ERROR; @@ -304,6 +311,34 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { char stagedPath[128]; snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str()); + // If this file is already present in staging from a previous run and + // matches the manifest, skip the download. CRC32 is checked when the + // manifest carries one (v2+); otherwise size + magic-byte check is the + // best we can do. + if (Storage.exists(stagedPath)) { + FsFile f; + bool sizeOk = false; + if (Storage.openFileForRead("FONT", stagedPath, f)) { + sizeOk = (f.fileSize() == file.size); + f.close(); + } + bool crcOk = !file.hasCrc32; + if (sizeOk && file.hasCrc32) { + uint32_t actualCrc = 0; + if (FontInstaller::computeFileCrc32(stagedPath, actualCrc)) { + crcOk = (actualCrc == file.crc32); + } + } + if (sizeOk && crcOk && fontInstaller_.validateCpfontFile(stagedPath)) { + LOG_DBG("FONT", "Resuming: reusing %s", stagedPath); + fileProgress_ = file.size; + fileTotal_ = file.size; + continue; + } + LOG_DBG("FONT", "Resuming: re-downloading stale %s (sizeOk=%d crcOk=%d)", stagedPath, sizeOk, crcOk); + Storage.remove(stagedPath); + } + // Make sure parent directories exist for the file std::string stagedPathStr(stagedPath); size_t lastSlash = stagedPathStr.find_last_of('/'); @@ -336,7 +371,9 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { if (result == HttpDownloader::ABORTED) { LOG_INF("FONT", "Download cancelled: %s", file.name.c_str()); - Storage.removeDir(stagingDir); + // Keep staging dir so the next launch can resume. + Storage.remove(stagedPath); + family.hasResumableDownload = !family.installed; cancelRequested_ = true; RenderLock lock(*this); state_ = FAMILY_LIST; @@ -345,7 +382,10 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { if (result != HttpDownloader::OK) { LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result); - Storage.removeDir(stagingDir); + // Drop just the file that failed; keep already-downloaded siblings so + // the next retry resumes from here. + Storage.remove(stagedPath); + family.hasResumableDownload = !family.installed; RenderLock lock(*this); state_ = ERROR; pendingErrorAction_ = PendingFontAction::Download; @@ -354,9 +394,37 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { return; } + // CRC32: matches upstream PR #1904 — catches truncated/torn writes. + if (file.hasCrc32) { + uint32_t actualCrc = 0; + if (!FontInstaller::computeFileCrc32(stagedPath, actualCrc)) { + LOG_ERR("FONT", "Failed to read for CRC: %s", stagedPath); + Storage.remove(stagedPath); + family.hasResumableDownload = !family.installed; + RenderLock lock(*this); + state_ = ERROR; + pendingErrorAction_ = PendingFontAction::Download; + downloadingFamilyIndex_ = static_cast(&family - families_.data()); + errorMessage_ = "Failed to verify: " + file.name; + return; + } + if (actualCrc != file.crc32) { + LOG_ERR("FONT", "CRC32 mismatch for %s: got %08x expected %08x", file.name.c_str(), actualCrc, file.crc32); + Storage.remove(stagedPath); + family.hasResumableDownload = !family.installed; + RenderLock lock(*this); + state_ = ERROR; + pendingErrorAction_ = PendingFontAction::Download; + downloadingFamilyIndex_ = static_cast(&family - families_.data()); + errorMessage_ = "Checksum mismatch: " + file.name; + return; + } + } + if (!fontInstaller_.validateCpfontFile(stagedPath)) { LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath); - Storage.removeDir(stagingDir); + Storage.remove(stagedPath); + family.hasResumableDownload = !family.installed; RenderLock lock(*this); state_ = ERROR; pendingErrorAction_ = PendingFontAction::Download; @@ -411,6 +479,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { fontInstaller_.refreshRegistry(); family.installed = true; family.hasUpdate = false; + family.hasResumableDownload = false; syncSelectedIndexForNewActionCount(); RenderLock lock(*this); @@ -471,6 +540,7 @@ std::string FontDownloadActivity::confirmButtonLabel() const { const auto& family = families_[familyIndexFromList(selectedIndex_)]; if (family.installed && !family.hasUpdate) return tr(STR_DELETE); if (family.hasUpdate) return tr(STR_UPDATE); + if (family.hasResumableDownload) return tr(STR_RESUME); return tr(STR_DOWNLOAD); } @@ -615,6 +685,7 @@ void FontDownloadActivity::render(RenderLock&&) { const auto& f = families_[familyIndexFromList(index)]; if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE); if (f.installed) return tr(STR_INSTALLED); + if (f.hasResumableDownload) return tr(STR_RESUME); return ""; }, true); @@ -657,7 +728,10 @@ void FontDownloadActivity::render(RenderLock&&) { if (!errorMessage_.empty()) { renderer.drawCenteredText(UI_10_FONT_ID, centerY + metrics.verticalSpacing, errorMessage_.c_str()); } - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", ""); + const bool canResume = pendingErrorAction_ == PendingFontAction::Download && downloadingFamilyIndex_ >= 0 && + downloadingFamilyIndex_ < static_cast(families_.size()) && + families_[downloadingFamilyIndex_].hasResumableDownload; + const auto labels = mappedInput.mapLabels(tr(STR_BACK), canResume ? tr(STR_RESUME) : tr(STR_RETRY), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index dfe5f5f3..2ef7b8ed 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -36,6 +37,8 @@ class FontDownloadActivity : public Activity { struct ManifestFile { std::string name; size_t size = 0; + uint32_t crc32 = 0; + bool hasCrc32 = false; // false = legacy v1 manifest, fall back to size-only check }; struct ManifestFamily { @@ -46,6 +49,10 @@ class FontDownloadActivity : public Activity { size_t totalSize = 0; bool installed = false; bool hasUpdate = false; + // True iff a leftover __staging dir from a previous interrupted download + // exists for this not-yet-installed family — the next confirm will resume + // rather than restart. + bool hasResumableDownload = false; }; State state_ = WIFI_SELECTION; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 9aa114b0..693c66d4 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1422,6 +1422,8 @@ namespace { struct RemoteManifestFile { std::string name; size_t size = 0; + uint32_t crc32 = 0; + bool hasCrc32 = false; }; struct RemoteManifestFamily { @@ -1475,7 +1477,9 @@ bool fetchRemoteFontManifest(FontInstaller& installer, std::vector(fileObj["size"] | 0); + if (fileObj["crc32"].is()) { + file.crc32 = fileObj["crc32"].as(); + file.hasCrc32 = true; + } if (!isValidFontFileName(file.name)) { LOG_ERR("WEB", "Manifest entry rejected, invalid file name in %s: %s", family.name.c_str(), file.name.c_str()); fileNamesOk = false; @@ -1577,11 +1585,9 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string& return false; } - if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) { - outError = "Failed to prepare staging area"; - return false; - } - if (!Storage.mkdir(stagingDir)) { + // Resumable staging: keep an existing __staging dir from a prior attempt so + // already-downloaded files can be reused (size + CRC validated per file). + if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) { outError = "Failed to create staging area"; return false; } @@ -1600,11 +1606,34 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string& char stagedPath[128]; int sn = snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str()); if (sn < 0 || static_cast(sn) >= sizeof(stagedPath)) { + // Path-length bugs are not resumable; nuke staging so we don't get stuck. Storage.removeDir(stagingDir); outError = std::string("File path too long: ") + localFilename; return false; } + // Reuse a previously-downloaded file if it still matches the manifest. + if (Storage.exists(stagedPath)) { + FsFile f; + bool sizeOk = false; + if (Storage.openFileForRead("WEB", stagedPath, f)) { + sizeOk = (static_cast(f.size()) == file.size); + f.close(); + } + bool crcOk = !file.hasCrc32; + if (sizeOk && file.hasCrc32) { + uint32_t actualCrc = 0; + if (FontInstaller::computeFileCrc32(stagedPath, actualCrc)) { + crcOk = (actualCrc == file.crc32); + } + } + if (sizeOk && crcOk && installer.validateCpfontFile(stagedPath)) { + LOG_DBG("WEB", "Resuming: reusing %s", stagedPath); + continue; + } + Storage.remove(stagedPath); + } + // Ensure intermediate subdirectories exist inside stagingDir std::string stagedPathStr(stagedPath); size_t lastSlash = stagedPathStr.find_last_of('/'); @@ -1616,13 +1645,23 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string& auto result = HttpDownloader::downloadToFile(url, stagedPath, nullptr); if (result != HttpDownloader::OK) { - Storage.removeDir(stagingDir); + // Drop just the failed file; keep already-downloaded siblings. + Storage.remove(stagedPath); outError = std::string("Download failed: ") + file.name; return false; } + if (file.hasCrc32) { + uint32_t actualCrc = 0; + if (!FontInstaller::computeFileCrc32(stagedPath, actualCrc) || actualCrc != file.crc32) { + Storage.remove(stagedPath); + outError = std::string("Checksum mismatch: ") + file.name; + return false; + } + } + if (!installer.validateCpfontFile(stagedPath)) { - Storage.removeDir(stagingDir); + Storage.remove(stagedPath); outError = std::string("Invalid font file: ") + file.name; return false; }