Some font management improvements
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <esp_rom_crc.h>
|
||||
|
||||
#include <cctype>
|
||||
#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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!isValidFamilyName(familyName)) {
|
||||
return Error::INVALID_FAMILY_NAME;
|
||||
|
||||
@@ -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/<family>/<filename>" to outBuf.
|
||||
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.
|
||||
/// If the deleted family is the active reader font, clears the setting.
|
||||
Error deleteFamily(const char* familyName);
|
||||
|
||||
@@ -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<uint32_t>()) {
|
||||
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
||||
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<int>(&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<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)) {
|
||||
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<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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RemoteManifes
|
||||
}
|
||||
|
||||
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";
|
||||
return false;
|
||||
}
|
||||
@@ -1501,6 +1505,10 @@ bool fetchRemoteFontManifest(FontInstaller& installer, std::vector<RemoteManifes
|
||||
RemoteManifestFile file;
|
||||
file.name = fileObj["name"] | "";
|
||||
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)) {
|
||||
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<size_t>(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<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
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user