path-semantics.mjs
1017 lines · archify/renderers/shared/path-semantics.mjs at a406594
1import crypto from 'node:crypto';
2import fs from 'node:fs';
3import path from 'node:path';
5const MAX_SYMLINK_DEPTH = 64;
6const RECENT_PROBE_NAME_LIMIT = 128;
7const recentProbeNames = new Set();
8const WINDOWS_SMB_SHARE_FORBIDDEN = /[\u0000-\u001F"\/\\[\]:|<>+=;,?*]/u;
10export function isWindowsIpcShare(share) {
11 return /^(?:IPC\$|pipe|mailslot)$/iu.test(share);
12}
14export function isValidWindowsSmbShareName(share) {
15 return typeof share === 'string'
16 && share.length > 0
17 && share.length <= 80
18 && !WINDOWS_SMB_SHARE_FORBIDDEN.test(share)
19 && !/[ .]$/u.test(share);
20}
22function result(status, code, details = {}) {
23 return {
24 status,
25 reason: {
26 code,
27 ...details,
28 },
29 };
30}
32function unknown(code, details = {}) {
33 return result('unknown', code, details);
34}
36function systemFailure(code, error, details = {}) {
37 return unknown(code, {
38 ...details,
39 ...(typeof error?.code === 'string' ? { systemCode: error.code } : {}),
40 ...(typeof error?.recoveryPath === 'string' ? { recoveryPath: error.recoveryPath } : {}),
41 ...(typeof error?.restoredAtTarget === 'boolean'
42 ? { restoredAtTarget: error.restoredAtTarget }
43 : {}),
44 });
45}
47function hostPath() {
48 return process.platform === 'win32' ? path.win32 : path.posix;
49}
51function absolutePath(input, side, basePath = process.cwd()) {
52 if (typeof input !== 'string' || input.length === 0) {
53 return {
54 ok: false,
55 failure: unknown('invalid-path', { side }),
56 };
57 }
59 try {
60 const pathApi = hostPath();
61 if (pathApi.isAbsolute(input)) return { ok: true, path: input };
62 if (pathApi.parse(input).root) {
63 return {
64 ok: false,
65 failure: unknown('drive-relative-path', { side }),
66 };
67 }
68 const separator = basePath.endsWith(pathApi.sep) ? '' : pathApi.sep;
69 return { ok: true, path: `${basePath}${separator}${input}` };
70 } catch (error) {
71 return {
72 ok: false,
73 failure: systemFailure('invalid-path', error, { side }),
74 };
75 }
76}
78function rootFailure(code, side) {
79 return {
80 ok: false,
81 failure: unknown(code, { side }),
82 };
83}
85function splitWindowsExtendedTail(tail, side, { internalRelativeLinkTarget = false } = {}) {
86 const authoredSegments = tail.split('\\');
87 // A single final separator is harmless, while an internal empty segment
88 // would require normalizing a raw extended path before it reaches Windows.
89 if (!internalRelativeLinkTarget
90 && authoredSegments.slice(0, -1).some((component) => component.length === 0)) {
91 return rootFailure('windows-root-invalid', side);
92 }
93 const segments = authoredSegments.filter(Boolean);
94 if (!internalRelativeLinkTarget
95 && segments.some((component) => component === '.' || component === '..')) {
96 return rootFailure('windows-root-invalid', side);
97 }
98 return { ok: true, segments };
99}
101function splitWindowsAbsolute(absolute, side, { internalRelativeLinkTarget = false } = {}) {
102 if (/^[\\/]{2}\.[\\/]/u.test(absolute)) {
103 return rootFailure('windows-namespace-unsupported', side);
104 }
105 if (/^[\\/]{2}\?[\\/]/u.test(absolute) && !absolute.startsWith('\\\\?\\')) {
106 return rootFailure('windows-namespace-unsupported', side);
107 }
109 if (absolute.startsWith('\\\\?\\')) {
110 if (absolute.includes('/')) return rootFailure('windows-root-invalid', side);
111 if (
112 /^\\\\\?\\(?:GLOBALROOT|Device)(?:\\|$)/iu.test(absolute)
113 || /^\\\\\?\\Volume\{/iu.test(absolute)
114 ) {
115 return rootFailure('windows-namespace-unsupported', side);
116 }
118 const extendedUnc = absolute.match(/^(\\\\\?\\UNC\\([^\\]+)\\([^\\]+))(?:\\(.*))?$/iu);
119 if (extendedUnc) {
120 if (isWindowsIpcShare(extendedUnc[3])) {
121 return rootFailure('windows-namespace-unsupported', side);
122 }
123 if (!isValidWindowsSmbShareName(extendedUnc[3])) {
124 return rootFailure('windows-root-invalid', side);
125 }
126 if ([extendedUnc[2], extendedUnc[3]].some((component) => component === '.' || component === '..')) {
127 return rootFailure('windows-root-invalid', side);
128 }
129 const tail = splitWindowsExtendedTail(
130 extendedUnc[4] || '',
131 side,
132 { internalRelativeLinkTarget },
133 );
134 if (!tail.ok) return tail;
135 return {
136 ok: true,
137 root: `${extendedUnc[1]}\\`,
138 segments: tail.segments,
139 };
140 }
142 const extendedDrive = absolute.match(/^(\\\\\?\\[A-Za-z]:\\)(.*)$/u);
143 if (extendedDrive) {
144 const tail = splitWindowsExtendedTail(
145 extendedDrive[2],
146 side,
147 { internalRelativeLinkTarget },
148 );
149 if (!tail.ok) return tail;
150 return {
151 ok: true,
152 root: extendedDrive[1],
153 segments: tail.segments,
154 };
155 }
156 return rootFailure('windows-root-invalid', side);
157 }
159 const unc = absolute.match(/^[\\/]{2}([^\\/]+)[\\/]([^\\/]+)(?:[\\/](.*))?$/u);
160 if (unc) {
161 if (isWindowsIpcShare(unc[2])) {
162 return rootFailure('windows-namespace-unsupported', side);
163 }
164 if (!isValidWindowsSmbShareName(unc[2])) {
165 return rootFailure('windows-root-invalid', side);
166 }
167 if ([unc[1], unc[2]].some((component) => component === '.' || component === '..')) {
168 return rootFailure('windows-root-invalid', side);
169 }
170 return {
171 ok: true,
172 root: `\\\\${unc[1]}\\${unc[2]}\\`,
173 segments: (unc[3] || '').split(/[\\/]+/u).filter(Boolean),
174 };
175 }
177 const drive = absolute.match(/^([A-Za-z]:)[\\/](.*)$/u);
178 if (drive) {
179 return {
180 ok: true,
181 root: `${drive[1]}\\`,
182 segments: drive[2].split(/[\\/]+/u).filter(Boolean),
183 };
184 }
185 return rootFailure('windows-root-invalid', side);
186}
188function splitAbsolute(absolute, side, options) {
189 if (process.platform === 'win32') return splitWindowsAbsolute(absolute, side, options);
190 const root = path.posix.parse(absolute).root;
191 return {
192 ok: true,
193 root,
194 segments: absolute.slice(root.length).split(/\/+/u).filter(Boolean),
195 };
196}
198function statBigInt(targetPath) {
199 return fs.statSync(targetPath, { bigint: true });
200}
202function ordinaryWindowsRoot(extendedRoot) {
203 const drive = extendedRoot.match(/^\\\\\?\\([A-Za-z]:\\)$/u);
204 if (drive) return drive[1];
206 const unc = extendedRoot.match(/^\\\\\?\\UNC\\([^\\]+)\\([^\\]+)\\$/iu);
207 return unc ? `\\\\${unc[1]}\\${unc[2]}\\` : null;
208}
210function hasStableIdentity(stat) {
211 return typeof stat?.dev === 'bigint'
212 && typeof stat?.ino === 'bigint'
213 && stat.ino !== 0n;
214}
216function compareKnownEntries(left, right, operation) {
217 if (hasStableIdentity(left.stat) && hasStableIdentity(right.stat)) {
218 const matches = left.stat.dev === right.stat.dev && left.stat.ino === right.stat.ino;
219 return result(matches ? 'match' : 'different', matches ? 'file-identity-match' : 'file-identity-different', {
220 operation,
221 method: 'bigint-stat',
222 });
223 }
225 // path-contract-allow: lexical-capability -- Canonical paths are compared inside the identity primitive.
226 if (left.realPath === right.realPath) {
227 return result('match', 'native-realpath-match', {
228 operation,
229 method: 'native-realpath',
230 });
231 }
233 return unknown('file-identity-unavailable', {
234 operation,
235 method: 'bigint-stat-and-native-realpath',
236 });
237}
239function rememberProbeName(name) {
240 recentProbeNames.add(name);
241 if (recentProbeNames.size > RECENT_PROBE_NAME_LIMIT) {
242 recentProbeNames.delete(recentProbeNames.values().next().value);
243 }
244}
246function probeInterference(message, code = 'ARCHIFY_PROBE_IDENTITY_CHANGED', details = {}) {
247 const error = new Error(message);
248 error.code = code;
249 Object.assign(error, details);
250 return error;
251}
253function sameStableIdentity(left, right) {
254 return hasStableIdentity(left)
255 && hasStableIdentity(right)
256 && left.dev === right.dev
257 && left.ino === right.ino;
258}
260function preserveQuarantinedReplacement(quarantinePath, targetPath) {
261 try {
262 fs.linkSync(quarantinePath, targetPath);
263 return { restoredAtTarget: true };
264 } catch {
265 // Preserve the quarantined entry when the original name was concurrently
266 // occupied or the host cannot create a hard link. Never overwrite either.
267 return { restoredAtTarget: false };
268 }
269}
271function removeOwnedProbe(targetPath, expectedStat) {
272 let currentStat;
273 try {
274 currentStat = fs.lstatSync(targetPath, { bigint: true });
275 } catch (error) {
276 if (error?.code === 'ENOENT') {
277 return probeInterference('filesystem probe disappeared before cleanup', 'ARCHIFY_PROBE_MISSING');
278 }
279 return error;
280 }
281 if (!sameStableIdentity(expectedStat, currentStat)) {
282 return probeInterference('filesystem probe identity changed before cleanup');
283 }
285 const quarantineName = `.archify-path-semantics-cleanup-${process.pid}-${crypto.randomBytes(10).toString('hex')}`;
286 const quarantinePath = path.join(path.dirname(targetPath), quarantineName);
287 rememberProbeName(quarantineName);
288 try {
289 // Move the checked name into a fresh, unpublished quarantine namespace.
290 // A replacement introduced after the check is moved rather than deleted.
291 fs.renameSync(targetPath, quarantinePath);
292 } catch (error) {
293 return error;
294 }
296 let quarantineStat;
297 try {
298 quarantineStat = fs.lstatSync(quarantinePath, { bigint: true });
299 } catch (error) {
300 error.recoveryPath = quarantinePath;
301 error.restoredAtTarget = false;
302 return error;
303 }
304 if (!sameStableIdentity(expectedStat, quarantineStat)) {
305 const recovery = preserveQuarantinedReplacement(quarantinePath, targetPath);
306 return probeInterference(
307 'filesystem probe identity changed during cleanup',
308 'ARCHIFY_PROBE_IDENTITY_CHANGED',
309 { recoveryPath: quarantinePath, ...recovery },
310 );
311 }
313 try {
314 // The quarantine spelling did not exist before the atomic rename and is
315 // never returned to callers. Legitimate target-path replacements therefore
316 // remain outside the only namespace we unlink.
317 fs.unlinkSync(quarantinePath);
318 return null;
319 } catch (error) {
320 error.recoveryPath = quarantinePath;
321 error.restoredAtTarget = false;
322 return error;
323 }
324}
326function resolvePlan({ root, segments }, side, depth) {
327 if (depth > MAX_SYMLINK_DEPTH) {
328 return {
329 ok: false,
330 failure: unknown('symlink-cycle', { side }),
331 };
332 }
334 let current;
335 let currentStat;
336 try {
337 current = fs.realpathSync.native(root);
338 } catch (error) {
339 // Node on Windows can report EISDIR while resolving a valid extended
340 // filesystem root (for example \\?\C:\). The namespace was already
341 // validated by splitWindowsAbsolute, so retain its exact spelling for
342 // traversal.
343 if (process.platform === 'win32'
344 && error?.code === 'EISDIR'
345 && ordinaryWindowsRoot(root)) {
346 current = root;
347 } else {
348 return {
349 ok: false,
350 failure: systemFailure('root-resolution-failed', error, { side }),
351 };
352 }
353 }
355 try {
356 currentStat = statBigInt(current);
357 } catch (error) {
358 // The same Node/Windows edge can occur during stat after realpath succeeds.
359 // A drive or share root is short enough for its ordinary spelling, while
360 // `current` remains namespaced so descendants keep long-path semantics.
361 const ordinaryRoot = process.platform === 'win32'
362 && error?.code === 'EISDIR'
363 ? ordinaryWindowsRoot(root)
364 : null;
365 if (ordinaryRoot && ordinaryRoot !== current) {
366 try {
367 currentStat = statBigInt(ordinaryRoot);
368 } catch (statError) {
369 return {
370 ok: false,
371 failure: systemFailure('root-resolution-failed', statError, { side }),
372 };
373 }
374 } else {
375 return {
376 ok: false,
377 failure: systemFailure('root-resolution-failed', error, { side }),
378 };
379 }
380 }
382 for (let index = 0; index < segments.length; index += 1) {
383 const segment = segments[index];
384 if (segment === '.') continue;
385 if (segment === '..') {
386 if (!currentStat.isDirectory()) {
387 return {
388 ok: false,
389 failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
390 };
391 }
392 try {
393 current = fs.realpathSync.native(hostPath().dirname(current));
394 currentStat = statBigInt(current);
395 } catch (error) {
396 return {
397 ok: false,
398 failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
399 };
400 }
401 continue;
402 }
404 const candidate = hostPath().join(current, segment);
405 let candidateStat;
406 try {
407 candidateStat = fs.lstatSync(candidate, { bigint: true });
408 } catch (error) {
409 if (error?.code === 'ENOENT') {
410 if (!currentStat.isDirectory()) {
411 return {
412 ok: false,
413 failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
414 };
415 }
416 const unresolved = segments.slice(index).filter((item) => item !== '.');
417 if (unresolved.includes('..')) {
418 return {
419 ok: false,
420 failure: unknown('future-parent-traversal-indeterminate', {
421 side,
422 segmentIndex: index,
423 }),
424 };
425 }
426 return {
427 ok: true,
428 existing: false,
429 ancestor: {
430 realPath: current,
431 stat: currentStat,
432 },
433 unresolved,
434 };
435 }
436 if (error?.code === 'ENOTDIR') {
437 return {
438 ok: false,
439 failure: systemFailure('ancestor-not-directory', error, { side, segmentIndex: index }),
440 };
441 }
442 return {
443 ok: false,
444 failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
445 };
446 }
448 if (candidateStat.isSymbolicLink()) {
449 let linkPlan;
450 try {
451 const authoredTarget = fs.readlinkSync(candidate);
452 const pathApi = hostPath();
453 const internalRelativeLinkTarget = !pathApi.isAbsolute(authoredTarget)
454 && !pathApi.parse(authoredTarget).root;
455 const parsedTarget = absolutePath(authoredTarget, side, hostPath().dirname(candidate));
456 if (!parsedTarget.ok) return parsedTarget;
457 const target = splitAbsolute(parsedTarget.path, side, { internalRelativeLinkTarget });
458 if (!target.ok) return target;
459 linkPlan = {
460 root: target.root,
461 segments: [...target.segments, ...segments.slice(index + 1)],
462 };
463 } catch (error) {
464 return {
465 ok: false,
466 failure: systemFailure('symlink-resolution-failed', error, { side, segmentIndex: index }),
467 };
468 }
469 return resolvePlan(linkPlan, side, depth + 1);
470 }
472 if (index < segments.length - 1 && !candidateStat.isDirectory()) {
473 return {
474 ok: false,
475 failure: unknown('ancestor-not-directory', { side, segmentIndex: index }),
476 };
477 }
479 try {
480 current = fs.realpathSync.native(candidate);
481 currentStat = statBigInt(current);
482 } catch (error) {
483 return {
484 ok: false,
485 failure: systemFailure('path-resolution-failed', error, { side, segmentIndex: index }),
486 };
487 }
488 }
490 return {
491 ok: true,
492 existing: true,
493 entry: {
494 realPath: current,
495 stat: currentStat,
496 },
497 unresolved: [],
498 };
499}
501function resolveLocation(input, side) {
502 const parsed = absolutePath(input, side);
503 if (!parsed.ok) return parsed;
504 const plan = splitAbsolute(parsed.path, side);
505 if (!plan.ok) return plan;
506 return resolvePlan(plan, side, 0);
507}
509function semanticNameEnvelope(name) {
510 const names = new Set([name]);
511 const pending = [name];
512 while (pending.length > 0 && names.size <= 64) {
513 const current = pending.shift();
514 const candidates = [
515 current.normalize('NFC'),
516 current.normalize('NFD'),
517 current.toLocaleLowerCase('en-US'),
518 current.toLocaleUpperCase('en-US'),
519 ];
520 for (const candidate of candidates) {
521 if (names.has(candidate)) continue;
522 names.add(candidate);
523 pending.push(candidate);
524 }
525 }
526 return names.size <= 64 ? names : null;
527}
529function semanticEnvelopesOverlap(leftName, rightName) {
530 const left = semanticNameEnvelope(leftName);
531 const right = semanticNameEnvelope(rightName);
532 if (!left || !right) return true;
533 for (const spelling of left) {
534 if (right.has(spelling)) return true;
535 }
536 return false;
537}
539function windowsDeviceName(name) {
540 const stem = name.split('.')[0].replace(/[ .]+$/u, '').toUpperCase();
541 return /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9¹²³]|LPT[1-9¹²³])$/u.test(stem);
542}
544function futureWindowsNameRisk(leftName, rightName) {
545 if (process.platform !== 'win32') return null;
546 const windowsShortName = /~[1-9][0-9]*(?:\.|$)/iu;
547 if (windowsShortName.test(leftName) || windowsShortName.test(rightName)) {
548 return unknown('future-short-name-indeterminate', { method: 'windows-8dot3' });
549 }
550 if (windowsDeviceName(leftName) || windowsDeviceName(rightName)) {
551 return unknown('future-device-name-indeterminate', { method: 'windows-device-name' });
552 }
553 if (/[ .]$/.test(leftName) || /[ .]$/.test(rightName)) {
554 return unknown('future-trim-name-indeterminate', { method: 'windows-trim-alias' });
555 }
556 return null;
557}
559/**
560 * Resolve one existing or future path through the host filesystem without
561 * exposing a string key for identity comparisons.
562 */
563export function resolvePhysicalLocation(targetPath) {
564 const resolution = resolveLocation(targetPath, 'target');
565 if (!resolution.ok) return resolution.failure;
566 if (resolution.existing) {
567 return {
568 status: 'resolved',
569 location: {
570 kind: 'existing',
571 path: resolution.entry.realPath,
572 },
573 reason: {
574 code: 'existing-native-realpath',
575 method: 'native-realpath',
576 },
577 };
578 }
579 return {
580 status: 'resolved',
581 location: {
582 kind: 'future',
583 ancestorPath: resolution.ancestor.realPath,
584 unresolved: [...resolution.unresolved],
585 },
586 reason: {
587 code: 'future-physical-ancestor',
588 method: 'native-realpath',
589 },
590 };
591}
593function probeNameAlias(directoryPath, leftName, rightName) {
594 if (leftName === rightName) {
595 return result('match', 'future-name-exact', { method: 'lexical-exact' });
596 }
597 if (recentProbeNames.has(leftName) || recentProbeNames.has(rightName)) {
598 return result('different', 'internal-probe-name', { method: 'probe-suppression' });
599 }
600 const windowsRisk = futureWindowsNameRisk(leftName, rightName);
601 if (windowsRisk) return windowsRisk;
602 if (!semanticEnvelopesOverlap(leftName, rightName)) {
603 return result('different', 'future-name-distinct', { method: 'semantic-envelope' });
604 }
606 const prefix = `.archify-path-semantics-${process.pid}-${crypto.randomBytes(10).toString('hex')}-`;
607 const probeName = `${prefix}${leftName}`;
608 const lookupName = `${prefix}${rightName}`;
609 const componentLength = (name) => process.platform === 'win32'
610 ? name.length
611 : Buffer.byteLength(name, 'utf8');
612 if (componentLength(probeName) > 255 || componentLength(lookupName) > 255) {
613 return unknown('future-name-probe-unrepresentable', {
614 method: 'direct-filesystem-probe',
615 systemCode: 'ENAMETOOLONG',
616 });
617 }
619 let probePath;
620 let probeStat;
621 let descriptor;
622 let comparison;
623 let cleanupError;
624 try {
625 // Probe entries must live directly in the directory being queried. On
626 // Windows case sensitivity is per-directory and a child may not inherit it.
627 rememberProbeName(probeName);
628 rememberProbeName(lookupName);
629 probePath = path.join(directoryPath, probeName);
630 const lookupPath = path.join(directoryPath, lookupName);
631 descriptor = fs.openSync(probePath, 'wx', 0o600);
632 probeStat = fs.fstatSync(descriptor, { bigint: true });
634 let authoredStat;
635 let lookupStat;
636 try {
637 authoredStat = statBigInt(probePath);
638 lookupStat = statBigInt(lookupPath);
639 } catch (error) {
640 if (error?.code === 'ENOENT') {
641 comparison = result('different', 'future-name-different', { method: 'filesystem-probe' });
642 } else {
643 comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' });
644 }
645 }
647 if (!comparison) {
648 const identity = compareKnownEntries(
649 { realPath: fs.realpathSync.native(probePath), stat: authoredStat },
650 { realPath: fs.realpathSync.native(lookupPath), stat: lookupStat },
651 'future-name',
652 );
653 if (identity.status === 'match') {
654 comparison = result('match', 'future-name-alias', { method: 'filesystem-probe' });
655 } else if (identity.status === 'different') {
656 comparison = result('different', 'future-name-different', { method: 'filesystem-probe' });
657 } else {
658 comparison = identity;
659 }
660 }
661 } catch (error) {
662 comparison = systemFailure('future-name-probe-failed', error, { method: 'filesystem-probe' });
663 } finally {
664 // Pin the inode until cleanup finishes. Once the final handle is closed,
665 // an unlinked probe's inode can be reused by a replacement at the same name.
666 if (probeStat) cleanupError = removeOwnedProbe(probePath, probeStat);
667 if (descriptor !== undefined) {
668 try {
669 fs.closeSync(descriptor);
670 } catch (error) {
671 cleanupError = cleanupError ?? error;
672 }
673 }
674 }
676 if (cleanupError) {
677 return systemFailure('future-name-probe-cleanup-failed', cleanupError, {
678 method: 'filesystem-probe',
679 ...(comparison ? { comparison: comparison.reason } : {}),
680 });
681 }
682 return comparison;
683}
685function sidecarCaseKey(component) {
686 // The candidate is accepted only after the containing filesystem confirms
687 // that the original grapheme and this spelling name the same entry.
688 return component.toLocaleUpperCase('en-US').toLocaleLowerCase('en-US');
689}
691function transformSidecarComponent(directoryPath, component, transform, phase) {
692 if (typeof Intl.Segmenter !== 'function') {
693 return {
694 ok: false,
695 failure: unknown('sidecar-unicode-segmentation-unavailable', {
696 operation: 'sidecar-namespace-key',
697 }),
698 };
699 }
700 const segmenter = new Intl.Segmenter('en-US', { granularity: 'grapheme' });
701 const comparisons = new Map();
702 const output = [];
703 for (const { segment } of segmenter.segment(component)) {
704 const candidate = transform(segment);
705 if (candidate === segment) {
706 output.push(segment);
707 continue;
708 }
709 const cacheKey = `${segment}\0${candidate}`;
710 let comparison = comparisons.get(cacheKey);
711 if (!comparison) {
712 comparison = probeNameAlias(directoryPath, segment, candidate);
713 comparisons.set(cacheKey, comparison);
714 }
715 if (comparison.status === 'unknown') {
716 return {
717 ok: false,
718 failure: unknown(`sidecar-${phase}-semantics-indeterminate`, {
719 operation: 'sidecar-namespace-key',
720 cause: comparison.reason,
721 }),
722 };
723 }
724 output.push(comparison.status === 'match' ? candidate : segment);
725 }
726 return {
727 ok: true,
728 component: output.join(''),
729 };
730}
732/**
733 * Produce a stable component for a sidecar namespace in one existing
734 * directory. The returned string is a collision-domain key, not a canonical
735 * path and must never be used to decide whether two filesystem entries match.
736 */
737export function sidecarNamespaceComponentKey(directoryPath, component) {
738 if (typeof component !== 'string' || component.length === 0
739 || component === '.' || component === '..'
740 || hostPath().basename(component) !== component) {
741 return unknown('invalid-sidecar-component', {
742 operation: 'sidecar-namespace-key',
743 });
744 }
746 const directory = resolveLocation(directoryPath, 'sidecar-directory');
747 if (!directory.ok) return directory.failure;
748 if (!directory.existing) {
749 return unknown('sidecar-directory-missing', {
750 operation: 'sidecar-namespace-key',
751 });
752 }
753 if (!directory.entry.stat.isDirectory()) {
754 return unknown('sidecar-parent-not-directory', {
755 operation: 'sidecar-namespace-key',
756 });
757 }
759 const normalized = transformSidecarComponent(
760 directory.entry.realPath,
761 component,
762 (segment) => segment.normalize('NFC'),
763 'normalization',
764 );
765 if (!normalized.ok) return normalized.failure;
766 const cased = transformSidecarComponent(
767 directory.entry.realPath,
768 normalized.component,
769 sidecarCaseKey,
770 'case',
771 );
772 if (!cased.ok) return cased.failure;
773 const renormalized = transformSidecarComponent(
774 directory.entry.realPath,
775 cased.component,
776 (segment) => segment.normalize('NFC'),
777 'normalization',
778 );
779 if (!renormalized.ok) return renormalized.failure;
781 return {
782 status: 'resolved',
783 directoryPath: directory.entry.realPath,
784 componentKey: renormalized.component,
785 reason: {
786 code: 'sidecar-namespace-key',
787 method: 'directory-filesystem-probe',
788 },
789 };
790}
792function compareUnmaterializedNames(leftName, rightName) {
793 if (leftName === rightName) {
794 return result('match', 'future-name-exact', { method: 'lexical-exact' });
795 }
796 const windowsRisk = futureWindowsNameRisk(leftName, rightName);
797 if (windowsRisk) return windowsRisk;
798 if (!semanticEnvelopesOverlap(leftName, rightName)) {
799 return result('different', 'future-name-distinct', { method: 'semantic-envelope' });
800 }
801 return unknown('future-descendant-semantics-indeterminate', {
802 method: 'unmaterialized-directory',
803 });
804}
806function compareFutureLocations(left, right) {
807 const ancestor = compareKnownEntries(left.ancestor, right.ancestor, 'future-ancestor');
808 if (ancestor.status !== 'match') return ancestor;
810 if (left.unresolved.length !== right.unresolved.length) {
811 return result('different', 'future-depth-different', {
812 operation: 'same-location',
813 leftDepth: left.unresolved.length,
814 rightDepth: right.unresolved.length,
815 });
816 }
818 let probedSegments = 0;
819 for (let index = 0; index < left.unresolved.length; index += 1) {
820 const names = index === 0
821 ? probeNameAlias(
822 left.ancestor.realPath,
823 left.unresolved[index],
824 right.unresolved[index],
825 )
826 : compareUnmaterializedNames(left.unresolved[index], right.unresolved[index]);
827 if (names.reason.method === 'filesystem-probe') probedSegments += 1;
828 if (names.status !== 'match') {
829 const { code, ...reasonDetails } = names.reason;
830 return result(names.status, code, {
831 ...reasonDetails,
832 operation: 'same-location',
833 segmentIndex: index,
834 });
835 }
836 }
838 return result('match', probedSegments > 0 ? 'future-location-alias' : 'future-location-exact', {
839 operation: 'same-location',
840 method: probedSegments > 0 ? 'filesystem-probe' : 'lexical-exact',
841 probedSegments,
842 });
843}
845/**
846 * Compare two existing filesystem entries by physical identity.
847 * Missing or unidentifiable entries return `unknown`, never `different`.
848 */
849export function sameEntry(leftPath, rightPath) {
850 const left = resolveLocation(leftPath, 'left');
851 if (!left.ok) return left.failure;
852 const right = resolveLocation(rightPath, 'right');
853 if (!right.ok) return right.failure;
855 if (!left.existing || !right.existing) {
856 return unknown('entry-missing', {
857 operation: 'same-entry',
858 missing: [
859 ...(!left.existing ? ['left'] : []),
860 ...(!right.existing ? ['right'] : []),
861 ],
862 });
863 }
865 return compareKnownEntries(left.entry, right.entry, 'same-entry');
866}
868/**
869 * Compare two existing or future write targets using the containing filesystem's
870 * actual name semantics. Existing hard links and resolved symbolic links match.
871 */
872export function sameLocation(leftPath, rightPath) {
873 const left = resolveLocation(leftPath, 'left');
874 if (!left.ok) return left.failure;
875 const right = resolveLocation(rightPath, 'right');
876 if (!right.ok) return right.failure;
878 if (left.existing && right.existing) {
879 return compareKnownEntries(left.entry, right.entry, 'same-location');
880 }
881 if (left.existing !== right.existing) {
882 return result('different', 'location-existence-different', {
883 operation: 'same-location',
884 existing: left.existing ? 'left' : 'right',
885 });
886 }
887 return compareFutureLocations(left, right);
888}
890/** Compare the physical locations of the two paths' parent directories. */
891export function sameParent(leftPath, rightPath) {
892 const left = absolutePath(leftPath, 'left');
893 if (!left.ok) return left.failure;
894 const right = absolutePath(rightPath, 'right');
895 if (!right.ok) return right.failure;
897 return sameLocation(hostPath().dirname(left.path), hostPath().dirname(right.path));
898}
900function parentEntry(entry) {
901 const parentPath = hostPath().dirname(entry.realPath);
902 // path-contract-allow: lexical-capability -- Both values are canonical paths captured by this primitive.
903 if (parentPath === entry.realPath) return null;
904 try {
905 const realPath = fs.realpathSync.native(parentPath);
906 return {
907 ok: true,
908 entry: {
909 realPath,
910 stat: statBigInt(realPath),
911 },
912 };
913 } catch (error) {
914 return {
915 ok: false,
916 failure: systemFailure('ancestor-resolution-failed', error, {
917 operation: 'contained-by',
918 }),
919 };
920 }
921}
923/**
924 * Determine physical containment in an existing directory. The comparison is
925 * inclusive and follows links; the target may exist or name a future location.
926 */
927export function containedBy(directoryPath, targetPath) {
928 const directory = resolveLocation(directoryPath, 'directory');
929 if (!directory.ok) return directory.failure;
930 if (directory.existing && !directory.entry.stat.isDirectory()) {
931 return unknown('container-not-directory', { operation: 'contained-by' });
932 }
934 const target = resolveLocation(targetPath, 'target');
935 if (!target.ok) return target.failure;
937 if (!directory.existing) {
938 if (target.existing) {
939 return unknown('container-missing', { operation: 'contained-by' });
940 }
941 const ancestor = compareKnownEntries(directory.ancestor, target.ancestor, 'contained-by');
942 if (ancestor.status !== 'match') return ancestor;
943 if (target.unresolved.length < directory.unresolved.length) {
944 return result('different', 'future-container-prefix-too-deep', {
945 operation: 'contained-by',
946 });
947 }
948 for (let index = 0; index < directory.unresolved.length; index += 1) {
949 const segment = index === 0
950 ? probeNameAlias(
951 directory.ancestor.realPath,
952 directory.unresolved[index],
953 target.unresolved[index],
954 )
955 : compareUnmaterializedNames(
956 directory.unresolved[index],
957 target.unresolved[index],
958 );
959 if (segment.status === 'match') continue;
960 const { code, ...reasonDetails } = segment.reason;
961 return result(segment.status, code, {
962 ...reasonDetails,
963 operation: 'contained-by',
964 segmentIndex: index,
965 });
966 }
967 return result('match', 'future-container-prefix-match', {
968 operation: 'contained-by',
969 target: 'future',
970 depth: target.unresolved.length - directory.unresolved.length,
971 });
972 }
974 let current;
975 if (!target.existing) {
976 current = target.ancestor;
977 } else if (target.entry.stat.isDirectory()) {
978 current = target.entry;
979 } else {
980 const parent = parentEntry(target.entry);
981 if (!parent?.ok) return parent?.failure ?? unknown('ancestor-resolution-failed', {
982 operation: 'contained-by',
983 });
984 current = parent.entry;
985 }
987 let depth = 0;
988 let identityWasUnknown = false;
989 while (current) {
990 const comparison = compareKnownEntries(directory.entry, current, 'contained-by');
991 if (comparison.status === 'match') {
992 return result('match', 'physical-ancestor-match', {
993 operation: 'contained-by',
994 depth,
995 target: target.existing ? 'existing' : 'future',
996 });
997 }
998 if (comparison.status === 'unknown') identityWasUnknown = true;
1000 const parent = parentEntry(current);
1001 if (parent === null) break;
1002 if (!parent.ok) return parent.failure;
1003 current = parent.entry;
1004 depth += 1;
1005 }
1007 if (identityWasUnknown) {
1008 return unknown('containment-identity-unavailable', {
1009 operation: 'contained-by',
1010 target: target.existing ? 'existing' : 'future',
1011 });
1012 }
1013 return result('different', 'physical-ancestor-not-found', {
1014 operation: 'contained-by',
1015 target: target.existing ? 'existing' : 'future',
1016 });
1017}