@@ -16,6 +16,10 @@ public import Foundation
16
16
private import UniformTypeIdentifiers
17
17
#endif
18
18
19
+ #if !SWT_NO_PROCESS_SPAWNING && os(Windows)
20
+ private import WinSDK
21
+ #endif
22
+
19
23
#if !SWT_NO_FILE_IO
20
24
extension URL {
21
25
/// The file system path of the URL, equivalent to `path`.
@@ -32,17 +36,13 @@ extension URL {
32
36
}
33
37
}
34
38
35
- #if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
36
- @available ( _uttypesAPI, * )
37
- extension UTType {
38
- /// A type that represents a `.tgz` archive, or `nil` if the system does not
39
- /// recognize that content type.
40
- fileprivate static let tgz = UTType ( " org.gnu.gnu-zip-tar-archive " )
41
- }
42
- #endif
43
-
44
39
@_spi ( Experimental)
45
40
extension Attachment where AttachableValue == Data {
41
+ #if SWT_TARGET_OS_APPLE
42
+ /// An operation queue to use for asynchronously reading data from disk.
43
+ private static let _operationQueue = OperationQueue ( )
44
+ #endif
45
+
46
46
/// Initialize an instance of this type with the contents of the given URL.
47
47
///
48
48
/// - Parameters:
@@ -65,8 +65,6 @@ extension Attachment where AttachableValue == Data {
65
65
throw CocoaError ( . featureUnsupported, userInfo: [ NSLocalizedDescriptionKey: " Attaching downloaded files is not supported " ] )
66
66
}
67
67
68
- // FIXME: use NSFileCoordinator on Darwin?
69
-
70
68
let url = url. resolvingSymlinksInPath ( )
71
69
let isDirectory = try url. resourceValues ( forKeys: [ . isDirectoryKey] ) . isDirectory!
72
70
@@ -83,79 +81,178 @@ extension Attachment where AttachableValue == Data {
83
81
// Ensure the preferred name of the archive has an appropriate extension.
84
82
preferredName = {
85
83
#if SWT_TARGET_OS_APPLE && canImport(UniformTypeIdentifiers)
86
- if #available( _uttypesAPI, * ) , let tgz = UTType . tgz {
87
- return ( preferredName as NSString ) . appendingPathExtension ( for: tgz )
84
+ if #available( _uttypesAPI, * ) {
85
+ return ( preferredName as NSString ) . appendingPathExtension ( for: . zip )
88
86
}
89
87
#endif
90
- return ( preferredName as NSString ) . appendingPathExtension ( " tgz " ) ?? preferredName
88
+ return ( preferredName as NSString ) . appendingPathExtension ( " zip " ) ?? preferredName
91
89
} ( )
90
+ }
92
91
93
- try await self . init ( Data ( compressedContentsOfDirectoryAt: url) , named: preferredName, sourceLocation: sourceLocation)
92
+ #if SWT_TARGET_OS_APPLE
93
+ let data : Data = try await withCheckedThrowingContinuation { continuation in
94
+ let fileCoordinator = NSFileCoordinator ( )
95
+ let fileAccessIntent = NSFileAccessIntent . readingIntent ( with: url, options: [ . forUploading] )
96
+
97
+ fileCoordinator. coordinate ( with: [ fileAccessIntent] , queue: Self . _operationQueue) { error in
98
+ let result = Result {
99
+ if let error {
100
+ throw error
101
+ }
102
+ return try Data ( contentsOf: fileAccessIntent. url, options: [ . mappedIfSafe] )
103
+ }
104
+ continuation. resume ( with: result)
105
+ }
106
+ }
107
+ #else
108
+ let data = if isDirectory {
109
+ try await _compressContentsOfDirectory ( at: url)
94
110
} else {
95
111
// Load the file.
96
- try self . init ( Data ( contentsOf: url, options: [ . mappedIfSafe] ) , named : preferredName , sourceLocation : sourceLocation )
112
+ try Data ( contentsOf: url, options: [ . mappedIfSafe] )
97
113
}
114
+ #endif
115
+
116
+ self . init ( data, named: preferredName, sourceLocation: sourceLocation)
98
117
}
99
118
}
100
119
101
- // MARK: - Attaching directories
120
+ #if !SWT_NO_PROCESS_SPAWNING && os(Windows)
121
+ /// The filename of the archiver tool.
122
+ private let _archiverName = " tar.exe "
102
123
103
- extension Data {
104
- /// Initialize an instance of this type by compressing the contents of a
105
- /// directory.
106
- ///
107
- /// - Parameters:
108
- /// - directoryURL: A URL referring to the directory to attach.
109
- ///
110
- /// - Throws: Any error encountered trying to compress the directory, or if
111
- /// directories cannot be compressed on this platform.
112
- ///
113
- /// This initializer asynchronously compresses the contents of `directoryURL`
114
- /// into an archive (currently of `.tgz` format, although this is subject to
115
- /// change) and stores a mapped copy of that archive.
116
- init ( compressedContentsOfDirectoryAt directoryURL: URL ) async throws {
117
- let temporaryName = " \( UUID ( ) . uuidString) .tgz "
118
- let temporaryURL = FileManager . default. temporaryDirectory. appendingPathComponent ( temporaryName)
124
+ /// The path to the archiver tool.
125
+ ///
126
+ /// This path refers to a file (named `_archiverName`) within the `"System32"`
127
+ /// folder of the current system, which is not always located in `"C:\Windows."`
128
+ ///
129
+ /// If the path cannot be determined, the value of this property is `nil`.
130
+ private let _archiverPath : String ? = {
131
+ let bufferCount = GetSystemDirectoryW ( nil , 0 )
132
+ guard bufferCount > 0 else {
133
+ return nil
134
+ }
135
+
136
+ return withUnsafeTemporaryAllocation ( of: wchar_t. self, capacity: Int ( bufferCount) ) { buffer -> String ? in
137
+ let bufferCount = GetSystemDirectoryW ( buffer. baseAddress!, UINT ( buffer. count) )
138
+ guard bufferCount > 0 && bufferCount < buffer. count else {
139
+ return nil
140
+ }
119
141
142
+ return _archiverName. withCString ( encodedAs: UTF16 . self) { archiverName -> String ? in
143
+ var result : UnsafeMutablePointer < wchar_t > ?
144
+
145
+ let flags = ULONG ( PATHCCH_ALLOW_LONG_PATHS . rawValue)
146
+ guard S_OK == PathAllocCombine ( buffer. baseAddress!, archiverName, flags, & result) else {
147
+ return nil
148
+ }
149
+ defer {
150
+ LocalFree ( result)
151
+ }
152
+
153
+ return result. flatMap { String . decodeCString ( $0, as: UTF16 . self) ? . result }
154
+ }
155
+ }
156
+ } ( )
157
+ #endif
158
+
159
+ /// Compress the contents of a directory to an archive, then map that archive
160
+ /// back into memory.
161
+ ///
162
+ /// - Parameters:
163
+ /// - directoryURL: A URL referring to the directory to attach.
164
+ ///
165
+ /// - Returns: An instance of `Data` containing the compressed contents of the
166
+ /// given directory.
167
+ ///
168
+ /// - Throws: Any error encountered trying to compress the directory, or if
169
+ /// directories cannot be compressed on this platform.
170
+ ///
171
+ /// This function asynchronously compresses the contents of `directoryURL` into
172
+ /// an archive (currently of `.zip` format, although this is subject to change.)
173
+ private func _compressContentsOfDirectory( at directoryURL: URL ) async throws -> Data {
120
174
#if !SWT_NO_PROCESS_SPAWNING
121
- #if os(Windows)
122
- let tarPath = #"C:\Windows\System32\tar.exe"#
175
+ let temporaryName = " \( UUID ( ) . uuidString) .zip "
176
+ let temporaryURL = FileManager . default. temporaryDirectory. appendingPathComponent ( temporaryName)
177
+ defer {
178
+ try ? FileManager ( ) . removeItem ( at: temporaryURL)
179
+ }
180
+
181
+ // The standard version of tar(1) does not (appear to) support writing PKZIP
182
+ // archives. FreeBSD's (AKA bsdtar) was long ago rebased atop libarchive and
183
+ // knows how to write PKZIP archives, while Windows inherited FreeBSD's tar
184
+ // tool in Windows 10 Build 17063 (per https://techcommunity.microsoft.com/blog/containers/tar-and-curl-come-to-windows/382409).
185
+ //
186
+ // On Linux (which does not have FreeBSD's version of tar(1)), we can use
187
+ // zip(1) instead.
188
+ #if os(Linux)
189
+ let archiverPath = " /usr/bin/zip "
190
+ #elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
191
+ let archiverPath = " /usr/bin/tar "
192
+ #elseif os(Windows)
193
+ guard let archiverPath = _archiverPath else {
194
+ throw CocoaError ( . fileWriteUnknown, userInfo: [
195
+ NSLocalizedDescriptionKey: " Could not determine the path to ' \( _archiverName) '. " ,
196
+ ] )
197
+ }
123
198
#else
124
- let tarPath = " /usr/bin/tar "
199
+ #warning("Platform-specific implementation missing: tar or zip tool unavailable")
200
+ let archiverPath = " "
201
+ throw CocoaError ( . featureUnsupported, userInfo: [ NSLocalizedDescriptionKey: " This platform does not support attaching directories to tests. " ] )
125
202
#endif
203
+
204
+ try await withCheckedThrowingContinuation { continuation in
205
+ let process = Process ( )
206
+
207
+ process. executableURL = URL ( fileURLWithPath: archiverPath, isDirectory: false )
208
+
126
209
let sourcePath = directoryURL. fileSystemPath
127
210
let destinationPath = temporaryURL. fileSystemPath
128
- defer {
129
- try ? FileManager ( ) . removeItem ( at: temporaryURL)
130
- }
211
+ #if os(Linux)
212
+ // The zip command constructs relative paths from the current working
213
+ // directory rather than from command-line arguments.
214
+ process. arguments = [ destinationPath, " --recurse-paths " , " . " ]
215
+ process. currentDirectoryURL = directoryURL
216
+ #elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
217
+ process. arguments = [ " --create " , " --auto-compress " , " --directory " , sourcePath, " --file " , destinationPath, " . " ]
218
+ #elseif os(Windows)
219
+ // The Windows version of bsdtar can handle relative paths for other archive
220
+ // formats, but produces empty archives when inferring the zip format with
221
+ // --auto-compress, so archive with absolute paths here.
222
+ //
223
+ // An alternative may be to use PowerShell's Compress-Archive command,
224
+ // however that comes with a security risk as we'd be responsible for two
225
+ // levels of command-line argument escaping.
226
+ process. arguments = [ " --create " , " --auto-compress " , " --file " , destinationPath, sourcePath]
227
+ #endif
131
228
132
- try await withCheckedThrowingContinuation { continuation in
133
- do {
134
- _ = try Process . run (
135
- URL ( fileURLWithPath: tarPath, isDirectory: false ) ,
136
- arguments: [ " --create " , " --gzip " , " --directory " , sourcePath, " --file " , destinationPath, " . " ]
137
- ) { process in
138
- let terminationReason = process. terminationReason
139
- let terminationStatus = process. terminationStatus
140
- if terminationReason == . exit && terminationStatus == EXIT_SUCCESS {
141
- continuation. resume ( )
142
- } else {
143
- let error = CocoaError ( . fileWriteUnknown, userInfo: [
144
- NSLocalizedDescriptionKey: " The directory at ' \( sourcePath) ' could not be compressed. " ,
145
- ] )
146
- continuation. resume ( throwing: error)
147
- }
148
- }
149
- } catch {
229
+ process. standardOutput = nil
230
+ process. standardError = nil
231
+
232
+ process. terminationHandler = { process in
233
+ let terminationReason = process. terminationReason
234
+ let terminationStatus = process. terminationStatus
235
+ if terminationReason == . exit && terminationStatus == EXIT_SUCCESS {
236
+ continuation. resume ( )
237
+ } else {
238
+ let error = CocoaError ( . fileWriteUnknown, userInfo: [
239
+ NSLocalizedDescriptionKey: " The directory at ' \( sourcePath) ' could not be compressed ( \( terminationStatus) ). " ,
240
+ ] )
150
241
continuation. resume ( throwing: error)
151
242
}
152
243
}
153
244
154
- try self . init ( contentsOf: temporaryURL, options: [ . mappedIfSafe] )
245
+ do {
246
+ try process. run ( )
247
+ } catch {
248
+ continuation. resume ( throwing: error)
249
+ }
250
+ }
251
+
252
+ return try Data ( contentsOf: temporaryURL, options: [ . mappedIfSafe] )
155
253
#else
156
- throw CocoaError ( . featureUnsupported, userInfo: [ NSLocalizedDescriptionKey: " This platform does not support attaching directories to tests. " ] )
254
+ throw CocoaError ( . featureUnsupported, userInfo: [ NSLocalizedDescriptionKey: " This platform does not support attaching directories to tests. " ] )
157
255
#endif
158
- }
159
256
}
160
257
#endif
161
258
#endif
0 commit comments