• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-swift / 19539918465

20 Nov 2025 02:17PM UTC coverage: 80.489% (-0.3%) from 80.835%
19539918465

push

github

web-flow
ci: enable linux test coverage (#856)

* ci: test in Linux

* keep only linux job

* import FoundationNetworking

* fix tests in Linux

* fix(realtime): stabilize channel tests on ci

* test(realtime): disable flaky realtime tests on linux

* revert CI to run all jobs

* fix test

* run tests in parallel and comment out flaky test

* ci: add caches

* disable parallel tests

18 of 48 new or added lines in 1 file covered. (37.5%)

39 existing lines in 3 files now uncovered.

6250 of 7765 relevant lines covered (80.49%)

27.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.67
/Sources/Storage/MultipartFormData.swift
1
// MutlipartFormData extracted from [Alamofire](https://github.com/Alamofire/Alamofire/blob/master/Source/Features/MultipartFormData.swift) for using as standalone.
2

3
//
4
//  MultipartFormData.swift
5
//
6
//  Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
7
//
8
//  Permission is hereby granted, free of charge, to any person obtaining a copy
9
//  of this software and associated documentation files (the "Software"), to deal
10
//  in the Software without restriction, including without limitation the rights
11
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
//  copies of the Software, and to permit persons to whom the Software is
13
//  furnished to do so, subject to the following conditions:
14
//
15
//  The above copyright notice and this permission notice shall be included in
16
//  all copies or substantial portions of the Software.
17
//
18
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
//  THE SOFTWARE.
25
//
26

27
import Foundation
28
import HTTPTypes
29

30
#if canImport(MobileCoreServices)
31
  import MobileCoreServices
32
#elseif canImport(CoreServices)
33
  import CoreServices
34
#endif
35

36
/// Constructs `multipart/form-data` for uploads within an HTTP or HTTPS body. There are currently two ways to encode
37
/// multipart form data. The first way is to encode the data directly in memory. This is very efficient, but can lead
38
/// to memory issues if the dataset is too large. The second way is designed for larger datasets and will write all the
39
/// data to a single file on disk with all the proper boundary segmentation. The second approach MUST be used for
40
/// larger datasets such as video content, otherwise your app may run out of memory when trying to encode the dataset.
41
///
42
/// For more information on `multipart/form-data` in general, please refer to the RFC-2388 and RFC-2045 specs as well
43
/// and the w3 form documentation.
44
///
45
/// - https://www.ietf.org/rfc/rfc2388.txt
46
/// - https://www.ietf.org/rfc/rfc2045.txt
47
/// - https://www.w3.org/TR/html401/interact/forms.html#h-17.13
48
class MultipartFormData {
49
  // MARK: - Helper Types
50

51
  enum EncodingCharacters {
52
    static let crlf = "\r\n"
53
  }
54

55
  enum BoundaryGenerator {
56
    enum BoundaryType {
57
      case initial, encapsulated, final
58
    }
59

60
    static func randomBoundary() -> String {
18✔
61
      let first = UInt32.random(in: UInt32.min...UInt32.max)
18✔
62
      let second = UInt32.random(in: UInt32.min...UInt32.max)
18✔
63

18✔
64
      return String(format: "alamofire.boundary.%08x%08x", first, second)
18✔
65
    }
18✔
66

67
    static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
28✔
68
      let boundaryText =
28✔
69
        switch boundaryType {
28✔
70
        case .initial:
28✔
71
          "--\(boundary)\(EncodingCharacters.crlf)"
9✔
72
        case .encapsulated:
28✔
73
          "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
10✔
74
        case .final:
28✔
75
          "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
9✔
76
        }
28✔
77

28✔
78
      return Data(boundaryText.utf8)
28✔
79
    }
28✔
80
  }
81

82
  class BodyPart {
83
    let headers: HTTPFields
84
    let bodyStream: InputStream
85
    let bodyContentLength: UInt64
86
    var hasInitialBoundary = false
31✔
87
    var hasFinalBoundary = false
31✔
88

89
    init(headers: HTTPFields, bodyStream: InputStream, bodyContentLength: UInt64) {
31✔
90
      self.headers = headers
31✔
91
      self.bodyStream = bodyStream
31✔
92
      self.bodyContentLength = bodyContentLength
31✔
93
    }
31✔
94
  }
95

96
  // MARK: - Properties
97

98
  /// Default memory threshold used when encoding `MultipartFormData`, in bytes.
99
  static let encodingMemoryThreshold: UInt64 = 10_000_000
100

101
  /// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
102
  open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
9✔
103

104
  /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries.
105
  var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
9✔
106

107
  /// The boundary used to separate the body parts in the encoded form data.
108
  let boundary: String
109

110
  let fileManager: FileManager
111

112
  private var bodyParts: [BodyPart]
113
  private var bodyPartError: MultipartFormDataError?
114
  private let streamBufferSize: Int
115

116
  // MARK: - Lifecycle
117

118
  /// Creates an instance.
119
  ///
120
  /// - Parameters:
121
  ///   - fileManager: `FileManager` to use for file operations, if needed.
122
  ///   - boundary: Boundary `String` used to separate body parts.
123
  init(fileManager: FileManager = .default, boundary: String? = nil) {
25✔
124
    self.fileManager = fileManager
25✔
125
    self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
25✔
126
    bodyParts = []
25✔
127

25✔
128
    //
25✔
129
    // The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more
25✔
130
    // information, please refer to the following article:
25✔
131
    //   - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html
25✔
132
    //
25✔
133
    streamBufferSize = 1024
25✔
134
  }
25✔
135

136
  // MARK: - Body Parts
137

138
  /// Creates a body part from the data and appends it to the instance.
139
  ///
140
  /// The body part data will be encoded using the following format:
141
  ///
142
  /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
143
  /// - `Content-Type: #{mimeType}` (HTTP Header)
144
  /// - Encoded file data
145
  /// - Multipart form boundary
146
  ///
147
  /// - Parameters:
148
  ///   - data:     `Data` to encoding into the instance.
149
  ///   - name:     Name to associate with the `Data` in the `Content-Disposition` HTTP header.
150
  ///   - fileName: Filename to associate with the `Data` in the `Content-Disposition` HTTP header.
151
  ///   - mimeType: MIME type to associate with the data in the `Content-Type` HTTP header.
152
  func append(
153
    _ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil
154
  ) {
25✔
155
    let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
25✔
156
    let stream = InputStream(data: data)
25✔
157
    let length = UInt64(data.count)
25✔
158

25✔
159
    append(stream, withLength: length, headers: headers)
25✔
160
  }
25✔
161

162
  /// Creates a body part from the file and appends it to the instance.
163
  ///
164
  /// The body part data will be encoded using the following format:
165
  ///
166
  /// - `Content-Disposition: form-data; name=#{name}; filename=#{generated filename}` (HTTP Header)
167
  /// - `Content-Type: #{generated mimeType}` (HTTP Header)
168
  /// - Encoded file data
169
  /// - Multipart form boundary
170
  ///
171
  /// The filename in the `Content-Disposition` HTTP header is generated from the last path component of the
172
  /// `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the
173
  /// system associated MIME type.
174
  ///
175
  /// - Parameters:
176
  ///   - fileURL: `URL` of the file whose content will be encoded into the instance.
177
  ///   - name:    Name to associate with the file content in the `Content-Disposition` HTTP header.
178
  func append(_ fileURL: URL, withName name: String) {
6✔
179
    let fileName = fileURL.lastPathComponent
6✔
180
    let pathExtension = fileURL.pathExtension
6✔
181

6✔
182
    if !fileName.isEmpty, !pathExtension.isEmpty {
6✔
183
      let mime = MultipartFormData.mimeType(forPathExtension: pathExtension)
5✔
184
      append(fileURL, withName: name, fileName: fileName, mimeType: mime)
5✔
185
    } else {
5✔
186
      setBodyPartError(.bodyPartFilenameInvalid(in: fileURL))
1✔
187
    }
1✔
188
  }
6✔
189

190
  /// Creates a body part from the file and appends it to the instance.
191
  ///
192
  /// The body part data will be encoded using the following format:
193
  ///
194
  /// - Content-Disposition: form-data; name=#{name}; filename=#{filename} (HTTP Header)
195
  /// - Content-Type: #{mimeType} (HTTP Header)
196
  /// - Encoded file data
197
  /// - Multipart form boundary
198
  ///
199
  /// - Parameters:
200
  ///   - fileURL:  `URL` of the file whose content will be encoded into the instance.
201
  ///   - name:     Name to associate with the file content in the `Content-Disposition` HTTP header.
202
  ///   - fileName: Filename to associate with the file content in the `Content-Disposition` HTTP header.
203
  ///   - mimeType: MIME type to associate with the file content in the `Content-Type` HTTP header.
204
  func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) {
7✔
205
    let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
7✔
206

7✔
207
    //============================================================
7✔
208
    //                 Check 1 - is file URL?
7✔
209
    //============================================================
7✔
210

7✔
211
    guard fileURL.isFileURL else {
7✔
212
      setBodyPartError(.bodyPartURLInvalid(url: fileURL))
1✔
213
      return
1✔
214
    }
6✔
215

6✔
216
    //============================================================
6✔
217
    //              Check 2 - is file URL reachable?
6✔
218
    //============================================================
6✔
219

6✔
220
    #if !(os(Linux) || os(Windows) || os(Android))
221
      do {
6✔
222
        let isReachable = try fileURL.checkPromisedItemIsReachable()
6✔
223
        guard isReachable else {
5✔
224
          setBodyPartError(.bodyPartFileNotReachable(at: fileURL))
×
225
          return
×
226
        }
5✔
227
      } catch {
5✔
228
        setBodyPartError(.bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
1✔
229
        return
1✔
230
      }
5✔
231
    #endif
232

5✔
233
    //============================================================
5✔
234
    //            Check 3 - is file URL a directory?
5✔
235
    //============================================================
5✔
236

5✔
237
    var isDirectory: ObjCBool = false
5✔
238
    let path = fileURL.path
5✔
239

5✔
240
    guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory), !isDirectory.boolValue
5✔
241
    else {
5✔
242
      setBodyPartError(.bodyPartFileIsDirectory(at: fileURL))
×
243
      return
×
244
    }
5✔
245

5✔
246
    //============================================================
5✔
247
    //          Check 4 - can the file size be extracted?
5✔
248
    //============================================================
5✔
249

5✔
250
    let bodyContentLength: UInt64
5✔
251

5✔
252
    do {
5✔
253
      guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else {
5✔
254
        setBodyPartError(.bodyPartFileSizeNotAvailable(at: fileURL))
×
255
        return
×
256
      }
5✔
257

5✔
258
      bodyContentLength = fileSize.uint64Value
5✔
259
    } catch {
5✔
260
      setBodyPartError(.bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
×
261
      return
×
262
    }
5✔
263

5✔
264
    //============================================================
5✔
265
    //       Check 5 - can a stream be created from file URL?
5✔
266
    //============================================================
5✔
267

5✔
268
    guard let stream = InputStream(url: fileURL) else {
5✔
269
      setBodyPartError(.bodyPartInputStreamCreationFailed(for: fileURL))
×
270
      return
×
271
    }
5✔
272

5✔
273
    append(stream, withLength: bodyContentLength, headers: headers)
5✔
274
  }
5✔
275

276
  /// Creates a body part from the stream and appends it to the instance.
277
  ///
278
  /// The body part data will be encoded using the following format:
279
  ///
280
  /// - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header)
281
  /// - `Content-Type: #{mimeType}` (HTTP Header)
282
  /// - Encoded stream data
283
  /// - Multipart form boundary
284
  ///
285
  /// - Parameters:
286
  ///   - stream:   `InputStream` to encode into the instance.
287
  ///   - length:   Length, in bytes, of the stream.
288
  ///   - name:     Name to associate with the stream content in the `Content-Disposition` HTTP header.
289
  ///   - fileName: Filename to associate with the stream content in the `Content-Disposition` HTTP header.
290
  ///   - mimeType: MIME type to associate with the stream content in the `Content-Type` HTTP header.
291
  func append(
292
    _ stream: InputStream,
293
    withLength length: UInt64,
294
    name: String,
295
    fileName: String,
296
    mimeType: String
297
  ) {
1✔
298
    let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
1✔
299
    append(stream, withLength: length, headers: headers)
1✔
300
  }
1✔
301

302
  /// Creates a body part with the stream, length, and headers and appends it to the instance.
303
  ///
304
  /// The body part data will be encoded using the following format:
305
  ///
306
  /// - HTTP headers
307
  /// - Encoded stream data
308
  /// - Multipart form boundary
309
  ///
310
  /// - Parameters:
311
  ///   - stream:  `InputStream` to encode into the instance.
312
  ///   - length:  Length, in bytes, of the stream.
313
  ///   - headers: `HTTPHeaders` for the body part.
314
  func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPFields) {
31✔
315
    let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
31✔
316
    bodyParts.append(bodyPart)
31✔
317
  }
31✔
318

319
  // MARK: - Data Encoding
320

321
  /// Encodes all appended body parts into a single `Data` value.
322
  ///
323
  /// - Note: This method will load all the appended body parts into memory all at the same time. This method should
324
  ///         only be used when the encoded data will have a small memory footprint. For large data cases, please use
325
  ///         the `writeEncodedData(to:))` method.
326
  ///
327
  /// - Returns: The encoded `Data`, if encoding is successful.
328
  /// - Throws:  An `AFError` if encoding encounters an error.
329
  func encode() throws -> Data {
12✔
330
    if let bodyPartError {
12✔
331
      throw bodyPartError
3✔
332
    }
9✔
333

9✔
334
    var encoded = Data()
9✔
335

9✔
336
    bodyParts.first?.hasInitialBoundary = true
9✔
337
    bodyParts.last?.hasFinalBoundary = true
9✔
338

9✔
339
    for bodyPart in bodyParts {
18✔
340
      let encodedData = try encode(bodyPart)
18✔
341
      encoded.append(encodedData)
18✔
342
    }
18✔
343

9✔
344
    return encoded
9✔
345
  }
12✔
346

347
  /// Writes all appended body parts to the given file `URL`.
348
  ///
349
  /// This process is facilitated by reading and writing with input and output streams, respectively. Thus,
350
  /// this approach is very memory efficient and should be used for large body part data.
351
  ///
352
  /// - Parameter fileURL: File `URL` to which to write the form data.
353
  /// - Throws:            An `AFError` if encoding encounters an error.
354
  func writeEncodedData(to fileURL: URL) throws {
3✔
355
    if let bodyPartError {
3✔
356
      throw bodyPartError
×
357
    }
3✔
358

3✔
359
    if fileManager.fileExists(atPath: fileURL.path) {
3✔
360
      throw MultipartFormDataError.outputStreamFileAlreadyExists(at: fileURL)
1✔
361
    } else if !fileURL.isFileURL {
2✔
362
      throw MultipartFormDataError.outputStreamURLInvalid(url: fileURL)
1✔
363
    }
1✔
364

1✔
365
    guard let outputStream = OutputStream(url: fileURL, append: false) else {
1✔
366
      throw MultipartFormDataError.outputStreamCreationFailed(for: fileURL)
×
367
    }
1✔
368

1✔
369
    outputStream.open()
1✔
370
    defer { outputStream.close() }
1✔
371

1✔
372
    bodyParts.first?.hasInitialBoundary = true
1✔
373
    bodyParts.last?.hasFinalBoundary = true
1✔
374

1✔
375
    for bodyPart in bodyParts {
1✔
376
      try write(bodyPart, to: outputStream)
1✔
377
    }
1✔
378
  }
1✔
379

380
  // MARK: - Private - Body Part Encoding
381

382
  private func encode(_ bodyPart: BodyPart) throws -> Data {
18✔
383
    var encoded = Data()
18✔
384

18✔
385
    let initialData =
18✔
386
      bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
18✔
387
    encoded.append(initialData)
18✔
388

18✔
389
    let headerData = encodeHeaders(for: bodyPart)
18✔
390
    encoded.append(headerData)
18✔
391

18✔
392
    let bodyStreamData = try encodeBodyStream(for: bodyPart)
18✔
393
    encoded.append(bodyStreamData)
18✔
394

18✔
395
    if bodyPart.hasFinalBoundary {
18✔
396
      encoded.append(finalBoundaryData())
8✔
397
    }
8✔
398

18✔
399
    return encoded
18✔
400
  }
18✔
401

402
  private func encodeHeaders(for bodyPart: BodyPart) -> Data {
19✔
403
    let headerText =
19✔
404
      bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
28✔
405
      .joined()
19✔
406
      + EncodingCharacters.crlf
19✔
407

19✔
408
    return Data(headerText.utf8)
19✔
409
  }
19✔
410

411
  private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data {
18✔
412
    let inputStream = bodyPart.bodyStream
18✔
413
    inputStream.open()
18✔
414
    defer { inputStream.close() }
18✔
415

18✔
416
    var encoded = Data()
18✔
417

18✔
418
    while inputStream.hasBytesAvailable {
1,087✔
419
      var buffer = [UInt8](repeating: 0, count: streamBufferSize)
1,072✔
420
      let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize)
1,072✔
421

1,072✔
422
      if bytesRead < 0 {
1,072✔
NEW
423
        if let error = inputStream.streamError {
×
NEW
424
          throw MultipartFormDataError.inputStreamReadFailed(error: error)
×
NEW
425
        } else {
×
NEW
426
          throw MultipartFormDataError.inputStreamReadFailed(
×
NEW
427
            error: MultipartFormDataError.UnexpectedInputStreamLength(
×
NEW
428
              bytesExpected: bodyPart.bodyContentLength,
×
NEW
429
              bytesRead: UInt64(encoded.count)
×
NEW
430
            )
×
NEW
431
          )
×
NEW
432
        }
×
433
      }
1,072✔
434

1,072✔
435
      if bytesRead > 0 {
1,072✔
436
        encoded.append(buffer, count: bytesRead)
1,069✔
437
      } else {
1,069✔
438
        break
3✔
439
      }
1,069✔
440
    }
1,069✔
441

18✔
442
    guard UInt64(encoded.count) == bodyPart.bodyContentLength else {
18✔
443
      let error = MultipartFormDataError.UnexpectedInputStreamLength(
×
444
        bytesExpected: bodyPart.bodyContentLength,
×
445
        bytesRead: UInt64(encoded.count)
×
446
      )
×
447
      throw MultipartFormDataError.inputStreamReadFailed(error: error)
×
448
    }
18✔
449

18✔
450
    return encoded
18✔
451
  }
18✔
452

453
  // MARK: - Private - Writing Body Part to Output Stream
454

455
  private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws {
1✔
456
    try writeInitialBoundaryData(for: bodyPart, to: outputStream)
1✔
457
    try writeHeaderData(for: bodyPart, to: outputStream)
1✔
458
    try writeBodyStream(for: bodyPart, to: outputStream)
1✔
459
    try writeFinalBoundaryData(for: bodyPart, to: outputStream)
1✔
460
  }
1✔
461

462
  private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream)
463
    throws
464
  {
1✔
465
    let initialData =
1✔
466
      bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
1✔
467
    return try write(initialData, to: outputStream)
1✔
468
  }
1✔
469

470
  private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
1✔
471
    let headerData = encodeHeaders(for: bodyPart)
1✔
472
    return try write(headerData, to: outputStream)
1✔
473
  }
1✔
474

475
  private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
1✔
476
    let inputStream = bodyPart.bodyStream
1✔
477

1✔
478
    inputStream.open()
1✔
479
    defer { inputStream.close() }
1✔
480

1✔
481
    var bytesLeftToRead = bodyPart.bodyContentLength
1✔
482
    while inputStream.hasBytesAvailable, bytesLeftToRead > 0 {
2✔
483
      let bufferSize = min(streamBufferSize, Int(bytesLeftToRead))
1✔
484
      var buffer = [UInt8](repeating: 0, count: bufferSize)
1✔
485
      let bytesRead = inputStream.read(&buffer, maxLength: bufferSize)
1✔
486
      if bytesRead < 0 {
1✔
NEW
487
        if let streamError = inputStream.streamError {
×
NEW
488
          throw MultipartFormDataError.inputStreamReadFailed(error: streamError)
×
NEW
489
        } else {
×
NEW
490
          throw MultipartFormDataError.inputStreamReadFailed(
×
NEW
491
            error: MultipartFormDataError.UnexpectedInputStreamLength(
×
NEW
492
              bytesExpected: bodyPart.bodyContentLength,
×
NEW
493
              bytesRead: bodyPart.bodyContentLength - bytesLeftToRead
×
NEW
494
            )
×
NEW
495
          )
×
NEW
496
        }
×
497
      }
1✔
498

1✔
499
      if bytesRead > 0 {
1✔
500
        if buffer.count != bytesRead {
1✔
501
          buffer = Array(buffer[0..<bytesRead])
×
502
        }
×
503

1✔
504
        try write(&buffer, to: outputStream)
1✔
505
        bytesLeftToRead -= UInt64(bytesRead)
1✔
506
      } else {
1✔
507
        break
×
508
      }
1✔
509
    }
1✔
510
  }
1✔
511

512
  private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws
513
  {
1✔
514
    if bodyPart.hasFinalBoundary {
1✔
515
      try write(finalBoundaryData(), to: outputStream)
1✔
516
    }
1✔
517
  }
1✔
518

519
  // MARK: - Private - Writing Buffered Data to Output Stream
520

521
  private func write(_ data: Data, to outputStream: OutputStream) throws {
3✔
522
    var buffer = [UInt8](repeating: 0, count: data.count)
3✔
523
    data.copyBytes(to: &buffer, count: data.count)
3✔
524

3✔
525
    return try write(&buffer, to: outputStream)
3✔
526
  }
3✔
527

528
  private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws {
4✔
529
    var bytesToWrite = buffer.count
4✔
530

4✔
531
    while bytesToWrite > 0, outputStream.hasSpaceAvailable {
8✔
532
      let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite)
4✔
533

4✔
534
      if bytesWritten < 0 {
4✔
NEW
535
        if let error = outputStream.streamError {
×
NEW
536
          throw MultipartFormDataError.outputStreamWriteFailed(error: error)
×
NEW
537
        } else {
×
NEW
538
          throw MultipartFormDataError.outputStreamWriteFailed(
×
NEW
539
            error: MultipartFormDataError.UnexpectedInputStreamLength(
×
NEW
540
              bytesExpected: UInt64(buffer.count),
×
NEW
541
              bytesRead: UInt64(buffer.count - bytesToWrite)
×
NEW
542
            )
×
NEW
543
          )
×
NEW
544
        }
×
545
      }
4✔
546

4✔
547
      bytesToWrite -= bytesWritten
4✔
548

4✔
549
      if bytesToWrite > 0 {
4✔
550
        buffer = Array(buffer[bytesWritten..<buffer.count])
×
551
      }
×
552
    }
4✔
553
  }
4✔
554

555
  // MARK: - Private - Content Headers
556

557
  private func contentHeaders(
558
    withName name: String, fileName: String? = nil, mimeType: String? = nil
559
  ) -> HTTPFields {
33✔
560
    var disposition = "form-data; name=\"\(name)\""
33✔
561
    if let fileName { disposition += "; filename=\"\(fileName)\"" }
33✔
562

33✔
563
    var headers: HTTPFields = [.contentDisposition: disposition]
33✔
564
    if let mimeType { headers[.contentType] = mimeType }
33✔
565

33✔
566
    return headers
33✔
567
  }
33✔
568

569
  // MARK: - Private - Boundary Encoding
570

571
  private func initialBoundaryData() -> Data {
9✔
572
    BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary)
9✔
573
  }
9✔
574

575
  private func encapsulatedBoundaryData() -> Data {
10✔
576
    BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary)
10✔
577
  }
10✔
578

579
  private func finalBoundaryData() -> Data {
9✔
580
    BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary)
9✔
581
  }
9✔
582

583
  // MARK: - Private - Errors
584

585
  private func setBodyPartError(_ error: MultipartFormDataError) {
3✔
586
    guard bodyPartError == nil else { return }
3✔
587
    bodyPartError = error
3✔
588
  }
3✔
589
}
590

591
#if canImport(UniformTypeIdentifiers)
592
  import UniformTypeIdentifiers
593

594
  extension MultipartFormData {
595
    // MARK: - Private - Mime Type
596

597
    static func mimeType(forPathExtension pathExtension: String) -> String {
5✔
598
      #if swift(>=5.9)
599
        if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, visionOS 1, *) {
5✔
600
          return UTType(filenameExtension: pathExtension)?.preferredMIMEType
5✔
601
            ?? "application/octet-stream"
5✔
602
        } else {
5✔
603
          if let id = UTTypeCreatePreferredIdentifierForTag(
×
604
            kUTTagClassFilenameExtension, pathExtension as CFString, nil
×
605
          )?.takeRetainedValue(),
×
606
            let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
×
607
              .takeRetainedValue()
×
608
          {
×
609
            return contentType as String
×
610
          }
×
611

×
612
          return "application/octet-stream"
×
613
        }
×
614
      #else
615
        if #available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) {
616
          return UTType(filenameExtension: pathExtension)?.preferredMIMEType
617
            ?? "application/octet-stream"
618
        } else {
619
          if let id = UTTypeCreatePreferredIdentifierForTag(
620
            kUTTagClassFilenameExtension, pathExtension as CFString, nil
621
          )?.takeRetainedValue(),
622
            let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
623
              .takeRetainedValue()
624
          {
625
            return contentType as String
626
          }
627

628
          return "application/octet-stream"
629
        }
630
      #endif
631
    }
5✔
632
  }
633

634
#else
635

636
  extension MultipartFormData {
637
    // MARK: - Private - Mime Type
638

639
    static func mimeType(forPathExtension pathExtension: String) -> String {
640
      #if canImport(CoreServices) || canImport(MobileCoreServices)
641
        if let id = UTTypeCreatePreferredIdentifierForTag(
642
          kUTTagClassFilenameExtension, pathExtension as CFString, nil
643
        )?.takeRetainedValue(),
644
          let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
645
            .takeRetainedValue()
646
        {
647
          return contentType as String
648
        }
649
      #endif
650

651
      return "application/octet-stream"
652
    }
653
  }
654

655
#endif
656

657
enum MultipartFormDataError: Error {
658
  case bodyPartURLInvalid(url: URL)
659
  case bodyPartFilenameInvalid(in: URL)
660
  case bodyPartFileNotReachable(at: URL)
661
  case bodyPartFileNotReachableWithError(atURL: URL, error: any Error)
662
  case bodyPartFileIsDirectory(at: URL)
663
  case bodyPartFileSizeNotAvailable(at: URL)
664
  case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: any Error)
665
  case bodyPartInputStreamCreationFailed(for: URL)
666
  case outputStreamFileAlreadyExists(at: URL)
667
  case outputStreamURLInvalid(url: URL)
668
  case outputStreamCreationFailed(for: URL)
669
  case inputStreamReadFailed(error: any Error)
670
  case outputStreamWriteFailed(error: any Error)
671

672
  struct UnexpectedInputStreamLength: Error {
673
    let bytesExpected: UInt64
674
    let bytesRead: UInt64
675
  }
676

677
  var underlyingError: (any Error)? {
2✔
678
    switch self {
2✔
679
    case .bodyPartFileNotReachableWithError(_, let error),
2✔
680
      .bodyPartFileSizeQueryFailedWithError(_, let error),
1✔
681
      .inputStreamReadFailed(let error),
1✔
682
      .outputStreamWriteFailed(let error):
1✔
683
      error
1✔
684

2✔
685
    case .bodyPartURLInvalid,
2✔
686
      .bodyPartFilenameInvalid,
1✔
687
      .bodyPartFileNotReachable,
1✔
688
      .bodyPartFileIsDirectory,
1✔
689
      .bodyPartFileSizeNotAvailable,
1✔
690
      .bodyPartInputStreamCreationFailed,
1✔
691
      .outputStreamFileAlreadyExists,
1✔
692
      .outputStreamURLInvalid,
1✔
693
      .outputStreamCreationFailed:
1✔
694
      nil
1✔
695
    }
2✔
696
  }
2✔
697

698
  var url: URL? {
2✔
699
    switch self {
2✔
700
    case .bodyPartURLInvalid(let url),
2✔
701
      .bodyPartFilenameInvalid(let url),
1✔
702
      .bodyPartFileNotReachable(let url),
1✔
703
      .bodyPartFileNotReachableWithError(let url, _),
1✔
704
      .bodyPartFileIsDirectory(let url),
1✔
705
      .bodyPartFileSizeNotAvailable(let url),
1✔
706
      .bodyPartFileSizeQueryFailedWithError(let url, _),
1✔
707
      .bodyPartInputStreamCreationFailed(let url),
1✔
708
      .outputStreamFileAlreadyExists(let url),
1✔
709
      .outputStreamURLInvalid(let url),
1✔
710
      .outputStreamCreationFailed(let url):
1✔
711
      url
1✔
712

2✔
713
    case .inputStreamReadFailed, .outputStreamWriteFailed:
2✔
714
      nil
1✔
715
    }
2✔
716
  }
2✔
717
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc