@@ -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,37 @@ 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+ var visited : Set < String > = [ url. standardizedFileURL. path]
182+ var url = url
183+ while followSymlinks && fileType == . typeSymbolicLink,
184+ let destination = try ? FileManager . default. destinationOfSymbolicLink ( atPath: url. path)
185+ {
186+ url = URL ( fileURLWithPath: destination, relativeTo: url)
187+ // If this URL is in the visited set, we must have a symlink cycle. Ignore it gracefully.
188+ guard !visited. contains ( url. standardizedFileURL. path) , let newType = typeOfFile ( at: url) else {
189+ return nil
190+ }
191+ visited. insert ( url. standardizedFileURL. path)
192+ fileType = newType
193+ }
194+ return ( url, fileType)
181195}
0 commit comments