- Inventory scans now verify xar extracted checksums themselves, under the caller's integrity policy. Under "fail" a mismatch still fails, now as artifact_integrity_mismatch with the logical path. Under "record-and-continue" the member stays in the graph as mismatched, with the declared and observed values, and is not expanded. A declared SHA-256 becomes an integrity contradiction. Extraction keeps verifying in the reader. - Read cpio symlink targets only up to 4096 bytes; longer declared targets are streamed past and recorded with a limitation instead of buffered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import type { Readable } from "node:stream";
|
|
|
|
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
|
import { streamChunkToBuffer } from "./StreamBytes.js";
|
|
|
|
/** Full stream digest and byte count with bounded classification evidence. */
|
|
export type HashResult = {
|
|
readonly sha256: string;
|
|
readonly bytes: number;
|
|
readonly prefix: Buffer;
|
|
/** Digest in the additionally requested algorithm, when one was requested. */
|
|
readonly also?: string;
|
|
};
|
|
|
|
/** Bounded classification evidence; this is not an executable format size limit. */
|
|
export const ARTIFACT_CLASSIFICATION_PREFIX_BYTES = 8_192;
|
|
|
|
/** Preserve the inventory cancellation reason at a stream boundary. */
|
|
export const abortIfNeeded = (signal?: AbortSignal): void => {
|
|
if (signal?.aborted === true)
|
|
throw new ArtifactReaderFailure(
|
|
"cancelled",
|
|
"Artifact inventory cancelled",
|
|
);
|
|
};
|
|
|
|
/** Hash every stream byte and retain its bounded classification prefix. */
|
|
export const hashReadable = async (
|
|
stream: Readable,
|
|
signal?: AbortSignal,
|
|
alsoAlgorithm?: string,
|
|
): Promise<HashResult> => {
|
|
const hash = createHash("sha256");
|
|
const also =
|
|
alsoAlgorithm === undefined ? undefined : createHash(alsoAlgorithm);
|
|
const prefixes: Buffer[] = [];
|
|
let prefixBytes = 0;
|
|
let bytes = 0;
|
|
for await (const raw of stream) {
|
|
abortIfNeeded(signal);
|
|
const chunk = streamChunkToBuffer(raw);
|
|
bytes += chunk.length;
|
|
hash.update(chunk);
|
|
also?.update(chunk);
|
|
if (prefixBytes < ARTIFACT_CLASSIFICATION_PREFIX_BYTES) {
|
|
const selected = chunk.subarray(
|
|
0,
|
|
ARTIFACT_CLASSIFICATION_PREFIX_BYTES - prefixBytes,
|
|
);
|
|
prefixes.push(selected);
|
|
prefixBytes += selected.length;
|
|
}
|
|
}
|
|
return {
|
|
sha256: hash.digest("hex"),
|
|
bytes,
|
|
prefix: Buffer.concat(prefixes),
|
|
...(also === undefined ? {} : { also: also.digest("hex") }),
|
|
};
|
|
};
|