VS Code extension that previews Archify diagrams from their JSON sources (live, as you type) and opens rendered Archify HTML in a viewer tab. Bundles the Archify 3.0.1 renderer and runs it on VS Code's Node runtime. Adds validation diagnostics, JSON schema help, source-link navigation, export saving, render-to-file and open-in-browser commands. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1018 lines
32 KiB
JavaScript
1018 lines
32 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const MAX_SYMLINK_DEPTH = 64;
|
|
const RECENT_PROBE_NAME_LIMIT = 128;
|
|
const recentProbeNames = new Set();
|
|
const WINDOWS_SMB_SHARE_FORBIDDEN = /[\u0000-\u001F"\/\\[\]:|<>+=;,?*]/u;
|
|
|
|
export function isWindowsIpcShare(share) {
|
|
return /^(?:IPC\$|pipe|mailslot)$/iu.test(share);
|
|
}
|
|
|
|
export function isValidWindowsSmbShareName(share) {
|
|
return typeof share === 'string'
|
|
&& share.length > 0
|
|
&& share.length <= 80
|
|
&& !WINDOWS_SMB_SHARE_FORBIDDEN.test(share)
|
|
&& !/[ .]$/u.test(share);
|
|
}
|
|
|
|
function result(status, code, details = {}) {
|
|
return {
|
|
status,
|
|
reason: {
|
|
code,
|
|
...details,
|
|
},
|
|
};
|
|
}
|
|
|
|
function unknown(code, details = {}) {
|
|
return result('unknown', code, details);
|
|
}
|
|
|
|
function systemFailure(code, error, details = {}) {
|
|
return unknown(code, {
|
|
...details,
|
|
...(typeof error?.code === 'string' ? { systemCode: error.code } : {}),
|
|
...(typeof error?.recoveryPath === 'string' ? { recoveryPath: error.recoveryPath } : {}),
|
|
...(typeof error?.restoredAtTarget === 'boolean'
|
|
? { restoredAtTarget: error.restoredAtTarget }
|
|
: {}),
|
|
});
|
|
}
|
|
|
|
function hostPath() {
|
|
return process.platform === 'win32' ? path.win32 : path.posix;
|
|
}
|
|
|
|
function absolutePath(input, side, basePath = process.cwd()) {
|
|
if (typeof input !== 'string' || input.length === 0) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('invalid-path', { side }),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const pathApi = hostPath();
|
|
if (pathApi.isAbsolute(input)) return { ok: true, path: input };
|
|
if (pathApi.parse(input).root) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('drive-relative-path', { side }),
|
|
};
|
|
}
|
|
const separator = basePath.endsWith(pathApi.sep) ? '' : pathApi.sep;
|
|
return { ok: true, path: `${basePath}${separator}${input}` };
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('invalid-path', error, { side }),
|
|
};
|
|
}
|
|
}
|
|
|
|
function rootFailure(code, side) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown(code, { side }),
|
|
};
|
|
}
|
|
|
|
function splitWindowsExtendedTail(tail, side, { internalRelativeLinkTarget = false } = {}) {
|
|
const authoredSegments = tail.split('\\');
|
|
// A single final separator is harmless, while an internal empty segment
|
|
// would require normalizing a raw extended path before it reaches Windows.
|
|
if (!internalRelativeLinkTarget
|
|
&& authoredSegments.slice(0, -1).some((component) => component.length === 0)) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
const segments = authoredSegments.filter(Boolean);
|
|
if (!internalRelativeLinkTarget
|
|
&& segments.some((component) => component === '.' || component === '..')) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
return { ok: true, segments };
|
|
}
|
|
|
|
function splitWindowsAbsolute(absolute, side, { internalRelativeLinkTarget = false } = {}) {
|
|
if (/^[\\/]{2}\.[\\/]/u.test(absolute)) {
|
|
return rootFailure('windows-namespace-unsupported', side);
|
|
}
|
|
if (/^[\\/]{2}\?[\\/]/u.test(absolute) && !absolute.startsWith('\\\\?\\')) {
|
|
return rootFailure('windows-namespace-unsupported', side);
|
|
}
|
|
|
|
if (absolute.startsWith('\\\\?\\')) {
|
|
if (absolute.includes('/')) return rootFailure('windows-root-invalid', side);
|
|
if (
|
|
/^\\\\\?\\(?:GLOBALROOT|Device)(?:\\|$)/iu.test(absolute)
|
|
|| /^\\\\\?\\Volume\{/iu.test(absolute)
|
|
) {
|
|
return rootFailure('windows-namespace-unsupported', side);
|
|
}
|
|
|
|
const extendedUnc = absolute.match(/^(\\\\\?\\UNC\\([^\\]+)\\([^\\]+))(?:\\(.*))?$/iu);
|
|
if (extendedUnc) {
|
|
if (isWindowsIpcShare(extendedUnc[3])) {
|
|
return rootFailure('windows-namespace-unsupported', side);
|
|
}
|
|
if (!isValidWindowsSmbShareName(extendedUnc[3])) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
if ([extendedUnc[2], extendedUnc[3]].some((component) => component === '.' || component === '..')) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
const tail = splitWindowsExtendedTail(
|
|
extendedUnc[4] || '',
|
|
side,
|
|
{ internalRelativeLinkTarget },
|
|
);
|
|
if (!tail.ok) return tail;
|
|
return {
|
|
ok: true,
|
|
root: `${extendedUnc[1]}\\`,
|
|
segments: tail.segments,
|
|
};
|
|
}
|
|
|
|
const extendedDrive = absolute.match(/^(\\\\\?\\[A-Za-z]:\\)(.*)$/u);
|
|
if (extendedDrive) {
|
|
const tail = splitWindowsExtendedTail(
|
|
extendedDrive[2],
|
|
side,
|
|
{ internalRelativeLinkTarget },
|
|
);
|
|
if (!tail.ok) return tail;
|
|
return {
|
|
ok: true,
|
|
root: extendedDrive[1],
|
|
segments: tail.segments,
|
|
};
|
|
}
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
|
|
const unc = absolute.match(/^[\\/]{2}([^\\/]+)[\\/]([^\\/]+)(?:[\\/](.*))?$/u);
|
|
if (unc) {
|
|
if (isWindowsIpcShare(unc[2])) {
|
|
return rootFailure('windows-namespace-unsupported', side);
|
|
}
|
|
if (!isValidWindowsSmbShareName(unc[2])) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
if ([unc[1], unc[2]].some((component) => component === '.' || component === '..')) {
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
return {
|
|
ok: true,
|
|
root: `\\\\${unc[1]}\\${unc[2]}\\`,
|
|
segments: (unc[3] || '').split(/[\\/]+/u).filter(Boolean),
|
|
};
|
|
}
|
|
|
|
const drive = absolute.match(/^([A-Za-z]:)[\\/](.*)$/u);
|
|
if (drive) {
|
|
return {
|
|
ok: true,
|
|
root: `${drive[1]}\\`,
|
|
segments: drive[2].split(/[\\/]+/u).filter(Boolean),
|
|
};
|
|
}
|
|
return rootFailure('windows-root-invalid', side);
|
|
}
|
|
|
|
function splitAbsolute(absolute, side, options) {
|
|
if (process.platform === 'win32') return splitWindowsAbsolute(absolute, side, options);
|
|
const root = path.posix.parse(absolute).root;
|
|
return {
|
|
ok: true,
|
|
root,
|
|
segments: absolute.slice(root.length).split(/\/+/u).filter(Boolean),
|
|
};
|
|
}
|
|
|
|
function statBigInt(targetPath) {
|
|
return fs.statSync(targetPath, { bigint: true });
|
|
}
|
|
|
|
function ordinaryWindowsRoot(extendedRoot) {
|
|
const drive = extendedRoot.match(/^\\\\\?\\([A-Za-z]:\\)$/u);
|
|
if (drive) return drive[1];
|
|
|
|
const unc = extendedRoot.match(/^\\\\\?\\UNC\\([^\\]+)\\([^\\]+)\\$/iu);
|
|
return unc ? `\\\\${unc[1]}\\${unc[2]}\\` : null;
|
|
}
|
|
|
|
function hasStableIdentity(stat) {
|
|
return typeof stat?.dev === 'bigint'
|
|
&& typeof stat?.ino === 'bigint'
|
|
&& stat.ino !== 0n;
|
|
}
|
|
|
|
function compareKnownEntries(left, right, operation) {
|
|
if (hasStableIdentity(left.stat) && hasStableIdentity(right.stat)) {
|
|
const matches = left.stat.dev === right.stat.dev && left.stat.ino === right.stat.ino;
|
|
return result(matches ? 'match' : 'different', matches ? 'file-identity-match' : 'file-identity-different', {
|
|
operation,
|
|
method: 'bigint-stat',
|
|
});
|
|
}
|
|
|
|
// path-contract-allow: lexical-capability -- Canonical paths are compared inside the identity primitive.
|
|
if (left.realPath === right.realPath) {
|
|
return result('match', 'native-realpath-match', {
|
|
operation,
|
|
method: 'native-realpath',
|
|
});
|
|
}
|
|
|
|
return unknown('file-identity-unavailable', {
|
|
operation,
|
|
method: 'bigint-stat-and-native-realpath',
|
|
});
|
|
}
|
|
|
|
function rememberProbeName(name) {
|
|
recentProbeNames.add(name);
|
|
if (recentProbeNames.size > RECENT_PROBE_NAME_LIMIT) {
|
|
recentProbeNames.delete(recentProbeNames.values().next().value);
|
|
}
|
|
}
|
|
|
|
function probeInterference(message, code = 'ARCHIFY_PROBE_IDENTITY_CHANGED', details = {}) {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
Object.assign(error, details);
|
|
return error;
|
|
}
|
|
|
|
function sameStableIdentity(left, right) {
|
|
return hasStableIdentity(left)
|
|
&& hasStableIdentity(right)
|
|
&& left.dev === right.dev
|
|
&& left.ino === right.ino;
|
|
}
|
|
|
|
function preserveQuarantinedReplacement(quarantinePath, targetPath) {
|
|
try {
|
|
fs.linkSync(quarantinePath, targetPath);
|
|
return { restoredAtTarget: true };
|
|
} catch {
|
|
// Preserve the quarantined entry when the original name was concurrently
|
|
// occupied or the host cannot create a hard link. Never overwrite either.
|
|
return { restoredAtTarget: false };
|
|
}
|
|
}
|
|
|
|
function removeOwnedProbe(targetPath, expectedStat) {
|
|
let currentStat;
|
|
try {
|
|
currentStat = fs.lstatSync(targetPath, { bigint: true });
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') {
|
|
return probeInterference('filesystem probe disappeared before cleanup', 'ARCHIFY_PROBE_MISSING');
|
|
}
|
|
return error;
|
|
}
|
|
if (!sameStableIdentity(expectedStat, currentStat)) {
|
|
return probeInterference('filesystem probe identity changed before cleanup');
|
|
}
|
|
|
|
const quarantineName = `.archify-path-semantics-cleanup-${process.pid}-${crypto.randomBytes(10).toString('hex')}`;
|
|
const quarantinePath = path.join(path.dirname(targetPath), quarantineName);
|
|
rememberProbeName(quarantineName);
|
|
try {
|
|
// Move the checked name into a fresh, unpublished quarantine namespace.
|
|
// A replacement introduced after the check is moved rather than deleted.
|
|
fs.renameSync(targetPath, quarantinePath);
|
|
} catch (error) {
|
|
return error;
|
|
}
|
|
|
|
let quarantineStat;
|
|
try {
|
|
quarantineStat = fs.lstatSync(quarantinePath, { bigint: true });
|
|
} catch (error) {
|
|
error.recoveryPath = quarantinePath;
|
|
error.restoredAtTarget = false;
|
|
return error;
|
|
}
|
|
if (!sameStableIdentity(expectedStat, quarantineStat)) {
|
|
const recovery = preserveQuarantinedReplacement(quarantinePath, targetPath);
|
|
return probeInterference(
|
|
'filesystem probe identity changed during cleanup',
|
|
'ARCHIFY_PROBE_IDENTITY_CHANGED',
|
|
{ recoveryPath: quarantinePath, ...recovery },
|
|
);
|
|
}
|
|
|
|
try {
|
|
// The quarantine spelling did not exist before the atomic rename and is
|
|
// never returned to callers. Legitimate target-path replacements therefore
|
|
// remain outside the only namespace we unlink.
|
|
fs.unlinkSync(quarantinePath);
|
|
return null;
|
|
} catch (error) {
|
|
error.recoveryPath = quarantinePath;
|
|
error.restoredAtTarget = false;
|
|
return error;
|
|
}
|
|
}
|
|
|
|
function resolvePlan({ root, segments }, side, depth) {
|
|
if (depth > MAX_SYMLINK_DEPTH) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('symlink-cycle', { side }),
|
|
};
|
|
}
|
|
|
|
let current;
|
|
let currentStat;
|
|
try {
|
|
current = fs.realpathSync.native(root);
|
|
} catch (error) {
|
|
// Node on Windows can report EISDIR while resolving a valid extended
|
|
// filesystem root (for example \\?\C:\). The namespace was already
|
|
// validated by splitWindowsAbsolute, so retain its exact spelling for
|
|
// traversal.
|
|
if (process.platform === 'win32'
|
|
&& error?.code === 'EISDIR'
|
|
&& ordinaryWindowsRoot(root)) {
|
|
current = root;
|
|
} else {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('root-resolution-failed', error, { side }),
|
|
};
|
|
}
|
|
}
|
|
|
|
try {
|
|
currentStat = statBigInt(current);
|
|
} catch (error) {
|
|
// The same Node/Windows edge can occur during stat after realpath succeeds.
|
|
// A drive or share root is short enough for its ordinary spelling, while
|
|
// `current` remains namespaced so descendants keep long-path semantics.
|
|
const ordinaryRoot = process.platform === 'win32'
|
|
&& error?.code === 'EISDIR'
|
|
? ordinaryWindowsRoot(root)
|
|
: null;
|
|
if (ordinaryRoot && ordinaryRoot !== current) {
|
|
try {
|
|
currentStat = statBigInt(ordinaryRoot);
|
|
} catch (statError) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('root-resolution-failed', statError, { side }),
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('root-resolution-failed', error, { side }),
|
|
};
|
|
}
|
|
}
|
|
|
|
for (let index = 0; index < segments.length; index += 1) {
|
|
const segment = segments[index];
|
|
if (segment === '.') continue;
|
|
if (segment === '..') {
|
|
if (!currentStat.isDirectory()) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
try {
|
|
current = fs.realpathSync.native(hostPath().dirname(current));
|
|
currentStat = statBigInt(current);
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const candidate = hostPath().join(current, segment);
|
|
let candidateStat;
|
|
try {
|
|
candidateStat = fs.lstatSync(candidate, { bigint: true });
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') {
|
|
if (!currentStat.isDirectory()) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
const unresolved = segments.slice(index).filter((item) => item !== '.');
|
|
if (unresolved.includes('..')) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('future-parent-traversal-indeterminate', {
|
|
side,
|
|
segmentIndex: index,
|
|
}),
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
existing: false,
|
|
ancestor: {
|
|
realPath: current,
|
|
stat: currentStat,
|
|
},
|
|
unresolved,
|
|
};
|
|
}
|
|
if (error?.code === 'ENOTDIR') {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('ancestor-not-directory', error, { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
|
|
if (candidateStat.isSymbolicLink()) {
|
|
let linkPlan;
|
|
try {
|
|
const authoredTarget = fs.readlinkSync(candidate);
|
|
const pathApi = hostPath();
|
|
const internalRelativeLinkTarget = !pathApi.isAbsolute(authoredTarget)
|
|
&& !pathApi.parse(authoredTarget).root;
|
|
const parsedTarget = absolutePath(authoredTarget, side, hostPath().dirname(candidate));
|
|
if (!parsedTarget.ok) return parsedTarget;
|
|
const target = splitAbsolute(parsedTarget.path, side, { internalRelativeLinkTarget });
|
|
if (!target.ok) return target;
|
|
linkPlan = {
|
|
root: target.root,
|
|
segments: [...target.segments, ...segments.slice(index + 1)],
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('symlink-resolution-failed', error, { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
return resolvePlan(linkPlan, side, depth + 1);
|
|
}
|
|
|
|
if (index < segments.length - 1 && !candidateStat.isDirectory()) {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
|
|
try {
|
|
current = fs.realpathSync.native(candidate);
|
|
currentStat = statBigInt(current);
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
existing: true,
|
|
entry: {
|
|
realPath: current,
|
|
stat: currentStat,
|
|
},
|
|
unresolved: [],
|
|
};
|
|
}
|
|
|
|
function resolveLocation(input, side) {
|
|
const parsed = absolutePath(input, side);
|
|
if (!parsed.ok) return parsed;
|
|
const plan = splitAbsolute(parsed.path, side);
|
|
if (!plan.ok) return plan;
|
|
return resolvePlan(plan, side, 0);
|
|
}
|
|
|
|
function semanticNameEnvelope(name) {
|
|
const names = new Set([name]);
|
|
const pending = [name];
|
|
while (pending.length > 0 && names.size <= 64) {
|
|
const current = pending.shift();
|
|
const candidates = [
|
|
current.normalize('NFC'),
|
|
current.normalize('NFD'),
|
|
current.toLocaleLowerCase('en-US'),
|
|
current.toLocaleUpperCase('en-US'),
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (names.has(candidate)) continue;
|
|
names.add(candidate);
|
|
pending.push(candidate);
|
|
}
|
|
}
|
|
return names.size <= 64 ? names : null;
|
|
}
|
|
|
|
function semanticEnvelopesOverlap(leftName, rightName) {
|
|
const left = semanticNameEnvelope(leftName);
|
|
const right = semanticNameEnvelope(rightName);
|
|
if (!left || !right) return true;
|
|
for (const spelling of left) {
|
|
if (right.has(spelling)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function windowsDeviceName(name) {
|
|
const stem = name.split('.')[0].replace(/[ .]+$/u, '').toUpperCase();
|
|
return /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])$/u.test(stem);
|
|
}
|
|
|
|
function futureWindowsNameRisk(leftName, rightName) {
|
|
if (process.platform !== 'win32') return null;
|
|
const windowsShortName = /~[1-9][0-9]*(?:\.|$)/iu;
|
|
if (windowsShortName.test(leftName) || windowsShortName.test(rightName)) {
|
|
return unknown('future-short-name-indeterminate', { method: 'windows-8dot3' });
|
|
}
|
|
if (windowsDeviceName(leftName) || windowsDeviceName(rightName)) {
|
|
return unknown('future-device-name-indeterminate', { method: 'windows-device-name' });
|
|
}
|
|
if (/[ .]$/.test(leftName) || /[ .]$/.test(rightName)) {
|
|
return unknown('future-trim-name-indeterminate', { method: 'windows-trim-alias' });
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolve one existing or future path through the host filesystem without
|
|
* exposing a string key for identity comparisons.
|
|
*/
|
|
export function resolvePhysicalLocation(targetPath) {
|
|
const resolution = resolveLocation(targetPath, 'target');
|
|
if (!resolution.ok) return resolution.failure;
|
|
if (resolution.existing) {
|
|
return {
|
|
status: 'resolved',
|
|
location: {
|
|
kind: 'existing',
|
|
path: resolution.entry.realPath,
|
|
},
|
|
reason: {
|
|
code: 'existing-native-realpath',
|
|
method: 'native-realpath',
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
status: 'resolved',
|
|
location: {
|
|
kind: 'future',
|
|
ancestorPath: resolution.ancestor.realPath,
|
|
unresolved: [...resolution.unresolved],
|
|
},
|
|
reason: {
|
|
code: 'future-physical-ancestor',
|
|
method: 'native-realpath',
|
|
},
|
|
};
|
|
}
|
|
|
|
function probeNameAlias(directoryPath, leftName, rightName) {
|
|
if (leftName === rightName) {
|
|
return result('match', 'future-name-exact', { method: 'lexical-exact' });
|
|
}
|
|
if (recentProbeNames.has(leftName) || recentProbeNames.has(rightName)) {
|
|
return result('different', 'internal-probe-name', { method: 'probe-suppression' });
|
|
}
|
|
const windowsRisk = futureWindowsNameRisk(leftName, rightName);
|
|
if (windowsRisk) return windowsRisk;
|
|
if (!semanticEnvelopesOverlap(leftName, rightName)) {
|
|
return result('different', 'future-name-distinct', { method: 'semantic-envelope' });
|
|
}
|
|
|
|
const prefix = `.archify-path-semantics-${process.pid}-${crypto.randomBytes(10).toString('hex')}-`;
|
|
const probeName = `${prefix}${leftName}`;
|
|
const lookupName = `${prefix}${rightName}`;
|
|
const componentLength = (name) => process.platform === 'win32'
|
|
? name.length
|
|
: Buffer.byteLength(name, 'utf8');
|
|
if (componentLength(probeName) > 255 || componentLength(lookupName) > 255) {
|
|
return unknown('future-name-probe-unrepresentable', {
|
|
method: 'direct-filesystem-probe',
|
|
systemCode: 'ENAMETOOLONG',
|
|
});
|
|
}
|
|
|
|
let probePath;
|
|
let probeStat;
|
|
let descriptor;
|
|
let comparison;
|
|
let cleanupError;
|
|
try {
|
|
// Probe entries must live directly in the directory being queried. On
|
|
// Windows case sensitivity is per-directory and a child may not inherit it.
|
|
rememberProbeName(probeName);
|
|
rememberProbeName(lookupName);
|
|
probePath = path.join(directoryPath, probeName);
|
|
const lookupPath = path.join(directoryPath, lookupName);
|
|
descriptor = fs.openSync(probePath, 'wx', 0o600);
|
|
probeStat = fs.fstatSync(descriptor, { bigint: true });
|
|
|
|
let authoredStat;
|
|
let lookupStat;
|
|
try {
|
|
authoredStat = statBigInt(probePath);
|
|
lookupStat = statBigInt(lookupPath);
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') {
|
|
comparison = result('different', 'future-name-different', { method: 'filesystem-probe' });
|
|
} else {
|
|
comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' });
|
|
}
|
|
}
|
|
|
|
if (!comparison) {
|
|
const identity = compareKnownEntries(
|
|
{ realPath: fs.realpathSync.native(probePath), stat: authoredStat },
|
|
{ realPath: fs.realpathSync.native(lookupPath), stat: lookupStat },
|
|
'future-name',
|
|
);
|
|
if (identity.status === 'match') {
|
|
comparison = result('match', 'future-name-alias', { method: 'filesystem-probe' });
|
|
} else if (identity.status === 'different') {
|
|
comparison = result('different', 'future-name-different', { method: 'filesystem-probe' });
|
|
} else {
|
|
comparison = identity;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' });
|
|
} finally {
|
|
// Pin the inode until cleanup finishes. Once the final handle is closed,
|
|
// an unlinked probe's inode can be reused by a replacement at the same name.
|
|
if (probeStat) cleanupError = removeOwnedProbe(probePath, probeStat);
|
|
if (descriptor !== undefined) {
|
|
try {
|
|
fs.closeSync(descriptor);
|
|
} catch (error) {
|
|
cleanupError = cleanupError ?? error;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cleanupError) {
|
|
return systemFailure('future-name-probe-cleanup-failed', cleanupError, {
|
|
method: 'filesystem-probe',
|
|
...(comparison ? { comparison: comparison.reason } : {}),
|
|
});
|
|
}
|
|
return comparison;
|
|
}
|
|
|
|
function sidecarCaseKey(component) {
|
|
// The candidate is accepted only after the containing filesystem confirms
|
|
// that the original grapheme and this spelling name the same entry.
|
|
return component.toLocaleUpperCase('en-US').toLocaleLowerCase('en-US');
|
|
}
|
|
|
|
function transformSidecarComponent(directoryPath, component, transform, phase) {
|
|
if (typeof Intl.Segmenter !== 'function') {
|
|
return {
|
|
ok: false,
|
|
failure: unknown('sidecar-unicode-segmentation-unavailable', {
|
|
operation: 'sidecar-namespace-key',
|
|
}),
|
|
};
|
|
}
|
|
const segmenter = new Intl.Segmenter('en-US', { granularity: 'grapheme' });
|
|
const comparisons = new Map();
|
|
const output = [];
|
|
for (const { segment } of segmenter.segment(component)) {
|
|
const candidate = transform(segment);
|
|
if (candidate === segment) {
|
|
output.push(segment);
|
|
continue;
|
|
}
|
|
const cacheKey = `${segment}\0${candidate}`;
|
|
let comparison = comparisons.get(cacheKey);
|
|
if (!comparison) {
|
|
comparison = probeNameAlias(directoryPath, segment, candidate);
|
|
comparisons.set(cacheKey, comparison);
|
|
}
|
|
if (comparison.status === 'unknown') {
|
|
return {
|
|
ok: false,
|
|
failure: unknown(`sidecar-${phase}-semantics-indeterminate`, {
|
|
operation: 'sidecar-namespace-key',
|
|
cause: comparison.reason,
|
|
}),
|
|
};
|
|
}
|
|
output.push(comparison.status === 'match' ? candidate : segment);
|
|
}
|
|
return {
|
|
ok: true,
|
|
component: output.join(''),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Produce a stable component for a sidecar namespace in one existing
|
|
* directory. The returned string is a collision-domain key, not a canonical
|
|
* path and must never be used to decide whether two filesystem entries match.
|
|
*/
|
|
export function sidecarNamespaceComponentKey(directoryPath, component) {
|
|
if (typeof component !== 'string' || component.length === 0
|
|
|| component === '.' || component === '..'
|
|
|| hostPath().basename(component) !== component) {
|
|
return unknown('invalid-sidecar-component', {
|
|
operation: 'sidecar-namespace-key',
|
|
});
|
|
}
|
|
|
|
const directory = resolveLocation(directoryPath, 'sidecar-directory');
|
|
if (!directory.ok) return directory.failure;
|
|
if (!directory.existing) {
|
|
return unknown('sidecar-directory-missing', {
|
|
operation: 'sidecar-namespace-key',
|
|
});
|
|
}
|
|
if (!directory.entry.stat.isDirectory()) {
|
|
return unknown('sidecar-parent-not-directory', {
|
|
operation: 'sidecar-namespace-key',
|
|
});
|
|
}
|
|
|
|
const normalized = transformSidecarComponent(
|
|
directory.entry.realPath,
|
|
component,
|
|
(segment) => segment.normalize('NFC'),
|
|
'normalization',
|
|
);
|
|
if (!normalized.ok) return normalized.failure;
|
|
const cased = transformSidecarComponent(
|
|
directory.entry.realPath,
|
|
normalized.component,
|
|
sidecarCaseKey,
|
|
'case',
|
|
);
|
|
if (!cased.ok) return cased.failure;
|
|
const renormalized = transformSidecarComponent(
|
|
directory.entry.realPath,
|
|
cased.component,
|
|
(segment) => segment.normalize('NFC'),
|
|
'normalization',
|
|
);
|
|
if (!renormalized.ok) return renormalized.failure;
|
|
|
|
return {
|
|
status: 'resolved',
|
|
directoryPath: directory.entry.realPath,
|
|
componentKey: renormalized.component,
|
|
reason: {
|
|
code: 'sidecar-namespace-key',
|
|
method: 'directory-filesystem-probe',
|
|
},
|
|
};
|
|
}
|
|
|
|
function compareUnmaterializedNames(leftName, rightName) {
|
|
if (leftName === rightName) {
|
|
return result('match', 'future-name-exact', { method: 'lexical-exact' });
|
|
}
|
|
const windowsRisk = futureWindowsNameRisk(leftName, rightName);
|
|
if (windowsRisk) return windowsRisk;
|
|
if (!semanticEnvelopesOverlap(leftName, rightName)) {
|
|
return result('different', 'future-name-distinct', { method: 'semantic-envelope' });
|
|
}
|
|
return unknown('future-descendant-semantics-indeterminate', {
|
|
method: 'unmaterialized-directory',
|
|
});
|
|
}
|
|
|
|
function compareFutureLocations(left, right) {
|
|
const ancestor = compareKnownEntries(left.ancestor, right.ancestor, 'future-ancestor');
|
|
if (ancestor.status !== 'match') return ancestor;
|
|
|
|
if (left.unresolved.length !== right.unresolved.length) {
|
|
return result('different', 'future-depth-different', {
|
|
operation: 'same-location',
|
|
leftDepth: left.unresolved.length,
|
|
rightDepth: right.unresolved.length,
|
|
});
|
|
}
|
|
|
|
let probedSegments = 0;
|
|
for (let index = 0; index < left.unresolved.length; index += 1) {
|
|
const names = index === 0
|
|
? probeNameAlias(
|
|
left.ancestor.realPath,
|
|
left.unresolved[index],
|
|
right.unresolved[index],
|
|
)
|
|
: compareUnmaterializedNames(left.unresolved[index], right.unresolved[index]);
|
|
if (names.reason.method === 'filesystem-probe') probedSegments += 1;
|
|
if (names.status !== 'match') {
|
|
const { code, ...reasonDetails } = names.reason;
|
|
return result(names.status, code, {
|
|
...reasonDetails,
|
|
operation: 'same-location',
|
|
segmentIndex: index,
|
|
});
|
|
}
|
|
}
|
|
|
|
return result('match', probedSegments > 0 ? 'future-location-alias' : 'future-location-exact', {
|
|
operation: 'same-location',
|
|
method: probedSegments > 0 ? 'filesystem-probe' : 'lexical-exact',
|
|
probedSegments,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Compare two existing filesystem entries by physical identity.
|
|
* Missing or unidentifiable entries return `unknown`, never `different`.
|
|
*/
|
|
export function sameEntry(leftPath, rightPath) {
|
|
const left = resolveLocation(leftPath, 'left');
|
|
if (!left.ok) return left.failure;
|
|
const right = resolveLocation(rightPath, 'right');
|
|
if (!right.ok) return right.failure;
|
|
|
|
if (!left.existing || !right.existing) {
|
|
return unknown('entry-missing', {
|
|
operation: 'same-entry',
|
|
missing: [
|
|
...(!left.existing ? ['left'] : []),
|
|
...(!right.existing ? ['right'] : []),
|
|
],
|
|
});
|
|
}
|
|
|
|
return compareKnownEntries(left.entry, right.entry, 'same-entry');
|
|
}
|
|
|
|
/**
|
|
* Compare two existing or future write targets using the containing filesystem's
|
|
* actual name semantics. Existing hard links and resolved symbolic links match.
|
|
*/
|
|
export function sameLocation(leftPath, rightPath) {
|
|
const left = resolveLocation(leftPath, 'left');
|
|
if (!left.ok) return left.failure;
|
|
const right = resolveLocation(rightPath, 'right');
|
|
if (!right.ok) return right.failure;
|
|
|
|
if (left.existing && right.existing) {
|
|
return compareKnownEntries(left.entry, right.entry, 'same-location');
|
|
}
|
|
if (left.existing !== right.existing) {
|
|
return result('different', 'location-existence-different', {
|
|
operation: 'same-location',
|
|
existing: left.existing ? 'left' : 'right',
|
|
});
|
|
}
|
|
return compareFutureLocations(left, right);
|
|
}
|
|
|
|
/** Compare the physical locations of the two paths' parent directories. */
|
|
export function sameParent(leftPath, rightPath) {
|
|
const left = absolutePath(leftPath, 'left');
|
|
if (!left.ok) return left.failure;
|
|
const right = absolutePath(rightPath, 'right');
|
|
if (!right.ok) return right.failure;
|
|
|
|
return sameLocation(hostPath().dirname(left.path), hostPath().dirname(right.path));
|
|
}
|
|
|
|
function parentEntry(entry) {
|
|
const parentPath = hostPath().dirname(entry.realPath);
|
|
// path-contract-allow: lexical-capability -- Both values are canonical paths captured by this primitive.
|
|
if (parentPath === entry.realPath) return null;
|
|
try {
|
|
const realPath = fs.realpathSync.native(parentPath);
|
|
return {
|
|
ok: true,
|
|
entry: {
|
|
realPath,
|
|
stat: statBigInt(realPath),
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
failure: systemFailure('ancestor-resolution-failed', error, {
|
|
operation: 'contained-by',
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine physical containment in an existing directory. The comparison is
|
|
* inclusive and follows links; the target may exist or name a future location.
|
|
*/
|
|
export function containedBy(directoryPath, targetPath) {
|
|
const directory = resolveLocation(directoryPath, 'directory');
|
|
if (!directory.ok) return directory.failure;
|
|
if (directory.existing && !directory.entry.stat.isDirectory()) {
|
|
return unknown('container-not-directory', { operation: 'contained-by' });
|
|
}
|
|
|
|
const target = resolveLocation(targetPath, 'target');
|
|
if (!target.ok) return target.failure;
|
|
|
|
if (!directory.existing) {
|
|
if (target.existing) {
|
|
return unknown('container-missing', { operation: 'contained-by' });
|
|
}
|
|
const ancestor = compareKnownEntries(directory.ancestor, target.ancestor, 'contained-by');
|
|
if (ancestor.status !== 'match') return ancestor;
|
|
if (target.unresolved.length < directory.unresolved.length) {
|
|
return result('different', 'future-container-prefix-too-deep', {
|
|
operation: 'contained-by',
|
|
});
|
|
}
|
|
for (let index = 0; index < directory.unresolved.length; index += 1) {
|
|
const segment = index === 0
|
|
? probeNameAlias(
|
|
directory.ancestor.realPath,
|
|
directory.unresolved[index],
|
|
target.unresolved[index],
|
|
)
|
|
: compareUnmaterializedNames(
|
|
directory.unresolved[index],
|
|
target.unresolved[index],
|
|
);
|
|
if (segment.status === 'match') continue;
|
|
const { code, ...reasonDetails } = segment.reason;
|
|
return result(segment.status, code, {
|
|
...reasonDetails,
|
|
operation: 'contained-by',
|
|
segmentIndex: index,
|
|
});
|
|
}
|
|
return result('match', 'future-container-prefix-match', {
|
|
operation: 'contained-by',
|
|
target: 'future',
|
|
depth: target.unresolved.length - directory.unresolved.length,
|
|
});
|
|
}
|
|
|
|
let current;
|
|
if (!target.existing) {
|
|
current = target.ancestor;
|
|
} else if (target.entry.stat.isDirectory()) {
|
|
current = target.entry;
|
|
} else {
|
|
const parent = parentEntry(target.entry);
|
|
if (!parent?.ok) return parent?.failure ?? unknown('ancestor-resolution-failed', {
|
|
operation: 'contained-by',
|
|
});
|
|
current = parent.entry;
|
|
}
|
|
|
|
let depth = 0;
|
|
let identityWasUnknown = false;
|
|
while (current) {
|
|
const comparison = compareKnownEntries(directory.entry, current, 'contained-by');
|
|
if (comparison.status === 'match') {
|
|
return result('match', 'physical-ancestor-match', {
|
|
operation: 'contained-by',
|
|
depth,
|
|
target: target.existing ? 'existing' : 'future',
|
|
});
|
|
}
|
|
if (comparison.status === 'unknown') identityWasUnknown = true;
|
|
|
|
const parent = parentEntry(current);
|
|
if (parent === null) break;
|
|
if (!parent.ok) return parent.failure;
|
|
current = parent.entry;
|
|
depth += 1;
|
|
}
|
|
|
|
if (identityWasUnknown) {
|
|
return unknown('containment-identity-unavailable', {
|
|
operation: 'contained-by',
|
|
target: target.existing ? 'existing' : 'future',
|
|
});
|
|
}
|
|
return result('different', 'physical-ancestor-not-found', {
|
|
operation: 'contained-by',
|
|
target: target.existing ? 'existing' : 'future',
|
|
});
|
|
}
|