Files
rea/src/artifacts/ArtifactHash.ts
T
Kaoru0822andClaude Opus 5.5 7857d2d329 fix(artifacts): apply the integrity policy to xar checksums and bound symlinks
- 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>
2026-10-07 12:50:52 +00:00

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") }),
};
};