diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index 434748c4..0d5ea9b3 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -20,13 +20,11 @@ std::string getFilename(const std::string& path) { std::string KOReaderDocumentId::getCacheFilePath(const std::string& filePath) { // Mirror the Epub cache directory convention so the hash file shares the // same per-book folder as other cached data. - return std::string("/.crosspoint/epub_") + - std::to_string(std::hash{}(filePath)) + - "/koreader_docid.txt"; + return std::string("/.crosspoint/epub_") + std::to_string(std::hash{}(filePath)) + "/koreader_docid.txt"; } -std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath, - const size_t fileSize) { +std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath, const size_t fileSize, + const std::string& currentFingerprint) { if (!Storage.exists(cacheFilePath.c_str())) { return ""; } @@ -36,42 +34,105 @@ std::string KOReaderDocumentId::loadCachedHash(const std::string& cacheFilePath, return ""; } - // Format: "\n<32-char-hex-hash>" + // Format: ":\n<32-char-hex-hash>" const int newlinePos = content.indexOf('\n'); if (newlinePos < 0) { return ""; } - const size_t cachedSize = static_cast(content.substring(0, newlinePos).toInt()); - if (cachedSize != fileSize) { - LOG_DBG("KODoc", "Hash cache invalidated: file size changed (%zu -> %zu)", cachedSize, fileSize); + const String header = content.substring(0, newlinePos); + const int colonPos = header.indexOf(':'); + if (colonPos < 0) { + LOG_DBG("KODoc", "Hash cache invalidated: header missing fingerprint"); 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(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(); // Trim any trailing whitespace / line endings while (!hash.empty() && (hash.back() == '\n' || hash.back() == '\r' || hash.back() == ' ')) { hash.pop_back(); } + // Hash must be exactly 32 hex characters. if (hash.size() != 32) { + LOG_DBG("KODoc", "Hash cache invalidated: wrong hash length (%zu)", hash.size()); 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()); return hash; } -void KOReaderDocumentId::saveCachedHash(const std::string& cacheFilePath, - const size_t fileSize, - const std::string& hash) { +void KOReaderDocumentId::saveCachedHash(const std::string& cacheFilePath, const size_t fileSize, + const std::string& fingerprint, const std::string& hash) { // Ensure the book's cache directory exists before writing const size_t lastSlash = cacheFilePath.rfind('/'); if (lastSlash != std::string::npos) { Storage.ensureDirectoryExists(cacheFilePath.substr(0, lastSlash).c_str()); } + // Format: ":\n" String content(std::to_string(fileSize).c_str()); + content += ':'; + content += fingerprint.c_str(); content += '\n'; content += hash.c_str(); @@ -115,9 +176,24 @@ std::string KOReaderDocumentId::calculate(const std::string& filePath) { 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 cached = loadCachedHash(cacheFilePath, fileSize); + const std::string cached = loadCachedHash(cacheFilePath, fileSize, fingerprintTok); if (!cached.empty()) { file.close(); 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); - saveCachedHash(cacheFilePath, fileSize, result); + saveCachedHash(cacheFilePath, fileSize, fingerprintTok, result); return result; } diff --git a/lib/KOReaderSync/KOReaderDocumentId.h b/lib/KOReaderSync/KOReaderDocumentId.h index a78c134f..de487c23 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.h +++ b/lib/KOReaderSync/KOReaderDocumentId.h @@ -48,9 +48,22 @@ class KOReaderDocumentId { // Uses the same directory convention as the Epub cache (/.crosspoint/epub_/). static std::string getCacheFilePath(const std::string& filePath); - // Returns the cached hash if the file size matches, or empty string on miss/invalidation. - static std::string loadCachedHash(const std::string& cacheFilePath, size_t fileSize); + // Returns the cached hash if the file size and fingerprint match, or empty + // 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. - static void saveCachedHash(const std::string& cacheFilePath, size_t fileSize, const std::string& hash); + // Persists the computed hash alongside the file size and fingerprint (the + // 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); };