cultcache-ts: an empty store file is refused, not read as empty
An existing zero-byte file is decoded and refused, like any other undecodable store, matching the Rust and C# runtimes. A missing file stays empty (ENOENT only), and tests pin that a symlink loop and a file-as-parent surface as ELOOP and ENOTDIR. `[0x90]` stays an empty legacy store. Refusal text no longer points at a variant-aware runtime: an unsupported header names the format found, and a v1 record with a fifth slot says the file is not a valid store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
202c4bbd63
commit
ff8d89d512
3 changed files with 47 additions and 9 deletions
|
|
@ -42,11 +42,9 @@ export class SingleFileMessagePackBackingStore implements CacheBackingStore {
|
|||
|
||||
async pullAll(): Promise<CultCacheEnvelope[]> {
|
||||
try {
|
||||
// Only a store that is not there is empty (ENOENT below). An empty file is not a
|
||||
// store, since no CultCache writer leaves one, so it is decoded and refused.
|
||||
const data = await readFile(this.filePath);
|
||||
if (data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const decoded = decode(data);
|
||||
const snapshot = decodeSnapshot(decoded);
|
||||
if (snapshot) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ export function isV1Snapshot(decoded: unknown): decoded is unknown[] {
|
|||
|
||||
if (decoded[0] !== STORE_FORMAT_VERSION) {
|
||||
throw new Error(
|
||||
`CultCache store format "${decoded[0]}" is not readable; this runtime reads "${STORE_FORMAT_VERSION}" only. ` +
|
||||
"The store needs a runtime that resolves document variants.",
|
||||
`CultCache store format "${decoded[0]}" is not one this runtime reads; it reads "${STORE_FORMAT_VERSION}" only.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -26,8 +25,8 @@ export function isV1Snapshot(decoded: unknown): decoded is unknown[] {
|
|||
export function requireV1RecordSlots(record: unknown[]): void {
|
||||
if (record.length > PERSISTED_RECORD_SLOTS) {
|
||||
throw new Error(
|
||||
`CultCache record "${String(record[0])}" (schema "${String(record[1])}") has ${record.length} slots; ` +
|
||||
`this runtime reads ${PERSISTED_RECORD_SLOTS}. The store needs a runtime that resolves document variants.`,
|
||||
`CultCache record "${String(record[0])}" (schema "${String(record[1])}") has ${record.length} slots, more than ` +
|
||||
`the ${PERSISTED_RECORD_SLOTS} of a ${STORE_FORMAT_VERSION} record, so this is not a valid store.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||
import { exec, execFile } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { existsSync } from "node:fs";
|
||||
import { access, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { access, mkdtemp, readFile, symlink, writeFile } from "node:fs/promises";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { dirname, join, resolve, delimiter } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
|
@ -1450,6 +1450,47 @@ test("SingleFileMessagePackBackingStore refuses the C#-written variant store by
|
|||
);
|
||||
});
|
||||
|
||||
test("SingleFileMessagePackBackingStore refusals say what was found without blaming variants", async () => {
|
||||
await assert.rejects(
|
||||
() => pullVector("unknown-header.msgpack"),
|
||||
(error: Error) => error.message.includes("not one this runtime reads") && !error.message.includes("variant"),
|
||||
);
|
||||
await assert.rejects(
|
||||
() => pullVector("extra-slot-full-payload.msgpack"),
|
||||
(error: Error) => error.message.includes("not a valid store") && !error.message.includes("variant"),
|
||||
);
|
||||
});
|
||||
|
||||
test("SingleFileMessagePackBackingStore reads only a missing store as empty", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "cultcache-absent-"));
|
||||
assert.deepEqual(await new SingleFileMessagePackBackingStore(join(dir, "store.cc")).pullAll(), []);
|
||||
|
||||
// An existing empty file is not a store: no CultCache writer leaves one.
|
||||
const empty = join(dir, "empty.cc");
|
||||
await writeFile(empty, new Uint8Array());
|
||||
await assert.rejects(() => new SingleFileMessagePackBackingStore(empty).pullAll());
|
||||
assert.equal((await readFile(empty)).length, 0);
|
||||
|
||||
// An empty array is the legacy envelope array with no envelopes: no header, no format claimed.
|
||||
const legacyEmpty = join(dir, "legacy-empty.cc");
|
||||
await writeFile(legacyEmpty, Uint8Array.of(0x90));
|
||||
assert.deepEqual(await new SingleFileMessagePackBackingStore(legacyEmpty).pullAll(), []);
|
||||
});
|
||||
|
||||
test("SingleFileMessagePackBackingStore reports a store it cannot reach as an I/O error", { skip: process.platform === "win32" }, async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "cultcache-unreachable-"));
|
||||
const loop = join(dir, "loop.cc");
|
||||
await symlink(loop, loop);
|
||||
const parent = join(dir, "body");
|
||||
await writeFile(parent, "not a directory");
|
||||
for (const [path, code] of [[loop, "ELOOP"], [join(parent, "store.cc"), "ENOTDIR"]] as const) {
|
||||
await assert.rejects(
|
||||
() => new SingleFileMessagePackBackingStore(path).pullAll(),
|
||||
(error: NodeJS.ErrnoException) => error.code === code,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("CultCache inspector refuses the same vectors by name", async () => {
|
||||
const inspect = async (name: string) => inspectCultCacheBytes(name, await readFile(join(variantVectors, name)));
|
||||
await assert.rejects(() => inspect("unknown-header.msgpack"), /cultcache\.store\.v9/u);
|
||||
|
|
|
|||
Loading…
Reference in a new issue