mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
fix(model): report malformed GGUF and Safetensors headers as invalid_format through ParseFailure (#10667)
* refactor(model): report GGUF and Safetensors format failures through ParseFailure * fix(model): report malformed model headers as invalid_format through the shared mapping Malformed and truncated GGUF and Safetensors headers now take the shared ParseFailure mapping (invalid_request, invalid_format on path) instead of the model provider's own BinaryTargetError file_format_rejected. Unsupported forms stay unsupported_target and model allowances keep their resource-constraint recovery.
This commit is contained in:
1 parent
2705aad365
commit
a22fb6e60b
9 files changed
+160
-136
No files matched your search
@@ -165,8 +165,11 @@ unknown fields on tensor entries are ignored within the reader's 32-level JSON
|
||||
nesting limit, while the required dtype, shape and data offsets are still
|
||||
validated. JSON duplicate keys, invalid UTF-8, out-of-file
|
||||
ranges, overlaps, gaps, trailing unindexed data and known dtype/shape byte-length
|
||||
contradictions are rejected with a typed `file_format_rejected` constraint (public code `target_unavailable`, category `unsupported_target`).
|
||||
A missing, non-regular or otherwise unusable selected path is also `target_unavailable`;
|
||||
contradictions are rejected as an invalid `path` (public code `invalid_request`,
|
||||
category `invalid_input`, issue reason `invalid_format`), like any other
|
||||
malformed or truncated model header. A form outside the reader's coverage, such
|
||||
as a PyTorch pickle, is `unsupported_target`.
|
||||
A missing, non-regular or otherwise unusable selected path is `target_unavailable`;
|
||||
host I/O or resource failures on a valid selection, such as `EMFILE` or `EIO`, are
|
||||
`execution_failure` with the path and reason in `details.diagnostics`. Empty
|
||||
tensors and scalar shapes are supported. The reader retains shape dimensions as
|
||||
|
||||
@@ -191,7 +191,6 @@
|
||||
"src/il2cpp/Il2cppBinaryFormat.ts": 1,
|
||||
"src/il2cpp/Il2cppMetadataFormat.ts": 1,
|
||||
"src/javascript/sourceMaps/SourceMapFormat.ts": 1,
|
||||
"src/model/ModelFormat.ts": 1,
|
||||
"src/native/AppleMachoSelection.ts": 1,
|
||||
"src/native/MachoCommandNames.ts": 1,
|
||||
"src/native/apple/AppleIps.errors.ts": 1,
|
||||
@@ -240,7 +239,6 @@
|
||||
"src/dotnet/ManagedStaticProvider.ts": 1,
|
||||
"src/firmware/FirmwareCommand.ts": 2,
|
||||
"src/il2cpp/Il2cppMetadataFile.ts": 2,
|
||||
"src/model/ModelFormat.ts": 2,
|
||||
"src/model/ModelHeaderReader.ts": 1,
|
||||
"src/native/AppleDispatchMetadata.ts": 3,
|
||||
"src/native/AppleMachoFixups.ts": 48,
|
||||
|
||||
@@ -8,10 +8,12 @@ import type {
|
||||
ModelArtifactPartialTensor,
|
||||
} from "../domain/model/modelArtifact.js";
|
||||
import { ModelHeaderReader } from "./ModelHeaderReader.js";
|
||||
import { unsupportedInput } from "../domain/parseFailure.js";
|
||||
import {
|
||||
MODEL_ARRAY_PREVIEW,
|
||||
MODEL_METADATA_NESTING,
|
||||
ModelFormatFailure,
|
||||
MODEL_FORMAT,
|
||||
modelCapacityFailure,
|
||||
ModelReportBudget,
|
||||
rejectModel,
|
||||
} from "./ModelFormat.js";
|
||||
@@ -125,10 +127,9 @@ export class GgufHeader {
|
||||
// occupies one, so charge it on entry instead of on a child visit.
|
||||
const frames = enclosingFrames + 1;
|
||||
if (frames > MODEL_METADATA_NESTING)
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
`GGUF metadata nesting exceeds ${String(MODEL_METADATA_NESTING)} array frames`,
|
||||
throw modelCapacityFailure(
|
||||
"metadata-nesting",
|
||||
`GGUF metadata nesting exceeds ${String(MODEL_METADATA_NESTING)} array frames`,
|
||||
);
|
||||
const element = Number(await this.#unsigned(4));
|
||||
const rawCount = await this.#unsigned(8);
|
||||
@@ -262,8 +263,8 @@ export class GgufHeader {
|
||||
this.#little = false;
|
||||
version = bigVersion;
|
||||
} else {
|
||||
throw new ModelFormatFailure(
|
||||
"unsupported",
|
||||
throw unsupportedInput(
|
||||
MODEL_FORMAT,
|
||||
`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`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
@@ -40,12 +38,15 @@ import { ModelHeaderReader } from "./ModelHeaderReader.js";
|
||||
import { GgufHeader } from "./GgufHeader.js";
|
||||
import { readSafetensorsHeader } from "./SafetensorsHeader.js";
|
||||
import {
|
||||
ModelFormatFailure,
|
||||
modelCapacityFailure,
|
||||
modelCapacityLimitOf,
|
||||
MODEL_REPORT_BYTES,
|
||||
modelCapacityRemediation,
|
||||
modelCapacityReportedLimits,
|
||||
} from "./ModelFormat.js";
|
||||
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
|
||||
import { ParseFailureError } from "../domain/parseFailure.js";
|
||||
import { parseFailureAnalysisError } from "../domain/parseFailureAnalysisError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { selectedFileTargetError } from "../artifacts/RegularFileErrorProjection.js";
|
||||
import {
|
||||
@@ -107,36 +108,33 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
);
|
||||
} catch (cause) {
|
||||
const { cause: primary, origin } = artifactFilePrimaryFacts(cause);
|
||||
const capacity =
|
||||
primary instanceof ParseFailureError
|
||||
? modelCapacityLimitOf(primary)
|
||||
: undefined;
|
||||
const formatError = parseFailureAnalysisError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
primary,
|
||||
);
|
||||
let error: AnalysisError;
|
||||
if (origin === "owned-or-classified" && primary instanceof AnalysisError)
|
||||
error = primary;
|
||||
else if (primary instanceof ModelFormatFailure) {
|
||||
const limit = primary.kind === "capacity" ? primary.limit : undefined;
|
||||
error =
|
||||
limit !== undefined
|
||||
? new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"memory",
|
||||
primary.message,
|
||||
modelCapacityReportedLimits(limit),
|
||||
{
|
||||
cause,
|
||||
retryable: false,
|
||||
remediationAction: modelCapacityRemediation(limit),
|
||||
},
|
||||
)
|
||||
: primary.kind === "unsupported"
|
||||
? new AnalysisUnsupportedTargetError(
|
||||
OPERATION,
|
||||
input.path,
|
||||
primary.message,
|
||||
{ cause },
|
||||
)
|
||||
: new BinaryTargetError(input.path, primary.message, {
|
||||
cause,
|
||||
constraint: "file_format_rejected",
|
||||
});
|
||||
} else if (
|
||||
else if (primary instanceof ParseFailureError && capacity !== undefined)
|
||||
// A model allowance names its own recovery.
|
||||
error = new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"memory",
|
||||
primary.message,
|
||||
modelCapacityReportedLimits(capacity),
|
||||
{
|
||||
cause,
|
||||
retryable: false,
|
||||
remediationAction: modelCapacityRemediation(capacity),
|
||||
},
|
||||
);
|
||||
else if (formatError !== undefined) error = formatError;
|
||||
else if (
|
||||
origin === "owned-or-classified" &&
|
||||
primary instanceof ArtifactReaderFailure &&
|
||||
primary.reason === "cancelled"
|
||||
@@ -245,10 +243,9 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
): Promise<ModelArtifact> {
|
||||
const initial = await file.stat({ bigint: true });
|
||||
if (initial.size > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
"Model file exceeds exact host offset precision",
|
||||
throw modelCapacityFailure(
|
||||
"exact-offset",
|
||||
"Model file exceeds exact host offset precision",
|
||||
);
|
||||
const reader = new ModelHeaderReader(file, Number(initial.size), signal);
|
||||
const first = await reader.take(4);
|
||||
@@ -430,11 +427,7 @@ export class ModelArtifactProvider implements ModelArtifactPort {
|
||||
],
|
||||
};
|
||||
if (jsonValueByteLength(report, MODEL_REPORT_BYTES) > MODEL_REPORT_BYTES)
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
"Model report exceeds 32 MiB",
|
||||
"report-bytes",
|
||||
);
|
||||
throw modelCapacityFailure("report-bytes", "Model report exceeds 32 MiB");
|
||||
return modelArtifactSchema.parse(report);
|
||||
}
|
||||
}
|
||||
+37
-26
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
malformedInput,
|
||||
ParseFailureError,
|
||||
unsupportedInput,
|
||||
} from "../domain/parseFailure.js";
|
||||
|
||||
/** Producing constraint for a model capacity refusal. Recovery uses this, not message text. */
|
||||
export const MODEL_CAPACITY_LIMITS = [
|
||||
"header-bytes",
|
||||
@@ -11,34 +17,40 @@ export const MODEL_CAPACITY_LIMITS = [
|
||||
/** Which fixed model allowance refused the request. */
|
||||
export type ModelCapacityLimit = (typeof MODEL_CAPACITY_LIMITS)[number];
|
||||
|
||||
/** Format failures stay separate from filesystem, capacity and cancellation failures. */
|
||||
export class ModelFormatFailure extends Error {
|
||||
/** Set only for `kind: "capacity"`. */
|
||||
readonly limit: ModelCapacityLimit | undefined;
|
||||
constructor(kind: "format" | "unsupported", message: string);
|
||||
constructor(kind: "capacity", message: string, limit: ModelCapacityLimit);
|
||||
constructor(
|
||||
readonly kind: "format" | "unsupported" | "capacity",
|
||||
message: string,
|
||||
limit?: ModelCapacityLimit,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ModelFormatFailure";
|
||||
if (kind === "capacity" && limit === undefined)
|
||||
throw new TypeError("Model capacity failures require a limit kind");
|
||||
if (kind !== "capacity" && limit !== undefined)
|
||||
throw new TypeError("Only model capacity failures carry a limit kind");
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
/** The ParseFailure format of GGUF and Safetensors model headers. */
|
||||
export const MODEL_FORMAT = "model";
|
||||
|
||||
/** A fixed model allowance refused the input; the limit name is its {@link ModelCapacityLimit}. */
|
||||
export const modelCapacityFailure = (
|
||||
limit: ModelCapacityLimit,
|
||||
detail: string,
|
||||
): ParseFailureError<"limit"> =>
|
||||
new ParseFailureError({
|
||||
kind: "limit",
|
||||
format: MODEL_FORMAT,
|
||||
detail,
|
||||
limit: {
|
||||
name: limit,
|
||||
maximum: Object.values(modelCapacityReportedLimits(limit))[0] ?? 0,
|
||||
},
|
||||
});
|
||||
|
||||
/** The model allowance a failure names, or undefined for any other failure. */
|
||||
export const modelCapacityLimitOf = (
|
||||
cause: ParseFailureError,
|
||||
): ModelCapacityLimit | undefined => {
|
||||
const { failure } = cause;
|
||||
if (failure.kind !== "limit") return undefined;
|
||||
return MODEL_CAPACITY_LIMITS.find((limit) => limit === failure.limit.name);
|
||||
};
|
||||
|
||||
export function rejectModel(message: string): never {
|
||||
throw new ModelFormatFailure("format", message);
|
||||
throw malformedInput(MODEL_FORMAT, message);
|
||||
}
|
||||
|
||||
export function rejectPickle(): never {
|
||||
throw new ModelFormatFailure(
|
||||
"unsupported",
|
||||
throw unsupportedInput(
|
||||
MODEL_FORMAT,
|
||||
"PyTorch pickle/ZIP model deserialization is unsupported; supply GGUF or safetensors instead",
|
||||
);
|
||||
}
|
||||
@@ -108,10 +120,9 @@ export class ModelReportBudget {
|
||||
#remaining = MODEL_REPORT_BYTES;
|
||||
charge(bytes: number): void {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > this.#remaining)
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
"Model facts exceed the 32 MiB retained representation budget. This release has no runtime override.",
|
||||
throw modelCapacityFailure(
|
||||
"representation-bytes",
|
||||
"Model facts exceed the 32 MiB retained representation budget. This release has no runtime override.",
|
||||
);
|
||||
this.#remaining -= bytes;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { FileHandle } from "node:fs/promises";
|
||||
import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
ModelFormatFailure,
|
||||
modelCapacityFailure,
|
||||
MODEL_HEADER_BYTES,
|
||||
rejectModel,
|
||||
} from "./ModelFormat.js";
|
||||
@@ -58,10 +58,9 @@ export class ModelHeaderReader {
|
||||
if (!Number.isSafeInteger(end) || end < 0 || end > this.size)
|
||||
rejectModel("Declared header extent is outside the file");
|
||||
if (end - this.#headerPrefixBytes > MODEL_HEADER_BYTES)
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
"Model header exceeds 100,000,000 bytes. This release has no runtime override.",
|
||||
throw modelCapacityFailure(
|
||||
"header-bytes",
|
||||
"Model header exceeds 100,000,000 bytes. This release has no runtime override.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ModelHeaderReader } from "./ModelHeaderReader.js";
|
||||
import {
|
||||
MODEL_HEADER_BYTES,
|
||||
MODEL_JSON_NESTING,
|
||||
ModelFormatFailure,
|
||||
modelCapacityFailure,
|
||||
ModelReportBudget,
|
||||
rejectModel,
|
||||
rejectPickle,
|
||||
@@ -73,10 +73,9 @@ class HeaderJson {
|
||||
this.#space();
|
||||
this.budget.charge(64);
|
||||
if (depth > MODEL_JSON_NESTING)
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
`Safetensors JSON nesting exceeds ${String(MODEL_JSON_NESTING)} levels`,
|
||||
throw modelCapacityFailure(
|
||||
"json-nesting",
|
||||
`Safetensors JSON nesting exceeds ${String(MODEL_JSON_NESTING)} levels`,
|
||||
);
|
||||
const c = this.text[this.#position];
|
||||
if (c === '"') return this.#string();
|
||||
@@ -190,10 +189,9 @@ const integersOf = (value: HeaderValue | undefined): bigint[] => {
|
||||
};
|
||||
const exactOffset = (value: bigint): number => {
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new ModelFormatFailure(
|
||||
"capacity",
|
||||
"Safetensors data offset exceeds exact host offset precision",
|
||||
throw modelCapacityFailure(
|
||||
"exact-offset",
|
||||
"Safetensors data offset exceeds exact host offset precision",
|
||||
);
|
||||
return Number(value);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,18 @@ import {
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest, createSharedMcpSession } from "../support/cli/cliFixture.js";
|
||||
|
||||
/** A malformed model header is an invalid `path` on both adapters. */
|
||||
const MALFORMED_MODEL = {
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
operation: "inspect_model_artifact",
|
||||
issues: [
|
||||
expect.objectContaining({ path: ["path"], reason: "invalid_format" }),
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Model queries are target-free; share immutable registration and schema setup.
|
||||
// Portable export keeps its separate post-CLI connection below.
|
||||
const modelMcp = createSharedMcpSession(async (registerCleanup) => {
|
||||
@@ -519,12 +531,17 @@ cliTest(
|
||||
// The raw extent is outside this empty file, and that check runs before
|
||||
// the offset is converted to a host number.
|
||||
expect(offsetFailure.error).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
constraint: "file_format_rejected",
|
||||
path,
|
||||
reason: "Declared extent is outside the file",
|
||||
operation: "inspect_model_artifact",
|
||||
issues: [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: `Selected artifact ${path}: Declared extent is outside the file`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -1085,17 +1102,13 @@ cliTest(
|
||||
});
|
||||
expect(malformedCli.exitCode).toBe(1);
|
||||
expect(malformedCli.json).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: { constraint: "file_format_rejected" },
|
||||
...MALFORMED_MODEL,
|
||||
});
|
||||
const malformedMcp = parseMcpToolError(
|
||||
await call("inspect_model_artifact", { path: malformedPath }),
|
||||
);
|
||||
expect(malformedMcp.error).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: { constraint: "file_format_rejected" },
|
||||
...MALFORMED_MODEL,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1194,17 +1207,13 @@ cliTest(
|
||||
});
|
||||
expect(cliResult.exitCode).toBe(1);
|
||||
expect(cliResult.json).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: { constraint: "file_format_rejected" },
|
||||
...MALFORMED_MODEL,
|
||||
});
|
||||
const mcpResult = parseMcpToolError(
|
||||
await call("inspect_model_artifact", { path }),
|
||||
);
|
||||
expect(mcpResult.error).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: { constraint: "file_format_rejected" },
|
||||
...MALFORMED_MODEL,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1283,9 +1292,7 @@ cliTest(
|
||||
await call("inspect_model_artifact", { path }),
|
||||
);
|
||||
expect(malformed.error).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: { constraint: "file_format_rejected" },
|
||||
...MALFORMED_MODEL,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
MODEL_JSON_NESTING,
|
||||
MODEL_METADATA_NESTING,
|
||||
MODEL_REPORT_BYTES,
|
||||
ModelFormatFailure,
|
||||
modelCapacityRemediation,
|
||||
modelCapacityReportedLimits,
|
||||
} from "../../../src/model/ModelFormat.js";
|
||||
import { readSafetensorsHeader } from "../../../src/model/SafetensorsHeader.js";
|
||||
import { ParseFailureError } from "../../../src/domain/parseFailure.js";
|
||||
import { GgufHeader } from "../../../src/model/GgufHeader.js";
|
||||
import { createModelArtifactService } from "../../../src/composition/model.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
@@ -25,6 +25,14 @@ import {
|
||||
safetensorsFixture,
|
||||
} from "../../fixtures/model/model.js";
|
||||
|
||||
/** A malformed model header is an invalid `path`, like every format failure. */
|
||||
const INVALID_FORMAT = {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [
|
||||
expect.objectContaining({ path: ["path"], reason: "invalid_format" }),
|
||||
],
|
||||
} as const;
|
||||
|
||||
const hash = (bytes: Buffer) =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
const providerAt = async (bytes: Buffer) => {
|
||||
@@ -82,8 +90,7 @@ it("continues refusing the unsupported big-endian GGUF v2 layout", async () => {
|
||||
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",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,7 +219,7 @@ it.each([
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Expected format refusal");
|
||||
expect(result.error).toMatchObject({
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -263,8 +270,7 @@ it.each([0, 1, 2, 4, 12])(
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Invalid alignment accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -289,8 +295,7 @@ it.each([
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Invalid metadata key accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -340,14 +345,18 @@ it("retains the absolute header budget for GGUF without allocating that extent",
|
||||
await file.truncate(100_000_009);
|
||||
const reader = new ModelHeaderReader(file, 100_000_009);
|
||||
expect(() => reader.guarantee(100_000_000)).not.toThrow();
|
||||
expect(() => reader.guarantee(100_000_001)).toThrow(ModelFormatFailure);
|
||||
expect(() => reader.guarantee(100_000_001)).toThrow(ParseFailureError);
|
||||
try {
|
||||
reader.guarantee(100_000_001);
|
||||
} catch (cause) {
|
||||
expect(cause).toBeInstanceOf(ModelFormatFailure);
|
||||
if (!(cause instanceof ModelFormatFailure))
|
||||
expect(cause).toBeInstanceOf(ParseFailureError);
|
||||
if (!(cause instanceof ParseFailureError))
|
||||
throw new Error("Expected a model header capacity failure");
|
||||
expect(cause.limit).toBe("header-bytes");
|
||||
expect(cause.failure).toMatchObject({
|
||||
kind: "limit",
|
||||
format: "model",
|
||||
limit: { name: "header-bytes", maximum: 100_000_000 },
|
||||
});
|
||||
expect(cause.message).toContain("100,000,000 bytes");
|
||||
expect(cause.message).not.toContain("Safetensors");
|
||||
}
|
||||
@@ -379,7 +388,7 @@ it("separates impossible string lengths from nesting capacity", async () => {
|
||||
const invalid = await providerAt(oversized.bytes);
|
||||
expect(
|
||||
await invalid.provider.inspect({ path: invalid.path, hash: "header" }),
|
||||
).toMatchObject({ ok: false, error: { _tag: "BinaryTargetError" } });
|
||||
).toMatchObject({ ok: false, error: INVALID_FORMAT });
|
||||
for (const [label, bytes, allowance] of [
|
||||
["nesting", deep.bytes, "32 array frames"],
|
||||
] as const) {
|
||||
@@ -656,8 +665,13 @@ it.each(ggufKLayouts)(
|
||||
const rejected = await provider.inspect({ path, hash: "header" });
|
||||
if (rejected.ok) throw new Error("Incomplete common K row admitted");
|
||||
expect(rejected.error).toMatchObject({
|
||||
constraint: "file_format_rejected",
|
||||
reason: expect.stringContaining("multiple of 64 elements"),
|
||||
...INVALID_FORMAT,
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("multiple of 64 elements"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -778,8 +792,7 @@ it("validates booleans beyond an array preview without allocating the full value
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Invalid omitted boolean was accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -861,9 +874,13 @@ it.each([
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Invalid omitted UTF-8 string was accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
message: expect.stringContaining("GGUF string is not valid UTF-8"),
|
||||
...INVALID_FORMAT,
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
reason: "invalid_format",
|
||||
message: expect.stringContaining("GGUF string is not valid UTF-8"),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1116,14 +1133,13 @@ const malformed = [
|
||||
],
|
||||
] as const;
|
||||
it.each(malformed)(
|
||||
"rejects %s with typed file_format_rejected",
|
||||
"rejects %s as an invalid_format path",
|
||||
async (_name, make) => {
|
||||
const { provider, path } = await providerAt(make());
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Malformed model was accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1172,8 +1188,7 @@ it("retains GGUF declarations and metadata when storage overlap is refused", asy
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Overlapping tensors admitted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
const evidence = parseEvidence(result.error.partialObservation);
|
||||
expect(evidence.subject).toBeNull();
|
||||
@@ -1322,8 +1337,7 @@ it("reports a truncated safetensors header whose length resembles a pickle marke
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Truncated model was accepted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
...INVALID_FORMAT,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1333,7 +1347,7 @@ it("bounds expansion before allocation and distinguishes unsupported pickle and
|
||||
const { provider, path, root } = await providerAt(fixture.bytes);
|
||||
const huge = await provider.inspect({ path, hash: "header" });
|
||||
if (huge.ok) throw new Error("Huge count admitted");
|
||||
expect(huge.error._tag).toBe("BinaryTargetError");
|
||||
expect(huge.error).toMatchObject(INVALID_FORMAT);
|
||||
await writeFile(path, Buffer.from([0x80, 4, 0, 0]));
|
||||
const pickle = await provider.inspect({ path, hash: "header" });
|
||||
if (pickle.ok) throw new Error("Pickle admitted");
|
||||
@@ -1585,7 +1599,7 @@ it.each([false, true])(
|
||||
const { provider, path } = await providerAt(bytes);
|
||||
expect(await provider.inspect({ path, hash: "header" })).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "BinaryTargetError" },
|
||||
error: INVALID_FORMAT,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1606,7 +1620,7 @@ it("rejects impossible safetensors header and data extents before numeric conver
|
||||
const { provider, path } = await providerAt(bytes);
|
||||
expect(await provider.inspect({ path, hash: "header" })).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "BinaryTargetError" },
|
||||
error: INVALID_FORMAT,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1620,7 +1634,7 @@ it("rejects a tensor directory whose old 20-byte estimate fits but fixed fields
|
||||
await truncate(path, 24 + 65537 * 20);
|
||||
expect(await provider.inspect({ path, hash: "header" })).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "BinaryTargetError" },
|
||||
error: INVALID_FORMAT,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user