Some font management improvements

This commit is contained in:
jpirnay
2026-05-20 14:11:50 +02:00
parent 438ed0b47e
commit b1e430f1ca
11 changed files with 256 additions and 40 deletions
+46 -12
View File
@@ -116,19 +116,16 @@ void SdCardFontRegistry::scanDirectory(const char* dirPath, SdCardFontFamilyInfo
dir.close(); dir.close();
} }
bool SdCardFontRegistry::discover() { void SdCardFontRegistry::scanRoot(const char* rootPath) {
families_.clear(); FsFile root = Storage.open(rootPath);
families_.reserve(MAX_SD_FAMILIES);
FsFile root = Storage.open(FONTS_DIR);
if (!root) { if (!root) {
LOG_DBG("SDREG", "Fonts directory not found: %s", FONTS_DIR); LOG_DBG("SDREG", "Fonts directory not found: %s", rootPath);
return false; return;
} }
if (!root.isDirectory()) { 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(); root.close();
return false; return;
} }
char nameBuffer[128]; char nameBuffer[128];
@@ -143,21 +140,58 @@ bool SdCardFontRegistry::discover() {
// Skip hidden/system directories (macOS ._*, .Trashes, etc.) // Skip hidden/system directories (macOS ._*, .Trashes, etc.)
if (nameBuffer[0] == '.' || nameBuffer[0] == '_') continue; 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; SdCardFontFamilyInfo family;
family.name = nameBuffer; family.name = nameBuffer;
std::string subDirPath = std::string(FONTS_DIR) + "/" + nameBuffer; std::string subDirPath = std::string(rootPath) + "/" + nameBuffer;
scanDirectory(subDirPath.c_str(), family); scanDirectory(subDirPath.c_str(), family);
if (!family.files.empty()) { if (!family.files.empty()) {
families_.push_back(std::move(family)); families_.push_back(std::move(family));
LOG_DBG("SDREG", "Found family: %s (%d files)", families_.back().name.c_str(), LOG_DBG("SDREG", "Found family: %s (%d files) in %s", families_.back().name.c_str(),
static_cast<int>(families_.back().files.size())); static_cast<int>(families_.back().files.size()), rootPath);
} }
} else { } else {
entry.close(); entry.close();
} }
} }
root.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 // Sort families alphabetically
std::sort(families_.begin(), families_.end(), std::sort(families_.begin(), families_.end(),
+7
View File
@@ -29,7 +29,13 @@ struct SdCardFontFamilyInfo {
class SdCardFontRegistry { class SdCardFontRegistry {
public: public:
static constexpr int MAX_SD_FAMILIES = 128; 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"; 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. // Scan SD card, populate families_. Returns true if any families found.
bool discover(); bool discover();
@@ -44,4 +50,5 @@ class SdCardFontRegistry {
static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style); static bool parseFilename(const char* filename, uint8_t& size, uint8_t& style);
void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family); void scanDirectory(const char* dirPath, SdCardFontFamilyInfo& family);
void scanRoot(const char* rootPath);
}; };
+1
View File
@@ -305,6 +305,7 @@ STR_CONNECT: "Connect"
STR_OPEN: "Open" STR_OPEN: "Open"
STR_DOWNLOAD: "Download" STR_DOWNLOAD: "Download"
STR_RETRY: "Retry" STR_RETRY: "Retry"
STR_RESUME: "Resume"
STR_YES: "Yes" STR_YES: "Yes"
STR_NO: "No" STR_NO: "No"
STR_SHOW: "Show" STR_SHOW: "Show"
+1
View File
@@ -244,6 +244,7 @@ STR_CONNECT: "Verbinden"
STR_OPEN: "Öffnen" STR_OPEN: "Öffnen"
STR_DOWNLOAD: "Herunterladen" STR_DOWNLOAD: "Herunterladen"
STR_RETRY: "Wiederh." STR_RETRY: "Wiederh."
STR_RESUME: "Fortsetzen"
STR_YES: "Ja" STR_YES: "Ja"
STR_NO: "Nein" STR_NO: "Nein"
STR_SHOW: "Zeigen" STR_SHOW: "Zeigen"
+16 -1
View File
@@ -22,6 +22,7 @@ import json
import os import os
import struct import struct
import sys import sys
import zlib
from pathlib import Path from pathlib import Path
# --- .cpfont binary format constants --- # --- .cpfont binary format constants ---
@@ -96,6 +97,17 @@ def read_cpfont_styles(filepath: Path) -> list[str]:
return styles 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: def parse_filename(filename: str) -> tuple[str, str] | None:
"""Parse '<FamilyName>_<size>.cpfont' into (family, size_str). """Parse '<FamilyName>_<size>.cpfont' into (family, size_str).
@@ -162,6 +174,7 @@ def build_manifest(
{ {
"name": filepath.relative_to(input_dir).as_posix(), "name": filepath.relative_to(input_dir).as_posix(),
"size": filepath.stat().st_size, "size": filepath.stat().st_size,
"crc32": compute_crc32(filepath),
} }
) )
@@ -175,7 +188,9 @@ def build_manifest(
) )
return { 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, "baseUrl": base_url,
"families": manifest_families, "families": manifest_families,
} }
+27
View File
@@ -2,6 +2,7 @@
#include <HalStorage.h> #include <HalStorage.h>
#include <Logging.h> #include <Logging.h>
#include <esp_rom_crc.h>
#include <cctype> #include <cctype>
#include <cstring> #include <cstring>
@@ -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); 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<uint32_t>(n));
}
f.close();
outCrc = crc;
return true;
}
FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) { FontInstaller::Error FontInstaller::deleteFamily(const char* familyName) {
if (!isValidFamilyName(familyName)) { if (!isValidFamilyName(familyName)) {
return Error::INVALID_FAMILY_NAME; return Error::INVALID_FAMILY_NAME;
+11
View File
@@ -31,10 +31,21 @@ class FontInstaller {
/// Validate a .cpfont file on disk (check magic bytes). /// Validate a .cpfont file on disk (check magic bytes).
bool validateCpfontFile(const char* path); 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. /// Build the full SD path for a font file.
/// Writes "/.crosspoint/fonts/<family>/<filename>" to outBuf. /// Writes "/.crosspoint/fonts/<family>/<filename>" to outBuf.
static void buildFontPath(const char* family, const char* filename, char* outBuf, size_t outBufSize); static void buildFontPath(const char* family, const char* filename, char* outBuf, size_t outBufSize);
/// Build the staging-dir path "<FONTS_DIR>/<family>__staging".
static void buildStagingDirPath(const char* family, char* outBuf, size_t outBufSize);
/// Build the backup-dir path "<FONTS_DIR>/<family>__backup".
static void buildBackupDirPath(const char* family, char* outBuf, size_t outBufSize);
/// Delete a family directory and all .cpfont files in it. /// Delete a family directory and all .cpfont files in it.
/// If the deleted family is the active reader font, clears the setting. /// If the deleted family is the active reader font, clears the setting.
Error deleteFamily(const char* familyName); Error deleteFamily(const char* familyName);
@@ -100,7 +100,10 @@ bool FontDownloadActivity::fetchAndParseManifest() {
} }
int version = doc["version"] | 0; 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); LOG_ERR("FONT", "Unsupported manifest version: %d", version);
errorMessage_ = "Unsupported manifest version"; errorMessage_ = "Unsupported manifest version";
return false; return false;
@@ -126,6 +129,10 @@ bool FontDownloadActivity::fetchAndParseManifest() {
ManifestFile file; ManifestFile file;
file.name = fileObj["name"] | ""; file.name = fileObj["name"] | "";
file.size = fileObj["size"] | 0; file.size = fileObj["size"] | 0;
if (fileObj["crc32"].is<uint32_t>()) {
file.crc32 = fileObj["crc32"].as<uint32_t>();
file.hasCrc32 = true;
}
family.totalSize += file.size; family.totalSize += file.size;
family.files.push_back(std::move(file)); family.files.push_back(std::move(file));
} }
@@ -155,6 +162,12 @@ bool FontDownloadActivity::fetchAndParseManifest() {
break; 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)); families_.push_back(std::move(family));
@@ -259,20 +272,14 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
char stagingDir[128]; char stagingDir[128];
char backupDir[128]; char backupDir[128];
snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); 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()); FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir));
snprintf(backupDir, sizeof(backupDir), "%s/%s__backup", SdCardFontRegistry::FONTS_DIR, family.name.c_str()); FontInstaller::buildBackupDirPath(family.name.c_str(), backupDir, sizeof(backupDir));
if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) { // Resume-aware staging: if a __staging dir is left over from a previous
LOG_ERR("FONT", "Failed to clean staging dir: %s", stagingDir); // interrupted download, keep it so files already on disk can be reused.
RenderLock lock(*this); // Files are individually re-verified below (size + CRC) before being
state_ = ERROR; // accepted, so half-written files are caught.
pendingErrorAction_ = PendingFontAction::Download; if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) {
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
errorMessage_ = "Failed to prepare staging area";
return;
}
if (!Storage.mkdir(stagingDir)) {
LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir); LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir);
RenderLock lock(*this); RenderLock lock(*this);
state_ = ERROR; state_ = ERROR;
@@ -304,6 +311,34 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
char stagedPath[128]; char stagedPath[128];
snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str()); 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 // Make sure parent directories exist for the file
std::string stagedPathStr(stagedPath); std::string stagedPathStr(stagedPath);
size_t lastSlash = stagedPathStr.find_last_of('/'); size_t lastSlash = stagedPathStr.find_last_of('/');
@@ -336,7 +371,9 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
if (result == HttpDownloader::ABORTED) { if (result == HttpDownloader::ABORTED) {
LOG_INF("FONT", "Download cancelled: %s", file.name.c_str()); 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; cancelRequested_ = true;
RenderLock lock(*this); RenderLock lock(*this);
state_ = FAMILY_LIST; state_ = FAMILY_LIST;
@@ -345,7 +382,10 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
if (result != HttpDownloader::OK) { if (result != HttpDownloader::OK) {
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result); 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); RenderLock lock(*this);
state_ = ERROR; state_ = ERROR;
pendingErrorAction_ = PendingFontAction::Download; pendingErrorAction_ = PendingFontAction::Download;
@@ -354,9 +394,37 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
return; 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<int>(&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<int>(&family - families_.data());
errorMessage_ = "Checksum mismatch: " + file.name;
return;
}
}
if (!fontInstaller_.validateCpfontFile(stagedPath)) { if (!fontInstaller_.validateCpfontFile(stagedPath)) {
LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath); LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath);
Storage.removeDir(stagingDir); Storage.remove(stagedPath);
family.hasResumableDownload = !family.installed;
RenderLock lock(*this); RenderLock lock(*this);
state_ = ERROR; state_ = ERROR;
pendingErrorAction_ = PendingFontAction::Download; pendingErrorAction_ = PendingFontAction::Download;
@@ -411,6 +479,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
fontInstaller_.refreshRegistry(); fontInstaller_.refreshRegistry();
family.installed = true; family.installed = true;
family.hasUpdate = false; family.hasUpdate = false;
family.hasResumableDownload = false;
syncSelectedIndexForNewActionCount(); syncSelectedIndexForNewActionCount();
RenderLock lock(*this); RenderLock lock(*this);
@@ -471,6 +540,7 @@ std::string FontDownloadActivity::confirmButtonLabel() const {
const auto& family = families_[familyIndexFromList(selectedIndex_)]; const auto& family = families_[familyIndexFromList(selectedIndex_)];
if (family.installed && !family.hasUpdate) return tr(STR_DELETE); if (family.installed && !family.hasUpdate) return tr(STR_DELETE);
if (family.hasUpdate) return tr(STR_UPDATE); if (family.hasUpdate) return tr(STR_UPDATE);
if (family.hasResumableDownload) return tr(STR_RESUME);
return tr(STR_DOWNLOAD); return tr(STR_DOWNLOAD);
} }
@@ -615,6 +685,7 @@ void FontDownloadActivity::render(RenderLock&&) {
const auto& f = families_[familyIndexFromList(index)]; const auto& f = families_[familyIndexFromList(index)];
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE); if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
if (f.installed) return tr(STR_INSTALLED); if (f.installed) return tr(STR_INSTALLED);
if (f.hasResumableDownload) return tr(STR_RESUME);
return ""; return "";
}, },
true); true);
@@ -657,7 +728,10 @@ void FontDownloadActivity::render(RenderLock&&) {
if (!errorMessage_.empty()) { if (!errorMessage_.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, centerY + metrics.verticalSpacing, errorMessage_.c_str()); 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<int>(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); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -36,6 +37,8 @@ class FontDownloadActivity : public Activity {
struct ManifestFile { struct ManifestFile {
std::string name; std::string name;
size_t size = 0; size_t size = 0;
uint32_t crc32 = 0;
bool hasCrc32 = false; // false = legacy v1 manifest, fall back to size-only check
}; };
struct ManifestFamily { struct ManifestFamily {
@@ -46,6 +49,10 @@ class FontDownloadActivity : public Activity {
size_t totalSize = 0; size_t totalSize = 0;
bool installed = false; bool installed = false;
bool hasUpdate = 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; State state_ = WIFI_SELECTION;
+47 -8
View File
@@ -1422,6 +1422,8 @@ namespace {
struct RemoteManifestFile { struct RemoteManifestFile {
std::string name; std::string name;
size_t size = 0; size_t size = 0;
uint32_t crc32 = 0;
bool hasCrc32 = false;
}; };
struct RemoteManifestFamily { struct RemoteManifestFamily {
@@ -1475,7 +1477,9 @@ bool fetchRemoteFontManifest(FontInstaller& installer, std::vector<RemoteManifes
} }
const int version = doc["version"] | 0; const int version = doc["version"] | 0;
if (version != 1) { // v1 (legacy, no crc32) and v2 (with crc32) — crc check is skipped per-file
// when absent. See upstream PR #1904 and scripts/generate-font-manifest.py.
if (version != 1 && version != 2) {
outError = "Unsupported manifest version"; outError = "Unsupported manifest version";
return false; return false;
} }
@@ -1501,6 +1505,10 @@ bool fetchRemoteFontManifest(FontInstaller& installer, std::vector<RemoteManifes
RemoteManifestFile file; RemoteManifestFile file;
file.name = fileObj["name"] | ""; file.name = fileObj["name"] | "";
file.size = static_cast<size_t>(fileObj["size"] | 0); file.size = static_cast<size_t>(fileObj["size"] | 0);
if (fileObj["crc32"].is<uint32_t>()) {
file.crc32 = fileObj["crc32"].as<uint32_t>();
file.hasCrc32 = true;
}
if (!isValidFontFileName(file.name)) { if (!isValidFontFileName(file.name)) {
LOG_ERR("WEB", "Manifest entry rejected, invalid file name in %s: %s", family.name.c_str(), file.name.c_str()); LOG_ERR("WEB", "Manifest entry rejected, invalid file name in %s: %s", family.name.c_str(), file.name.c_str());
fileNamesOk = false; fileNamesOk = false;
@@ -1577,11 +1585,9 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string&
return false; return false;
} }
if (Storage.exists(stagingDir) && !Storage.removeDir(stagingDir)) { // Resumable staging: keep an existing __staging dir from a prior attempt so
outError = "Failed to prepare staging area"; // already-downloaded files can be reused (size + CRC validated per file).
return false; if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) {
}
if (!Storage.mkdir(stagingDir)) {
outError = "Failed to create staging area"; outError = "Failed to create staging area";
return false; return false;
} }
@@ -1600,11 +1606,34 @@ bool installRemoteFamily(const RemoteManifestFamily& family, const std::string&
char stagedPath[128]; char stagedPath[128];
int sn = snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str()); int sn = snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str());
if (sn < 0 || static_cast<size_t>(sn) >= sizeof(stagedPath)) { if (sn < 0 || static_cast<size_t>(sn) >= sizeof(stagedPath)) {
// Path-length bugs are not resumable; nuke staging so we don't get stuck.
Storage.removeDir(stagingDir); Storage.removeDir(stagingDir);
outError = std::string("File path too long: ") + localFilename; outError = std::string("File path too long: ") + localFilename;
return false; 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<size_t>(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 // Ensure intermediate subdirectories exist inside stagingDir
std::string stagedPathStr(stagedPath); std::string stagedPathStr(stagedPath);
size_t lastSlash = stagedPathStr.find_last_of('/'); 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); auto result = HttpDownloader::downloadToFile(url, stagedPath, nullptr);
if (result != HttpDownloader::OK) { 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; outError = std::string("Download failed: ") + file.name;
return false; 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)) { if (!installer.validateCpfontFile(stagedPath)) {
Storage.removeDir(stagingDir); Storage.remove(stagedPath);
outError = std::string("Invalid font file: ") + file.name; outError = std::string("Invalid font file: ") + file.name;
return false; return false;
} }