2026-10-16 04:41:40 +08:00
import { posix } from "node:path" ;
2026-09-08 10:59:13 +08:00
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js" ;
2026-10-16 04:41:40 +08:00
import { createReferenceSourcePathLookup } from "../domain/referenceSourcePathIdentity.js" ;
2026-07-12 21:12:18 +00:00
import { err , ok } from "../domain/result.js" ;
2026-10-11 06:06:28 +08:00
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js" ;
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js" ;
2026-07-12 21:12:18 +00:00
import { traverseDirectory } from "./ReferenceSourceReaderEntries.js" ;
2026-10-10 21:04:05 -07:00
import type {
ReferenceSourceReaderOptions ,
2026-10-16 04:41:40 +08:00
ReferenceSourceEntry ,
2026-10-19 08:36:53 +08:00
ReferenceSourceRootFailure ,
2026-10-10 21:04:05 -07:00
ReferenceSourceRead ,
ReferenceSourceResult ,
TraversalState ,
2026-07-12 21:12:18 +00:00
} from "./ReferenceSourceReaderTypes.js" ;
import {
isAborted ,
noFollowOpenSupported ,
prepareRoot ,
} from "./ReferenceSourceReaderValidate.js" ;
2026-10-22 10:19:28 +08:00
import { referenceSourceResourceFailure } from "./ReferenceSourceReaderErrors.js" ;
2026-07-12 21:12:18 +00:00
const PATH_RACE_LIMITATION =
"Path identity is revalidated around operations; Node lacks portable descriptor-relative openat traversal, so a syscall-boundary pathname race remains." ;
2026-09-29 14:53:20 +08:00
/** Read a source tree without intentionally following symbolic links. */
2026-07-12 21:12:18 +00:00
export const readReferenceSource = async (
root : string ,
2026-10-11 06:06:28 +08:00
resources : ArtifactResourceScope ,
2026-07-12 21:12:18 +00:00
options : ReferenceSourceReaderOptions = {},
2026-10-11 06:06:28 +08:00
) : Promise < ReferenceSourceResult < ReferenceSourceRead >> => {
try {
return await resources . run (() => readSource ( root , resources , options ));
} catch ( cause : unknown ) {
if ( ! ( cause instanceof ArtifactReaderFailure )) throw cause ;
2026-10-22 10:19:28 +08:00
return err ( referenceSourceResourceFailure ( cause ));
2026-10-11 06:06:28 +08:00
}
};
const readSource = async (
root : string ,
resources : ArtifactResourceScope ,
options : ReferenceSourceReaderOptions ,
2026-07-12 21:12:18 +00:00
) : Promise < ReferenceSourceResult < ReferenceSourceRead >> => {
if ( ! noFollowOpenSupported ())
return err ({
tag : "reference-source-reader" ,
code : "unsupported" ,
message : "Safe no-follow file opens are unavailable" ,
});
2026-09-29 14:53:20 +08:00
const prepared = await prepareRoot ( root , options . signal );
2026-07-12 21:12:18 +00:00
if ( ! prepared . ok ) return prepared ;
const { canonicalRoot , rootIdentity } = prepared . value ;
const state : TraversalState = {
2026-10-11 06:06:28 +08:00
resources ,
2026-07-12 21:12:18 +00:00
root : canonicalRoot ,
rootIdentity ,
...( options . signal === undefined ? {} : { signal : options.signal }),
...( options . shouldExclude === undefined
? {}
: { shouldExclude : options.shouldExclude }),
2026-10-24 02:27:18 +08:00
...( options . beforeDirectory === undefined
? {}
: { beforeDirectory : options.beforeDirectory }),
2026-07-12 21:12:18 +00:00
entries : [],
2026-10-16 04:41:40 +08:00
exclusions : [],
2026-09-29 14:53:20 +08:00
pending : [{ path : canonicalRoot }],
2026-07-12 21:12:18 +00:00
bytesRead : 0 ,
};
const traversal = await traverse ( state );
2026-10-16 04:41:40 +08:00
if (
! isAborted ( state . signal ) &&
( traversal . ok || traversal . error . code !== "cancelled" )
)
reconcileUnobservedSymlinkTargets (
state . entries ,
state . exclusions ,
state . signal ,
2026-10-19 08:36:53 +08:00
state . rootFailure ,
2026-10-16 04:41:40 +08:00
);
2026-09-08 10:59:13 +08:00
state . entries . sort (( left , right ) =>
compareUnicodeCodePoints ( left . path , right . path ),
);
2026-10-11 06:06:28 +08:00
const read : ReferenceSourceRead = {
2026-07-12 21:12:18 +00:00
root : canonicalRoot ,
2026-10-19 08:36:53 +08:00
...( state . rootFailure === undefined
? {}
: { rootFailure : state.rootFailure }),
2026-07-12 21:12:18 +00:00
entries : state.entries ,
bytesRead : state.bytesRead ,
2026-09-29 14:53:20 +08:00
limitations : [ PATH_RACE_LIMITATION ],
2026-10-11 06:06:28 +08:00
};
2026-10-24 02:39:16 +08:00
if ( traversal . ok && state . rootFailure ? . code === "io" )
return err ({
tag : "reference-source-reader" ,
code : "io" ,
message : state.rootFailure.message ,
partial : read ,
});
2026-10-11 06:06:28 +08:00
return traversal . ok ? ok ( read ) : err ({ ... traversal . error , partial : read });
2026-07-12 21:12:18 +00:00
};
2026-10-16 04:41:40 +08:00
/**
* Resolve internal link observations against the completed reader inventory.
* If the closest containing directory failed before enumeration completed,
* the target remains unknown; a broader failed ancestor cannot override a
* successfully enumerated directory or a recorded exclusion.
*/
export const reconcileUnobservedSymlinkTargets = (
entries : ReferenceSourceEntry [],
exclusions : TraversalState [ "exclusions" ] = [],
signal? : AbortSignal ,
2026-10-19 08:36:53 +08:00
rootFailure? : ReferenceSourceRootFailure ,
2026-10-16 04:41:40 +08:00
) : void => {
const failedDirectories = new Map (
entries . flatMap (( entry ) =>
entry . status === "failed" &&
entry . kind === "directory" &&
entry . code !== "cancelled"
? [[ entry . path , entry ] as const ]
: [],
),
);
2026-10-19 08:36:53 +08:00
if (
( failedDirectories . size === 0 && rootFailure === undefined ) ||
isAborted ( signal )
)
return ;
2026-10-16 04:41:40 +08:00
const directoryEntries = entries . filter (
( entry ) => entry . kind === "directory" ,
);
const directoryPaths = createReferenceSourcePathLookup (
directoryEntries . map (({ path }) => path ),
);
const observedPaths = createReferenceSourcePathLookup (
entries . map (({ path }) => path ),
);
const excludedPaths = createReferenceSourcePathLookup (
exclusions . map (({ path }) => path ),
);
for ( const [ index , entry ] of entries . entries ()) {
if ( isAborted ( signal )) return ;
if (
entry . status !== "read" ||
entry . kind !== "symlink" ||
entry . targetState !== "internal" ||
observedPaths . resolve ( entry . target ) !== undefined ||
excludedPaths . resolve ( entry . target ) !== undefined
)
continue ;
const ancestor = nearestFailedAncestor (
entry . target ,
directoryPaths ,
failedDirectories ,
excludedPaths ,
signal ,
2026-10-19 08:36:53 +08:00
rootFailure ,
2026-10-16 04:41:40 +08:00
);
if ( ancestor === undefined ) continue ;
entries [ index ] = {
status : "failed" ,
kind : "symlink" ,
path : entry.path ,
code : ancestor.code ,
2026-10-16 04:52:19 +08:00
message : `Symbolic link target ${ JSON . stringify ( entry . target ) } was not inventoried because directory ${ JSON . stringify ( ancestor . path ) } could not be enumerated: ${ ancestor . message } ` ,
2026-10-16 04:41:40 +08:00
};
}
};
const nearestFailedAncestor = (
target : string ,
directoryPaths : ReturnType < typeof createReferenceSourcePathLookup >,
failedDirectories : ReadonlyMap <
string ,
Extract < ReferenceSourceEntry , { status : "failed" }>
> ,
excludedPaths : ReturnType < typeof createReferenceSourcePathLookup >,
signal : AbortSignal | undefined ,
2026-10-19 08:36:53 +08:00
rootFailure : ReferenceSourceRootFailure | undefined ,
) : ( ReferenceSourceRootFailure & { readonly path : string }) | undefined => {
2026-10-16 04:41:40 +08:00
for (
let candidate = posix . dirname ( target );
candidate !== "." && candidate !== "/" ;
candidate = posix . dirname ( candidate )
) {
if ( isAborted ( signal )) return undefined ;
if ( excludedPaths . resolve ( candidate ) !== undefined ) return undefined ;
const observedDirectory = directoryPaths . resolve ( candidate );
if ( observedDirectory !== undefined )
return failedDirectories . get ( observedDirectory );
}
if ( isAborted ( signal )) return undefined ;
if ( excludedPaths . resolve ( "." ) !== undefined ) return undefined ;
2026-10-19 08:36:53 +08:00
return rootFailure === undefined ? undefined : { path : "." , ... rootFailure };
2026-10-16 04:41:40 +08:00
};
2026-07-12 21:12:18 +00:00
const traverse = async (
state : TraversalState ,
) : Promise < ReferenceSourceResult < undefined >> => {
2026-09-29 14:53:20 +08:00
while ( state . pending . length > 0 ) {
2026-07-12 21:12:18 +00:00
if ( isAborted ( state . signal ))
return err ({
tag : "reference-source-reader" ,
code : "cancelled" ,
message : "Reference source traversal cancelled" ,
});
const current = state . pending . pop ();
if ( current === undefined ) break ;
const result = await traverseDirectory ( state , current );
if ( ! result . ok ) return result ;
}
return ok ( undefined );
};