diff --git a/src/execution-world/metadata.ts b/src/execution-world/metadata.ts index a06e35c..c76c5ec 100644 --- a/src/execution-world/metadata.ts +++ b/src/execution-world/metadata.ts @@ -1,6 +1,13 @@ import { constants } from "node:fs"; import { open } from "node:fs/promises"; +const admissionFailure = ( + code: string, + kind: "file-bytes" | "total-bytes" | "owner-bytes" | "candidates", + measured: number, + limit: number, +) => Object.assign(Error(code), { metadataAdmission: { kind, measured, limit } }); + /** Admission estimates, not a hard V8 heap limit. A process/cgroup ceiling is * still required. Charge input, parse trees and derived indexes before reading * or parsing. Each live candidate retains its reservation until explicit close. */ @@ -27,7 +34,8 @@ export class CandidateMetadataBudget { } acquire(owner: string) { if (!owner || owner.length > 4096) throw Error("INVALID_CANDIDATE_METADATA_OWNER"); - if (this.scopes >= this.limits.candidates) throw Error("CANDIDATE_METADATA_LIMIT"); + if (this.scopes >= this.limits.candidates) + throw admissionFailure("CANDIDATE_METADATA_LIMIT", "candidates", this.scopes + 1, this.limits.candidates); this.scopes++; let bytes = 0, closed = false; @@ -35,8 +43,10 @@ export class CandidateMetadataBudget { if (closed) throw Error("CANDIDATE_METADATA_CLOSED"); if (!Number.isSafeInteger(amount) || amount < 0) throw Error("INVALID_CANDIDATE_METADATA_CHARGE"); const owned = this.owners.get(owner) ?? 0; - if (this.bytes + amount > this.limits.bytes || owned + amount > this.limits.ownerBytes) - throw Error("CANDIDATE_METADATA_LIMIT"); + if (this.bytes + amount > this.limits.bytes) + throw admissionFailure("CANDIDATE_METADATA_LIMIT", "total-bytes", this.bytes + amount, this.limits.bytes); + if (owned + amount > this.limits.ownerBytes) + throw admissionFailure("CANDIDATE_METADATA_LIMIT", "owner-bytes", owned + amount, this.limits.ownerBytes); bytes += amount; this.bytes += amount; this.owners.set(owner, owned + amount); @@ -116,8 +126,8 @@ export async function readCandidateMetadata( let releaseInput: (() => void) | undefined; try { const stat = await handle.stat(); - if (!stat.isFile() || !Number.isSafeInteger(stat.size) || stat.size > limit) - throw Error("CANDIDATE_METADATA_FILE_LIMIT"); + if (!stat.isFile() || !Number.isSafeInteger(stat.size)) throw Error("CANDIDATE_METADATA_FILE_LIMIT"); + if (stat.size > limit) throw admissionFailure("CANDIDATE_METADATA_FILE_LIMIT", "file-bytes", stat.size, limit); // Reserve the input buffer and UTF-16 decoding before allocating either. releaseInput = scope.reserve((stat.size + 1) * 3); const bytes = Buffer.alloc(stat.size + 1);