Skip to content

Commit c5784ca

Browse files
authored
Replace import Foundation with FoundationEssentials (#897)
Replaces all the foundation imports. One issue is that `HTTPClient.init?(httpsURLWithSocketPath socketPath: String, uri: String = "/")` uses `addingPercentEncoding()` from Foundation. So instead, we use a pure Swift impl. that does the same. We also need to disable default traits from `swift-configuration` to prevent linking Foundation, because the `JSON` trait does that. This also adds a linkage test to prevent regressions to CI.
1 parent c3a3925 commit c5784ca

32 files changed

Lines changed: 319 additions & 21 deletions

.github/workflows/pull_request.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,32 @@ jobs:
3838
release-builds:
3939
name: Release builds
4040
uses: apple/swift-nio/.github/workflows/release_builds.yml@main
41+
42+
construct-linkage-test-matrix:
43+
name: Construct linkage matrix
44+
runs-on: ubuntu-latest
45+
outputs:
46+
linkage-test-matrix: '${{ steps.generate-matrix.outputs.linkage-test-matrix }}'
47+
steps:
48+
- name: Checkout repository
49+
uses: actions/checkout@v6
50+
with:
51+
persist-credentials: false
52+
- id: generate-matrix
53+
run: echo "linkage-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT"
54+
env:
55+
MATRIX_LINUX_SETUP_COMMAND: apt-get update -y && apt-get install -yq jq && git config --global --add safe.directory /async-http-client
56+
MATRIX_LINUX_COMMAND: ./scripts/run-linkage-test.sh
57+
MATRIX_LINUX_5_10_ENABLED: false
58+
MATRIX_LINUX_6_0_ENABLED: false
59+
MATRIX_LINUX_6_1_ENABLED: false
60+
MATRIX_LINUX_NIGHTLY_NEXT_ENABLED: false
61+
MATRIX_LINUX_NIGHTLY_MAIN_ENABLED: false
62+
63+
linkage-test:
64+
name: Linkage test
65+
needs: construct-linkage-test-matrix
66+
uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main
67+
with:
68+
name: "Linkage test"
69+
matrix_string: '${{ needs.construct-linkage-test-matrix.outputs.linkage-test-matrix }}'

Package.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ let package = Package(
4444
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
4545
.package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
4646
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.3.0"),
47-
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0"),
47+
// Disable all traits to prevent linking Foundation
48+
.package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: []),
4849
.package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0"),
4950
],
5051
targets: [
@@ -68,7 +69,11 @@ let package = Package(
6869
.product(name: "NIOSSL", package: "swift-nio-ssl"),
6970
.product(name: "NIOHTTPCompression", package: "swift-nio-extras"),
7071
.product(name: "NIOSOCKS", package: "swift-nio-extras"),
71-
.product(name: "NIOTransportServices", package: "swift-nio-transport-services"),
72+
.product(
73+
name: "NIOTransportServices",
74+
package: "swift-nio-transport-services",
75+
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .macCatalyst, .visionOS])
76+
),
7277
.product(name: "Atomics", package: "swift-atomics"),
7378
.product(name: "Algorithms", package: "swift-algorithms"),
7479
.product(name: "Configuration", package: "swift-configuration"),

Sources/AsyncHTTPClient/AsyncAwait/HTTPClient+execute.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import NIOCore
1717
import NIOHTTP1
1818
import Tracing
1919

20+
#if canImport(FoundationEssentials)
21+
import struct FoundationEssentials.URL
22+
#else
2023
import struct Foundation.URL
24+
#endif
2125

2226
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2327
extension HTTPClient {

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+Prepared.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import NIOHTTP1
1818
import NIOSSL
1919
import ServiceContextModule
2020

21+
#if canImport(FoundationEssentials)
22+
import struct FoundationEssentials.URL
23+
#else
2124
import struct Foundation.URL
25+
#endif
2226

2327
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
2428
extension HTTPClientRequest {

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientRequest+auth.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
#if canImport(FoundationEssentials)
16+
import FoundationEssentials
17+
#else
1518
import Foundation
19+
#endif
1620

1721
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
1822
extension HTTPClientRequest {

Sources/AsyncHTTPClient/AsyncAwait/HTTPClientResponse.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
import NIOCore
1616
import NIOHTTP1
1717

18+
#if canImport(FoundationEssentials)
19+
import struct FoundationEssentials.URL
20+
#else
1821
import struct Foundation.URL
22+
#endif
1923

2024
/// A representation of an HTTP response for the Swift Concurrency HTTPClient API.
2125
///

Sources/AsyncHTTPClient/BasicAuth.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import Foundation
1615
import NIOHTTP1
1716

17+
#if canImport(FoundationEssentials)
18+
import FoundationEssentials
19+
#else
20+
import Foundation
21+
#endif
22+
1823
/// Generates base64 encoded username + password for http basic auth.
1924
///
2025
/// - Parameters:

Sources/AsyncHTTPClient/DeconstructedURL.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
#if canImport(FoundationEssentials)
16+
import struct FoundationEssentials.URL
17+
#else
1518
import struct Foundation.URL
19+
#endif
1620

1721
struct DeconstructedURL {
1822
var scheme: Scheme

Sources/AsyncHTTPClient/FileDownloadDelegate.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import NIOCore
1717
import NIOHTTP1
1818
import NIOPosix
1919

20+
#if canImport(FoundationEssentials)
21+
import struct FoundationEssentials.URL
22+
#else
2023
import struct Foundation.URL
24+
#endif
2125

2226
/// Handles a streaming download to a given file path, allowing headers and progress to be reported.
2327
public final class FileDownloadDelegate: HTTPClientResponseDelegate {

Sources/AsyncHTTPClient/FoundationExtensions.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
// Extensions which provide better ergonomics when using Foundation types,
1616
// or by using Foundation APIs.
1717

18+
#if canImport(FoundationEssentials)
19+
import FoundationEssentials
20+
#else
1821
import Foundation
22+
#endif
1923

2024
extension HTTPClient.Cookie {
2125
/// The cookie's expiration date.
@@ -73,3 +77,66 @@ extension HTTPClient.Body {
7377
self.bytes(data)
7478
}
7579
}
80+
81+
extension StringProtocol {
82+
func addingPercentEncodingAllowingURLHost() -> String {
83+
guard !self.isEmpty else { return String(self) }
84+
85+
let percent = UInt8(ascii: "%")
86+
let utf8Buffer = self.utf8
87+
let maxLength = utf8Buffer.count * 3
88+
return withUnsafeTemporaryAllocation(of: UInt8.self, capacity: maxLength) { outputBuffer in
89+
var i = 0
90+
for byte in utf8Buffer {
91+
if byte.isURLHostAllowed {
92+
outputBuffer[i] = byte
93+
i += 1
94+
} else {
95+
outputBuffer[i] = percent
96+
outputBuffer[i + 1] = hexToAscii(byte >> 4)
97+
outputBuffer[i + 2] = hexToAscii(byte & 0xF)
98+
i += 3
99+
}
100+
}
101+
return String(decoding: outputBuffer[..<i], as: UTF8.self)
102+
}
103+
}
104+
}
105+
106+
private func hexToAscii(_ hex: UInt8) -> UInt8 {
107+
switch hex {
108+
case 0x0: return UInt8(ascii: "0")
109+
case 0x1: return UInt8(ascii: "1")
110+
case 0x2: return UInt8(ascii: "2")
111+
case 0x3: return UInt8(ascii: "3")
112+
case 0x4: return UInt8(ascii: "4")
113+
case 0x5: return UInt8(ascii: "5")
114+
case 0x6: return UInt8(ascii: "6")
115+
case 0x7: return UInt8(ascii: "7")
116+
case 0x8: return UInt8(ascii: "8")
117+
case 0x9: return UInt8(ascii: "9")
118+
case 0xA: return UInt8(ascii: "A")
119+
case 0xB: return UInt8(ascii: "B")
120+
case 0xC: return UInt8(ascii: "C")
121+
case 0xD: return UInt8(ascii: "D")
122+
case 0xE: return UInt8(ascii: "E")
123+
case 0xF: return UInt8(ascii: "F")
124+
default: fatalError("Invalid hex digit: \(hex)")
125+
}
126+
}
127+
128+
extension UInt8 {
129+
fileprivate var isURLHostAllowed: Bool {
130+
switch self {
131+
case UInt8(ascii: "0")...UInt8(ascii: "9"),
132+
UInt8(ascii: "A")...UInt8(ascii: "Z"),
133+
UInt8(ascii: "a")...UInt8(ascii: "z"),
134+
UInt8(ascii: "!"), UInt8(ascii: "$"), UInt8(ascii: "&"), UInt8(ascii: "'"),
135+
UInt8(ascii: "("), UInt8(ascii: ")"), UInt8(ascii: "*"), UInt8(ascii: "+"),
136+
UInt8(ascii: ","), UInt8(ascii: "-"), UInt8(ascii: "."), UInt8(ascii: ";"),
137+
UInt8(ascii: "="), UInt8(ascii: "_"), UInt8(ascii: "~"):
138+
return true
139+
default: return false
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)