cultcache-ts: the whole-store writer decodes the stamp with the decoder a read uses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0820681533
commit
1a3780621e
3 changed files with 24 additions and 8 deletions
|
|
@ -27,7 +27,8 @@ All notable changes to `@gamecult/cultcache-ts` are recorded here, following
|
|||
- A full `pushAll` reads the store's header under the lock first and refuses a store format
|
||||
this runtime does not read (the same error a read gives), leaving the file as it was. A
|
||||
file with no readable stamp is still replaced, so a whole write heals it. Soft `pushAll`
|
||||
is unchanged.
|
||||
is unchanged. The stamp is decoded by the decoder a read uses, so a stamp followed by bytes
|
||||
that are not UTF-8 is refused by a whole write as a read refuses it.
|
||||
|
||||
### Dependencies
|
||||
- `koffi` 3.3.0, loaded only on the first write.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { decode } from "@msgpack/msgpack";
|
||||
|
||||
export const STORE_FORMAT_VERSION = "cultcache.store.v1";
|
||||
|
||||
const STORE_FORMAT_PREFIX = "cultcache.store.";
|
||||
|
|
@ -36,8 +38,9 @@ function refuseUnreadFormat(header: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
// The first element of a leading non-empty MessagePack array when it is a complete UTF-8
|
||||
// string; anything else is undefined.
|
||||
// The first element of a leading non-empty MessagePack array when it is a complete string;
|
||||
// anything else is undefined. The string is decoded by the decoder a read uses, so a stamp
|
||||
// gets one verdict from a read and from a whole write.
|
||||
function peekHeader(bytes: Uint8Array): string | undefined {
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const lead = bytes[0];
|
||||
|
|
@ -47,6 +50,7 @@ function peekHeader(bytes: Uint8Array): string | undefined {
|
|||
else if (lead === 0xdc && bytes.length >= 3 && view.getUint16(1) > 0) offset = 3;
|
||||
else if (lead === 0xdd && bytes.length >= 5 && view.getUint32(1) > 0) offset = 5;
|
||||
else return undefined;
|
||||
const start = offset;
|
||||
const tag = bytes[offset];
|
||||
let length: number;
|
||||
if (tag === undefined) return undefined;
|
||||
|
|
@ -56,11 +60,8 @@ function peekHeader(bytes: Uint8Array): string | undefined {
|
|||
else if (tag === 0xdb && bytes.length >= offset + 5) { length = view.getUint32(offset + 1); offset += 5; }
|
||||
else return undefined;
|
||||
if (bytes.length < offset + length) return undefined;
|
||||
try {
|
||||
return new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(offset, offset + length));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const header = decode(bytes.subarray(start, offset + length));
|
||||
return typeof header === "string" ? header : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1540,6 +1540,20 @@ test("a store's header is judged from its first bytes, in every MessagePack arra
|
|||
assert.equal(headerRefused(padded.subarray(2)), true);
|
||||
});
|
||||
|
||||
test("a stamp followed by bytes that are not UTF-8 gets the verdict a read gives", async () => {
|
||||
const stamp = [...Buffer.from("cultcache.store.v3", "utf8")];
|
||||
const forms = { fixstr: [0x91, 0xb3, ...stamp, 0xff], str8: [0x91, 0xd9, 19, ...stamp, 0xff] };
|
||||
const envelope = { key: "probe", type: "probe", payload: documentPayloadFromBytes(new Uint8Array([0x80])), storedAt: "2026-10-22T00:00:00Z" };
|
||||
for (const [name, bytes] of Object.entries(forms)) {
|
||||
const file = join(await mkdtemp(join(tmpdir(), "cultcache-stamp-")), "store.msgpack");
|
||||
await writeFile(file, new Uint8Array(bytes));
|
||||
const store = new SingleFileMessagePackBackingStore(file);
|
||||
await assert.rejects(() => store.pullAll(), /not one this runtime reads/u, `${name} read`);
|
||||
await assert.rejects(() => store.pushAll([envelope]), /not one this runtime reads/u, `${name} pushAll`);
|
||||
assert.deepEqual([...(await readFile(file))], bytes, `${name} left as it was`);
|
||||
}
|
||||
});
|
||||
|
||||
test("bytes with no readable store stamp are not refused", () => {
|
||||
const stamp = [...Buffer.from("cultcache.store.v3", "utf8")];
|
||||
const cases: Record<string, number[]> = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue