fix: code review

This commit is contained in:
2026-06-20 19:38:51 +00:00
parent 74f76a820d
commit 595cdad2ef
3 changed files with 32 additions and 9 deletions

View File

@@ -72,9 +72,6 @@ function quote(v: string, force = false): string {
return `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}
/** Emit a frontmatter block in a fixed, deterministic key order.
* `forceQuote` quotes all scalar string values (used for the cc.md export,
* matching Campaign Codex's quoted frontmatter). */
/** Emit the lines for one frontmatter key (scalar / list / nested map). */
function emitKey(key: string, val: FmValue, forceQuote: boolean, lines: string[]): void {
if (Array.isArray(val)) {

View File

@@ -26,10 +26,12 @@ export interface NameUuidIndex {
}
/** Build a NameUuidIndex from a list of {name, uuid} (e.g. relay /search minified
* results). First occurrence wins per name/uuid, mirroring JournalDb's load(). */
* results). First occurrence wins per name/uuid, mirroring JournalDb's load().
* Records use null prototypes so a name like "__proto__" stores as an own key
* instead of hitting the prototype setter. */
export function nameUuidIndexFromEntries(entries: { name: string; uuid: string }[]): NameUuidIndex {
const nameToUuid: Record<string, string> = {};
const uuidToName: Record<string, string> = {};
const nameToUuid: Record<string, string> = Object.create(null);
const uuidToName: Record<string, string> = Object.create(null);
for (const e of entries) {
if (!e.name || !e.uuid) continue;
if (!nameToUuid[e.name]) nameToUuid[e.name] = e.uuid;
@@ -39,15 +41,20 @@ export function nameUuidIndexFromEntries(entries: { name: string; uuid: string }
}
/** A NameResolver backed by a name↔uuid index (loaded from name-uuid.json).
* Queries the records directly — no need to copy them into Maps. */
* Queries the records directly — no need to copy them into Maps. Lookups guard
* with hasOwnProperty because JSON.parse-loaded records carry Object.prototype,
* so a name colliding with a prototype property ("constructor", "toString",
* "__proto__") would otherwise return an inherited member instead of undefined. */
export class MapNameResolver implements NameResolver {
constructor(private readonly idx: NameUuidIndex) {}
uuidOf(name: string): string | undefined {
return this.idx.nameToUuid[name];
const m = this.idx.nameToUuid;
return Object.prototype.hasOwnProperty.call(m, name) ? m[name] : undefined;
}
nameOf(uuid: string): string | undefined {
return this.idx.uuidToName[uuid];
const m = this.idx.uuidToName;
return Object.prototype.hasOwnProperty.call(m, uuid) ? m[uuid] : undefined;
}
}

View File

@@ -43,6 +43,25 @@ describe("MapNameResolver", () => {
expect(r.nameOf("JournalEntry.zzz")).toBeUndefined();
});
it("does not return Object.prototype members for prototype-colliding names", () => {
// A JSON.parse-loaded index carries Object.prototype, so a name like
// "constructor" must NOT resolve to Object.prototype.constructor (a truthy
// function) — that would corrupt pushed Foundry @UUID links. Regression
// guard for the Map.get → record-access switch.
const r = new MapNameResolver(JSON.parse('{"nameToUuid":{"Fenris":"JournalEntry.aaa"},"uuidToName":{"JournalEntry.aaa":"Fenris"}}') as NameUuidIndex);
expect(r.uuidOf("constructor")).toBeUndefined();
expect(r.uuidOf("toString")).toBeUndefined();
expect(r.uuidOf("hasOwnProperty")).toBeUndefined();
expect(r.nameOf("constructor")).toBeUndefined();
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");
});
it("stores a name literally named __proto__ as an own key", () => {
const idx = nameUuidIndexFromEntries([{ name: "__proto__", uuid: "JournalEntry.zzz" }]);
expect(idx.nameToUuid["__proto__"]).toBe("JournalEntry.zzz");
expect(new MapNameResolver(idx).uuidOf("__proto__")).toBe("JournalEntry.zzz");
});
it("satisfies the NameResolver interface (duck-types like JournalDb)", () => {
const r: { uuidOf(n: string): string | undefined; nameOf(u: string): string | undefined } = new MapNameResolver(idx);
expect(r.uuidOf("Fenris")).toBe("JournalEntry.aaa");