apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsTransferResponseCollector.swift (4586B)


      1 import Foundation
      2 
      3 /// Bounds bytes as they arrive. Header strings are bounded before normalization;
      4 /// no request headers or arbitrary response headers enter the receipt.
      5 final class RadrootsTransferResponseCollector: @unchecked Sendable {
      6     private static let maximumBodyBytes = 65536
      7     private let lock = NSLock()
      8     private var limits: [Int: Int] = [:]
      9     private var bodies: [Int: Data] = [:]
     10     private var counts: [Int: Int] = [:]
     11     private var failures: [Int: RadrootsBackgroundTransferFailure] = [:]
     12 
     13     func register(_ limit: Int, taskIdentifier: Int) {
     14         lock.withLock { limits[taskIdentifier] = min(max(limit, 0), Self.maximumBodyBytes) }
     15     }
     16 
     17     func begin(_ response: URLResponse, taskIdentifier: Int, fallbackLimit: Int) -> Bool {
     18         lock.withLock {
     19             guard let http = response as? HTTPURLResponse else {
     20                 failures[taskIdentifier] = .responseInvalid
     21                 return false
     22             }
     23             let limit = effectiveLimit(taskIdentifier, fallback: fallbackLimit)
     24             let failure = Self.headerFailure(http) ?? (
     25                 response.expectedContentLength > Int64(limit) ? .responseTooLarge : nil
     26             )
     27             if let failure {
     28                 failures[taskIdentifier] = failure
     29             }
     30             return failure == nil
     31         }
     32     }
     33 
     34     /// Returns true when the task must be cancelled, including discard-policy
     35     /// responses that exceed the absolute transport bound.
     36     func append(_ data: Data, taskIdentifier: Int, fallbackLimit: Int) -> Bool {
     37         lock.withLock {
     38             guard failures[taskIdentifier] == nil else { return true }
     39             let limit = effectiveLimit(taskIdentifier, fallback: fallbackLimit)
     40             let count = counts[taskIdentifier] ?? 0
     41             guard data.count <= limit - count else {
     42                 bodies.removeValue(forKey: taskIdentifier)
     43                 failures[taskIdentifier] = .responseTooLarge
     44                 return true
     45             }
     46             counts[taskIdentifier] = count + data.count
     47             if (limits[taskIdentifier] ?? fallbackLimit) > 0 {
     48                 bodies[taskIdentifier, default: Data()].append(data)
     49             }
     50             return false
     51         }
     52     }
     53 
     54     func take(taskIdentifier: Int, response: HTTPURLResponse?,
     55               destinationMismatch: Bool) -> RadrootsBackgroundHTTPResult {
     56         lock.withLock {
     57             let body = bodies.removeValue(forKey: taskIdentifier)
     58             let failure = failures.removeValue(forKey: taskIdentifier) ?? response.flatMap(Self.headerFailure)
     59             limits.removeValue(forKey: taskIdentifier)
     60             counts.removeValue(forKey: taskIdentifier)
     61             let rawType = response?.value(forHTTPHeaderField: "Content-Type")
     62             let mediaType = rawType.flatMap { value -> String? in
     63                 guard value.utf8.count <= 256 else { return nil }
     64                 return try? RadrootsBackgroundTransferValidation.normalizedMediaType(value)
     65             }
     66             let rawEncoding = response?.value(forHTTPHeaderField: "Content-Encoding")
     67             let encoding = rawEncoding.flatMap { value -> String? in
     68                 guard value.utf8.count <= 32 else { return "invalid" }
     69                 return value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
     70             }
     71             return RadrootsBackgroundHTTPResult(
     72                 statusCode: response?.statusCode, mediaType: mediaType, body: body, contentEncoding: encoding,
     73                 bodyExceeded: failure == .responseTooLarge, mediaTypeWasMalformed: rawType != nil && mediaType == nil,
     74                 destinationMismatch: destinationMismatch, headerFailure: failure
     75             )
     76         }
     77     }
     78 
     79     private func effectiveLimit(_ identifier: Int, fallback: Int) -> Int {
     80         let limit = limits[identifier] ?? min(max(fallback, 0), Self.maximumBodyBytes)
     81         return limit == 0 ? Self.maximumBodyBytes : limit
     82     }
     83 
     84     private static func headerFailure(_ response: HTTPURLResponse) -> RadrootsBackgroundTransferFailure? {
     85         if let value = response.value(forHTTPHeaderField: "Content-Encoding"),
     86            value.utf8.count > 32 || value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() != "identity" {
     87             return .responseContentEncoding
     88         }
     89         if let value = response.value(forHTTPHeaderField: "Content-Type"),
     90            value.utf8.count > 256 || (try? RadrootsBackgroundTransferValidation.normalizedMediaType(value)) == nil {
     91             return .responseMediaType
     92         }
     93         return nil
     94     }
     95 }