Files
2026-07-18 00:02:59 +00:00

140 lines
5.5 KiB
JavaScript

"use strict";
const crypto = require("node:crypto");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { fsyncDirectoryAsync } = require("../durability");
const { canonicalJson } = require("../canonical-json");
const {
CATALOG_IDENTITY_FILE,
DIGEST_VERSION,
cacheError,
catalogCachePath,
validateCacheRoot,
validateCatalogIdentity,
} = require("./identity");
const { scanDataDirectory } = require("./scan");
const { catalogStatus } = require("./status");
async function fsyncDirectory(directoryPath) {
await fsyncDirectoryAsync(directoryPath);
}
async function writeFileCrashSafe(filePath, bytes) {
const handle = await fsp.open(filePath, "wx", 0o600);
try {
await handle.writeFile(bytes);
await handle.sync();
} finally {
await handle.close();
}
}
async function ensureRealDirectory(directoryPath, created) {
try {
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw cacheError("AAS_CACHE_DIRECTORY_UNSAFE", `cache path is not a real directory: ${directoryPath}`);
} catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = path.dirname(directoryPath);
if (parent !== directoryPath) await ensureRealDirectory(parent, created);
try {
await fsp.mkdir(directoryPath, { mode: 0o700 });
created.push(directoryPath);
} catch (mkdirError) {
if (mkdirError.code !== "EEXIST") throw mkdirError;
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw cacheError("AAS_CACHE_DIRECTORY_UNSAFE", `cache path is not a real directory: ${directoryPath}`);
}
}
}
async function removeCreatedEmptyDirectories(created) {
for (const directoryPath of [...created].reverse()) {
try {
await fsp.rmdir(directoryPath);
} catch (error) {
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY") throw error;
}
}
}
async function promoteCatalogDirectory({ cacheRoot, sourceDir, allowlist, controlPaths = [], identity, limits }) {
// Validate every untrusted input byte before the cache filesystem is mutated.
const scan = await scanDataDirectory({ sourceDir, allowlist, ignoredPaths: controlPaths, limits });
const normalizedIdentity = validateCatalogIdentity(identity);
if (normalizedIdentity.catalogDigest !== scan.catalogDigest) {
throw cacheError("AAS_CACHE_CATALOG_DIGEST_MISMATCH", "catalog content does not match the approved catalog digest");
}
const existing = await catalogStatus({
cacheRoot,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
integrity: normalizedIdentity.integrity,
});
if (existing.status === "verified") return { status: "alreadyPresent", identity: existing.identity, targetPath: existing.targetPath };
if (existing.status === "invalid") throw cacheError("AAS_CACHE_EXISTING_INVALID", "an invalid object already occupies the immutable catalog cache key");
const root = validateCacheRoot(cacheRoot);
const targetPath = catalogCachePath({
cacheRoot: root,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
});
const versionDirectory = path.dirname(targetPath);
const created = [];
let stagePath;
let promoted = false;
try {
await ensureRealDirectory(versionDirectory, created);
stagePath = path.join(versionDirectory, `.stage-${process.pid}-${crypto.randomBytes(12).toString("hex")}`);
await fsp.mkdir(stagePath, { mode: 0o700 });
const madeDirectories = new Set([stagePath]);
for (const record of [...scan.records, ...scan.ignoredRecords]) {
const segments = record.path.split("/");
const outputPath = path.join(stagePath, ...segments);
let cursor = stagePath;
for (const segment of segments.slice(0, -1)) {
cursor = path.join(cursor, segment);
if (!madeDirectories.has(cursor)) {
await fsp.mkdir(cursor, { mode: 0o700 });
madeDirectories.add(cursor);
}
}
await writeFileCrashSafe(outputPath, record.bytes);
}
const storedIdentity = {
...normalizedIdentity,
digestVersion: DIGEST_VERSION,
assets: scan.publicRecords,
controls: scan.publicIgnoredRecords,
};
await writeFileCrashSafe(path.join(stagePath, CATALOG_IDENTITY_FILE), Buffer.from(`${canonicalJson(storedIdentity)}\n`));
for (const directoryPath of [...madeDirectories].sort((left, right) => right.length - left.length)) await fsyncDirectory(directoryPath);
try {
await fsp.rename(stagePath, targetPath);
promoted = true;
} catch (error) {
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY") throw error;
const raced = await catalogStatus({
cacheRoot: root,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
integrity: normalizedIdentity.integrity,
});
if (raced.status !== "verified") throw cacheError("AAS_CACHE_PROMOTION_CONFLICT", "catalog cache key was occupied during promotion");
return { status: "alreadyPresent", identity: raced.identity, targetPath: raced.targetPath };
}
await fsyncDirectory(versionDirectory);
return { status: "promoted", identity: storedIdentity, targetPath };
} finally {
if (stagePath && !promoted) await fsp.rm(stagePath, { recursive: true, force: true });
if (!promoted) await removeCreatedEmptyDirectories(created);
}
}
module.exports = { promoteCatalogDirectory };