mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-06-04 06:28:45 -04:00
* test: backend integration * docs: mounted shares acls * feat: smb expose real ACLs when available * fix: re-init repo on setup * chore: add missing @hono/standard-validator package * chore: add happy-dom dev dep
151 lines
4.2 KiB
TypeScript
151 lines
4.2 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type { Stats } from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import type { ExpectedEntry } from "./config";
|
|
|
|
export type SnapshotNode = {
|
|
name: string;
|
|
type: string;
|
|
path: string;
|
|
uid?: number;
|
|
gid?: number;
|
|
mode?: number;
|
|
size?: number;
|
|
};
|
|
|
|
function normalizeRelativePath(input: string): string {
|
|
const normalized = path.posix.normalize(input);
|
|
if (normalized === "") {
|
|
return ".";
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeEntryType(type: string): string {
|
|
if (type === "dir" || type === "directory") {
|
|
return "directory";
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
async function readSha256(filePath: string): Promise<string> {
|
|
const digest = crypto.createHash("sha256");
|
|
digest.update(await fs.readFile(filePath));
|
|
return digest.digest("hex");
|
|
}
|
|
|
|
function assertMetadata(
|
|
entryLabel: string,
|
|
expected: ExpectedEntry,
|
|
actual: { uid?: number; gid?: number; mode?: number },
|
|
) {
|
|
if (expected.uid !== undefined && actual.uid !== expected.uid) {
|
|
throw new Error(`${entryLabel} uid mismatch: expected ${expected.uid}, got ${String(actual.uid)}`);
|
|
}
|
|
|
|
if (expected.gid !== undefined && actual.gid !== expected.gid) {
|
|
throw new Error(`${entryLabel} gid mismatch: expected ${expected.gid}, got ${String(actual.gid)}`);
|
|
}
|
|
|
|
if (expected.mode === undefined) {
|
|
return;
|
|
}
|
|
|
|
if (actual.mode === undefined) {
|
|
throw new Error(`${entryLabel} mode is missing`);
|
|
}
|
|
|
|
const permissionBits = actual.mode & 0o7777;
|
|
if (permissionBits !== expected.mode) {
|
|
throw new Error(
|
|
`${entryLabel} mode mismatch: expected ${expected.mode.toString(8)}, got ${permissionBits.toString(8)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getFilesystemEntryType(stats: Stats): ExpectedEntry["type"] {
|
|
if (stats.isDirectory()) {
|
|
return "directory";
|
|
}
|
|
|
|
if (stats.isSymbolicLink()) {
|
|
return "symlink";
|
|
}
|
|
|
|
return "file";
|
|
}
|
|
|
|
export async function verifyFilesystemEntries(basePath: string, expectedEntries: ExpectedEntry[], label: string) {
|
|
for (const expected of expectedEntries) {
|
|
const targetPath = path.resolve(basePath, expected.path);
|
|
const stats = await fs.lstat(targetPath);
|
|
|
|
const actualType = getFilesystemEntryType(stats);
|
|
if (actualType !== expected.type) {
|
|
throw new Error(`${label}: ${expected.path} type mismatch: expected ${expected.type}, got ${actualType}`);
|
|
}
|
|
|
|
assertMetadata(`${label}: ${expected.path}`, expected, stats);
|
|
|
|
if (expected.type === "file" && expected.text !== undefined) {
|
|
const actualText = await fs.readFile(targetPath, "utf8");
|
|
if (actualText !== expected.text) {
|
|
throw new Error(`${label}: ${expected.path} text content mismatch`);
|
|
}
|
|
}
|
|
|
|
if (expected.type === "file" && expected.sha256 !== undefined) {
|
|
const actualSha = await readSha256(targetPath);
|
|
if (actualSha !== expected.sha256.toLowerCase()) {
|
|
throw new Error(`${label}: ${expected.path} sha256 mismatch`);
|
|
}
|
|
}
|
|
|
|
if (expected.type !== "symlink" || expected.linkTarget === undefined) {
|
|
continue;
|
|
}
|
|
|
|
const actualTarget = await fs.readlink(targetPath);
|
|
if (actualTarget !== expected.linkTarget) {
|
|
throw new Error(
|
|
`${label}: ${expected.path} symlink target mismatch: expected ${expected.linkTarget}, got ${actualTarget}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function verifySnapshotEntries(
|
|
snapshotRootPath: string,
|
|
nodes: SnapshotNode[],
|
|
expectedEntries: ExpectedEntry[],
|
|
) {
|
|
const nodesByRelativePath = new Map<string, SnapshotNode>();
|
|
|
|
for (const node of nodes) {
|
|
const relativePath = path.posix.relative(snapshotRootPath, node.path);
|
|
if (!relativePath || relativePath === "." || relativePath === ".." || relativePath.startsWith("../")) {
|
|
continue;
|
|
}
|
|
|
|
nodesByRelativePath.set(normalizeRelativePath(relativePath), node);
|
|
}
|
|
|
|
for (const expected of expectedEntries) {
|
|
const normalizedExpectedPath = normalizeRelativePath(expected.path);
|
|
const node = nodesByRelativePath.get(normalizedExpectedPath);
|
|
if (!node) {
|
|
throw new Error(`snapshot: missing ${expected.path}`);
|
|
}
|
|
|
|
const actualType = normalizeEntryType(node.type);
|
|
if (actualType !== expected.type) {
|
|
throw new Error(`snapshot: ${expected.path} type mismatch: expected ${expected.type}, got ${actualType}`);
|
|
}
|
|
|
|
assertMetadata(`snapshot: ${expected.path}`, expected, node);
|
|
}
|
|
}
|