@@ -2,7 +2,7 @@ import * as common from '../common/index.mjs';
22import tmpdir from '../common/tmpdir.js' ;
33import { resolve , dirname , sep , relative , join , isAbsolute } from 'node:path' ;
44import { mkdir , writeFile , symlink , glob as asyncGlob } from 'node:fs/promises' ;
5- import { glob , globSync , Dirent , chmodSync } from 'node:fs' ;
5+ import { glob , globSync , Dirent , chmodSync , mkdirSync , writeFileSync , symlinkSync } from 'node:fs' ;
66import { test , describe } from 'node:test' ;
77import { pathToFileURL } from 'node:url' ;
88import { promisify } from 'node:util' ;
@@ -544,21 +544,66 @@ describe('glob - with restricted directory', function() {
544544 } ) ;
545545} ) ;
546546
547- describe ( 'glob follow ' , ( ) => {
548- test ( 'should return matched files in symlinked directory when follow is true ' , async ( ) => {
547+ describe ( 'glob followSymlinks ' , ( ) => {
548+ test ( 'should not throw ELOOP with followSymlinks on symlink loop (async) ' , async ( ) => {
549549 if ( common . isWindows ) return ;
550- const relFilesPromise = asyncGlob ( '**' , { cwd : fixtureDir , followSymlinks : true } ) ;
551- let count = 0 ;
552- // eslint-disable-next-line no-unused-vars
553- for await ( const file of relFilesPromise ) {
554- count ++ ;
550+ const files = [ ] ;
551+ for await ( const file of asyncGlob ( '**' , { cwd : fixtureDir , followSymlinks : true } ) ) {
552+ files . push ( file ) ;
555553 }
556- assert . ok ( count > 0 ) ;
554+ // Should have results but no infinite loop
555+ assert . ok ( files . length > 0 ) ;
556+ assert . ok ( files . some ( ( f ) => f === 'a/symlink/a/b/c' ) ) ;
557+ assert . ok ( files . some ( ( f ) => f . startsWith ( 'a/symlink/a/b/c/a/' ) ) ) ;
558+ const depth = Math . max ( ...files . filter ( ( f ) => f . startsWith ( 'a/symlink/' ) ) . map ( ( f ) => f . split ( '/' ) . length ) ) ;
559+ assert . ok ( depth < 20 ) ;
557560 } ) ;
558561
559- test ( 'should return matched files in symlinked directory when follow is true (sync)' , ( ) => {
562+ test ( 'should not throw ELOOP with followSymlinks on symlink loop (sync)' , ( ) => {
560563 if ( common . isWindows ) return ;
561- const relFiles = globSync ( '**' , { cwd : fixtureDir , followSymlinks : true } ) ;
562- assert . ok ( relFiles . length > 0 ) ;
564+ const files = globSync ( '**' , { cwd : fixtureDir , followSymlinks : true } ) ;
565+ assert . ok ( files . length > 0 ) ;
566+ assert . ok ( files . some ( ( f ) => f === 'a/symlink/a/b/c' ) ) ;
567+ assert . ok ( files . some ( ( f ) => f . startsWith ( 'a/symlink/a/b/c/a/' ) ) ) ;
568+ const depth = Math . max ( ...files . filter ( ( f ) => f . startsWith ( 'a/symlink/' ) ) . map ( ( f ) => f . split ( '/' ) . length ) ) ;
569+ assert . ok ( depth < 20 ) ;
570+ } ) ;
571+
572+ test ( 'should handle symlinks without cycles (async)' , async ( ) => {
573+ if ( common . isWindows ) return ;
574+ const deepLinkDir = tmpdir . resolve ( 'deep-links' ) ;
575+ await mkdir ( deepLinkDir , { recursive : true } ) ;
576+
577+ await mkdir ( join ( deepLinkDir , 'level1' ) , { recursive : true } ) ;
578+ await mkdir ( join ( deepLinkDir , 'level1' , 'level2' ) , { recursive : true } ) ;
579+ await writeFile ( join ( deepLinkDir , 'level1' , 'level2' , 'file.txt' ) , 'deep' ) ;
580+ await symlink ( 'level1/level2' , join ( deepLinkDir , 'link-to-level2' ) ) ;
581+
582+ const files = [ ] ;
583+ for await ( const file of asyncGlob ( '**/*.txt' , { cwd : deepLinkDir , followSymlinks : true } ) ) {
584+ files . push ( file ) ;
585+ }
586+
587+ assert . ok ( files . some ( ( f ) => f . includes ( 'level1/level2/file.txt' ) ) ) ;
588+ assert . ok ( files . some ( ( f ) => f . includes ( 'link-to-level2/file.txt' ) ) ) ;
589+ } ) ;
590+
591+ test ( 'should handle symlinks without cycles (sync)' , ( ) => {
592+ if ( common . isWindows ) return ;
593+ const deepLinkDir = tmpdir . resolve ( 'deep-links-sync' ) ;
594+ try {
595+ mkdirSync ( deepLinkDir , { recursive : true } ) ;
596+ mkdirSync ( join ( deepLinkDir , 'level1' ) , { recursive : true } ) ;
597+ mkdirSync ( join ( deepLinkDir , 'level1' , 'level2' ) , { recursive : true } ) ;
598+ writeFileSync ( join ( deepLinkDir , 'level1' , 'level2' , 'file.txt' ) , 'deep' ) ;
599+ symlinkSync ( 'level1/level2' , join ( deepLinkDir , 'link-to-level2' ) ) ;
600+
601+ const files = globSync ( '**/*.txt' , { cwd : deepLinkDir , followSymlinks : true } ) ;
602+
603+ assert . ok ( files . some ( ( f ) => f . includes ( 'level1/level2/file.txt' ) ) ) ;
604+ assert . ok ( files . some ( ( f ) => f . includes ( 'link-to-level2/file.txt' ) ) ) ;
605+ } catch {
606+ // Cleanup errors are ok
607+ }
563608 } ) ;
564609} ) ;
0 commit comments