feat: verify CRC32 checksum for font files (#1904)
## Summary * **What is the goal of this PR?** Add end-to-end integrity verification for downloaded font files by including CRC32 checksums in the font manifest and validating downloaded `.cpfont` files on device. * **What changes are included?** - `generate-font-manifest.py`: compute and include `crc32` for each `.cpfont` asset in the generated `fonts.json` manifest. - `FontDownloadActivity.h`: extend manifest file metadata with `crc32` and declare checksum helper. - `FontDownloadActivity.cpp`: parse `crc32` from manifest, compute CRC32 of downloaded files using `esp_rom_crc32_le`. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_
This commit is contained in:
@@ -21,6 +21,7 @@ import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
# Import canonical version constants from the shared file in lib/EpdFont/scripts/
|
||||
@@ -115,6 +116,15 @@ def parse_filename(filename: str) -> tuple[str, str] | None:
|
||||
return family, size_str
|
||||
|
||||
|
||||
def compute_crc32(filepath: Path) -> int:
|
||||
"""Compute CRC32 of a file, matching esp_rom_crc32_le(0xFFFFFFFF, ...) ^ 0xFFFFFFFF."""
|
||||
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 scan_cpfont_files(input_dir: Path) -> dict[str, list[Path]]:
|
||||
"""Scan input directory for .cpfont files, grouped by family name.
|
||||
|
||||
@@ -164,6 +174,7 @@ def build_manifest(
|
||||
{
|
||||
"name": filepath.name,
|
||||
"size": filepath.stat().st_size,
|
||||
"crc32": compute_crc32(filepath),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_rom_crc.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "SdCardFontGlobals.h"
|
||||
@@ -123,6 +124,14 @@ bool FontDownloadActivity::fetchAndParseManifest() {
|
||||
ManifestFile file;
|
||||
file.name = fileObj["name"] | "";
|
||||
file.size = fileObj["size"] | 0;
|
||||
|
||||
if (!fileObj["crc32"].is<uint32_t>()) {
|
||||
LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str());
|
||||
errorMessage_ = "Invalid font manifest";
|
||||
return false;
|
||||
}
|
||||
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
||||
|
||||
family.totalSize += file.size;
|
||||
family.files.push_back(std::move(file));
|
||||
}
|
||||
@@ -181,6 +190,24 @@ size_t FontDownloadActivity::totalUninstalledSize() const {
|
||||
return total;
|
||||
}
|
||||
|
||||
// Standard CRC32 matching zlib/Python zlib.crc32().
|
||||
bool FontDownloadActivity::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));
|
||||
}
|
||||
outCrc = crc;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
@@ -233,6 +260,29 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t actualCrc = 0;
|
||||
if (!computeFileCrc32(destPath, actualCrc)) {
|
||||
LOG_ERR("FONT", "Failed to open file for CRC check: %s", destPath);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
family.installed = false;
|
||||
family.hasUpdate = false;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
errorMessage_ = "Failed to compute checksum: " + 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);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
family.installed = false;
|
||||
family.hasUpdate = false;
|
||||
RenderLock lock(*this);
|
||||
state_ = ERROR;
|
||||
errorMessage_ = "Checksum mismatch: " + file.name;
|
||||
return;
|
||||
}
|
||||
LOG_DBG("FONT", "Downloaded %s (size=%zu crc32=%08x)", file.name.c_str(), file.size, actualCrc);
|
||||
|
||||
if (!fontInstaller_.validateCpfontFile(destPath)) {
|
||||
LOG_ERR("FONT", "Invalid .cpfont: %s", destPath);
|
||||
fontInstaller_.deleteFamily(family.name.c_str());
|
||||
|
||||
@@ -50,6 +50,7 @@ class FontDownloadActivity : public Activity {
|
||||
struct ManifestFile {
|
||||
std::string name;
|
||||
size_t size = 0;
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct ManifestFamily {
|
||||
@@ -83,6 +84,7 @@ class FontDownloadActivity : public Activity {
|
||||
bool fetchAndParseManifest();
|
||||
void downloadFamily(ManifestFamily& family);
|
||||
void downloadAll();
|
||||
static bool computeFileCrc32(const char* path, uint32_t& outCrc);
|
||||
bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); }
|
||||
int familyIndexFromList(int listIndex) const { return listIndex - 1; }
|
||||
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + 1; }
|
||||
|
||||
Reference in New Issue
Block a user