cultnet-ts: validateManifest refuses sizes the reference's types cannot hold
Manifest total and chunk sizes must be whole numbers below 2^53, and a chunk size must fit an int (2147483647), for hand-built values as well as decoded ones. Messages for the whole-number rule now name the bound. No C#-written vector changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
508625e20e
commit
1082b55f51
2 changed files with 28 additions and 8 deletions
|
|
@ -97,18 +97,23 @@ export async function artifactRecordKey(artifactId: string | null, version: stri
|
|||
|
||||
// ---- validation ------------------------------------------------------------------------------------------------------
|
||||
|
||||
const INT32_MAX = 2 ** 31 - 1;
|
||||
|
||||
/**
|
||||
* `ValidateManifestShape`, in its order and with its messages, then the metadata keys a dictionary could not hold twice.
|
||||
* C# sizes are `long`, so a fraction, NaN or Infinity cannot exist on the wire; a hand-built manifest is refused here, once.
|
||||
* Sizes are the reference's types: the manifest total and every offset a `long` (held here below 2^53, the largest integer a
|
||||
* number keeps exact) and a chunk size an `int`. A fraction, NaN, Infinity or a size past those bounds cannot exist on the wire
|
||||
* and is refused here, once, whether it was decoded or hand-built.
|
||||
*/
|
||||
export function validateManifest(manifest: CultMeshCdnArtifactManifest): void {
|
||||
if (manifest.sizeBytes < 0) throw new Error("CDN artifact manifest has a negative size.");
|
||||
if (!Number.isInteger(manifest.sizeBytes)) throw new Error("CDN artifact manifest size is not a whole number.");
|
||||
if (!Number.isSafeInteger(manifest.sizeBytes)) throw new Error("CDN artifact manifest size is not a whole number below 2^53.");
|
||||
let expectedOffset = 0;
|
||||
for (const reference of [...manifest.chunks].sort((a, b) => a.offset - b.offset)) {
|
||||
if (reference.offset !== expectedOffset) throw new Error("CDN artifact chunks are not contiguous.");
|
||||
if (reference.sizeBytes < 0) throw new Error("CDN artifact chunk has a negative size.");
|
||||
if (!Number.isInteger(reference.sizeBytes)) throw new Error("CDN artifact chunk size is not a whole number.");
|
||||
if (!Number.isSafeInteger(reference.sizeBytes)) throw new Error("CDN artifact chunk size is not a whole number below 2^53.");
|
||||
if (reference.sizeBytes > INT32_MAX) throw new Error("CDN artifact chunk size is larger than 2147483647.");
|
||||
expectedOffset += reference.sizeBytes;
|
||||
}
|
||||
if (expectedOffset !== manifest.sizeBytes) throw new Error("CDN artifact chunk sizes do not sum to the manifest size.");
|
||||
|
|
@ -286,7 +291,7 @@ export async function packContent(
|
|||
): Promise<{ manifest: CultMeshCdnArtifactManifest; chunks: CultMeshCdnPackedChunk[] }> {
|
||||
if (isNullOrWhiteSpaceCSharp(artifactId)) throw new Error("Artifact id must be non-empty. (Parameter 'artifactId')");
|
||||
const size = options.chunkSizeBytes ?? DEFAULT_CHUNK_SIZE_BYTES;
|
||||
if (!Number.isInteger(size) || size < 1 || size > 2 ** 31 - 1) throw new Error("ChunkSizeBytes must be between 1 and 2147483647.");
|
||||
if (!Number.isInteger(size) || size < 1 || size > INT32_MAX) throw new Error("ChunkSizeBytes must be between 1 and 2147483647.");
|
||||
const offsets = bytes.length === 0 ? [0] : Array.from({ length: Math.ceil(bytes.length / size) }, (_, index) => index * size);
|
||||
const chunks = await Promise.all(offsets.map(async offset => {
|
||||
const payload = bytes.slice(offset, offset + size);
|
||||
|
|
|
|||
|
|
@ -231,12 +231,12 @@ test("validateManifest refuses negative size, a gap, an overlap and a size-sum m
|
|||
validateManifest(manifestOf({ sizeBytes: 0, chunks: [] }));
|
||||
});
|
||||
|
||||
test("validateManifest refuses a size that is not a whole number: a fraction, NaN or Infinity, in the manifest or in a chunk", () => {
|
||||
test("validateManifest refuses a size that is not a whole number below 2^53 and a chunk size an int cannot hold, whether decoded or hand-built", () => {
|
||||
const chunk = (offset: number, sizeBytes: number) => ({ chunkHash: HASH, offset, sizeBytes, recordKey: "k" });
|
||||
const refuses = (over: Partial<CultMeshCdnArtifactManifest>, message: string) =>
|
||||
assert.throws(() => validateManifest(manifestOf(over)), new Error(message));
|
||||
const manifestMessage = "CDN artifact manifest size is not a whole number.";
|
||||
const chunkMessage = "CDN artifact chunk size is not a whole number.";
|
||||
const manifestMessage = "CDN artifact manifest size is not a whole number below 2^53.";
|
||||
const chunkMessage = "CDN artifact chunk size is not a whole number below 2^53.";
|
||||
for (const size of [1.5, Number.NaN, Number.POSITIVE_INFINITY]) refuses({ sizeBytes: size, chunks: [] }, manifestMessage);
|
||||
// fractional chunk sizes that still tile the manifest are contiguous, so only the whole-number rule refuses them
|
||||
refuses({ sizeBytes: 6, chunks: [chunk(0, 4), chunk(4, 1.5), chunk(5.5, 0.5)] }, chunkMessage);
|
||||
|
|
@ -245,7 +245,22 @@ test("validateManifest refuses a size that is not a whole number: a fraction, Na
|
|||
refuses({ sizeBytes: Number.POSITIVE_INFINITY, chunks: [chunk(0, Number.POSITIVE_INFINITY)] }, manifestMessage);
|
||||
refuses({ sizeBytes: 4, chunks: [chunk(0, 2), chunk(2, Number.POSITIVE_INFINITY)] }, chunkMessage);
|
||||
refuses({ sizeBytes: 0, chunks: [chunk(0, -1)] }, "CDN artifact chunk has a negative size.");
|
||||
validateManifest(manifestOf({ sizeBytes: 2 ** 53 - 1, chunks: [chunk(0, 2 ** 53 - 1)] }));
|
||||
// C# holds the manifest total and offsets in a long, a chunk size in an int; a number holds integers exactly only below 2^53
|
||||
const int32Max = 2 ** 31 - 1;
|
||||
const tooBig = "CDN artifact chunk size is larger than 2147483647.";
|
||||
refuses({ sizeBytes: 2 ** 31, chunks: [chunk(0, 2 ** 31)] }, tooBig);
|
||||
refuses({ sizeBytes: 2 ** 31 + 5, chunks: [chunk(0, 5), chunk(5, 2 ** 31)] }, tooBig);
|
||||
refuses({ sizeBytes: 2 ** 53 - 1, chunks: [chunk(0, 2 ** 53 - 1)] }, tooBig);
|
||||
for (const size of [2 ** 53, 2 ** 63, 2 ** 64]) {
|
||||
refuses({ sizeBytes: size, chunks: [] }, manifestMessage);
|
||||
refuses({ sizeBytes: size, chunks: [chunk(0, size)] }, manifestMessage);
|
||||
refuses({ sizeBytes: 4, chunks: [chunk(0, size)] }, chunkMessage);
|
||||
}
|
||||
// the sum can only pass 2^53 by exceeding the manifest total, which is below it; an offset past 2^53 matches no sum
|
||||
refuses({ sizeBytes: 2 ** 53, chunks: [chunk(0, int32Max), chunk(int32Max, 2 ** 53 - int32Max)] }, manifestMessage);
|
||||
refuses({ sizeBytes: 1, chunks: [chunk(2 ** 53, 1)] }, "CDN artifact chunks are not contiguous.");
|
||||
validateManifest(manifestOf({ sizeBytes: 2 ** 31 + 2, chunks: [chunk(0, 1), chunk(1, int32Max), chunk(2 ** 31, 2)] }));
|
||||
validateManifest(manifestOf({ sizeBytes: int32Max, chunks: [chunk(0, int32Max)] }));
|
||||
});
|
||||
|
||||
test("normalizeHash: 'SHA256:ABC ' -> 'abc', ' sha256:abc' -> 'sha256:abc', blank throws", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue