diff --git a/docs/model-artifacts.md b/docs/model-artifacts.md index 59b5da04f1..f59c2c7a32 100644 --- a/docs/model-artifacts.md +++ b/docs/model-artifacts.md @@ -5,6 +5,11 @@ GGUF v2/v3 or safetensors file. Detection uses content, including GGUF byte order, rather than its extension. REA does not load a model, interpret weights, run inference, import Python, open external-data references or use the network. +Unsupported GGUF versions remain refused. Their diagnostic preserves the four +version bytes and both UINT32 interpretations with byte order unknown, rather +than inferring big-endian from an unrecognized little-endian value. Little-endian +v2/v3 and big-endian v3 remain supported; big-endian v2 remains refused. + ```bash rea inspect-model-artifact /absolute/path/model.gguf --json rea inspect-model-artifact /absolute/path/model.safetensors --hash full --json diff --git a/src/model/GgufHeader.ts b/src/model/GgufHeader.ts index d30c109466..1f2899f82c 100644 --- a/src/model/GgufHeader.ts +++ b/src/model/GgufHeader.ts @@ -247,16 +247,21 @@ export class GgufHeader { tensors: ModelTensor[]; }> { const versionBytes = await this.reader.take(4); - this.#little = - versionBytes.readUInt32LE() === 2 || versionBytes.readUInt32LE() === 3; - const version = this.#little - ? versionBytes.readUInt32LE() - : versionBytes.readUInt32BE(); - if (version !== 2 && version !== 3) + const littleVersion = versionBytes.readUInt32LE(); + const bigVersion = versionBytes.readUInt32BE(); + let version: number; + if (littleVersion === 2 || littleVersion === 3) { + this.#little = true; + version = littleVersion; + } else if (bigVersion === 2 || bigVersion === 3) { + this.#little = false; + version = bigVersion; + } else { throw new ModelFormatFailure( "unsupported", - `GGUF version ${String(version)} is unsupported; supported versions are 2 and 3`, + `GGUF version bytes ${versionBytes.toString("hex")} are unsupported (little-endian ${String(littleVersion)}, big-endian ${String(bigVersion)}; byte order unknown); supported versions are 2 and 3`, ); + } if (!this.#little && version !== 3) rejectModel("Big-endian GGUF requires version 3"); const tensorCount = await this.#unsigned(8); diff --git a/tests/boundary/model/modelArtifact.test.ts b/tests/boundary/model/modelArtifact.test.ts index 8cbb08ac07..dd7d9d8683 100644 --- a/tests/boundary/model/modelArtifact.test.ts +++ b/tests/boundary/model/modelArtifact.test.ts @@ -22,6 +22,57 @@ const providerAt = async (bytes: Buffer) => { return { provider, path, root }; }; +it.each([ + ["01000000", 1, 16_777_216], + ["00000001", 16_777_216, 1], + ["04000000", 4, 67_108_864], + ["00000004", 67_108_864, 4], + ["e7030000", 999, 3_875_733_504], + ["000003e7", 3_875_733_504, 999], + ["00000000", 0, 0], + ["ffffffff", 4_294_967_295, 4_294_967_295], +] as const)( + "preserves unsupported GGUF version bytes %s without assuming byte order", + async (versionBytes, little, big) => { + // No counts are needed to explain a version refusal. Do not reinterpret + // an unknown version's subsequent layout or publish a header observation. + const { provider, path } = await providerAt( + Buffer.from(`47475546${versionBytes}`, "hex"), + ); + const result = await provider.inspect({ path, hash: "header" }); + if (result.ok) throw new Error("Unknown version admitted"); + expect(result.error).toMatchObject({ + _tag: "AnalysisUnsupportedTargetError", + reason: `GGUF version bytes ${versionBytes} are unsupported (little-endian ${String(little)}, big-endian ${String(big)}; byte order unknown); supported versions are 2 and 3`, + }); + }, +); + +it("continues reading little-endian GGUF v2 declarations", async () => { + const { provider, path } = await providerAt( + ggufFixture({ version: 2 }).bytes, + ); + const result = await provider.inspect({ path, hash: "header" }); + if (!result.ok) throw result.error; + expect(result.value).toMatchObject({ version: 2, byte_order: "little" }); + expect(result.value.tensors.map(({ name }) => name)).toEqual([ + "weight", + "quant", + ]); +}); + +it("continues refusing the unsupported big-endian GGUF v2 layout", async () => { + const { provider, path } = await providerAt( + ggufFixture({ version: 2, bigEndian: true }).bytes, + ); + const result = await provider.inspect({ path, hash: "header" }); + if (result.ok) throw new Error("Big-endian GGUF v2 admitted"); + expect(result.error).toMatchObject({ + _tag: "BinaryTargetError", + constraint: "file_format_rejected", + }); +}); + it.each([false, true])( "reads exact GGUF declarations, endian=%s, without hashing tensor payload", async (bigEndian) => {