73 lines
2.6 KiB
JavaScript
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 };
|