@@ -71,25 +71,18 @@ public struct FileIterator: Sequence, IteratorProtocol {
71
71
if dirIterator != nil {
72
72
output = nextInDirectory ( )
73
73
} else {
74
- guard var next = urlIterator. next ( ) else {
74
+ guard let next = urlIterator. next ( ) else {
75
75
// If we've reached the end of all the URLs we wanted to iterate over, exit now.
76
76
return nil
77
77
}
78
-
79
- guard let fileType = fileType ( at: next) else {
78
+ guard let ( next, fileType) = fileAndType ( at: next, followSymlinks: followSymlinks) else {
80
79
continue
81
80
}
82
81
83
82
switch fileType {
84
83
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
93
86
94
87
case . typeDirectory:
95
88
dirIterator = FileManager . default. enumerator (
@@ -132,27 +125,20 @@ public struct FileIterator: Sequence, IteratorProtocol {
132
125
}
133
126
#endif
134
127
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 {
136
131
continue
137
132
}
138
133
139
- var path = item. path
140
134
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
-
150
135
case . typeRegular:
151
136
// We attempt to relativize the URLs based on the current working directory, not the
152
137
// directory being iterated over, so that they can be displayed better in diagnostics. Thus,
153
138
// if the user passes paths that are relative to the current working directory, they will
154
139
// be displayed as relative paths. Otherwise, they will still be displayed as absolute
155
140
// paths.
141
+ let path = item. path
156
142
let relativePath : String
157
143
if !workingDirectory. isRoot, path. hasPrefix ( workingDirectory. path) {
158
144
relativePath = String ( path. dropFirst ( workingDirectory. path. count) . drop ( while: { $0 == " / " || $0 == #"\"# } ) )
@@ -173,9 +159,41 @@ public struct FileIterator: Sequence, IteratorProtocol {
173
159
}
174
160
}
175
161
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)
181
199
}
0 commit comments