123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- //
- // RequestCompression.swift
- //
- // Copyright (c) 2023 Alamofire Software Foundation (http://alamofire.org/)
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- //
- #if canImport(zlib)
- import Foundation
- import zlib
- /// `RequestAdapter` which compresses outgoing `URLRequest` bodies using the `deflate` `Content-Encoding` and adds the
- /// appropriate header.
- ///
- /// - Note: Most requests to most APIs are small and so would only be slowed down by applying this adapter. Measure the
- /// size of your request bodies and the performance impact of using this adapter before use. Using this adapter
- /// with already compressed data, such as images, will, at best, have no effect. Additionally, body compression
- /// is a synchronous operation, so measuring the performance impact may be important to determine whether you
- /// want to use a dedicated `requestQueue` in your `Session` instance. Finally, not all servers support request
- /// compression, so test with all of your server configurations before deploying.
- @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
- public struct DeflateRequestCompressor: RequestInterceptor {
- /// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header.
- public enum DuplicateHeaderBehavior {
- /// Throws a `DuplicateHeaderError`. The default.
- case error
- /// Replaces the existing header value with `deflate`.
- case replace
- /// Silently skips compression when the header exists.
- case skip
- }
- /// `Error` produced when the outgoing `URLRequest` already has a `Content-Encoding` header, when the instance has
- /// been configured to produce an error.
- public struct DuplicateHeaderError: Error {}
- /// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header.
- public let duplicateHeaderBehavior: DuplicateHeaderBehavior
- /// Closure which determines whether the outgoing body data should be compressed.
- public let shouldCompressBodyData: (_ bodyData: Data) -> Bool
- /// Creates an instance with the provided parameters.
- ///
- /// - Parameters:
- /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default.
- /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
- public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error,
- shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }) {
- self.duplicateHeaderBehavior = duplicateHeaderBehavior
- self.shouldCompressBodyData = shouldCompressBodyData
- }
- public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
- // No need to compress unless we have body data. No support for compressing streams.
- guard let bodyData = urlRequest.httpBody else {
- completion(.success(urlRequest))
- return
- }
- guard shouldCompressBodyData(bodyData) else {
- completion(.success(urlRequest))
- return
- }
- if urlRequest.headers.value(for: "Content-Encoding") != nil {
- switch duplicateHeaderBehavior {
- case .error:
- completion(.failure(DuplicateHeaderError()))
- return
- case .replace:
- // Header will be replaced once the body data is compressed.
- break
- case .skip:
- completion(.success(urlRequest))
- return
- }
- }
- var compressedRequest = urlRequest
- do {
- compressedRequest.httpBody = try deflate(bodyData)
- compressedRequest.headers.update(.contentEncoding("deflate"))
- completion(.success(compressedRequest))
- } catch {
- completion(.failure(error))
- }
- }
- func deflate(_ data: Data) throws -> Data {
- var output = Data([0x78, 0x5E]) // Header
- try output.append((data as NSData).compressed(using: .zlib) as Data)
- var checksum = adler32Checksum(of: data).bigEndian
- output.append(Data(bytes: &checksum, count: MemoryLayout<UInt32>.size))
- return output
- }
- func adler32Checksum(of data: Data) -> UInt32 {
- #if swift(>=5.6)
- data.withUnsafeBytes { buffer in
- UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
- }
- #else
- data.withUnsafeBytes { buffer in
- let buffer = buffer.bindMemory(to: UInt8.self)
- return UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
- }
- #endif
- }
- }
- @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
- extension RequestInterceptor where Self == DeflateRequestCompressor {
- /// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values.
- public static var deflateCompressor: DeflateRequestCompressor {
- DeflateRequestCompressor()
- }
- /// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData`
- /// closure.
- ///
- /// - Parameters:
- /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use.
- /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
- ///
- /// - Returns: The `DeflateRequestCompressor`.
- public static func deflateCompressor(
- duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error,
- shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }
- ) -> DeflateRequestCompressor {
- DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior,
- shouldCompressBodyData: shouldCompressBodyData)
- }
- }
- #endif
|