@@ -301,6 +301,27 @@ const defaultRemoveOptions = objectFreeze({
301301 retryDelay : 200 ,
302302} )
303303
304+ // Cache for resolved allowed directories
305+ let _cachedAllowedDirs : string [ ] | undefined
306+
307+ /**
308+ * Get resolved allowed directories for safe deletion with lazy caching.
309+ * These directories are resolved once and cached for the process lifetime.
310+ * @private
311+ */
312+ function getAllowedDirectories ( ) : string [ ] {
313+ if ( _cachedAllowedDirs === undefined ) {
314+ const path = getPath ( )
315+
316+ _cachedAllowedDirs = [
317+ path . resolve ( getOsTmpDir ( ) ) ,
318+ path . resolve ( getSocketCacacheDir ( ) ) ,
319+ path . resolve ( getSocketUserDir ( ) ) ,
320+ ]
321+ }
322+ return _cachedAllowedDirs
323+ }
324+
304325let _buffer : typeof import ( 'node:buffer' ) | undefined
305326/**
306327 * Lazily load the buffer module.
@@ -706,6 +727,19 @@ export function findUpSync(
706727 return undefined
707728}
708729
730+ /**
731+ * Invalidate the cached allowed directories.
732+ * Called automatically by the paths/rewire module when paths are overridden in tests.
733+ *
734+ * @internal Used for test rewiring
735+ */
736+ export function invalidatePathCache ( ) : void {
737+ _cachedAllowedDirs = undefined
738+ }
739+
740+ // Register cache invalidation with the rewire module
741+ registerCacheInvalidation ( invalidatePathCache )
742+
709743/**
710744 * Check if a path is a directory asynchronously.
711745 * Returns `true` for directories, `false` for files or non-existent paths.
@@ -821,74 +855,6 @@ export function isSymLinkSync(filepath: PathLike) {
821855 return false
822856}
823857
824- /**
825- * Result of file readability validation.
826- * Contains lists of valid and invalid file paths.
827- */
828- export interface ValidateFilesResult {
829- /**
830- * File paths that passed validation and are readable.
831- */
832- validPaths : string [ ]
833- /**
834- * File paths that failed validation (unreadable, permission denied, or non-existent).
835- * Common with Yarn Berry PnP virtual filesystem, pnpm symlinks, or filesystem race conditions.
836- */
837- invalidPaths : string [ ]
838- }
839-
840- /**
841- * Validate that file paths are readable before processing.
842- * Filters out files from glob results that cannot be accessed (common with
843- * Yarn Berry PnP virtual filesystem, pnpm content-addressable store symlinks,
844- * or filesystem race conditions in CI/CD environments).
845- *
846- * This defensive pattern prevents ENOENT errors when files exist in glob
847- * results but are not accessible via standard filesystem operations.
848- *
849- * @param filepaths - Array of file paths to validate
850- * @returns Object with `validPaths` (readable) and `invalidPaths` (unreadable)
851- *
852- * @example
853- * ```ts
854- * import { validateFiles } from '@socketsecurity/lib/fs'
855- *
856- * const files = ['package.json', '.pnp.cjs/virtual-file.json']
857- * const { validPaths, invalidPaths } = validateFiles(files)
858- *
859- * console.log(`Valid: ${validPaths.length}`)
860- * console.log(`Invalid: ${invalidPaths.length}`)
861- * ```
862- *
863- * @example
864- * ```ts
865- * // Typical usage in Socket CLI commands
866- * const packagePaths = await getPackageFilesForScan(targets)
867- * const { validPaths } = validateFiles(packagePaths)
868- * await sdk.uploadManifestFiles(orgSlug, validPaths)
869- * ```
870- */
871- /*@__NO_SIDE_EFFECTS__ */
872- export function validateFiles (
873- filepaths : string [ ] | readonly string [ ] ,
874- ) : ValidateFilesResult {
875- const fs = getFs ( )
876- const validPaths : string [ ] = [ ]
877- const invalidPaths : string [ ] = [ ]
878- const { R_OK } = fs . constants
879-
880- for ( const filepath of filepaths ) {
881- try {
882- fs . accessSync ( filepath , R_OK )
883- validPaths . push ( filepath )
884- } catch {
885- invalidPaths . push ( filepath )
886- }
887- }
888-
889- return { __proto__ : null , validPaths, invalidPaths } as ValidateFilesResult
890- }
891-
892858/**
893859 * Read directory names asynchronously with filtering and sorting.
894860 * Returns only directory names (not files), with optional filtering for empty directories
@@ -1251,39 +1217,6 @@ export function readJsonSync(
12511217 } )
12521218}
12531219
1254- // Cache for resolved allowed directories
1255- let _cachedAllowedDirs : string [ ] | undefined
1256-
1257- /**
1258- * Get resolved allowed directories for safe deletion with lazy caching.
1259- * These directories are resolved once and cached for the process lifetime.
1260- */
1261- function getAllowedDirectories ( ) : string [ ] {
1262- if ( _cachedAllowedDirs === undefined ) {
1263- const path = getPath ( )
1264-
1265- _cachedAllowedDirs = [
1266- path . resolve ( getOsTmpDir ( ) ) ,
1267- path . resolve ( getSocketCacacheDir ( ) ) ,
1268- path . resolve ( getSocketUserDir ( ) ) ,
1269- ]
1270- }
1271- return _cachedAllowedDirs
1272- }
1273-
1274- /**
1275- * Invalidate the cached allowed directories.
1276- * Called automatically by the paths/rewire module when paths are overridden in tests.
1277- *
1278- * @internal Used for test rewiring
1279- */
1280- export function invalidatePathCache ( ) : void {
1281- _cachedAllowedDirs = undefined
1282- }
1283-
1284- // Register cache invalidation with the rewire module
1285- registerCacheInvalidation ( invalidatePathCache )
1286-
12871220/**
12881221 * Safely delete a file or directory asynchronously with built-in protections.
12891222 * Uses `del` for safer deletion that prevents removing cwd and above by default.
@@ -1581,10 +1514,12 @@ export async function safeReadFile(
15811514 : ( { __proto__ : null , ...options } as SafeReadOptions )
15821515 const { defaultValue, ...rawReadOpts } = opts as SafeReadOptions
15831516 const readOpts = { __proto__ : null , ...rawReadOpts } as ReadOptions
1584- let { encoding = 'utf8' } = readOpts
1585- // Normalize encoding to canonical form.
1586- encoding = encoding === null ? null : normalizeEncoding ( encoding )
1587- const shouldReturnBuffer = encoding === null
1517+ // Check for null encoding before normalization to preserve Buffer return type.
1518+ const shouldReturnBuffer = readOpts . encoding === null
1519+ // Normalize encoding to canonical form (only if not null).
1520+ const encoding = shouldReturnBuffer
1521+ ? null
1522+ : normalizeEncoding ( readOpts . encoding )
15881523 const fs = getFs ( )
15891524 try {
15901525 return await fs . promises . readFile ( filepath , {
@@ -1650,10 +1585,12 @@ export function safeReadFileSync(
16501585 : ( { __proto__ : null , ...options } as SafeReadOptions )
16511586 const { defaultValue, ...rawReadOpts } = opts as SafeReadOptions
16521587 const readOpts = { __proto__ : null , ...rawReadOpts } as ReadOptions
1653- let { encoding = 'utf8' } = readOpts
1654- // Normalize encoding to canonical form.
1655- encoding = encoding === null ? null : normalizeEncoding ( encoding )
1656- const shouldReturnBuffer = encoding === null
1588+ // Check for null encoding before normalization to preserve Buffer return type.
1589+ const shouldReturnBuffer = readOpts . encoding === null
1590+ // Normalize encoding to canonical form (only if not null).
1591+ const encoding = shouldReturnBuffer
1592+ ? null
1593+ : normalizeEncoding ( readOpts . encoding )
16571594 const fs = getFs ( )
16581595 try {
16591596 return fs . readFileSync ( filepath , {
@@ -1780,6 +1717,74 @@ export function uniqueSync(filepath: PathLike): string {
17801717 return normalizePath ( uniquePath )
17811718}
17821719
1720+ /**
1721+ * Result of file readability validation.
1722+ * Contains lists of valid and invalid file paths.
1723+ */
1724+ export interface ValidateFilesResult {
1725+ /**
1726+ * File paths that passed validation and are readable.
1727+ */
1728+ validPaths : string [ ]
1729+ /**
1730+ * File paths that failed validation (unreadable, permission denied, or non-existent).
1731+ * Common with Yarn Berry PnP virtual filesystem, pnpm symlinks, or filesystem race conditions.
1732+ */
1733+ invalidPaths : string [ ]
1734+ }
1735+
1736+ /**
1737+ * Validate that file paths are readable before processing.
1738+ * Filters out files from glob results that cannot be accessed (common with
1739+ * Yarn Berry PnP virtual filesystem, pnpm content-addressable store symlinks,
1740+ * or filesystem race conditions in CI/CD environments).
1741+ *
1742+ * This defensive pattern prevents ENOENT errors when files exist in glob
1743+ * results but are not accessible via standard filesystem operations.
1744+ *
1745+ * @param filepaths - Array of file paths to validate
1746+ * @returns Object with `validPaths` (readable) and `invalidPaths` (unreadable)
1747+ *
1748+ * @example
1749+ * ```ts
1750+ * import { validateFiles } from '@socketsecurity/lib/fs'
1751+ *
1752+ * const files = ['package.json', '.pnp.cjs/virtual-file.json']
1753+ * const { validPaths, invalidPaths } = validateFiles(files)
1754+ *
1755+ * console.log(`Valid: ${validPaths.length}`)
1756+ * console.log(`Invalid: ${invalidPaths.length}`)
1757+ * ```
1758+ *
1759+ * @example
1760+ * ```ts
1761+ * // Typical usage in Socket CLI commands
1762+ * const packagePaths = await getPackageFilesForScan(targets)
1763+ * const { validPaths } = validateFiles(packagePaths)
1764+ * await sdk.uploadManifestFiles(orgSlug, validPaths)
1765+ * ```
1766+ */
1767+ /*@__NO_SIDE_EFFECTS__ */
1768+ export function validateFiles (
1769+ filepaths : string [ ] | readonly string [ ] ,
1770+ ) : ValidateFilesResult {
1771+ const fs = getFs ( )
1772+ const validPaths : string [ ] = [ ]
1773+ const invalidPaths : string [ ] = [ ]
1774+ const { R_OK } = fs . constants
1775+
1776+ for ( const filepath of filepaths ) {
1777+ try {
1778+ fs . accessSync ( filepath , R_OK )
1779+ validPaths . push ( filepath )
1780+ } catch {
1781+ invalidPaths . push ( filepath )
1782+ }
1783+ }
1784+
1785+ return { __proto__ : null , validPaths, invalidPaths } as ValidateFilesResult
1786+ }
1787+
17831788/**
17841789 * Write JSON content to a file asynchronously with formatting.
17851790 * Stringifies the value with configurable indentation and line endings.
0 commit comments