| … | … | @@ -40,9 +40,9 @@ const DEFAULT_IDLE_TIMEOUT = 10000 |
| 40 | 40 | // crypto.data(buffer): blake2b(LEAF_TYPE || uint64-LE(size) || bytes), which is |
| 41 | 41 | // exactly createStreamingHasher({ size }). Returns the size so callers can re-seed |
| 42 | 42 | // without a second stat. |
| 43 | | async function verifyOnDisk (filePath, contentHash) { |
| 43 | async function hashOnDisk (filePath) { |
| 44 | 44 | let size |
| 45 | | try { size = fs.statSync(filePath).size } catch { return { ok: false, size: 0 } } |
| 45 | try { size = fs.statSync(filePath).size } catch { return null } |
| 46 | 46 | const h = createStreamingHasher({ size }) |
| 47 | 47 | try { |
| 48 | 48 | await new Promise((resolve, reject) => { |
| … | … | @@ -51,8 +51,14 @@ async function verifyOnDisk (filePath, contentHash) { |
| 51 | 51 | rs.on('end', resolve) |
| 52 | 52 | rs.on('error', reject) |
| 53 | 53 | }) |
| 54 | | } catch { return { ok: false, size } } |
| 55 | | return { ok: h.digest() === contentHash, size } |
| 54 | } catch { return null } |
| 55 | return { contentHash: h.digest(), size } |
| 56 | } |
| 57 | |
| 58 | async function verifyOnDisk (filePath, contentHash) { |
| 59 | const r = await hashOnDisk(filePath) |
| 60 | if (!r) return { ok: false, size: 0 } |
| 61 | return { ok: r.contentHash === contentHash, size: r.size } |
| 56 | 62 | } |
| 57 | 63 | |
| 58 | 64 | export class HyperOverlay extends ReadyResource { |
| … | … | @@ -298,6 +304,23 @@ export class HyperOverlay extends ReadyResource { |
| 298 | 304 | return prepared ? { contentHash: prepared.contentHash, size: prepared.size } : null |
| 299 | 305 | } |
| 300 | 306 | |
| 307 | // READ-ONLY: the content hash the bytes currently on disk at `filePath` would be registered under, |
| 308 | // plus their size. Returns null when the file is absent or unreadable. Writes nothing — no index |
| 309 | // entry, no registration, no fetch — so it is safe to run against a live store. |
| 310 | // |
| 311 | // WHY THIS IS PUBLIC: a content-addressed store's correctness rests on "the bytes at this path hash |
| 312 | // to the oid the pointer says", and nothing could ask that question from outside. `registerFile` |
| 313 | // answers it only as a side effect of WRITING an index entry, and `verifyOnDisk` (used by the fetch |
| 314 | // path) is module-private and boolean-only. So a consumer holding a converged pointer had no way to |
| 315 | // check its own local file against it — which is exactly how a STALE local file goes unnoticed: |
| 316 | // OverlayRepoStore.ensureLocal returns a local hit without ever comparing it to the pointer's oid, |
| 317 | // so a node keeps serving superseded bytes forever and never re-fetches. Found on wj62, where two |
| 318 | // co-owners held the same 139 converged pointers but different bytes for app.bsky.actor.profile/self |
| 319 | // (two different avatar blobs), so the signed repo flipped content on every signer change. |
| 320 | async hashFile (filePath) { |
| 321 | return hashOnDisk(filePath) |
| 322 | } |
| 323 | |
| 301 | 324 | async evictContent (contentHash) { |
| 302 | 325 | await this._ensure() |
| 303 | 326 | return this._index.evictContent(contentHash) |