Add fingerprint
This commit is contained in:
@@ -20,13 +20,11 @@ std::string getFilename(const std::string& path) {
|
|||||||
std::string KOReaderDocumentId::getCacheFilePath(const std::string& filePath) {
|
std::string KOReaderDocumentId::getCacheFilePath(const std::string& filePath) {
|
||||||
// Mirror the Epub cache directory convention so the hash file shares the
|
// Mirror the Epub cache directory convention so the hash file shares the
|
||||||
// same per-book folder as other cached data.
|
// same per-book folder as other cached data.
|
||||||
return std::string("/.crosspoint/epub_") +
|
return std::string("/.crosspoint/epub_") + std::to_string(std::hash<std::string>{}(filePath)) + "/koreader_docid.txt";
|
||||||
std::to_string(std::hash<std::string>{}(filePath)) +
|
|
||||||
"/koreader_docid.txt";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath,
|
std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath, const size_t fileSize,
|
||||||
const size_t fileSize) {
|
const std::string& currentFingerprint) {
|
||||||
if (!Storage.exists(cacheFilePath.c_str())) {
|
if (!Storage.exists(cacheFilePath.c_str())) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -36,42 +34,105 @@ std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath,
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format: "<filesize>\n<32-char-hex-hash>"
|
// Format: "<filesize>:<fingerprint>\n<32-char-hex-hash>"
|
||||||
const int newlinePos = content.indexOf('\n');
|
const int newlinePos = content.indexOf('\n');
|
||||||
if (newlinePos < 0) {
|
if (newlinePos < 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t cachedSize = static_cast<size_t>(content.substring(0, newlinePos).toInt());
|
const String header = content.substring(0, newlinePos);
|
||||||
if (cachedSize != fileSize) {
|
const int colonPos = header.indexOf(':');
|
||||||
LOG_DBG("KODoc", "Hash cache invalidated: file size changed (%zu -> %zu)", cachedSize, fileSize);
|
if (colonPos < 0) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: header missing fingerprint");
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const String sizeTok = header.substring(0, colonPos);
|
||||||
|
const String fpTok = header.substring(colonPos + 1);
|
||||||
|
|
||||||
|
// Validate the filesize token – it must consist of ASCII digits and parse
|
||||||
|
// correctly to the expected size.
|
||||||
|
bool digitsOnly = true;
|
||||||
|
for (size_t i = 0; i < sizeTok.length(); ++i) {
|
||||||
|
const char ch = sizeTok[i];
|
||||||
|
if (ch < '0' || ch > '9') {
|
||||||
|
digitsOnly = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!digitsOnly) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: size token not numeric ('%s')", sizeTok.c_str());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const long parsed = sizeTok.toInt();
|
||||||
|
if (parsed < 0) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: size token parse error ('%s')", sizeTok.c_str());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const size_t cachedSize = static_cast<size_t>(parsed);
|
||||||
|
if (cachedSize != fileSize) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: file size or fingerprint changed (%zu -> %zu)", cachedSize, fileSize);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate stored fingerprint format (8 hex characters)
|
||||||
|
if (fpTok.length() != 8) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: bad fingerprint length (%zu)", fpTok.length());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < fpTok.length(); ++i) {
|
||||||
|
char c = fpTok[i];
|
||||||
|
bool hex = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
|
||||||
|
if (!hex) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: non-hex character '%c' in fingerprint", c);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
String currentFpStr(currentFingerprint.c_str());
|
||||||
|
if (fpTok != currentFpStr) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: fingerprint changed (%s != %s)", fpTok.c_str(),
|
||||||
|
currentFingerprint.c_str());
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string hash = content.substring(newlinePos + 1).c_str();
|
std::string hash = content.substring(newlinePos + 1).c_str();
|
||||||
// Trim any trailing whitespace / line endings
|
// Trim any trailing whitespace / line endings
|
||||||
while (!hash.empty() && (hash.back() == '\n' || hash.back() == '\r' || hash.back() == ' ')) {
|
while (!hash.empty() && (hash.back() == '\n' || hash.back() == '\r' || hash.back() == ' ')) {
|
||||||
hash.pop_back();
|
hash.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash must be exactly 32 hex characters.
|
||||||
if (hash.size() != 32) {
|
if (hash.size() != 32) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: wrong hash length (%zu)", hash.size());
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
for (char c : hash) {
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||||
|
LOG_DBG("KODoc", "Hash cache invalidated: non-hex character '%c' in hash", c);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DBG("KODoc", "Hash cache hit: %s", hash.c_str());
|
LOG_DBG("KODoc", "Hash cache hit: %s", hash.c_str());
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderDocumentId::saveCachedHash(const std::string& cacheFilePath,
|
void KOReaderDocumentId::saveCachedHash(const std::string& cacheFilePath, const size_t fileSize,
|
||||||
const size_t fileSize,
|
const std::string& fingerprint, const std::string& hash) {
|
||||||
const std::string& hash) {
|
|
||||||
// Ensure the book's cache directory exists before writing
|
// Ensure the book's cache directory exists before writing
|
||||||
const size_t lastSlash = cacheFilePath.rfind('/');
|
const size_t lastSlash = cacheFilePath.rfind('/');
|
||||||
if (lastSlash != std::string::npos) {
|
if (lastSlash != std::string::npos) {
|
||||||
Storage.ensureDirectoryExists(cacheFilePath.substr(0, lastSlash).c_str());
|
Storage.ensureDirectoryExists(cacheFilePath.substr(0, lastSlash).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format: "<filesize>:<fingerprint>\n<hash>"
|
||||||
String content(std::to_string(fileSize).c_str());
|
String content(std::to_string(fileSize).c_str());
|
||||||
|
content += ':';
|
||||||
|
content += fingerprint.c_str();
|
||||||
content += '\n';
|
content += '\n';
|
||||||
content += hash.c_str();
|
content += hash.c_str();
|
||||||
|
|
||||||
@@ -115,9 +176,24 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
|
|
||||||
const size_t fileSize = file.fileSize();
|
const size_t fileSize = file.fileSize();
|
||||||
|
|
||||||
// Return persisted hash if the file size hasn't changed since it was cached
|
// Compute a lightweight fingerprint from the file's modification time.
|
||||||
|
// The underlying FsFile API provides getModifyDateTime which returns two
|
||||||
|
// packed 16-bit values (date and time). Concatenate these as eight hex
|
||||||
|
// digits to produce the token stored in the cache header.
|
||||||
|
uint16_t date = 0, time = 0;
|
||||||
|
if (!file.getModifyDateTime(&date, &time)) {
|
||||||
|
// If timestamp isn't available for some reason, fall back to a sentinel.
|
||||||
|
date = 0;
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
char fpBuf[9];
|
||||||
|
// two 16-bit numbers => 4 hex digits each
|
||||||
|
sprintf(fpBuf, "%04x%04x", date, time);
|
||||||
|
const std::string fingerprintTok(fpBuf);
|
||||||
|
|
||||||
|
// Return persisted hash if the file size and fingerprint haven't changed.
|
||||||
const std::string cacheFilePath = getCacheFilePath(filePath);
|
const std::string cacheFilePath = getCacheFilePath(filePath);
|
||||||
const std::string cached = loadCachedHash(cacheFilePath, fileSize);
|
const std::string cached = loadCachedHash(cacheFilePath, fileSize, fingerprintTok);
|
||||||
if (!cached.empty()) {
|
if (!cached.empty()) {
|
||||||
file.close();
|
file.close();
|
||||||
return cached;
|
return cached;
|
||||||
@@ -166,7 +242,7 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) {
|
|||||||
|
|
||||||
LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
|
LOG_DBG("KODoc", "Hash calculated: %s (from %zu bytes)", result.c_str(), totalBytesRead);
|
||||||
|
|
||||||
saveCachedHash(cacheFilePath, fileSize, result);
|
saveCachedHash(cacheFilePath, fileSize, fingerprintTok, result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,22 @@ class KOReaderDocumentId {
|
|||||||
// Uses the same directory convention as the Epub cache (/.crosspoint/epub_<hash>/).
|
// Uses the same directory convention as the Epub cache (/.crosspoint/epub_<hash>/).
|
||||||
static std::string getCacheFilePath(const std::string& filePath);
|
static std::string getCacheFilePath(const std::string& filePath);
|
||||||
|
|
||||||
// Returns the cached hash if the file size matches, or empty string on miss/invalidation.
|
// Returns the cached hash if the file size and fingerprint match, or empty
|
||||||
static std::string loadCachedHash(const std::string& cacheFilePath, size_t fileSize);
|
// string on miss/invalidation.
|
||||||
|
//
|
||||||
|
// The fingerprint is derived from the file's modification timestamp. We
|
||||||
|
// call `FsFile::getModifyDateTime` to retrieve the packed date/time fields
|
||||||
|
// from the filesystem. These two 16‑bit values are concatenated as
|
||||||
|
// eight hex digits (YYYYYYTTTT? actually date and time bits) and used as a
|
||||||
|
// lightweight change signal; any change to the file's mtime will cause the
|
||||||
|
// fingerprint to differ and the cache to be invalidated. Since the full
|
||||||
|
// document hash is expensive to compute, using mtime gives us a quick way to
|
||||||
|
// detect modifications without reading file contents.
|
||||||
|
static std::string loadCachedHash(const std::string& cacheFilePath, size_t fileSize,
|
||||||
|
const std::string& currentFingerprint);
|
||||||
|
|
||||||
// Persists the computed hash alongside the file size used to compute it.
|
// Persists the computed hash alongside the file size and fingerprint (the
|
||||||
static void saveCachedHash(const std::string& cacheFilePath, size_t fileSize, const std::string& hash);
|
// modification-timestamp token) used to generate it.
|
||||||
|
static void saveCachedHash(const std::string& cacheFilePath, size_t fileSize, const std::string& fingerprint,
|
||||||
|
const std::string& hash);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user