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