Files

60 lines
2.9 KiB
JavaScript

import { createHash } from "node:crypto";
import { extname } from "node:path";
import { gitBytes } from "../../project/src/git.mjs";
const MAX_BYTES = 1024 * 1024;
const MAX_PATH_BYTES = 4096;
export class ArtifactError extends Error {
constructor(code, message) {
super(message);
this.code = code;
}
}
const fail = (code, message) => { throw new ArtifactError(code, message); };
export async function readReviewedArtifact({ repo, commit, path, maxBytes = MAX_BYTES }) {
if (typeof path !== "string" || !path || path.startsWith("/") || /[\\\0]/.test(path)
|| path.split("/").some(segment => !segment || segment === "." || segment === "..")
|| Buffer.byteLength(path) > MAX_PATH_BYTES) {
fail("artifact-path-invalid", "expected a bounded relative Git path");
}
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || maxBytes > MAX_BYTES) {
fail("artifact-limit-invalid", "invalid artifact size limit");
}
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) {
fail("artifact-commit-invalid", "expected a full commit object ID");
}
const read = (args, limit) => gitBytes(repo, ["--no-replace-objects", ...args], limit);
let type;
try { type = (await read(["cat-file", "-t", commit], 128)).toString("utf8").trim(); }
catch { fail("artifact-commit-invalid", "commit object is not readable"); }
if (type !== "commit") fail("artifact-commit-invalid", "object is not a commit");
let listing;
try { listing = await read(["ls-tree", "-z", "--full-tree", commit, "--", `:(literal)${path}`], MAX_PATH_BYTES + 128); }
catch { fail("artifact-git", "could not read committed tree"); }
if (listing.length === 0) fail("artifact-missing", "path is absent at commit");
const match = /^(\d{6}) (blob|tree|commit) ([0-9a-f]{40})\t([^\0]+)\0$/.exec(listing.toString("utf8"));
if (!match || match[4] !== path) fail("artifact-git", "unexpected committed tree entry");
if (!["100644", "100755"].includes(match[1]) || match[2] !== "blob") {
fail("artifact-not-regular", "expected a regular committed blob");
}
const blob = match[3];
let sizeText;
try { sizeText = (await read(["cat-file", "-s", blob], 128)).toString("utf8").trim(); }
catch { fail("artifact-git", "could not read blob size"); }
if (!/^\d+$/.test(sizeText) || !Number.isSafeInteger(Number(sizeText))) fail("artifact-git", "invalid blob size");
const size = Number(sizeText);
if (size > maxBytes) fail("artifact-too-large", "artifact exceeds size limit");
let bytes;
try { bytes = await read(["cat-file", "blob", blob], maxBytes); }
catch { fail("artifact-git", "could not read bounded blob"); }
if (bytes.length !== size) fail("artifact-git", "blob size changed during read");
const mime = [".html", ".htm"].includes(extname(path).toLowerCase()) ? "text/html" : "application/octet-stream";
return { bytes, blob, sha256: createHash("sha256").update(bytes).digest("hex"), mime };
}