@@ -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