import * as plugins from './plugins.js'; import { controllerFilesystemIdentityMaximumComponents, type IControllerFilesystemIdentity, type IControllerFilesystemIdentityComponent, type IControllerProjectFilesystemIdentity, type TControllerFilesystemObjectType, } from './interfaces.projects.js'; export class CanonicalDirectoryError extends Error { constructor(messageArg: string, optionsArg?: ErrorOptions) { super(messageArg, optionsArg); this.name = 'CanonicalDirectoryError'; } } export interface ICanonicalDirectoryResolution extends IControllerProjectFilesystemIdentity { directory: string; } export interface ICanonicalFilesystemObjectResolution extends IControllerFilesystemIdentity { path: string; } export interface ICanonicalFilesystemObjectExpectation { path: string; targetType: TControllerFilesystemObjectType; expectedIdentity: IControllerFilesystemIdentity; } interface ICanonicalFilesystemObjectRequest { path: string; targetType: TControllerFilesystemObjectType; } const filesystemIdentityComponent = ( statArg: plugins.fs.BigIntStats, ): IControllerFilesystemIdentityComponent => ({ device: statArg.dev.toString(10), inode: statArg.ino.toString(10), type: statArg.isDirectory() ? 'directory' : 'file', }); const pathComponents = (pathArg: string): string[] => { if ( !plugins.path.isAbsolute(pathArg) || pathArg.includes('\0') || plugins.path.normalize(pathArg) !== pathArg ) { throw new CanonicalDirectoryError('The filesystem path is not canonical and absolute.'); } const root = plugins.path.parse(pathArg).root; const relative = pathArg.slice(root.length); const components = [ root, ...relative.split(plugins.path.sep).filter((component) => component.length > 0), ]; if (components.length > controllerFilesystemIdentityMaximumComponents) { throw new CanonicalDirectoryError('The filesystem path exceeds the identity component limit.'); } return components; }; const componentPaths = (pathArg: string): string[] => { const components = pathComponents(pathArg); const paths = [components[0]!]; for (const component of components.slice(1)) { paths.push(plugins.path.join(paths.at(-1)!, component)); } return paths; }; const statComponentPath = async ( pathArg: string, targetTypeArg: TControllerFilesystemObjectType, ): Promise => { const stat = await plugins.fs.promises.lstat(pathArg, { bigint: true }); if (stat.isSymbolicLink()) { throw new CanonicalDirectoryError('The filesystem path contains a symbolic link.'); } const type = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : undefined; if (!type || type !== targetTypeArg) { throw new CanonicalDirectoryError(`The filesystem path is not a canonical ${targetTypeArg}.`); } return filesystemIdentityComponent(stat); }; export const filesystemIdentitiesEqual = ( leftArg: IControllerFilesystemIdentity, rightArg: IControllerFilesystemIdentity, ): boolean => leftArg.ancestry.length === rightArg.ancestry.length && leftArg.ancestry.every((component, index) => { const other = rightArg.ancestry[index]; return component.device === other?.device && component.inode === other.inode && component.type === other.type; }); const resolveCanonicalFilesystemObjects = async ( requestsArg: readonly ICanonicalFilesystemObjectRequest[], ): Promise => { try { const requests = requestsArg.map((request) => ({ ...request, componentPaths: componentPaths(request.path), })); const componentTypes = new Map(); for (const request of requests) { for (let index = 0; index < request.componentPaths.length; index += 1) { const path = request.componentPaths[index]!; const type = index === request.componentPaths.length - 1 ? request.targetType : 'directory'; const existing = componentTypes.get(path); if (existing && existing !== type) { throw new CanonicalDirectoryError( 'Overlapping filesystem paths require conflicting object types.', ); } componentTypes.set(path, type); } } const collect = async (): Promise> => { const identities = new Map(); for (const [path, type] of componentTypes) { identities.set(path, await statComponentPath(path, type)); } return identities; }; const before = await collect(); const after = await collect(); for (const [path, beforeIdentity] of before) { const afterIdentity = after.get(path)!; if (!filesystemIdentitiesEqual( { ancestry: [beforeIdentity] }, { ancestry: [afterIdentity] }, )) { throw new CanonicalDirectoryError( 'The filesystem ancestry changed during identity validation.', ); } } return requests.map((request) => ({ path: request.path, ancestry: request.componentPaths.map((path) => after.get(path)!), })); } catch (errorArg) { if (errorArg instanceof CanonicalDirectoryError) throw errorArg; throw new CanonicalDirectoryError('The canonical filesystem path is unavailable.', { cause: errorArg, }); } }; export const resolveCanonicalFilesystemObject = async ( pathArg: string, targetTypeArg: TControllerFilesystemObjectType, ): Promise => { return (await resolveCanonicalFilesystemObjects([ { path: pathArg, targetType: targetTypeArg }, ]))[0]!; }; export const assertCanonicalFilesystemObject = async ( pathArg: string, targetTypeArg: TControllerFilesystemObjectType, expectedIdentityArg: IControllerFilesystemIdentity, ): Promise => { const resolved = await resolveCanonicalFilesystemObject(pathArg, targetTypeArg); if (!filesystemIdentitiesEqual(resolved, expectedIdentityArg)) { throw new CanonicalDirectoryError( 'The filesystem ancestry no longer matches its registered identity.', ); } return resolved.path; }; export const assertCanonicalFilesystemObjects = async ( expectationsArg: readonly ICanonicalFilesystemObjectExpectation[], ): Promise => { try { const resolved = await resolveCanonicalFilesystemObjects(expectationsArg); for (let index = 0; index < expectationsArg.length; index += 1) { if (!filesystemIdentitiesEqual(resolved[index]!, expectationsArg[index]!.expectedIdentity)) { throw new CanonicalDirectoryError( 'The filesystem ancestry no longer matches its registered identity.', ); } } } catch (errorArg) { if (errorArg instanceof CanonicalDirectoryError) throw errorArg; throw new CanonicalDirectoryError('The canonical filesystem path is unavailable.', { cause: errorArg, }); } }; export const resolveCanonicalDirectory = async ( directoryArg: string, ): Promise => { const resolved = await resolveCanonicalFilesystemObject(directoryArg, 'directory'); return { directory: resolved.path, ancestry: resolved.ancestry }; }; export const assertCanonicalDirectory = async ( directoryArg: string, expectedIdentityArg?: IControllerProjectFilesystemIdentity, ): Promise => { if (expectedIdentityArg) { return assertCanonicalFilesystemObject(directoryArg, 'directory', expectedIdentityArg); } return (await resolveCanonicalDirectory(directoryArg)).directory; };