@@ -71,25 +71,18 @@ public struct FileIterator: Sequence, IteratorProtocol {
7171      if  dirIterator !=  nil  { 
7272        output =  nextInDirectory ( ) 
7373      }  else  { 
74-         guard  var  next =  urlIterator. next ( )  else  { 
74+         guard  let  next =  urlIterator. next ( )  else  { 
7575          // If we've reached the end of all the URLs we wanted to iterate over, exit now.
7676          return  nil 
7777        } 
78- 
79-         guard  let  fileType =  fileType ( at:  next)  else  { 
78+         guard  let  ( next,  fileType)  =  fileAndType ( at:  next,  followSymlinks:  followSymlinks)  else  { 
8079          continue 
8180        } 
8281
8382        switch  fileType { 
8483        case  . typeSymbolicLink: 
85-           guard 
86-             followSymlinks, 
87-             let  destination =  try ? FileManager . default. destinationOfSymbolicLink ( atPath:  next. path) 
88-           else  { 
89-             break 
90-           } 
91-           next =  URL ( fileURLWithPath:  destination,  relativeTo:  next) 
92-           fallthrough 
84+           // If we got here, we encountered a symlink but didn't follow it. Skip it.
85+           continue 
9386
9487        case  . typeDirectory: 
9588          dirIterator =  FileManager . default. enumerator ( 
@@ -132,27 +125,20 @@ public struct FileIterator: Sequence, IteratorProtocol {
132125      } 
133126      #endif 
134127
135-       guard  item. lastPathComponent. hasSuffix ( fileSuffix) ,  let  fileType =  fileType ( at:  item)  else  { 
128+       guard  item. lastPathComponent. hasSuffix ( fileSuffix) , 
129+         let  ( item,  fileType)  =  fileAndType ( at:  item,  followSymlinks:  followSymlinks) 
130+       else  { 
136131        continue 
137132      } 
138133
139-       var  path  =  item. path
140134      switch  fileType { 
141-       case  . typeSymbolicLink where  followSymlinks: 
142-         guard 
143-           let  destination =  try ? FileManager . default. destinationOfSymbolicLink ( atPath:  path) 
144-         else  { 
145-           break 
146-         } 
147-         path =  URL ( fileURLWithPath:  destination,  relativeTo:  item) . path
148-         fallthrough 
149- 
150135      case  . typeRegular: 
151136        // We attempt to relativize the URLs based on the current working directory, not the
152137        // directory being iterated over, so that they can be displayed better in diagnostics. Thus,
153138        // if the user passes paths that are relative to the current working directory, they will
154139        // be displayed as relative paths. Otherwise, they will still be displayed as absolute
155140        // paths.
141+         let  path  =  item. path
156142        let  relativePath :  String 
157143        if  !workingDirectory. isRoot,  path. hasPrefix ( workingDirectory. path)  { 
158144          relativePath =  String ( path. dropFirst ( workingDirectory. path. count) . drop ( while:  {  $0 ==  " / "  || $0 ==  #"\"#  } ) ) 
@@ -173,9 +159,41 @@ public struct FileIterator: Sequence, IteratorProtocol {
173159  } 
174160} 
175161
176- /// Returns the type of the file at the given URL.
177- private  func  fileType( at url:  URL )  ->  FileAttributeType ? { 
178-   // We cannot use `URL.resourceValues(forKeys:)` here because it appears to behave incorrectly on
179-   // Linux.
180-   return  try ? FileManager . default. attributesOfItem ( atPath:  url. path) [ . type]  as?  FileAttributeType 
162+ /// Returns the actual URL and type of the file at the given URL, following symlinks if requested.
163+ ///
164+ /// - Parameters:
165+ ///   - url: The URL to get the file and type of.
166+ ///   - followSymlinks: Whether to follow symlinks.
167+ /// - Returns: The actual URL and type of the file at the given URL, or `nil` if the file does not
168+ ///   exist or is not a supported file type. If `followSymlinks` is `true`, the returned URL may be
169+ ///   different from the given URL; otherwise, it will be the same.
170+ private  func  fileAndType( at url:  URL ,  followSymlinks:  Bool )  ->  ( URL ,  FileAttributeType ) ? { 
171+   func  typeOfFile( at url:  URL )  ->  FileAttributeType ? { 
172+     // We cannot use `URL.resourceValues(forKeys:)` here because it appears to behave incorrectly on
173+     // Linux.
174+     return  try ? FileManager . default. attributesOfItem ( atPath:  url. path) [ . type]  as?  FileAttributeType 
175+   } 
176+ 
177+   guard  var  fileType =  typeOfFile ( at:  url)  else  { 
178+     return  nil 
179+   } 
180+ 
181+   // We would use `standardizedFileURL.path` here as we do in the iterator above to ensure that
182+   // path components like `.` and `..` are resolved, but the standardized URLs returned by
183+   // Foundation pre-Swift-6.0 resolve symlinks. This causes the file type of a URL and its
184+   // standardized path to not match.
185+   var  visited :  Set < String >  =  [ url. absoluteString] 
186+   var  url  =  url
187+   while  followSymlinks && fileType ==  . typeSymbolicLink, 
188+     let  destination =  try ? FileManager . default. destinationOfSymbolicLink ( atPath:  url. path) 
189+   { 
190+     url =  URL ( fileURLWithPath:  destination,  relativeTo:  url) 
191+     // If this URL is in the visited set, we must have a symlink cycle. Ignore it gracefully.
192+     guard  !visited. contains ( url. absoluteString) ,  let  newType =  typeOfFile ( at:  url)  else  { 
193+       return  nil 
194+     } 
195+     visited. insert ( url. absoluteString) 
196+     fileType =  newType
197+   } 
198+   return  ( url,  fileType) 
181199} 
0 commit comments