Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move foundation extensions behind traits #81

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.1

import PackageDescription

Expand All @@ -8,6 +8,10 @@ let package = Package(
.library(name: "HTTPTypes", targets: ["HTTPTypes"]),
.library(name: "HTTPTypesFoundation", targets: ["HTTPTypesFoundation"]),
],
traits: [
"FoundationEssentialExtensions",
"FoundationNetworkingExtensions",
],
targets: [
.target(name: "HTTPTypes"),
.target(
Expand Down
214 changes: 214 additions & 0 deletions Sources/HTTPTypes/FoundationExtensions/HTTPRequest+URL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if FoundationExtensions
#if canImport(FoundationEssentials)
public import struct FoundationEssentials.URL
#else
public import struct Foundation.URL
#endif

#if canImport(CoreFoundation)
public import CoreFoundation
#endif // canImport(CoreFoundation)

extension HTTPRequest {
/// The URL of the request synthesized from the scheme, authority, and path pseudo header
/// fields.
public var url: URL? {
get {
if let schemeField = self.pseudoHeaderFields.scheme,
let authorityField = self.pseudoHeaderFields.authority,
let pathField = self.pseudoHeaderFields.path
{
return schemeField.withUnsafeBytesOfValue { scheme in
authorityField.withUnsafeBytesOfValue { authority in
pathField.withUnsafeBytesOfValue { path in
URL(scheme: scheme, authority: authority, path: path)
}
}
}
} else {
return nil
}
}
set {
if let newValue {
let (scheme, authority, path) = newValue.httpRequestComponents
self.scheme = String(decoding: scheme, as: UTF8.self)
self.authority = authority.map { String(decoding: $0, as: UTF8.self) }
self.path = String(decoding: path, as: UTF8.self)
} else {
self.pseudoHeaderFields.scheme = nil
self.pseudoHeaderFields.authority = nil
self.pseudoHeaderFields.path = nil
}
}
}

/// Create an HTTP request with a method, a URL, and header fields.
/// - Parameters:
/// - method: The request method, defaults to GET.
/// - url: The URL to populate the scheme, authority, and path pseudo header fields.
/// - headerFields: The request header fields.
public init(method: Method = .get, url: URL, headerFields: HTTPFields = [:]) {
let (scheme, authority, path) = url.httpRequestComponents
let schemeString = String(decoding: scheme, as: UTF8.self)
let authorityString = authority.map { String(decoding: $0, as: UTF8.self) }
let pathString = String(decoding: path, as: UTF8.self)

self.init(
method: method,
scheme: schemeString,
authority: authorityString,
path: pathString,
headerFields: headerFields
)
}
}

extension URL {
fileprivate init?(scheme: some Collection<UInt8>, authority: some Collection<UInt8>, path: some Collection<UInt8>) {
var buffer = [UInt8]()
buffer.reserveCapacity(scheme.count + 3 + authority.count + path.count)
buffer.append(contentsOf: scheme)
buffer.append(contentsOf: "://".utf8)
buffer.append(contentsOf: authority)
buffer.append(contentsOf: path)

#if canImport(CoreFoundation)
if let url = buffer.withUnsafeBytes({ buffer in
CFURLCreateAbsoluteURLWithBytes(
kCFAllocatorDefault,
buffer.baseAddress,
buffer.count,
CFStringBuiltInEncodings.ASCII.rawValue,
nil,
false
).map { unsafeBitCast($0, to: NSURL.self) as URL }
}) {
self = url
} else {
return nil
}
#else // canImport(CoreFoundation)
// This initializer does not preserve WHATWG URLs
self.init(string: String(decoding: buffer, as: UTF8.self))
#endif // canImport(CoreFoundation)
}

fileprivate var httpRequestComponents: (scheme: [UInt8], authority: [UInt8]?, path: [UInt8]) {
#if canImport(CoreFoundation)
// CFURL parser based on byte ranges does not unnecessarily percent-encode WHATWG URL
let url = unsafeBitCast(self.absoluteURL as NSURL, to: CFURL.self)
let length = CFURLGetBytes(url, nil, 0)
return withUnsafeTemporaryAllocation(of: UInt8.self, capacity: length) { buffer in
CFURLGetBytes(url, buffer.baseAddress, buffer.count)

func unionRange(_ first: CFRange, _ second: CFRange) -> CFRange {
if first.location == kCFNotFound { return second }
if second.location == kCFNotFound { return first }
return CFRange(location: first.location, length: second.location + second.length - first.location)
}

func bufferSlice(_ range: CFRange) -> UnsafeMutableBufferPointer<UInt8> {
UnsafeMutableBufferPointer(rebasing: buffer[range.location..<range.location + range.length])
}

let schemeRange = CFURLGetByteRangeForComponent(url, .scheme, nil)
precondition(schemeRange.location != kCFNotFound, "Schemeless URL is not supported")
let scheme = Array(bufferSlice(schemeRange))

let authority: [UInt8]?
let hostRange = CFURLGetByteRangeForComponent(url, .host, nil)
if hostRange.location != kCFNotFound {
let portRange = CFURLGetByteRangeForComponent(url, .port, nil)
let authorityRange = unionRange(hostRange, portRange)
authority = Array(bufferSlice(authorityRange))
} else {
authority = nil
}

let path: [UInt8]
let pathRange = CFURLGetByteRangeForComponent(url, .path, nil)
let queryRange = CFURLGetByteRangeForComponent(url, .query, nil)
let requestPathRange = unionRange(pathRange, queryRange)
if pathRange.length == 0 {
if requestPathRange.length == 0 {
path = [UInt8(ascii: "/")]
} else {
let pathBuffer = bufferSlice(requestPathRange)
path = [UInt8](unsafeUninitializedCapacity: pathBuffer.count + 1) { buffer, initializedCount in
buffer[0] = UInt8(ascii: "/")
UnsafeMutableRawBufferPointer(UnsafeMutableBufferPointer(rebasing: buffer[1...])).copyMemory(
from: UnsafeRawBufferPointer(pathBuffer)
)
initializedCount = pathBuffer.count + 1
}
}
} else {
path = Array(bufferSlice(requestPathRange))
}
return (scheme, authority, path)
}
#else // canImport(CoreFoundation)
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true),
let urlString = components.string
else {
fatalError("Invalid URL")
}

guard let schemeRange = components.rangeOfScheme else {
fatalError("Schemeless URL is not supported")
}
let scheme = Array(urlString[schemeRange].utf8)

let authority: [UInt8]?
if let hostRange = components.rangeOfHost {
let authorityRange =
if let portRange = components.rangeOfPort {
hostRange.lowerBound..<portRange.upperBound
} else {
hostRange
}
authority = Array(urlString[authorityRange].utf8)
} else {
authority = nil
}

let pathRange = components.rangeOfPath
let queryRange = components.rangeOfQuery
let requestPathRange: Range<String.Index>?
if let lowerBound = pathRange?.lowerBound ?? queryRange?.lowerBound,
let upperBound = queryRange?.upperBound ?? pathRange?.upperBound
{
requestPathRange = lowerBound..<upperBound
} else {
requestPathRange = nil
}
let path: [UInt8]
if let pathRange, !pathRange.isEmpty, let requestPathRange {
path = Array(urlString[requestPathRange].utf8)
} else {
if let requestPathRange, !requestPathRange.isEmpty {
path = [UInt8(ascii: "/")] + Array(urlString[requestPathRange].utf8)
} else {
path = [UInt8(ascii: "/")]
}
}
return (scheme, authority, path)
#endif // canImport(CoreFoundation)
}
}
#endif
46 changes: 46 additions & 0 deletions Sources/HTTPTypes/FoundationExtensions/HTTPTypes+ISOLatin1.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

extension HTTPField {
init(name: Name, isoLatin1Value: String) {
if isoLatin1Value.isASCII {
self.init(name: name, value: isoLatin1Value)
} else {
self = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: isoLatin1Value.unicodeScalars.count) {
buffer in
for (index, scalar) in isoLatin1Value.unicodeScalars.enumerated() {
if scalar.value > UInt8.max {
buffer[index] = 0x20
} else {
buffer[index] = UInt8(truncatingIfNeeded: scalar.value)
}
}
return HTTPField(name: name, value: buffer)
}
}
}

var isoLatin1Value: String {
if self.value.isASCII {
return self.value
} else {
return self.withUnsafeBytesOfValue { buffer in
let scalars = buffer.lazy.map { UnicodeScalar(UInt32($0))! }
var string = ""
string.unicodeScalars.append(contentsOf: scalars)
return string
}
}
}
}
71 changes: 71 additions & 0 deletions Sources/HTTPTypes/FoundationExtensions/URLRequest+HTTPTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if !os(WASI)
#if FoundationNetworkingExtensions

#if canImport(FoundationNetworking)
public import struct FoundationNetworking.URLRequest
#else
public import struct Foundation.URLRequest
#endif

extension URLRequest {
/// Create a `URLRequest` from an `HTTPRequest`.
/// - Parameter httpRequest: The HTTP request to convert from.
public init?(httpRequest: HTTPRequest) {
guard let url = httpRequest.url else {
return nil
}
var request = URLRequest(url: url)
request.httpMethod = httpRequest.method.rawValue
var combinedFields = [HTTPField.Name: String](minimumCapacity: httpRequest.headerFields.count)
for field in httpRequest.headerFields {
if let existingValue = combinedFields[field.name] {
let separator = field.name == .cookie ? "; " : ", "
combinedFields[field.name] = "\(existingValue)\(separator)\(field.isoLatin1Value)"
} else {
combinedFields[field.name] = field.isoLatin1Value
}
}
var headerFields = [String: String](minimumCapacity: combinedFields.count)
for (name, value) in combinedFields {
headerFields[name.rawName] = value
}
request.allHTTPHeaderFields = headerFields
self = request
}

/// Convert the `URLRequest` into an `HTTPRequest`.
public var httpRequest: HTTPRequest? {
guard let method = HTTPRequest.Method(self.httpMethod ?? "GET"),
let url
else {
return nil
}
var request = HTTPRequest(method: method, url: url)
if let allHTTPHeaderFields = self.allHTTPHeaderFields {
request.headerFields.reserveCapacity(allHTTPHeaderFields.count)
for (name, value) in allHTTPHeaderFields {
if let name = HTTPField.Name(name) {
request.headerFields.append(HTTPField(name: name, isoLatin1Value: value))
}
}
}
return request
}
}

#endif
#endif
Loading
Loading