From 6799142ffb980e5af417a2197ff8084553f356e6 Mon Sep 17 00:00:00 2001 From: Zhenchao Li Date: Wed, 23 Jul 2025 17:13:32 -0700 Subject: [PATCH 1/3] Add brotli as a dependency --- Package.swift | 9 +++++- .../modules/WindowsSwiftPMDependencies.cmake | 31 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 4a9dc5dc71..7ba345e7c3 100644 --- a/Package.swift +++ b/Package.swift @@ -58,7 +58,9 @@ if let environmentPath = Context.environment["CURL_INCLUDE_PATH"] { var curlLinkFlags: [LinkerSetting] = [ .linkedLibrary("libcurl.lib", .when(platforms: [.windows])), - .linkedLibrary("zlibstatic.lib", .when(platforms: [.windows])) + .linkedLibrary("zlibstatic.lib", .when(platforms: [.windows])), + .linkedLibrary("brotlicommon.lib", .when(platforms: [.windows])), + .linkedLibrary("brotlidec.lib", .when(platforms: [.windows])) ] if let environmentPath = Context.environment["CURL_LIBRARY_PATH"] { curlLinkFlags.append(.unsafeFlags([ @@ -70,6 +72,11 @@ if let environmentPath = Context.environment["ZLIB_LIBRARY_PATH"] { "-L\(environmentPath)" ])) } +if let environmentPath = Context.environment["BROTLI_LIBRARY_PATH"] { + curlLinkFlags.append(.unsafeFlags([ + "-L\(environmentPath)" + ])) +} var libxmlLinkFlags: [LinkerSetting] = [ .linkedLibrary("libxml2s.lib", .when(platforms: [.windows])) diff --git a/cmake/modules/WindowsSwiftPMDependencies.cmake b/cmake/modules/WindowsSwiftPMDependencies.cmake index 4346a734b8..555187218f 100644 --- a/cmake/modules/WindowsSwiftPMDependencies.cmake +++ b/cmake/modules/WindowsSwiftPMDependencies.cmake @@ -37,6 +37,18 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) EXCLUDE_FROM_ALL YES ) + ExternalProject_Add(brotli + GIT_REPOSITORY https://github.com/google/brotli + GIT_TAG v1.1.0 + CMAKE_ARGS + -DCMAKE_INSTALL_PREFIX=${DEST_DIR}/brotli + -DCMAKE_C_COMPILER=cl + -DBUILD_SHARED_LIBS=NO + -DCMAKE_POSITION_INDEPENDENT_CODE=YES + -DCMAKE_BUILD_TYPE=Release + EXCLUDE_FROM_ALL YES + ) + ExternalProject_Add(libxml GIT_REPOSITORY https://github.com/gnome/libxml2.git GIT_TAG v2.11.5 @@ -63,6 +75,15 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) # Add a custom target for zlib's install step that curl can depend on ExternalProject_Add_StepTargets(zlib install) + set(BROTLI_ROOT "${DEST_DIR}/brotli") + set(BROTLI_LIBRARY_DIR "${BROTLI_ROOT}/lib") + set(BROTLI_INCLUDE_DIR "${BROTLI_ROOT}/include") + set(BROTLICOMMON_LIBRARY_PATH "${BROTLI_LIBRARY_DIR}/brotlicommon.lib") + set(BROTLIDEC_LIBRARY_PATH "${BROTLI_LIBRARY_DIR}/brotlidec.lib") + + # Add a custom target for brotli's install step that curl can depend on + ExternalProject_Add_StepTargets(brotli install) + ExternalProject_Add(curl GIT_REPOSITORY https://github.com/curl/curl.git GIT_TAG curl-8_9_1 @@ -75,7 +96,7 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) -DCURL_CA_BUNDLE=none -DCURL_CA_FALLBACK=NO -DCURL_CA_PATH=none - -DCURL_BROTLI=NO + -DCURL_BROTLI=YES -DCURL_DISABLE_ALTSVC=NO -DCURL_DISABLE_AWS=YES -DCURL_DISABLE_BASIC_AUTH=NO @@ -150,7 +171,10 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) -DZLIB_ROOT=${ZLIB_ROOT} -DZLIB_LIBRARY=${ZLIB_LIBRARY_PATH} -DZLIB_INCLUDE_DIR=${ZLIB_INCLUDE_DIR} - DEPENDS zlib-install + -DBROTLIDEC_LIBRARY=${BROTLIDEC_LIBRARY_PATH} + -DBROTLICOMMON_LIBRARY=${BROTLICOMMON_LIBRARY_PATH} + -DBROTLI_INCLUDE_DIR=${BROTLI_INCLUDE_DIR} + DEPENDS zlib-install brotli-install EXCLUDE_FROM_ALL YES ) @@ -166,6 +190,7 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) message(STATUS "CURL_INCLUDE_PATH=${CURL_INCLUDE_DIR}") message(STATUS "CURL_LIBRARY_PATH=${CURL_LIBRARY_DIR}") message(STATUS "ZLIB_LIBRARY_PATH=${ZLIB_LIBRARY_DIR}") + message(STATUS "BROTLI_LIBRARY_PATH=${BROTLI_LIBRARY_DIR}") ExternalProject_Add_StepTargets(libxml install) ExternalProject_Add_StepTargets(curl install) @@ -186,5 +211,7 @@ function(_foundation_setup_windows_swiftpm_dependencies_target) COMMAND echo CURL_LIBRARY_PATH=${CURL_LIBRARY_DIR}) add_custom_command(TARGET WindowsSwiftPMDependencies POST_BUILD COMMAND echo ZLIB_LIBRARY_PATH=${ZLIB_LIBRARY_DIR}) + add_custom_command(TARGET WindowsSwiftPMDependencies POST_BUILD + COMMAND echo BROTLI_LIBRARY_PATH=${BROTLI_LIBRARY_DIR}) endfunction() From f0fbde1f514560e3b1199614e35d49ea70c3c37a Mon Sep 17 00:00:00 2001 From: Zhenchao Li Date: Tue, 29 Jul 2025 14:49:10 -0700 Subject: [PATCH 2/3] Add basic tests for brotli support --- Tests/Foundation/HTTPServer.swift | 24 +++++++++++++++++++++ Tests/Foundation/TestURLSession.swift | 30 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/Tests/Foundation/HTTPServer.swift b/Tests/Foundation/HTTPServer.swift index a6457f285f..7b42864d50 100644 --- a/Tests/Foundation/HTTPServer.swift +++ b/Tests/Foundation/HTTPServer.swift @@ -481,6 +481,18 @@ class _HTTPServer: CustomStringConvertible { "\r\n").data(using: .utf8)! try tcpSocket.writeRawData(responseData) } + + func respondWithAcceptEncoding(request: _HTTPRequest) throws { + var responseData: Data + if let acceptEncoding = request.getHeader(for: "Accept-Encoding") { + let content = acceptEncoding.data(using: .utf8)! + responseData = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=ISO-8859-1\r\nContent-Length: \(content.count)\r\n\r\n".data(using: .utf8)! + responseData.append(content) + } else { + responseData = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=ISO-8859-1\r\nContent-Length: 0\r\n\r\n".data(using: .utf8)! + } + try tcpSocket.writeRawData(responseData) + } } struct _HTTPRequest: CustomStringConvertible { @@ -690,6 +702,8 @@ public class TestURLSessionServer: CustomStringConvertible { try httpServer.respondWithUnauthorizedHeader() } else if req.uri.hasPrefix("/web-socket") { try handleWebSocketRequest(req) + } else if req.uri.hasPrefix("/accept-encoding") { + try httpServer.respondWithAcceptEncoding(request: req) } else { let response = try getResponse(request: req) try httpServer.respond(with: response) @@ -852,6 +866,16 @@ public class TestURLSessionServer: CustomStringConvertible { "Content-Encoding: gzip"].joined(separator: _HTTPUtils.CRLF), bodyData: helloWorld) } + + if uri == "/brotli-response" { + // This is "Hello World!" brotli encoded. + let helloWorld = Data([0x8B, 0x05, 0x80, 0x48, 0x65, 0x6C, 0x6C, 0x6F, + 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64, 0x21, 0x03]) + return _HTTPResponse(response: .OK, + headers: ["Content-Length: \(helloWorld.count)", + "Content-Encoding: br"].joined(separator: _HTTPUtils.CRLF), + bodyData: helloWorld) + } if uri == "/echo-query" { let body = request.parameters.map { "\($0.key)=\($0.value)" }.joined(separator: "&") diff --git a/Tests/Foundation/TestURLSession.swift b/Tests/Foundation/TestURLSession.swift index df12d9a085..b58581fe19 100644 --- a/Tests/Foundation/TestURLSession.swift +++ b/Tests/Foundation/TestURLSession.swift @@ -31,6 +31,21 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { } } + func test_dataTaskWithAcceptEncoding() async { + #if !os(Windows) + throw XCTSkip("This test is currently only enabled on Windows") + #else + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/accept-encoding" + let url = URL(string: urlString)! + let d = DataTask(with: expectation(description: "GET \(urlString): with a delegate")) + d.run(with: url) + waitForExpectations(timeout: 12) + if !d.error { + XCTAssertEqual(d.capital, "deflate, gzip, br", "test_dataTaskWithURLRequest returned an unexpected result") + } + #endif + } + func test_dataTaskWithURLCompletionHandler() async { //shared session await dataTaskWithURLCompletionHandler(with: URLSession.shared) @@ -256,6 +271,21 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { } } + func test_brotliDataTask() async { + #if !os(Windows) + throw XCTSkip("This test is currently only enabled on Windows") + #else + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/brotli-response" + let url = URL(string: urlString)! + let d = DataTask(with: expectation(description: "GET \(urlString): brotli response")) + d.run(with: url) + waitForExpectations(timeout: 12) + if !d.error { + XCTAssertEqual(d.capital, "Hello World!") + } + #endif + } + func test_downloadTaskWithURL() async { let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/country.txt" let url = URL(string: urlString)! From 84ecba7f7eb8dbea666f7d994cc8a2922e38fd57 Mon Sep 17 00:00:00 2001 From: Zhenchao Li Date: Thu, 31 Jul 2025 17:50:56 -0700 Subject: [PATCH 3/3] Make brotli test available on Linux --- Tests/Foundation/TestURLSession.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Tests/Foundation/TestURLSession.swift b/Tests/Foundation/TestURLSession.swift index b58581fe19..9bbfb922cd 100644 --- a/Tests/Foundation/TestURLSession.swift +++ b/Tests/Foundation/TestURLSession.swift @@ -32,18 +32,15 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { } func test_dataTaskWithAcceptEncoding() async { - #if !os(Windows) - throw XCTSkip("This test is currently only enabled on Windows") - #else let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/accept-encoding" let url = URL(string: urlString)! let d = DataTask(with: expectation(description: "GET \(urlString): with a delegate")) d.run(with: url) waitForExpectations(timeout: 12) if !d.error { - XCTAssertEqual(d.capital, "deflate, gzip, br", "test_dataTaskWithURLRequest returned an unexpected result") + let supportedEncodings = d.capital.split(separator: ",").map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines ) } + XCTAssert(supportedEncodings.contains("br"), "test_dataTaskWithURLRequest returned an unexpected result") } - #endif } func test_dataTaskWithURLCompletionHandler() async { @@ -272,9 +269,6 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { } func test_brotliDataTask() async { - #if !os(Windows) - throw XCTSkip("This test is currently only enabled on Windows") - #else let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/brotli-response" let url = URL(string: urlString)! let d = DataTask(with: expectation(description: "GET \(urlString): brotli response")) @@ -283,7 +277,6 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { if !d.error { XCTAssertEqual(d.capital, "Hello World!") } - #endif } func test_downloadTaskWithURL() async {