Files
playbook/antigravity-awesome-skills/tools/lib/aas-v1/transaction/digest.js
T
2026-07-18 00:02:59 +00:00

73 lines
2.6 KiB
JavaScript

"use strict";
const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
const { canonicalJson, sha256 } = require("../canonical-json");
const { transactionError } = require("./errors");
function compareStrings(left, right) {
return left < right ? -1 : (left > right ? 1 : 0);
}
function fileDigest(filePath) {
const hash = crypto.createHash("sha256");
const descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
try {
const stat = fs.fstatSync(descriptor);
if (!stat.isFile()) throw transactionError("AAS_TRANSACTION_FILE_UNSAFE", "filesystem", {});
const buffer = Buffer.allocUnsafe(64 * 1024);
for (;;) {
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, null);
if (bytes === 0) break;
hash.update(buffer.subarray(0, bytes));
}
} finally {
fs.closeSync(descriptor);
}
return `sha256-${hash.digest("hex")}`;
}
function treeManifest(root) {
const rootStat = fs.lstatSync(root);
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
throw transactionError("AAS_TRANSACTION_TREE_UNSAFE", "filesystem", { reason: "rootNotDirectory" });
}
const entries = [];
const collisionKeys = new Set();
function visit(directory, relativeDirectory) {
const names = fs.readdirSync(directory).sort(compareStrings);
for (const name of names) {
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
const normalized = relative.normalize("NFC").toLowerCase();
if (collisionKeys.has(normalized)) {
throw transactionError("AAS_TRANSACTION_TREE_COLLISION", "filesystem", { logicalPath: relative });
}
collisionKeys.add(normalized);
const absolute = path.join(directory, name);
const stat = fs.lstatSync(absolute);
if (stat.isSymbolicLink()) {
throw transactionError("AAS_TRANSACTION_TREE_SYMLINK", "filesystem", { logicalPath: relative });
}
if (stat.isDirectory()) {
entries.push({ path: relative, type: "directory" });
visit(absolute, relative);
} else if (stat.isFile() && stat.nlink === 1) {
entries.push({ path: relative, type: "file", size: stat.size, digest: fileDigest(absolute) });
} else {
throw transactionError("AAS_TRANSACTION_TREE_UNSAFE", "filesystem", { logicalPath: relative });
}
}
}
visit(root, "");
return { schemaVersion: 1, entries };
}
function treeDigest(root) {
return sha256(canonicalJson(treeManifest(root)));
}
module.exports = { fileDigest, treeDigest, treeManifest };