├── .gitignore ├── patches ├── swift-corelibs-foundation │ ├── 0014-add-swift-syntax-source-dir-variable.patch │ ├── 0015-add-empty-CFPosixSpawnFileActionsChdir-for-android.patch │ ├── 0008-avoid-bundle-localized-string-for-android.patch │ ├── 0007-add-CF_MAIN_BUNDLE_PATH-for-android.patch │ ├── 0012-fix-swift-6-in-certificate-trust-and--basic-auth.patch │ ├── 0009-dont-crash-on-non-critical-network-errors.patch │ ├── 0013-fix-memory-leak-in-nsattributedstring.patch │ ├── 0001-use-content-length-if-present-from-headers-for-streamed-body.patch │ ├── 0005-update-URLProtectionSpace-tests-after-support-Digest-and-NTLM-authentications.patch │ ├── 0010-fix-dispatch-source-write-issue-v4.patch │ ├── 0002-request-new-stream-when-restarting-request due-to-redirect-or-auth-challenge.patch │ ├── 0011-fix-parsing-tzdata-version-2-plus.patch │ ├── 0004-add-ability-to-trust-invalid-certificate.patch │ ├── 0006-handle-HTTP-Basic-Auth-and-auth-failure.patch │ ├── 0003-support-digest-and-ntlm-auth.patch │ └── 0015-fix-websocket-buffered-read-fragment-support.patch ├── swift │ ├── 0003-Enable-build-ids.patch │ ├── 0004-Support-16-KB-page-sizes.patch │ ├── 0001-Add-presets-for-android-x86-x86_64.patch │ └── 0002-Forward-android-arch-form-build-script-to-build-scri.patch ├── swift-foundation │ └── 0001-open-range-attributed-string-from-nsrange.patch ├── swift-corelibs-libdispatch │ └── 0001-Add-LIBDISPATCH_DEFAULT_THREAD_POOL_SIZE-env-variabl.patch ├── swift-syntax │ └── 0001-replace-bionic-with-android.patch └── swift-foundation-icu │ └── revert-hide-all-icu-c++-symbols.patch ├── LICENSE ├── README.md └── .github └── workflows └── build-android-toolchain.yml /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | swift-android-*/ 3 | swift-android-*.zip 4 | vagrant/out/ 5 | *.log 6 | .vagrant 7 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0014-add-swift-syntax-source-dir-variable.patch: -------------------------------------------------------------------------------- 1 | From 5634e39e22b102d27a70b98b2579d599090f4d8c Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Mon, 14 Apr 2025 17:17:58 +0300 4 | Subject: [PATCH] Add SwiftSyntax source dir variable 5 | 6 | --- 7 | CMakeLists.txt | 5 +++++ 8 | 1 file changed, 5 insertions(+) 9 | 10 | diff --git a/CMakeLists.txt b/CMakeLists.txt 11 | index c0d46e43..3c611c97 100644 12 | --- a/CMakeLists.txt 13 | +++ b/CMakeLists.txt 14 | @@ -83,6 +83,11 @@ set(CMAKE_POSITION_INDEPENDENT_CODE YES) 15 | 16 | # Fetchable dependencies 17 | include(FetchContent) 18 | +if (_SwiftSyntax_SourceDIR) 19 | + FetchContent_Declare(SwiftSyntax 20 | + SOURCE_DIR ${_SwiftSyntax_SourceDIR}) 21 | + message(STATUS "_SwiftSyntax_SourceDIR: ${_SwiftSyntax_SourceDIR}") 22 | +endif() 23 | if (_SwiftFoundationICU_SourceDIR) 24 | FetchContent_Declare(SwiftFoundationICU 25 | SOURCE_DIR ${_SwiftFoundationICU_SourceDIR}) 26 | -- 27 | 2.46.0 28 | 29 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0015-add-empty-CFPosixSpawnFileActionsChdir-for-android.patch: -------------------------------------------------------------------------------- 1 | From 15c1f65dbeb3a2151d6ac36c03ccb67d7173850d Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 10 May 2025 15:26:22 +0300 4 | Subject: [PATCH] Add empty _CFPosixSpawnFileActionsChdir 5 | 6 | --- 7 | Sources/CoreFoundation/CFPlatform.c | 4 ++++ 8 | 1 file changed, 4 insertions(+) 9 | 10 | diff --git a/Sources/CoreFoundation/CFPlatform.c b/Sources/CoreFoundation/CFPlatform.c 11 | index 90f4aa78..0eea2f3b 100644 12 | --- a/Sources/CoreFoundation/CFPlatform.c 13 | +++ b/Sources/CoreFoundation/CFPlatform.c 14 | @@ -2248,6 +2248,10 @@ CF_EXPORT int _CFPosixSpawn(pid_t *_CF_RESTRICT pid, const char *_CF_RESTRICT pa 15 | return _CFPosixSpawnImpl(pid, path, file_actions, attrp, argv, envp); 16 | } 17 | 18 | +CF_EXPORT int _CFPosixSpawnFileActionsChdir(_CFPosixSpawnFileActionsRef file_actions, const char *path) { 19 | + return 0; 20 | +} 21 | + 22 | #elif !TARGET_OS_WIN32 && !TARGET_OS_WASI 23 | 24 | #include 25 | -- 26 | 2.46.0 27 | 28 | -------------------------------------------------------------------------------- /patches/swift/0003-Enable-build-ids.patch: -------------------------------------------------------------------------------- 1 | From 39a43f6e80bf0e63f844b93f24e8de3887a93996 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 12 Apr 2025 15:26:32 +0300 4 | Subject: [PATCH] Enable crashlytics 5 | 6 | --- 7 | stdlib/cmake/modules/AddSwiftStdlib.cmake | 2 + 8 | 1 file changed, 2 insertions(+) 9 | 10 | diff --git a/stdlib/cmake/modules/AddSwiftStdlib.cmake b/stdlib/cmake/modules/AddSwiftStdlib.cmake 11 | index fd5e97675ff..88c09e534e6 100644 12 | --- a/stdlib/cmake/modules/AddSwiftStdlib.cmake 13 | +++ b/stdlib/cmake/modules/AddSwiftStdlib.cmake 14 | @@ -2464,6 +2464,8 @@ function(add_swift_target_library name) 15 | list(APPEND swiftlib_link_flags_all "-shared") 16 | # TODO: Instead of `lib${name}.so` find variable or target property which already have this value. 17 | list(APPEND swiftlib_link_flags_all "-Wl,-soname,lib${name}.so") 18 | + # Add the build-id flag to ensure a unique identifier is included in the output binary. 19 | + list(APPEND swiftlib_link_flags_all "-Wl,--build-id") 20 | endif() 21 | 22 | if (SWIFTLIB_BACK_DEPLOYMENT_LIBRARY) 23 | -- 24 | 2.46.0 25 | 26 | -------------------------------------------------------------------------------- /patches/swift/0004-Support-16-KB-page-sizes.patch: -------------------------------------------------------------------------------- 1 | From 4943ffce5edf11eb99b594f34cd946a63c3a9a71 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Fri, 16 May 2025 22:40:01 +0300 4 | Subject: [PATCH] Add 16Kb page sizes support 5 | 6 | --- 7 | stdlib/cmake/modules/AddSwiftStdlib.cmake | 2 ++ 8 | 1 file changed, 2 insertions(+) 9 | 10 | diff --git a/stdlib/cmake/modules/AddSwiftStdlib.cmake b/stdlib/cmake/modules/AddSwiftStdlib.cmake 11 | index 88c09e534e6..477f0ef00ca 100644 12 | --- a/stdlib/cmake/modules/AddSwiftStdlib.cmake 13 | +++ b/stdlib/cmake/modules/AddSwiftStdlib.cmake 14 | @@ -2466,6 +2466,8 @@ function(add_swift_target_library name) 15 | list(APPEND swiftlib_link_flags_all "-Wl,-soname,lib${name}.so") 16 | # Add the build-id flag to ensure a unique identifier is included in the output binary. 17 | list(APPEND swiftlib_link_flags_all "-Wl,--build-id") 18 | + # Ensure compatibility with Android 15+ devices using 16KB memory pages. 19 | + list(APPEND swiftlib_link_flags_all "-Wl,-z,max-page-size=16384") 20 | endif() 21 | 22 | if (SWIFTLIB_BACK_DEPLOYMENT_LIBRARY) 23 | -- 24 | 2.46.0 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Readdle Inc. 4 | Copyright (c) 2016 Gonzalo Larralde 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0008-avoid-bundle-localized-string-for-android.patch: -------------------------------------------------------------------------------- 1 | From ec9152bc5f94f8bb7551b5b49d5d201cb515e66a Mon Sep 17 00:00:00 2001 2 | From: Albert Aleksieiev 3 | Date: Tue, 2 Apr 2019 17:41:29 +0300 4 | Subject: [PATCH] Avoid bundle localized string for android 5 | 6 | --- 7 | Sources/Foundation/NSString.swift | 6 +++++- 8 | 1 file changed, 5 insertions(+), 1 deletion(-) 9 | 10 | diff --git a/Sources/Foundation/NSString.swift b/Sources/Foundation/NSString.swift 11 | index 79ec002d..0379ff4d 100644 12 | --- a/Sources/Foundation/NSString.swift 13 | +++ b/Sources/Foundation/NSString.swift 14 | @@ -26,7 +26,11 @@ func NSLocalizedString(_ key: String, 15 | bundle: Bundle = Bundle.main, 16 | value: String = "", 17 | comment: String) -> String { 18 | - return bundle.localizedString(forKey: key, value: value, table: tableName) 19 | + #if os(Android) 20 | + return key 21 | + #else 22 | + return bundle.localizedString(forKey: key, value: value, table: tableName) 23 | + #endif 24 | } 25 | 26 | internal let kCFStringEncodingMacRoman = CFStringBuiltInEncodings.macRoman.rawValue 27 | -- 28 | 2.46.0 29 | 30 | -------------------------------------------------------------------------------- /patches/swift/0001-Add-presets-for-android-x86-x86_64.patch: -------------------------------------------------------------------------------- 1 | From 83574307e1f3aa5f67da93f4d93dc58f3bffe0fc Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 12 Apr 2025 15:19:51 +0300 4 | Subject: [PATCH] Add presets for android x86 x86_64 5 | 6 | --- 7 | utils/build-presets.ini | 14 ++++++++++++++ 8 | 1 file changed, 14 insertions(+) 9 | 10 | diff --git a/utils/build-presets.ini b/utils/build-presets.ini 11 | index 14b9a7006d2..23abcd95d91 100644 12 | --- a/utils/build-presets.ini 13 | +++ b/utils/build-presets.ini 14 | @@ -1026,6 +1026,20 @@ mixin-preset=buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build 15 | 16 | android-arch=aarch64 17 | 18 | +[preset: buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build,x86] 19 | +mixin-preset=buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build 20 | + 21 | +dash-dash 22 | + 23 | +android-arch=i686 24 | + 25 | +[preset: buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build,x86_64] 26 | +mixin-preset=buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build 27 | + 28 | +dash-dash 29 | + 30 | +android-arch=x86_64 31 | + 32 | # Ubuntu 18.04 preset for backwards compat and future customizations. 33 | [preset: buildbot_linux_1804] 34 | mixin-preset=buildbot_linux 35 | -- 36 | 2.46.0 37 | 38 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0007-add-CF_MAIN_BUNDLE_PATH-for-android.patch: -------------------------------------------------------------------------------- 1 | From 5f8efde9c38b44dd4257e7194862c01b2c79687a Mon Sep 17 00:00:00 2001 2 | From: Anton Pogonets 3 | Date: Tue, 8 Oct 2019 05:30:55 +0300 4 | Subject: [PATCH] Add CF_MAIN_BUNDLE_PATH for android 5 | 6 | (cherry picked from commit 80dbe1d691b6a05b8b0ed81bf48afcfcdadd2ec1) 7 | --- 8 | Sources/CoreFoundation/CFBundle_Main.c | 12 ++++++++++++ 9 | 1 file changed, 12 insertions(+) 10 | 11 | diff --git a/Sources/CoreFoundation/CFBundle_Main.c b/Sources/CoreFoundation/CFBundle_Main.c 12 | index 9afe05fa..00826d08 100644 13 | --- a/Sources/CoreFoundation/CFBundle_Main.c 14 | +++ b/Sources/CoreFoundation/CFBundle_Main.c 15 | @@ -80,7 +80,19 @@ static CFBundleRef _CFBundleGetMainBundleAlreadyLocked(void) { 16 | CFStringRef str = NULL; 17 | CFURLRef executableURL = NULL, bundleURL = NULL; 18 | _initedMainBundle = true; 19 | + 20 | +#if defined(__ANDROID__) 21 | + const char *bundlePath = getenv("CF_MAIN_BUNDLE_PATH"); 22 | + if (bundlePath) { 23 | + str = CFStringCreateWithFileSystemRepresentation(kCFAllocatorSystemDefault, bundlePath); 24 | + bundleURL = CFURLCreateWithFileSystemPath(kCFAllocatorSystemDefault, str, PLATFORM_PATH_STYLE, true); 25 | + processPath = NULL; 26 | + } else { 27 | + processPath = _CFProcessPath(); 28 | + } 29 | +#else 30 | processPath = _CFProcessPath(); 31 | +#endif 32 | if (processPath) { 33 | str = CFStringCreateWithFileSystemRepresentation(kCFAllocatorSystemDefault, processPath); 34 | if (!executableURL) executableURL = CFURLCreateWithFileSystemPath(kCFAllocatorSystemDefault, str, PLATFORM_PATH_STYLE, false); 35 | -- 36 | 2.46.0 37 | 38 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0012-fix-swift-6-in-certificate-trust-and--basic-auth.patch: -------------------------------------------------------------------------------- 1 | From 9a0389d66c1964c9cc7fb642bc6d7e55a73c67ca Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Tue, 24 Dec 2024 00:55:48 +0200 4 | Subject: [PATCH] Fix Swift 6 in certificate trust and basic auth 5 | 6 | --- 7 | Sources/FoundationNetworking/URLProtectionSpace.swift | 1 - 8 | Sources/FoundationNetworking/URLProtocol.swift | 1 - 9 | 2 files changed, 2 deletions(-) 10 | 11 | diff --git a/Sources/FoundationNetworking/URLProtectionSpace.swift b/Sources/FoundationNetworking/URLProtectionSpace.swift 12 | index 2c93c486..286549b0 100644 13 | --- a/Sources/FoundationNetworking/URLProtectionSpace.swift 14 | +++ b/Sources/FoundationNetworking/URLProtectionSpace.swift 15 | @@ -103,7 +103,6 @@ public let NSURLAuthenticationMethodClientCertificate: String = "NSURLAuthentica 16 | @const NSURLAuthenticationMethodServerTrust 17 | @abstract SecTrustRef validation required. Applies to any protocol. 18 | */ 19 | -@available(*, unavailable, message: "swift-corelibs-foundation does not support methods of authentication that rely on the Darwin Security framework.") 20 | public let NSURLAuthenticationMethodServerTrust: String = "NSURLAuthenticationMethodServerTrust" 21 | 22 | 23 | diff --git a/Sources/FoundationNetworking/URLProtocol.swift b/Sources/FoundationNetworking/URLProtocol.swift 24 | index b59159e2..a2fde729 100644 25 | --- a/Sources/FoundationNetworking/URLProtocol.swift 26 | +++ b/Sources/FoundationNetworking/URLProtocol.swift 27 | @@ -156,7 +156,6 @@ internal class _ProtocolClient : NSObject, @unchecked Sendable { 28 | var cacheableResponse: URLResponse? 29 | } 30 | 31 | -@available(*, unavailable) 32 | extension URLProtocol : @unchecked Sendable { } 33 | 34 | /*! 35 | -- 36 | 2.46.0 37 | 38 | -------------------------------------------------------------------------------- /patches/swift-foundation/0001-open-range-attributed-string-from-nsrange.patch: -------------------------------------------------------------------------------- 1 | From d403cbb5aa39add8631c5a0fa9aed9e3b7a6aee9 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 29 Mar 2025 15:00:55 +0200 4 | Subject: [PATCH] Add Range.init(location:length:in:) 5 | 6 | --- 7 | .../AttributedString/Conversion.swift | 16 ++++++++++++++++ 8 | 1 file changed, 16 insertions(+) 9 | 10 | diff --git a/Sources/FoundationEssentials/AttributedString/Conversion.swift b/Sources/FoundationEssentials/AttributedString/Conversion.swift 11 | index acaee31..dd4afd1 100644 12 | --- a/Sources/FoundationEssentials/AttributedString/Conversion.swift 13 | +++ b/Sources/FoundationEssentials/AttributedString/Conversion.swift 14 | @@ -466,6 +466,22 @@ extension Range where Bound == AttributedString.Index { 15 | let start = bstr.utf16.index(bstr.startIndex, offsetBy: range.location) 16 | let end = bstr.utf16.index(start, offsetBy: range.length) 17 | 18 | + guard start >= string.startIndex._value, end <= string.endIndex._value else { return nil } 19 | + self.init(uncheckedBounds: (.init(start), .init(end))) 20 | + } 21 | +#else 22 | + public init?(location: Int, length: Int, in string: S) { 23 | + // FIXME: This can return indices addressing trailing surrogates, which isn't a thing 24 | + // FIXME: AttributedString is normally prepared to handle. 25 | + // FIXME: Consider rounding everything down to the nearest scalar boundary. 26 | + guard location >= 0, length >= 0 else { return nil } 27 | + let endOffset = location + length 28 | + let bstr = string.__guts.string 29 | + guard endOffset <= bstr.utf16.count else { return nil } 30 | + 31 | + let start = bstr.utf16.index(bstr.startIndex, offsetBy: location) 32 | + let end = bstr.utf16.index(start, offsetBy: length) 33 | + 34 | guard start >= string.startIndex._value, end <= string.endIndex._value else { return nil } 35 | self.init(uncheckedBounds: (.init(start), .init(end))) 36 | } 37 | -- 38 | 2.46.0 39 | 40 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0009-dont-crash-on-non-critical-network-errors.patch: -------------------------------------------------------------------------------- 1 | From a7735878a9f85f7246c5738c1a05355c5bcf7e2d Mon Sep 17 00:00:00 2001 2 | From: Alexander Smarus 3 | Date: Tue, 16 Jan 2024 19:03:37 +0200 4 | Subject: [PATCH] Don't crash on non-critical network errors 5 | 6 | (cherry picked from commit 2989907d3ca2d37a80c5f62c131ea2b517c89e77) 7 | --- 8 | .../URLSession/NativeProtocol.swift | 11 +++++++++-- 9 | 1 file changed, 9 insertions(+), 2 deletions(-) 10 | 11 | diff --git a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 12 | index 965c24ab..39adf264 100644 13 | --- a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 14 | +++ b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 15 | @@ -66,7 +66,10 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { 16 | suspend() 17 | } else { 18 | self.internalState = .transferFailed 19 | - guard let error = self.task?.error else { fatalError() } 20 | + guard let error = self.task?.error else { 21 | + completeTask(withError: NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown)) 22 | + return 23 | + } 24 | completeTask(withError: error) 25 | } 26 | } 27 | @@ -237,7 +240,11 @@ internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate { 28 | } 29 | 30 | guard let response = ts.response else { 31 | - fatalError("Transfer completed, but there's no response.") 32 | + internalState = .transferFailed 33 | + let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, 34 | + userInfo: [NSLocalizedDescriptionKey: "Transfer completed, but there's no response."]) 35 | + failWith(error: error, request: request) 36 | + return 37 | } 38 | internalState = .transferCompleted(response: response, bodyDataDrain: ts.bodyDataDrain) 39 | let action = completionAction(forCompletedRequest: request, response: response) 40 | -- 41 | 2.46.0 42 | 43 | -------------------------------------------------------------------------------- /patches/swift-corelibs-libdispatch/0001-Add-LIBDISPATCH_DEFAULT_THREAD_POOL_SIZE-env-variabl.patch: -------------------------------------------------------------------------------- 1 | From c49c13cd47983030cf6b880ba18317203c629c5e Mon Sep 17 00:00:00 2001 2 | From: Anton Pogonets 3 | Date: Wed, 21 Nov 2018 19:31:39 +0200 4 | Subject: [PATCH] Add LIBDISPATCH_DEFAULT_THREAD_POOL_SIZE env variable 5 | 6 | --- 7 | src/internal.h | 8 +++++++- 8 | src/queue.c | 11 ++++++++++- 9 | 2 files changed, 17 insertions(+), 2 deletions(-) 10 | 11 | diff --git a/src/internal.h b/src/internal.h 12 | index 3cc16fc..da0e36a 100644 13 | --- a/src/internal.h 14 | +++ b/src/internal.h 15 | @@ -1112,6 +1112,12 @@ extern bool _dispatch_kevent_workqueue_enabled; 16 | #include "inline_internal.h" 17 | #include "firehose/firehose_internal.h" 18 | 19 | -__END_DECLS 20 | +#if !HAVE_PTHREAD_WORKQUEUE_KEVENT 21 | +// copied from https://opensource.apple.com/source/libpthread/libpthread-301.50.1/kern/workqueue_internal.h.auto.html 22 | +#define WORKQUEUE_MAXTHREADS 512 23 | +#define WORKQUEUE_CONSTRAINED_MAXTHREADS (WORKQUEUE_MAXTHREADS >> 3) 24 | +#define WORKQUEUE_CONSTRAINED_FACTOR 5 25 | +#endif 26 | 27 | +__END_DECLS 28 | #endif /* __DISPATCH_INTERNAL__ */ 29 | diff --git a/src/queue.c b/src/queue.c 30 | index 90f3cfa..9537d6f 100644 31 | --- a/src/queue.c 32 | +++ b/src/queue.c 33 | @@ -6168,10 +6168,19 @@ _dispatch_root_queue_init_pthread_pool(dispatch_queue_global_t dq, 34 | int pool_size, dispatch_priority_t pri) 35 | { 36 | dispatch_pthread_root_queue_context_t pqc = dq->do_ctxt; 37 | + int32_t default_pool_size = 0; 38 | + char* default_pool_size_env = getenv("LIBDISPATCH_DEFAULT_THREAD_POOL_SIZE"); 39 | + if (default_pool_size_env) { 40 | + default_pool_size = (int32_t) atoi(default_pool_size_env); 41 | + } 42 | + if (!default_pool_size) { 43 | + default_pool_size = (int32_t) MAX(dispatch_hw_config(active_cpus) * 5, WORKQUEUE_CONSTRAINED_MAXTHREADS); 44 | + } 45 | int thread_pool_size = DISPATCH_WORKQ_MAX_PTHREAD_COUNT; 46 | if (!(pri & DISPATCH_PRIORITY_FLAG_OVERCOMMIT)) { 47 | - thread_pool_size = (int32_t)dispatch_hw_config(active_cpus); 48 | + thread_pool_size = default_pool_size; 49 | } 50 | + 51 | if (pool_size && pool_size < thread_pool_size) thread_pool_size = pool_size; 52 | dq->dgq_thread_pool_size = thread_pool_size; 53 | qos_class_t cls = _dispatch_qos_to_qos_class(_dispatch_priority_qos(pri) ?: 54 | -- 55 | 2.29.2 56 | 57 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0013-fix-memory-leak-in-nsattributedstring.patch: -------------------------------------------------------------------------------- 1 | From fe1d7db7bb6c53b43a5254826e326caffdf882f8 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Tue, 1 Apr 2025 12:42:55 +0300 4 | Subject: [PATCH] Fix memory leak in NSAttributedString 5 | 6 | --- 7 | Sources/CoreFoundation/CFRunArray.c | 8 ++++++++ 8 | Sources/CoreFoundation/include/CFRunArray.h | 2 ++ 9 | Sources/Foundation/NSAttributedString.swift | 7 ++++++- 10 | 3 files changed, 16 insertions(+), 1 deletion(-) 11 | 12 | diff --git a/Sources/CoreFoundation/CFRunArray.c b/Sources/CoreFoundation/CFRunArray.c 13 | index 03fb1cdc..7a79ea13 100644 14 | --- a/Sources/CoreFoundation/CFRunArray.c 15 | +++ b/Sources/CoreFoundation/CFRunArray.c 16 | @@ -200,6 +200,14 @@ CFRunArrayRef CFRunArrayCreate(CFAllocatorRef allocator) { 17 | return array; 18 | } 19 | 20 | +CFRunArrayRef CFRunArrayRetain(CFRunArrayRef array) { 21 | + return COPY(array); 22 | +} 23 | + 24 | +void CFRunArrayRelease(CFRunArrayRef array) { 25 | + FREE(array); 26 | +} 27 | + 28 | CFIndex CFRunArrayGetCount(CFRunArrayRef array) { 29 | return array->guts->length; 30 | } 31 | diff --git a/Sources/CoreFoundation/include/CFRunArray.h b/Sources/CoreFoundation/include/CFRunArray.h 32 | index 6d966cff..f46386ca 100644 33 | --- a/Sources/CoreFoundation/include/CFRunArray.h 34 | +++ b/Sources/CoreFoundation/include/CFRunArray.h 35 | @@ -34,6 +34,8 @@ Returns the type identifier of all CFAttributedString instances. 36 | CF_EXPORT CFTypeID CFRunArrayGetTypeID(void); 37 | 38 | CF_EXPORT CFRunArrayRef CFRunArrayCreate(CFAllocatorRef allocator); 39 | +CF_EXPORT CFRunArrayRef CFRunArrayRetain(CFRunArrayRef array); 40 | +CF_EXPORT void CFRunArrayRelease(CFRunArrayRef array); 41 | 42 | CF_EXPORT CFIndex CFRunArrayGetCount(CFRunArrayRef array); 43 | CF_EXPORT CFTypeRef CFRunArrayGetValueAtIndex(CFRunArrayRef array, CFIndex loc, CFRange *effectiveRange, CFIndex *runArrayIndexPtr); 44 | diff --git a/Sources/Foundation/NSAttributedString.swift b/Sources/Foundation/NSAttributedString.swift 45 | index c83818d7..7378ac1f 100644 46 | --- a/Sources/Foundation/NSAttributedString.swift 47 | +++ b/Sources/Foundation/NSAttributedString.swift 48 | @@ -223,7 +223,12 @@ open class NSAttributedString: NSObject, NSCopying, NSMutableCopying, NSSecureCo 49 | 50 | // use the resulting _string and _attributeArray to initialize a new instance 51 | _string = mutableAttributedString._string 52 | - _attributeArray = mutableAttributedString._attributeArray 53 | + _attributeArray = CFRunArrayRetain(mutableAttributedString._attributeArray) 54 | + } 55 | + 56 | + deinit { 57 | + // Release the CFRunArray created in init methods 58 | + CFRunArrayRelease(_attributeArray) 59 | } 60 | 61 | /// Executes the block for each attribute in the range. 62 | -- 63 | 2.46.0 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Android Toolchain [![Download](https://img.shields.io/github/v/release/readdle/swift-android-toolchain?label=Download)](https://github.com/readdle/swift-android-toolchain/releases/latest) 2 | 3 | 4 | Automated scripts to build Swift Android cross compilation toolchain for macOS 5 | 6 | # Installation 7 | Prebuilt toolchains are located on [Github Releases](https://github.com/readdle/swift-android-toolchain/releases) 8 | 9 | ### Prepare environment (macOS x86_64 or macOS arm64) 10 | 11 | 1. Install Swift 6.1.0 toolchain with [swiftly](https://www.swift.org/install/macos/) 12 | 13 | ``` 14 | swiftly install 6.1.0 15 | ``` 16 | 17 | 2. Install the [Android NDK 27c](https://developer.android.com/ndk/downloads) 18 | * If you have [Android SDK Command-Line Tools](https://developer.android.com/tools#tools-sdk) installed: 19 | ``` 20 | sdkmanager --install "ndk;27.2.12479018" 21 | ``` 22 | 23 | 3. Install the Swift Android Toolchain 24 | ``` 25 | curl -L -O https://github.com/readdle/swift-android-toolchain/releases/latest/download/swift-android.zip 26 | unzip swift-android.zip 27 | ``` 28 | 29 | 4. Set Up Environment Variables 30 | ``` 31 | export ANDROID_NDK_HOME= 32 | export SWIFT_ANDROID_HOME= 33 | 34 | export PATH=$ANDROID_NDK_HOME:$PATH 35 | export PATH=$SWIFT_ANDROID_HOME/build-tools:$PATH 36 | ``` 37 | 38 | ### Build and Test swift modules 39 | 40 | Our current swift build system is tiny wrapper over Swift PM. See [Swift Package Manager](https://github.com/apple/swift-package-manager/blob/master/Documentation/Usage.md) docs for more info. 41 | 42 | | Command | Description | 43 | |-------------------------------------|------------------------------| 44 | | swift-android build | Build all products | 45 | | swift-android build --build-tests | Build all products and tests | 46 | | swift-android test | Connect to Android device and run all tests | 47 | 48 | `swift-android build` wrapper scripts works as `swift build` from Swift Package Manager but is configured for Android. You can pass additional parameters such as `-Xswiftc -DDEBUG` , `-Xswiftc -suppress-warnings` or `--configuration release` 49 | 50 | Example of compilation flags: 51 | * Debug 52 | ``` 53 | swift-android build --configuration debug -Xswiftc -DDEBUG 54 | ``` 55 | * Release 56 | ``` 57 | swift-android build --configuration release 58 | ``` 59 | 60 | `swift-android test` wrapper script builds, copies, and runs Swift Package Manager (SPM) tests on a connected Android device. 61 | 62 | ### Build swift modules with Android Studio 63 | 64 | This [plugin](https://github.com/readdle/swift-android-gradle) integrates Swift Android Toolchain to Gradle 65 | 66 | ### Other swift releated projects and articles 67 | 68 | 1. [Swift for Android: Our Experience and Tools](https://readdle.com/blog/swift-for-android-our-experience-and-tools) 69 | 2. [Anotation Processor for generating JNI code](https://github.com/readdle/swift-java-codegen) 70 | 3. [Cross-platform swift weather app](https://github.com/andriydruk/swift-weather-app) 71 | -------------------------------------------------------------------------------- /patches/swift-syntax/0001-replace-bionic-with-android.patch: -------------------------------------------------------------------------------- 1 | From 8c034c74bd78046b0686e521136a1258f800d1cb Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Mon, 14 Apr 2025 18:18:40 +0300 4 | Subject: [PATCH] Replace Bionic with Android 5 | 6 | --- 7 | .../LibraryPluginProvider.swift | 2 ++ 8 | Sources/SwiftSyntax/SyntaxText.swift | 10 ++++++---- 9 | .../swift-syntax-dev-utils/common/ProcessRunner.swift | 2 +- 10 | 3 files changed, 9 insertions(+), 5 deletions(-) 11 | 12 | diff --git a/Sources/SwiftLibraryPluginProvider/LibraryPluginProvider.swift b/Sources/SwiftLibraryPluginProvider/LibraryPluginProvider.swift 13 | index 44be07e2..69bb6db8 100644 14 | --- a/Sources/SwiftLibraryPluginProvider/LibraryPluginProvider.swift 15 | +++ b/Sources/SwiftLibraryPluginProvider/LibraryPluginProvider.swift 16 | @@ -35,6 +35,8 @@ import SwiftSyntaxMacros 17 | @_implementationOnly import Glibc 18 | #elseif canImport(Musl) 19 | @_implementationOnly import Musl 20 | +#elseif canImport(Android) 21 | +@_implementationOnly import Android 22 | #endif 23 | #endif 24 | 25 | diff --git a/Sources/SwiftSyntax/SyntaxText.swift b/Sources/SwiftSyntax/SyntaxText.swift 26 | index 5229771f..53c0a37a 100644 27 | --- a/Sources/SwiftSyntax/SyntaxText.swift 28 | +++ b/Sources/SwiftSyntax/SyntaxText.swift 29 | @@ -15,8 +15,8 @@ 30 | private import Darwin 31 | #elseif canImport(Glibc) 32 | private import Glibc 33 | -#elseif canImport(Bionic) 34 | -private import Bionic 35 | +#elseif canImport(Android) 36 | +private import Android 37 | #elseif canImport(Musl) 38 | private import Musl 39 | #endif 40 | @@ -25,6 +25,8 @@ private import Musl 41 | import Darwin 42 | #elseif canImport(Glibc) 43 | import Glibc 44 | +#elseif canImport(Android) 45 | +import Android 46 | #elseif canImport(Musl) 47 | import Musl 48 | #endif 49 | @@ -296,8 +298,8 @@ private func compareMemory( 50 | return Darwin.memcmp(s1, s2, count) == 0 51 | #elseif canImport(Glibc) 52 | return Glibc.memcmp(s1, s2, count) == 0 53 | - #elseif canImport(Bionic) 54 | - return Bionic.memcmp(s1, s2, count) == 0 55 | + #elseif canImport(Android) 56 | + return Android.memcmp(s1, s2, count) == 0 57 | #else 58 | return UnsafeBufferPointer(start: s1, count: count) 59 | .elementsEqual(UnsafeBufferPointer(start: s2, count: count)) 60 | diff --git a/SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/common/ProcessRunner.swift b/SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/common/ProcessRunner.swift 61 | index e88611cb..4f14ab6c 100644 62 | --- a/SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/common/ProcessRunner.swift 63 | +++ b/SwiftSyntaxDevUtils/Sources/swift-syntax-dev-utils/common/ProcessRunner.swift 64 | @@ -24,7 +24,7 @@ class SigIntListener { 65 | /// Registers a `SIGINT` signal handler that forwards `SIGINT` to all 66 | /// subprocesses that are registered in `runningSubprocesses` 67 | static func registerSigIntSubprocessTerminationHandler() { 68 | - #if canImport(Darwin) || canImport(Glibc) || canImport(Bionic) 69 | + #if canImport(Darwin) || canImport(Glibc) || canImport(Android) 70 | signal(SIGINT) { _ in 71 | SigIntListener.hasReceivedSigInt = true 72 | for process in SigIntListener.runningSubprocesses { 73 | -- 74 | 2.46.0 75 | 76 | -------------------------------------------------------------------------------- /patches/swift-foundation-icu/revert-hide-all-icu-c++-symbols.patch: -------------------------------------------------------------------------------- 1 | From 36314e5ff55b4907f70326531f3382f599d8dbce Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 14 Jun 2025 19:53:01 +0300 4 | Subject: [PATCH] Revert "Hide all ICU public C++ Symbols (#34)" 5 | 6 | This reverts commit 0b2f6411bd55a34333fdc094a2bfc391c14d84c2. 7 | --- 8 | icuSources/i18n/uspoof.cpp | 4 +-- 9 | .../include/_foundation_unicode/utypes.h | 32 ++++--------------- 10 | 2 files changed, 9 insertions(+), 27 deletions(-) 11 | 12 | diff --git a/icuSources/i18n/uspoof.cpp b/icuSources/i18n/uspoof.cpp 13 | index 7a6bb72..ee22170 100644 14 | --- a/icuSources/i18n/uspoof.cpp 15 | +++ b/icuSources/i18n/uspoof.cpp 16 | @@ -939,13 +939,13 @@ uspoof_getRecommendedSet(UErrorCode *status) { 17 | return gRecommendedSet->toUSet(); 18 | } 19 | 20 | -U_CAPI const UnicodeSet * U_EXPORT2 21 | +U_I18N_API const UnicodeSet * U_EXPORT2 22 | uspoof_getInclusionUnicodeSet(UErrorCode *status) { 23 | umtx_initOnce(gSpoofInitStaticsOnce, &initializeStatics, *status); 24 | return gInclusionSet; 25 | } 26 | 27 | -U_CAPI const UnicodeSet * U_EXPORT2 28 | +U_I18N_API const UnicodeSet * U_EXPORT2 29 | uspoof_getRecommendedUnicodeSet(UErrorCode *status) { 30 | umtx_initOnce(gSpoofInitStaticsOnce, &initializeStatics, *status); 31 | return gRecommendedSet; 32 | diff --git a/icuSources/include/_foundation_unicode/utypes.h b/icuSources/include/_foundation_unicode/utypes.h 33 | index e88f70a..3cd95e9 100644 34 | --- a/icuSources/include/_foundation_unicode/utypes.h 35 | +++ b/icuSources/include/_foundation_unicode/utypes.h 36 | @@ -354,31 +354,13 @@ typedef double UDate; 37 | #endif 38 | 39 | #if defined(U_COMBINED_IMPLEMENTATION) 40 | -// SwiftFoundationICU Changes: hide all C++ public symbols 41 | -// Rationale: When FoundationInternationalization tests are executed, 42 | -// we effectively load two ICU instances into memory: 43 | -// 44 | -// 1) The system ICU loaded by XCTest via system Foundation; 45 | -// 2) The package ICU SwiftFoundation utilizes. 46 | -// 47 | -// These two ICUs cause symbol collisions for dyld due to the fact that 48 | -// all public C++ symbols share a global namespace and coalesce across all loaded dylibs. 49 | -// Consequently, we encounter sporadic test failures in SwiftFoundation as dyld 50 | -// arbitrarily selects ICU symbols and occasionally chooses the system one. 51 | -// 52 | -// To address this issue, we resolved to hide all C++ APIs, 53 | -// ensuring they are not weakly referenced and potentially bound to 54 | -// the system ICU implementation. 55 | -// 56 | -// This solution proves effective for SwiftFoundation, 57 | -// as it does not actually utilize the C++ APIs. 58 | -#define U_DATA_API __attribute__((visibility("hidden"))) 59 | -#define U_COMMON_API __attribute__((visibility("hidden"))) 60 | -#define U_I18N_API __attribute__((visibility("hidden"))) 61 | -#define U_LAYOUT_API __attribute__((visibility("hidden"))) 62 | -#define U_LAYOUTEX_API __attribute__((visibility("hidden"))) 63 | -#define U_IO_API __attribute__((visibility("hidden"))) 64 | -#define U_TOOLUTIL_API __attribute__((visibility("hidden"))) 65 | +#define U_DATA_API U_EXPORT 66 | +#define U_COMMON_API U_EXPORT 67 | +#define U_I18N_API U_EXPORT 68 | +#define U_LAYOUT_API U_EXPORT 69 | +#define U_LAYOUTEX_API U_EXPORT 70 | +#define U_IO_API U_EXPORT 71 | +#define U_TOOLUTIL_API U_EXPORT 72 | #elif defined(U_STATIC_IMPLEMENTATION) 73 | #define U_DATA_API 74 | #define U_COMMON_API 75 | -- 76 | 2.46.0 77 | 78 | -------------------------------------------------------------------------------- /patches/swift/0002-Forward-android-arch-form-build-script-to-build-scri.patch: -------------------------------------------------------------------------------- 1 | From 68f24e51020ddf9245f3ac51b822989a06dfd46e Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 12 Apr 2025 15:24:06 +0300 4 | Subject: [PATCH] Forward android arch form build script to build script 5 | 6 | --- 7 | utils/build-presets.ini | 7 +++++++ 8 | utils/build-script | 3 +++ 9 | utils/build_swift/build_swift/driver_arguments.py | 4 ++-- 10 | utils/swift_build_support/swift_build_support/targets.py | 2 +- 11 | 4 files changed, 13 insertions(+), 3 deletions(-) 12 | 13 | diff --git a/utils/build-presets.ini b/utils/build-presets.ini 14 | index 23abcd95d91..76003cdd7bd 100644 15 | --- a/utils/build-presets.ini 16 | +++ b/utils/build-presets.ini 17 | @@ -1021,6 +1021,13 @@ skip-early-swiftsyntax 18 | 19 | reconfigure 20 | 21 | +[preset: buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build,armv7] 22 | +mixin-preset=buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build 23 | + 24 | +dash-dash 25 | + 26 | +android-arch=armv7 27 | + 28 | [preset: buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build,aarch64] 29 | mixin-preset=buildbot_linux_crosscompile_android,tools=RA,stdlib=RD,build 30 | 31 | diff --git a/utils/build-script b/utils/build-script 32 | index d37c50ca35e..e9d6b5ebc41 100755 33 | --- a/utils/build-script 34 | +++ b/utils/build-script 35 | @@ -372,6 +372,9 @@ def apply_default_arguments(toolchain, args): 36 | elif args.android_arch == "aarch64": 37 | args.stdlib_deployment_targets.append( 38 | StdlibDeploymentTarget.Android.aarch64.name) 39 | + elif args.android_arch == "i686": 40 | + args.stdlib_deployment_targets.append( 41 | + StdlibDeploymentTarget.Android.i686.name) 42 | elif args.android_arch == "x86_64": 43 | args.stdlib_deployment_targets.append( 44 | StdlibDeploymentTarget.Android.x86_64.name) 45 | diff --git a/utils/build_swift/build_swift/driver_arguments.py b/utils/build_swift/build_swift/driver_arguments.py 46 | index 6d5759cab41..1cab2936fc3 100644 47 | --- a/utils/build_swift/build_swift/driver_arguments.py 48 | +++ b/utils/build_swift/build_swift/driver_arguments.py 49 | @@ -1423,10 +1423,10 @@ def create_argument_parser(): 50 | android.adb.commands.DEVICE_TEMP_DIR)) 51 | 52 | option('--android-arch', store, 53 | - choices=['armv7', 'aarch64', 'x86_64'], 54 | + choices=['armv7', 'aarch64', 'i686', 'x86_64'], 55 | default='aarch64', 56 | help='The target architecture when building for Android. ' 57 | - 'Currently, only armv7, aarch64, and x86_64 are supported. ' 58 | + 'Currently, only armv7, aarch64, i686 and x86_64 are supported. ' 59 | '%(default)s is the default.') 60 | 61 | # ------------------------------------------------------------------------- 62 | diff --git a/utils/swift_build_support/swift_build_support/targets.py b/utils/swift_build_support/swift_build_support/targets.py 63 | index 6447c84f665..82ee2e37228 100644 64 | --- a/utils/swift_build_support/swift_build_support/targets.py 65 | +++ b/utils/swift_build_support/swift_build_support/targets.py 66 | @@ -300,7 +300,7 @@ class StdlibDeploymentTarget(object): 67 | 68 | Cygwin = Platform("cygwin", archs=["x86_64"]) 69 | 70 | - Android = AndroidPlatform("android", archs=["armv7", "aarch64", "x86_64"]) 71 | + Android = AndroidPlatform("android", archs=["armv7", "aarch64", "i686", "x86_64"]) 72 | 73 | Windows = Platform("windows", archs=["x86_64"]) 74 | 75 | -- 76 | 2.46.0 77 | 78 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0001-use-content-length-if-present-from-headers-for-streamed-body.patch: -------------------------------------------------------------------------------- 1 | From 21ebccd585e465a40bfe1ed3f5fac38bdc8d289a Mon Sep 17 00:00:00 2001 2 | From: Alexander Smarus 3 | Date: Sat, 26 Sep 2020 17:35:23 +0300 4 | Subject: [PATCH] Use content length, if present, from headers for streamed 5 | body 6 | 7 | Otherwise curl would switch to chunked transfer, which is 8 | not correct (see https://tools.ietf.org/html/rfc7230#section-3.3.2) 9 | --- 10 | .../URLSession/HTTP/HTTPURLProtocol.swift | 7 ++++- 11 | Tests/Foundation/HTTPServer.swift | 5 ++++ 12 | Tests/Foundation/TestURLSession.swift | 26 ++++++++++++++++++- 13 | 3 files changed, 36 insertions(+), 2 deletions(-) 14 | 15 | diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 16 | index 90a19611..a00eec13 100644 17 | --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 18 | +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 19 | @@ -342,7 +342,12 @@ internal class _HTTPURLProtocol: _NativeProtocol { 20 | set(requestBodyLength: .length(length)) 21 | task!.countOfBytesExpectedToSend = Int64(length) 22 | case (_, nil): 23 | - set(requestBodyLength: .unknown) 24 | + if let contentLengthValue = request.value(forHTTPHeaderField: "Content-Length"), let contentLength = UInt64(contentLengthValue) { 25 | + set(requestBodyLength: .length(contentLength)) 26 | + task!.countOfBytesExpectedToSend = Int64(contentLength) 27 | + } else { 28 | + set(requestBodyLength: .unknown) 29 | + } 30 | } 31 | } catch let e { 32 | // Fail the request here. 33 | diff --git a/Tests/Foundation/HTTPServer.swift b/Tests/Foundation/HTTPServer.swift 34 | index 96d849f2..07f3f07a 100644 35 | --- a/Tests/Foundation/HTTPServer.swift 36 | +++ b/Tests/Foundation/HTTPServer.swift 37 | @@ -742,6 +742,11 @@ public class TestURLSessionServer: CustomStringConvertible { 38 | } 39 | 40 | if uri == "/upload" { 41 | + if request.getHeader(for: "transfer-encoding") != nil && request.getHeader(for: "content-length") != nil { 42 | + // https://tools.ietf.org/html/rfc7230#section-3.3.2 43 | + // A sender MUST NOT send a Content-Length header field in any message that contains a Transfer-Encoding header field. 44 | + return try _HTTPResponse(response: .BAD_REQUEST, body: "Content-Length not allowed with Transfer-Encoding") 45 | + } 46 | if let contentLength = request.getHeader(for: "content-length") { 47 | let text = "Upload completed!, Content-Length: \(contentLength)" 48 | return try _HTTPResponse(response: .OK, body: text) 49 | diff --git a/Tests/Foundation/TestURLSession.swift b/Tests/Foundation/TestURLSession.swift 50 | index 9f202151..14a810ff 100644 51 | --- a/Tests/Foundation/TestURLSession.swift 52 | +++ b/Tests/Foundation/TestURLSession.swift 53 | @@ -135,7 +135,31 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { 54 | XCTAssertEqual("London", result, "Did not receive expected value") 55 | XCTAssertEqual("London", delegate.capital) 56 | } 57 | - 58 | + 59 | + func test_dataTaskWithHttpInputStreamContentLength() throws { 60 | + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/upload" 61 | + let url = try XCTUnwrap(URL(string: urlString)) 62 | + 63 | + let dataString = "IdOnly" 64 | + 65 | + let data = try XCTUnwrap(dataString.data(using: .utf8)) 66 | + 67 | + var urlRequest = URLRequest(url: url) 68 | + urlRequest.httpMethod = "POST" 69 | + urlRequest.httpBodyStream = InputStream(data: data) 70 | + urlRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length") 71 | + 72 | + let delegate = SessionDelegate(with: expectation(description: "POST \(urlString): with HTTP Body as InputStream")) 73 | + delegate.run(with: urlRequest, timeoutInterval: 3) 74 | + waitForExpectations(timeout: 4) 75 | + 76 | + let httpResponse = delegate.response as? HTTPURLResponse 77 | + 78 | + XCTAssertNil(delegate.error) 79 | + XCTAssertNotNil(delegate.response) 80 | + XCTAssertEqual(httpResponse?.statusCode, 200) 81 | + } 82 | + 83 | func test_dataTaskWithHttpInputStream() async throws { 84 | throw XCTSkip("This test is disabled (Flaky test)") 85 | #if false 86 | -- 87 | 2.46.0 88 | 89 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0005-update-URLProtectionSpace-tests-after-support-Digest-and-NTLM-authentications.patch: -------------------------------------------------------------------------------- 1 | From 592541a37b90bb4486abd6fbc4a4fc470073cd56 Mon Sep 17 00:00:00 2001 2 | From: Konstiantyn Gominyuk 3 | Date: Tue, 1 Dec 2020 11:27:54 +0200 4 | Subject: [PATCH] Update URLProtectionSpace tests after support Digest and NTLM 5 | authentications 6 | 7 | --- 8 | Tests/Foundation/TestURLProtectionSpace.swift | 43 +++++++++++++------ 9 | 1 file changed, 30 insertions(+), 13 deletions(-) 10 | 11 | diff --git a/Tests/Foundation/TestURLProtectionSpace.swift b/Tests/Foundation/TestURLProtectionSpace.swift 12 | index 9ffbe11f..9b924b1d 100644 13 | --- a/Tests/Foundation/TestURLProtectionSpace.swift 14 | +++ b/Tests/Foundation/TestURLProtectionSpace.swift 15 | @@ -88,20 +88,30 @@ class TestURLProtectionSpace : XCTestCase { 16 | XCTAssertEqual(space2.port, 443) 17 | XCTAssertEqual(space2.realm, "test") 18 | 19 | - // Digest is not supported 20 | + // Digest is supported 21 | let authenticate3 = "Digest realm=\"Test\", domain=\"/HTTP/Digest\", nonce=\"be2e96ad8ab8acb7ccfb49bc7e162914\"" 22 | let response3 = try XCTUnwrap(HTTPURLResponse(url: URL(string: "http://jigsaw.w3.org/HTTP/Basic/")!, 23 | statusCode: 401, 24 | httpVersion: "HTTP/1.1", 25 | headerFields: ["www-authenticate" : authenticate3])) 26 | - XCTAssertNil(URLProtectionSpace.create(with: response3), "Digest scheme is not supported, should not create protection space") 27 | - 28 | - // NTLM is not supported 29 | + let space3 = try XCTUnwrap(URLProtectionSpace.create(with: response3), "Failed to create protection space from valid response") 30 | + XCTAssertEqual(space3.authenticationMethod, NSURLAuthenticationMethodHTTPDigest) 31 | + XCTAssertEqual(space3.protocol, "http") 32 | + XCTAssertEqual(space3.host, "jigsaw.w3.org") 33 | + XCTAssertEqual(space3.port, 80) 34 | + XCTAssertEqual(space3.realm, "Test") 35 | + 36 | + // NTLM is supported 37 | let response4 = try XCTUnwrap(HTTPURLResponse(url: URL(string: "http://apple.com:333")!, 38 | statusCode: 401, 39 | httpVersion: "HTTP/1.1", 40 | headerFields: ["www-authenTicate" : "NTLM realm=\"\""])) 41 | - XCTAssertNil(URLProtectionSpace.create(with: response4), "NTLM scheme is not supported, should not create protection space") 42 | + let space4 = try XCTUnwrap(URLProtectionSpace.create(with: response4), "Failed to create protection space from valid response") 43 | + XCTAssertEqual(space4.authenticationMethod, NSURLAuthenticationMethodNTLM) 44 | + XCTAssertEqual(space4.protocol, "http") 45 | + XCTAssertEqual(space4.host, "apple.com") 46 | + XCTAssertEqual(space4.port, 333) 47 | + XCTAssertEqual(space4.realm, "") 48 | 49 | // Some broken headers 50 | let response5 = try XCTUnwrap(HTTPURLResponse(url: URL(string: "http://apple.com")!, 51 | @@ -141,20 +151,27 @@ class TestURLProtectionSpace : XCTestCase { 52 | 53 | // Several challenges, but only two of them should be valid 54 | let challenges2 = _HTTPURLProtocol._HTTPMessage._Challenge.challenges(from: "Digest realm=\"Unsupported\", Basic, basic realm = \"First \\\" realm\", Basic realm=\"Second realm\"") 55 | - XCTAssertEqual(challenges2.count, 2, "String contains 2 valid challenges") 56 | - let challenge2_1 = try XCTUnwrap(challenges2.first) 57 | - XCTAssertEqual(challenge2_1.authScheme, "basic") 58 | + XCTAssertEqual(challenges2.count, 3, "String contains 3 valid challenges") 59 | + let challenge2_1 = try XCTUnwrap(challenges2.count > 0 ? challenges2[0] : nil) 60 | + XCTAssertEqual(challenge2_1.authScheme, "Digest") 61 | XCTAssertEqual(challenge2_1.authParameters.count, 1, "Wrong number of parameters in challenge") 62 | let param2_1_1 = try XCTUnwrap(challenge2_1.parameter(withName: "realm")) 63 | XCTAssertEqual(param2_1_1.name, "realm") 64 | - XCTAssertEqual(param2_1_1.value, "First \" realm") // contains escaped quote 65 | - 66 | - let challenge2_2 = try XCTUnwrap(challenges2.last) 67 | - XCTAssertEqual(challenge2_2.authScheme, "Basic") 68 | + XCTAssertEqual(param2_1_1.value, "Unsupported") 69 | + 70 | + let challenge2_2 = try XCTUnwrap(challenges2.count > 1 ? challenges2[1] : nil) 71 | + XCTAssertEqual(challenge2_2.authScheme, "basic") 72 | XCTAssertEqual(challenge2_2.authParameters.count, 1, "Wrong number of parameters in challenge") 73 | let param2_2_1 = try XCTUnwrap(challenge2_2.parameter(withName: "realm")) 74 | XCTAssertEqual(param2_2_1.name, "realm") 75 | - XCTAssertEqual(param2_2_1.value, "Second realm") 76 | + XCTAssertEqual(param2_2_1.value, "First \" realm") // contains escaped quote 77 | + 78 | + let challenge2_3 = try XCTUnwrap(challenges2.count > 2 ? challenges2[2] : nil) 79 | + XCTAssertEqual(challenge2_3.authScheme, "Basic") 80 | + XCTAssertEqual(challenge2_3.authParameters.count, 1, "Wrong number of parameters in challenge") 81 | + let param2_3_1 = try XCTUnwrap(challenge2_3.parameter(withName: "realm")) 82 | + XCTAssertEqual(param2_3_1.name, "realm") 83 | + XCTAssertEqual(param2_3_1.value, "Second realm") 84 | 85 | // Some tricky and broken strings to test edge cases in parse process 86 | let challenges3 = _HTTPURLProtocol._HTTPMessage._Challenge.challenges(from: "not real, Basic realm=\"Second realm\"") 87 | -- 88 | 2.46.0 89 | 90 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0010-fix-dispatch-source-write-issue-v4.patch: -------------------------------------------------------------------------------- 1 | From 88e3782be7f5536f5c83901d7c25e29b32c3fab5 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 12 Apr 2025 16:26:57 +0300 4 | Subject: [PATCH] [PATCH] Fix dispatch source write issue 5 | 6 | --- 7 | .../URLSession/libcurl/MultiHandle.swift | 35 ++++++++++++++++--- 8 | .../include/CFURLSessionInterface.h | 4 +-- 9 | 2 files changed, 32 insertions(+), 7 deletions(-) 10 | 11 | diff --git a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 12 | index ed9e6bd7..d0dc7ec5 100644 13 | --- a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 14 | +++ b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 15 | @@ -54,6 +54,12 @@ extension URLSession { 16 | configure(with: configuration) 17 | } 18 | deinit { 19 | + // Remove callbacks 20 | + try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionSOCKETDATA, nil) 21 | + try! CFURLSession_multi_setopt_sf(rawHandle, CFURLSessionMultiOptionSOCKETFUNCTION, nil) 22 | + try! CFURLSession_multi_setopt_ptr(rawHandle, CFURLSessionMultiOptionTIMERDATA, nil) 23 | + try! CFURLSession_multi_setopt_tf(rawHandle, CFURLSessionMultiOptionTIMERFUNCTION, nil) 24 | + 25 | // C.f.: 26 | easyHandles.forEach { 27 | try! CFURLSessionMultiHandleRemoveHandle(rawHandle, $0.rawHandle).asError() 28 | @@ -528,12 +534,17 @@ fileprivate class _SocketSources { 29 | var readSource: DispatchSource? 30 | var writeSource: DispatchSource? 31 | 32 | + var readDupSocket: Int32? 33 | + var writeDupSocket: Int32? 34 | + 35 | func createReadSource(socket: CFURLSession_socket_t, queue: DispatchQueue, handler: DispatchWorkItem) { 36 | guard readSource == nil else { return } 37 | #if os(Windows) 38 | let s = DispatchSource.makeReadSource(handle: HANDLE(bitPattern: Int(socket))!, queue: queue) 39 | #else 40 | - let s = DispatchSource.makeReadSource(fileDescriptor: socket, queue: queue) 41 | + let dupSocket = dup(socket) 42 | + readDupSocket = dupSocket 43 | + let s = DispatchSource.makeReadSource(fileDescriptor: dupSocket, queue: queue) 44 | #endif 45 | s.setEventHandler(handler: handler) 46 | readSource = s as? DispatchSource 47 | @@ -545,7 +556,9 @@ fileprivate class _SocketSources { 48 | #if os(Windows) 49 | let s = DispatchSource.makeWriteSource(handle: HANDLE(bitPattern: Int(socket))!, queue: queue) 50 | #else 51 | - let s = DispatchSource.makeWriteSource(fileDescriptor: socket, queue: queue) 52 | + let dupSocket = dup(socket) 53 | + writeDupSocket = dupSocket 54 | + let s = DispatchSource.makeWriteSource(fileDescriptor: dupSocket, queue: queue) 55 | #endif 56 | s.setEventHandler(handler: handler) 57 | writeSource = s as? DispatchSource 58 | @@ -565,13 +578,25 @@ fileprivate class _SocketSources { 59 | // the reference. 60 | let socketReference = handle.beginOperation(for: socket) 61 | let cancelHandlerGroup = DispatchGroup() 62 | - [readSource, writeSource].compactMap({ $0 }).forEach { source in 63 | + 64 | + if let readSource = readSource, let readDupSocket = readDupSocket { 65 | cancelHandlerGroup.enter() 66 | - source.setCancelHandler { 67 | + readSource.setCancelHandler { 68 | + close(readDupSocket) 69 | cancelHandlerGroup.leave() 70 | } 71 | - source.cancel() 72 | + readSource.cancel() 73 | } 74 | + 75 | + if let writeSource = writeSource, let writeDupSocket = writeDupSocket { 76 | + cancelHandlerGroup.enter() 77 | + writeSource.setCancelHandler { 78 | + close(writeDupSocket) 79 | + cancelHandlerGroup.leave() 80 | + } 81 | + writeSource.cancel() 82 | + } 83 | + 84 | cancelHandlerGroup.notify(queue: queue) { 85 | handle.endOperation(for: socketReference) 86 | } 87 | diff --git a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 88 | index 2c0264b1..3005fce5 100644 89 | --- a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 90 | +++ b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 91 | @@ -663,8 +663,8 @@ CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_getinfo_charp(CFURLSessionEasyH 92 | 93 | CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_ptr(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, void *_Nullable a); 94 | CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_l(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, long a); 95 | -CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_sf(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, int (*_Nonnull a)(CFURLSessionEasyHandle _Nonnull, CFURLSession_socket_t, int, void *_Nullable, void *_Nullable)); 96 | -CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_tf(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, int (*_Nonnull a)(CFURLSessionMultiHandle _Nonnull, long, void *_Nullable)); 97 | +CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_sf(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, int (*_Nullable a)(CFURLSessionEasyHandle _Nonnull, CFURLSession_socket_t, int, void *_Nullable, void *_Nullable)); 98 | +CF_EXPORT CFURLSessionMultiCode CFURLSession_multi_setopt_tf(CFURLSessionMultiHandle _Nonnull multi_handle, CFURLSessionMultiOption option, int (*_Nullable a)(CFURLSessionMultiHandle _Nonnull, long, void *_Nullable)); 99 | 100 | CF_EXPORT CFURLSessionEasyCode CFURLSessionInit(void); 101 | 102 | -- 103 | 2.46.0 104 | 105 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0002-request-new-stream-when-restarting-request due-to-redirect-or-auth-challenge.patch: -------------------------------------------------------------------------------- 1 | From f4b8709e72f6b83c622cec71eeef1cdc81500ca5 Mon Sep 17 00:00:00 2001 2 | From: Alexander Smarus 3 | Date: Sat, 26 Sep 2020 19:29:21 +0300 4 | Subject: [PATCH] Request new stream when restarting request due to redirect or 5 | auth challenge 6 | 7 | This matches Darwin behavior. Otherwise _BodyStreamSource will have nothing to read. 8 | --- 9 | .../URLSession/HTTP/HTTPURLProtocol.swift | 4 +- 10 | .../URLSession/URLSessionTask.swift | 4 +- 11 | Tests/Foundation/TestURLSession.swift | 57 +++++++++++++++++++ 12 | 3 files changed, 62 insertions(+), 3 deletions(-) 13 | 14 | diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 15 | index a00eec13..dfe18758 100644 16 | --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 17 | +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 18 | @@ -502,7 +502,7 @@ internal class _HTTPURLProtocol: _NativeProtocol { 19 | } else { 20 | // Follow the redirect. Need to configure new request with cookies, etc. 21 | let configuredRequest = session._configuration.configure(request: request) 22 | - task?.knownBody = URLSessionTask._Body.none 23 | + task?.knownBody = nil 24 | startNewTransfer(with: configuredRequest) 25 | } 26 | } 27 | @@ -714,7 +714,7 @@ extension _HTTPURLProtocol { 28 | // Otherwise, we'll start a new transfer with the passed in request. 29 | if let r = request { 30 | lastRedirectBody = nil 31 | - task?.knownBody = URLSessionTask._Body.none 32 | + task!.knownBody = nil 33 | startNewTransfer(with: r) 34 | } else { 35 | // If the redirect is not followed, return the redirect itself as the response 36 | diff --git a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 37 | index 4968494b..76ebb561 100644 38 | --- a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 39 | +++ b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 40 | @@ -1258,7 +1258,9 @@ extension _ProtocolClient : URLProtocolClient { 41 | } 42 | task._protocolStorage = .existing(_HTTPURLProtocol(task: task, cachedResponse: nil, client: nil)) 43 | } 44 | - 45 | + if case .stream(let stream) = task.knownBody, stream.streamStatus != .notOpen { 46 | + task.knownBody = nil 47 | + } 48 | task.resume() 49 | } 50 | 51 | diff --git a/Tests/Foundation/TestURLSession.swift b/Tests/Foundation/TestURLSession.swift 52 | index 14a810ff..32dfd0d2 100644 53 | --- a/Tests/Foundation/TestURLSession.swift 54 | +++ b/Tests/Foundation/TestURLSession.swift 55 | @@ -1738,6 +1738,63 @@ final class TestURLSession: LoopbackServerTest, @unchecked Sendable { 56 | waitForExpectations(timeout: 60) 57 | } 58 | 59 | + func test_basicAuthRequestWithBodyStream() throws { 60 | + let dataString = "IdOnly" 61 | + let data = try XCTUnwrap(dataString.data(using: .utf8)) 62 | + let expectedCallbacks = [ 63 | + "urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)", 64 | + "urlSession(_:dataTask:didReceive:completionHandler:)", 65 | + "urlSession(_:task:didReceive:completionHandler:)", 66 | + "urlSession(_:task:needNewBodyStream:)", 67 | + "urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)", 68 | + "urlSession(_:dataTask:didReceive:completionHandler:)", 69 | + "urlSession(_:dataTask:didReceive:)", 70 | + "urlSession(_:task:didCompleteWithError:)" 71 | + ] 72 | + 73 | + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/auth/basic" 74 | + let url = URL(string: urlString)! 75 | + var urlRequest = URLRequest(url: url) 76 | + urlRequest.httpMethod = "POST" 77 | + urlRequest.httpBodyStream = InputStream(data: data) 78 | + let delegate = SessionDelegate(with: expectation(description: "POST \(urlString): Upload data")) 79 | + delegate.newBodyStreamHandler = { (completionHandler: @escaping (InputStream?) -> Void) in 80 | + completionHandler(InputStream(data: data)) 81 | + } 82 | + delegate.challengeHandler = { (challenge: URLAuthenticationChallenge) -> (disposition: URLSession.AuthChallengeDisposition, credetial: URLCredential?) in 83 | + return (.useCredential, URLCredential(user: "user", password: "passwd", persistence: .none)) 84 | + } 85 | + delegate.run(with: urlRequest, timeoutInterval: 4) 86 | + waitForExpectations(timeout: 5) 87 | + XCTAssertEqual(delegate.callbacks, expectedCallbacks) 88 | + XCTAssertEqual(delegate.authenticationChallenges.count, 1) 89 | + } 90 | + 91 | + func test_httpRedirectionWithBodyStream() throws { 92 | + let dataString = "Quando l'accento divide il tempo in gruppi di due movimenti, il tempo si dice binario" 93 | + let data = try XCTUnwrap(dataString.data(using: .utf8)) 94 | + let expectedCallbacks: [String] = [ 95 | + "urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)", 96 | + "urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)", 97 | + "urlSession(_:task:needNewBodyStream:)", 98 | + "urlSession(_:task:didCompleteWithError:)" 99 | + ] 100 | + 101 | + let urlString = "http://127.0.0.1:\(TestURLSession.serverPort)/301?location=jsonBody" 102 | + let url = URL(string: urlString)! 103 | + var urlRequest = URLRequest(url: url) 104 | + urlRequest.httpMethod = "POST" 105 | + urlRequest.httpBodyStream = InputStream(data: data) 106 | + let delegate = SessionDelegate(with: expectation(description: "POST \(urlString): Redirection")) 107 | + delegate.newBodyStreamHandler = { (completionHandler: @escaping (InputStream?) -> Void) in 108 | + completionHandler(InputStream(data: data)) 109 | + } 110 | + delegate.run(with: urlRequest, timeoutInterval: 4) 111 | + waitForExpectations(timeout: 5) 112 | + XCTAssertEqual(delegate.callbacks, expectedCallbacks) 113 | + XCTAssertEqual(delegate.redirectionResponse?.value(forHTTPHeaderField: "Location"), "jsonBody") 114 | + } 115 | + 116 | /* Test for SR-8970 to verify that content-type header is not added to post with empty body */ 117 | func test_postWithEmptyBody() async { 118 | let config = URLSessionConfiguration.default 119 | -- 120 | 2.46.0 121 | 122 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0011-fix-parsing-tzdata-version-2-plus.patch: -------------------------------------------------------------------------------- 1 | From 4c69ab6c166e2a8c9689ed747f7089813b8918a3 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Fri, 6 Sep 2024 15:19:53 +0300 4 | Subject: [PATCH] Fix parsing tzdata version 2+ by @ybiletskyi 5 | 6 | (cherry picked from commit fac1218bfa3d49cec4f96d7f161f6a08a5281716) 7 | --- 8 | Sources/CoreFoundation/CFTimeZone.c | 95 +++++++++++++++++++++++++---- 9 | 1 file changed, 83 insertions(+), 12 deletions(-) 10 | 11 | diff --git a/Sources/CoreFoundation/CFTimeZone.c b/Sources/CoreFoundation/CFTimeZone.c 12 | index 29f59b3b..535693f0 100644 13 | --- a/Sources/CoreFoundation/CFTimeZone.c 14 | +++ b/Sources/CoreFoundation/CFTimeZone.c 15 | @@ -61,14 +61,15 @@ 16 | 17 | #if TARGET_OS_LINUX || TARGET_OS_BSD || TARGET_OS_WIN32 || TARGET_OS_WASI 18 | struct tzhead { 19 | - char tzh_magic[4]; /* TZ_MAGIC */ 20 | - char tzh_reserved[16]; /* reserved for future use */ 21 | - char tzh_ttisgmtcnt[4]; /* coded number of trans. time flags */ 22 | - char tzh_ttisstdcnt[4]; /* coded number of trans. time flags */ 23 | - char tzh_leapcnt[4]; /* coded number of leap seconds */ 24 | - char tzh_timecnt[4]; /* coded number of transition times */ 25 | - char tzh_typecnt[4]; /* coded number of local time types */ 26 | - char tzh_charcnt[4]; /* coded number of abbr. chars */ 27 | + char tzh_magic[4]; /* TZ_MAGIC */ 28 | + char version[1]; /* version */ 29 | + char tzh_reserved[15]; /* reserved for future use */ 30 | + char tzh_ttisgmtcnt[4]; /* coded number of trans. time flags */ 31 | + char tzh_ttisstdcnt[4]; /* coded number of trans. time flags */ 32 | + char tzh_leapcnt[4]; /* coded number of leap seconds */ 33 | + char tzh_timecnt[4]; /* coded number of transition times */ 34 | + char tzh_typecnt[4]; /* coded number of local time types */ 35 | + char tzh_charcnt[4]; /* coded number of abbr. chars */ 36 | }; 37 | #endif 38 | 39 | @@ -491,6 +492,20 @@ CF_INLINE int32_t __CFDetzcode(const unsigned char *bufp) { 40 | return (int32_t)result; 41 | } 42 | 43 | +CF_INLINE int64_t __CFDetzcode64(const unsigned char *bufp) { 44 | + // `result` is uint64_t to avoid undefined behaviour of shifting left negative values 45 | + uint64_t result = (bufp[0] & 0x80) ? ~0L : 0L; 46 | + result = (result << 8) | (bufp[0] & 0xff); 47 | + result = (result << 8) | (bufp[1] & 0xff); 48 | + result = (result << 8) | (bufp[2] & 0xff); 49 | + result = (result << 8) | (bufp[3] & 0xff); 50 | + result = (result << 8) | (bufp[4] & 0xff); 51 | + result = (result << 8) | (bufp[5] & 0xff); 52 | + result = (result << 8) | (bufp[6] & 0xff); 53 | + result = (result << 8) | (bufp[7] & 0xff); 54 | + return (int64_t)result; 55 | +} 56 | + 57 | CF_INLINE void __CFEntzcode(int32_t value, unsigned char *bufp) { 58 | bufp[0] = (value >> 24) & 0xff; 59 | bufp[1] = (value >> 16) & 0xff; 60 | @@ -499,10 +514,11 @@ CF_INLINE void __CFEntzcode(int32_t value, unsigned char *bufp) { 61 | } 62 | 63 | static Boolean __CFParseTimeZoneData(CFAllocatorRef allocator, CFDataRef data, CFTZPeriod **tzpp, CFIndex *cntp) { 64 | - int32_t len, timecnt, typecnt, charcnt, idx, cnt; 65 | + int32_t len, timecnt, typecnt, charcnt, leapcnt, ttisgmtcnt, ttisstdcnt, idx, cnt, version; 66 | const uint8_t *p, *timep, *typep, *ttisp, *charp; 67 | CFStringRef *abbrs; 68 | Boolean result = true; 69 | + int32_t trans_size = 4; 70 | 71 | p = CFDataGetBytePtr(data); 72 | len = CFDataGetLength(data); 73 | @@ -510,9 +526,22 @@ static Boolean __CFParseTimeZoneData(CFAllocatorRef allocator, CFDataRef data, C 74 | return false; 75 | } 76 | 77 | + /* read version 1 header */ 78 | if (!(p[0] == 'T' && p[1] == 'Z' && p[2] == 'i' && p[3] == 'f')) return false; /* Don't parse without TZif at head of file */ 79 | 80 | - p += 20 + 4 + 4 + 4; /* skip reserved, ttisgmtcnt, ttisstdcnt, leapcnt */ 81 | + p += 4; 82 | + version = __CFDetzcode(p); 83 | + if (version > 1) { 84 | + trans_size = 8; 85 | + } 86 | + p += 1; 87 | + p += 15; /* skip reserved */ 88 | + ttisgmtcnt = __CFDetzcode(p); 89 | + p += 4; 90 | + ttisstdcnt = __CFDetzcode(p); 91 | + p += 4; 92 | + leapcnt = __CFDetzcode(p); 93 | + p += 4; 94 | timecnt = __CFDetzcode(p); 95 | p += 4; 96 | typecnt = __CFDetzcode(p); 97 | @@ -530,8 +559,46 @@ static Boolean __CFParseTimeZoneData(CFAllocatorRef allocator, CFDataRef data, C 98 | if (len - (int32_t)sizeof(struct tzhead) < (4 + 1) * timecnt + (4 + 1 + 1) * typecnt + charcnt) { 99 | return false; 100 | } 101 | + 102 | + if (trans_size == 8) { 103 | + int32_t skip = timecnt * 5 + typecnt * 6 + charcnt + leapcnt * 8 + ttisstdcnt + ttisgmtcnt; 104 | + p += skip; /* skip version 1 data block */ 105 | + 106 | + /* read version 2+ header */ 107 | + if (!(p[0] == 'T' && p[1] == 'Z' && p[2] == 'i' && p[3] == 'f')) return false; /* Don't parse without TZif at head of file */ 108 | + p += 4; 109 | + version = __CFDetzcode(p); 110 | + p += 1; 111 | + 112 | + p += 15; /* skip reserved */ 113 | + ttisgmtcnt = __CFDetzcode(p); 114 | + p += 4; 115 | + ttisstdcnt = __CFDetzcode(p); 116 | + p += 4; 117 | + leapcnt = __CFDetzcode(p); 118 | + p += 4; 119 | + timecnt = __CFDetzcode(p); 120 | + p += 4; 121 | + typecnt = __CFDetzcode(p); 122 | + p += 4; 123 | + charcnt = __CFDetzcode(p); 124 | + p += 4; 125 | + 126 | + if (typecnt <= 0 || timecnt < 0 || charcnt < 0) { 127 | + return false; 128 | + } 129 | + if (1024 < timecnt || 32 < typecnt || 128 < charcnt) { 130 | + // reject excessive timezones to avoid arithmetic overflows for 131 | + // security reasons and to reject potentially corrupt files 132 | + return false; 133 | + } 134 | + if (len - (((int32_t)sizeof(struct tzhead)) * 2) - skip < (trans_size + 1) * timecnt + (4 + 1 + 1) * typecnt + charcnt) { 135 | + return false; 136 | + } 137 | + } 138 | + 139 | timep = p; 140 | - typep = timep + 4 * timecnt; 141 | + typep = timep + trans_size * timecnt; 142 | ttisp = typep + timecnt; 143 | charp = ttisp + (4 + 1 + 1) * typecnt; 144 | cnt = (0 < timecnt) ? timecnt : 1; 145 | @@ -548,12 +615,16 @@ static Boolean __CFParseTimeZoneData(CFAllocatorRef allocator, CFDataRef data, C 146 | int32_t itime, offset; 147 | uint8_t type, dst, abbridx; 148 | 149 | + if (trans_size == 8) { 150 | + at = (CFAbsoluteTime)(__CFDetzcode64(timep) + 0.0) - kCFAbsoluteTimeIntervalSince1970; 151 | + } else { 152 | at = (CFAbsoluteTime)(__CFDetzcode(timep) + 0.0) - kCFAbsoluteTimeIntervalSince1970; 153 | + } 154 | if (0 == timecnt) itime = INT_MIN; 155 | else if (at < (CFAbsoluteTime)INT_MIN) itime = INT_MIN; 156 | else if ((CFAbsoluteTime)INT_MAX < at) itime = INT_MAX; 157 | else itime = (int32_t)at; 158 | - timep += 4; /* harmless if 0 == timecnt */ 159 | + timep += trans_size; /* harmless if 0 == timecnt */ 160 | type = (0 < timecnt) ? (uint8_t)*typep++ : 0; 161 | if (typecnt <= type) { 162 | result = false; 163 | -- 164 | 2.46.0 165 | 166 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0004-add-ability-to-trust-invalid-certificate.patch: -------------------------------------------------------------------------------- 1 | From f500222518c61e68e43b52de756f396c1f8d18be Mon Sep 17 00:00:00 2001 2 | From: Kostiantyn Gominyuk 3 | Date: Fri, 12 Mar 2021 09:50:12 +0200 4 | Subject: [PATCH] Add ability to trust invalid certificate 5 | 6 | --- 7 | .../FoundationNetworking/URLCredential.swift | 7 +++++++ 8 | Sources/FoundationNetworking/URLRequest.swift | 1 + 9 | .../URLSession/HTTP/HTTPURLProtocol.swift | 4 ++++ 10 | .../URLSession/URLSessionTask.swift | 19 +++++++++++++++++++ 11 | .../URLSession/libcurl/EasyHandle.swift | 5 +++++ 12 | .../URLSession/libcurl/MultiHandle.swift | 4 ++++ 13 | .../include/CFURLSessionInterface.h | 2 +- 14 | 7 files changed, 41 insertions(+), 1 deletion(-) 15 | 16 | diff --git a/Sources/FoundationNetworking/URLCredential.swift b/Sources/FoundationNetworking/URLCredential.swift 17 | index 556d45f6..fa28ab53 100644 18 | --- a/Sources/FoundationNetworking/URLCredential.swift 19 | +++ b/Sources/FoundationNetworking/URLCredential.swift 20 | @@ -48,6 +48,8 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying, @unchecked Senda 21 | private let _user : String 22 | private let _password : String 23 | private let _persistence : Persistence 24 | + 25 | + public private(set) var _trustAllCertificates: Bool? 26 | 27 | /*! 28 | @method initWithUser:password:persistence: 29 | @@ -63,6 +65,11 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying, @unchecked Senda 30 | _persistence = persistence 31 | super.init() 32 | } 33 | + 34 | + public convenience init(trust: Bool) { 35 | + self.init(user: "", password: "", persistence: Persistence.none) 36 | + self._trustAllCertificates = trust 37 | + } 38 | 39 | /*! 40 | @method credentialWithUser:password:persistence: 41 | diff --git a/Sources/FoundationNetworking/URLRequest.swift b/Sources/FoundationNetworking/URLRequest.swift 42 | index 042e6365..de50c419 100644 43 | --- a/Sources/FoundationNetworking/URLRequest.swift 44 | +++ b/Sources/FoundationNetworking/URLRequest.swift 45 | @@ -73,6 +73,7 @@ public struct URLRequest : ReferenceConvertible, Equatable, Hashable, Sendable { 46 | 47 | internal var authMethod: String? 48 | internal var credential: URLCredential? 49 | + internal var trustAllCertificates: Bool? 50 | 51 | /// Returns the timeout interval of the receiver. 52 | /// - discussion: The timeout interval specifies the limit on the idle 53 | diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 54 | index 18e56456..908d1d48 100644 55 | --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 56 | +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 57 | @@ -454,6 +454,10 @@ internal class _HTTPURLProtocol: _NativeProtocol { 58 | easyHandle.set(username: username, password: password) 59 | } 60 | } 61 | + 62 | + if let trustAllCertificates = request.trustAllCertificates { 63 | + easyHandle.set(trustAllCertificates: trustAllCertificates) 64 | + } 65 | } 66 | 67 | /// What action to take 68 | diff --git a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 69 | index 556e79e5..af6e7722 100644 70 | --- a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 71 | +++ b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 72 | @@ -1337,6 +1337,19 @@ extension _ProtocolClient : URLProtocolClient { 73 | 74 | func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error) { 75 | guard let task = `protocol`.task else { fatalError() } 76 | + 77 | + if error._code == NSURLErrorServerCertificateUntrusted { 78 | + let protectionSpace = URLProtectionSpace(host: "", port: 443, protocol: "https", realm: "", 79 | + authenticationMethod: NSURLAuthenticationMethodServerTrust) 80 | + 81 | + let authenticationChallenge = URLAuthenticationChallenge(protectionSpace: protectionSpace, proposedCredential: nil, 82 | + previousFailureCount: task.previousFailureCount, failureResponse: nil, error: error, 83 | + sender: URLSessionAuthenticationChallengeSender()) 84 | + task.previousFailureCount += 1 85 | + urlProtocol(`protocol`, didReceive: authenticationChallenge) 86 | + return 87 | + } 88 | + 89 | urlProtocol(task: task, didFailWithError: error) 90 | } 91 | 92 | @@ -1410,6 +1423,7 @@ extension URLSessionTask { 93 | NSURLAuthenticationMethodHTTPBasic : basicAuth, 94 | NSURLAuthenticationMethodHTTPDigest: digestAuth, 95 | NSURLAuthenticationMethodNTLM : ntlmAuth, 96 | + NSURLAuthenticationMethodServerTrust: serverTrustAuth, 97 | ] 98 | return handlers[authScheme] 99 | } 100 | @@ -1434,6 +1448,11 @@ extension URLSessionTask { 101 | task.authRequest?.credential = credential 102 | } 103 | 104 | + static func serverTrustAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { 105 | + task.authRequest = task.originalRequest 106 | + task.authRequest?.trustAllCertificates = credential?._trustAllCertificates 107 | + } 108 | + 109 | } 110 | 111 | extension URLProtocol { 112 | diff --git a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 113 | index a7bdb4cf..cd8c473b 100644 114 | --- a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 115 | +++ b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 116 | @@ -381,6 +381,11 @@ extension _EasyHandle { 117 | CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionUSERPWD, UnsafeMutablePointer(mutating: $0)) 118 | } 119 | } 120 | + 121 | + func set(trustAllCertificates: Bool) { 122 | + CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionSSL_VERIFYPEER, trustAllCertificates ? 0 : 1) 123 | + CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionSSL_VERIFYHOST, trustAllCertificates ? 0 : 1) 124 | + } 125 | } 126 | 127 | /// WebSocket support 128 | diff --git a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 129 | index 67f77f4b..ed9e6bd7 100644 130 | --- a/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 131 | +++ b/Sources/FoundationNetworking/URLSession/libcurl/MultiHandle.swift 132 | @@ -380,6 +380,10 @@ fileprivate extension _EasyHandle { 133 | return NSURLErrorTimedOut 134 | case (CFURLSessionEasyCodeOPERATION_TIMEDOUT, _): 135 | return NSURLErrorTimedOut 136 | + case (CFURLSessionEasyCodePEER_FAILED_VERIFICATION, _): 137 | + return NSURLErrorServerCertificateUntrusted 138 | + case (CFURLSessionEasyCodeSSL_CONNECT_ERROR, _): 139 | + return NSURLErrorSecureConnectionFailed 140 | default: 141 | //TODO: Need to map to one of the NSURLError... constants 142 | return NSURLErrorUnknown 143 | diff --git a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 144 | index e126b445..2c0264b1 100644 145 | --- a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 146 | +++ b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 147 | @@ -269,7 +269,7 @@ CF_EXPORT CFURLSessionOption const CFURLSessionOptionEGDSOCKET; // CURLOPT_EGDSO 148 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionCONNECTTIMEOUT; // CURLOPT_CONNECTTIMEOUT 149 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionHEADERFUNCTION; // CURLOPT_HEADERFUNCTION 150 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionHTTPGET; // CURLOPT_HTTPGET 151 | -//CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSL_VERIFYHOST; // CURLOPT_SSL_VERIFYHOST 152 | +CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSL_VERIFYHOST; // CURLOPT_SSL_VERIFYHOST 153 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionCOOKIEJAR; // CURLOPT_COOKIEJAR 154 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSL_CIPHER_LIST; // CURLOPT_SSL_CIPHER_LIST 155 | CF_EXPORT CFURLSessionOption const CFURLSessionOptionHTTP_VERSION; // CURLOPT_HTTP_VERSION 156 | -- 157 | 2.46.0 158 | 159 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0006-handle-HTTP-Basic-Auth-and-auth-failure.patch: -------------------------------------------------------------------------------- 1 | From de54a9145156c61d5432896c51b0f9ce38048ab4 Mon Sep 17 00:00:00 2001 2 | From: Alexander Smarus 3 | Date: Wed, 11 Jan 2023 15:15:06 +0200 4 | Subject: [PATCH] Handle HTTP Basic Auth and auth failure 5 | 6 | --- 7 | .../URLSession/URLSessionTask.swift | 159 ++++++++++-------- 8 | 1 file changed, 88 insertions(+), 71 deletions(-) 9 | 10 | diff --git a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 11 | index af6e7722..17aa1689 100644 12 | --- a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 13 | +++ b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 14 | @@ -1105,7 +1105,9 @@ extension _ProtocolClient : URLProtocolClient { 15 | let proposedCredential: URLCredential? 16 | let last = task._protocolLock.performLocked { task._lastCredentialUsedFromStorageDuringAuthentication } 17 | 18 | - if last?.credential != credential { 19 | + if last?.credential != nil && credential == nil { 20 | + proposedCredential = last?.credential 21 | + } else if last?.credential != credential { 22 | proposedCredential = credential 23 | } else { 24 | proposedCredential = nil 25 | @@ -1162,71 +1164,7 @@ extension _ProtocolClient : URLProtocolClient { 26 | } 27 | } 28 | 29 | - switch session.behaviour(for: task) { 30 | - case .taskDelegate(let delegate): 31 | - if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask { 32 | - let temporaryFileURL = urlProtocol.properties[URLProtocol._PropertyKey.temporaryFileURL] as! URL 33 | - session.delegateQueue.addOperation { 34 | - downloadDelegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: temporaryFileURL) 35 | - } 36 | - } else if let webSocketDelegate = delegate as? URLSessionWebSocketDelegate, 37 | - let webSocketTask = task as? URLSessionWebSocketTask { 38 | - session.delegateQueue.addOperation { 39 | - webSocketDelegate.urlSession(session, webSocketTask: webSocketTask, didCloseWith: webSocketTask.closeCode, reason: webSocketTask.closeReason) 40 | - } 41 | - } 42 | - session.delegateQueue.addOperation { 43 | - guard task.state != .completed else { return } 44 | - delegate.urlSession(session, task: task, didCompleteWithError: nil) 45 | - task.state = .completed 46 | - session.workQueue.async { 47 | - session.taskRegistry.remove(task) 48 | - } 49 | - } 50 | - case .noDelegate: 51 | - guard task.state != .completed else { break } 52 | - task.state = .completed 53 | - session.workQueue.async { 54 | - session.taskRegistry.remove(task) 55 | - } 56 | - case .dataCompletionHandler(let completion), 57 | - .dataCompletionHandlerWithTaskDelegate(let completion, _): 58 | - nonisolated(unsafe) let nonisolatedURLProtocol = urlProtocol 59 | - let dataCompletion : @Sendable () -> () = { 60 | - guard task.state != .completed else { return } 61 | - completion(nonisolatedURLProtocol.properties[URLProtocol._PropertyKey.responseData] as? Data ?? Data(), task.response, nil) 62 | - task.state = .completed 63 | - session.workQueue.async { 64 | - session.taskRegistry.remove(task) 65 | - } 66 | - } 67 | - if task._callCompletionHandlerInline { 68 | - dataCompletion() 69 | - } else { 70 | - session.delegateQueue.addOperation { 71 | - dataCompletion() 72 | - } 73 | - } 74 | - case .downloadCompletionHandler(let completion), 75 | - .downloadCompletionHandlerWithTaskDelegate(let completion, _): 76 | - nonisolated(unsafe) let nonisolatedURLProtocol = urlProtocol 77 | - let downloadCompletion : @Sendable () -> () = { 78 | - guard task.state != .completed else { return } 79 | - completion(nonisolatedURLProtocol.properties[URLProtocol._PropertyKey.temporaryFileURL] as? URL, task.response, nil) 80 | - task.state = .completed 81 | - session.workQueue.async { 82 | - session.taskRegistry.remove(task) 83 | - } 84 | - } 85 | - if task._callCompletionHandlerInline { 86 | - downloadCompletion() 87 | - } else { 88 | - session.delegateQueue.addOperation { 89 | - downloadCompletion() 90 | - } 91 | - } 92 | - } 93 | - task._invalidateProtocol() 94 | + urlProtocolDidComplete(urlProtocol) 95 | } 96 | 97 | func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge) { 98 | @@ -1269,14 +1207,21 @@ extension _ProtocolClient : URLProtocolClient { 99 | } 100 | 101 | @Sendable func attemptProceedingWithDefaultCredential() { 102 | - if let credential = challenge.proposedCredential { 103 | + let credential: URLCredential? = { 104 | + guard let proposedCredential = challenge.proposedCredential else { 105 | + return nil 106 | + } 107 | let last = task._protocolLock.performLocked { task._lastCredentialUsedFromStorageDuringAuthentication } 108 | 109 | - if last?.credential != credential { 110 | - proceed(using: credential) 111 | - } else { 112 | - task.cancel() 113 | + guard proposedCredential != last?.credential else { 114 | + return nil 115 | } 116 | + return proposedCredential 117 | + }() 118 | + if let credential = credential { 119 | + proceed(using: credential) 120 | + } else { 121 | + urlProtocolDidComplete(`protocol`) 122 | } 123 | } 124 | 125 | @@ -1414,6 +1359,78 @@ extension _ProtocolClient : URLProtocolClient { 126 | func urlProtocol(_ protocol: URLProtocol, wasRedirectedTo request: URLRequest, redirectResponse: URLResponse) { 127 | fatalError("The URLSession swift-corelibs-foundation implementation doesn't currently handle redirects directly.") 128 | } 129 | + 130 | + private func urlProtocolDidComplete(_ urlProtocol: URLProtocol) { 131 | + guard let task = urlProtocol.task else { fatalError("Received response, but there's no task.") } 132 | + guard let session = task.session as? URLSession else { fatalError("Task not associated with URLSession.") } 133 | + 134 | + switch session.behaviour(for: task) { 135 | + case .taskDelegate(let delegate): 136 | + if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask { 137 | + let temporaryFileURL = urlProtocol.properties[URLProtocol._PropertyKey.temporaryFileURL] as! URL 138 | + session.delegateQueue.addOperation { 139 | + downloadDelegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: temporaryFileURL) 140 | + } 141 | + } else if let webSocketDelegate = delegate as? URLSessionWebSocketDelegate, 142 | + let webSocketTask = task as? URLSessionWebSocketTask { 143 | + session.delegateQueue.addOperation { 144 | + webSocketDelegate.urlSession(session, webSocketTask: webSocketTask, didCloseWith: webSocketTask.closeCode, reason: webSocketTask.closeReason) 145 | + } 146 | + } 147 | + session.delegateQueue.addOperation { 148 | + guard task.state != .completed else { return } 149 | + delegate.urlSession(session, task: task, didCompleteWithError: nil) 150 | + task.state = .completed 151 | + session.workQueue.async { 152 | + session.taskRegistry.remove(task) 153 | + } 154 | + } 155 | + case .noDelegate: 156 | + guard task.state != .completed else { break } 157 | + task.state = .completed 158 | + session.workQueue.async { 159 | + session.taskRegistry.remove(task) 160 | + } 161 | + case .dataCompletionHandler(let completion), 162 | + .dataCompletionHandlerWithTaskDelegate(let completion, _): 163 | + nonisolated(unsafe) let nonisolatedURLProtocol = urlProtocol 164 | + let dataCompletion : @Sendable () -> () = { 165 | + guard task.state != .completed else { return } 166 | + completion(nonisolatedURLProtocol.properties[URLProtocol._PropertyKey.responseData] as? Data ?? Data(), task.response, nil) 167 | + task.state = .completed 168 | + session.workQueue.async { 169 | + session.taskRegistry.remove(task) 170 | + } 171 | + } 172 | + if task._callCompletionHandlerInline { 173 | + dataCompletion() 174 | + } else { 175 | + session.delegateQueue.addOperation { 176 | + dataCompletion() 177 | + } 178 | + } 179 | + case .downloadCompletionHandler(let completion), 180 | + .downloadCompletionHandlerWithTaskDelegate(let completion, _): 181 | + nonisolated(unsafe) let nonisolatedURLProtocol = urlProtocol 182 | + let downloadCompletion : @Sendable () -> () = { 183 | + guard task.state != .completed else { return } 184 | + completion(nonisolatedURLProtocol.properties[URLProtocol._PropertyKey.temporaryFileURL] as? URL, task.response, nil) 185 | + task.state = .completed 186 | + session.workQueue.async { 187 | + session.taskRegistry.remove(task) 188 | + } 189 | + } 190 | + if task._callCompletionHandlerInline { 191 | + downloadCompletion() 192 | + } else { 193 | + session.delegateQueue.addOperation { 194 | + downloadCompletion() 195 | + } 196 | + } 197 | + } 198 | + task._invalidateProtocol() 199 | + } 200 | + 201 | } 202 | extension URLSessionTask { 203 | typealias _AuthHandler = ((URLSessionTask, URLSession.AuthChallengeDisposition, URLCredential?) -> ()) 204 | -- 205 | 2.46.0 206 | 207 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0003-support-digest-and-ntlm-auth.patch: -------------------------------------------------------------------------------- 1 | From 439d6c4dafe939f365c077896126ac5a32cc5965 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Sat, 12 Apr 2025 16:17:35 +0300 4 | Subject: [PATCH] Support digest and ntlm auth 5 | 6 | --- 7 | .../URLProtectionSpace.swift | 2 ++ 8 | Sources/FoundationNetworking/URLRequest.swift | 3 ++ 9 | .../URLSession/HTTP/HTTPMessage.swift | 22 +++++++++++-- 10 | .../URLSession/HTTP/HTTPURLProtocol.swift | 12 +++++++ 11 | .../URLSession/NativeProtocol.swift | 16 +++++++++ 12 | .../URLSession/URLSessionTask.swift | 33 ++++++++++++++----- 13 | .../URLSession/libcurl/EasyHandle.swift | 22 +++++++++++++ 14 | .../CFURLSessionInterface.c | 7 ++++ 15 | .../include/CFURLSessionInterface.h | 5 +++ 16 | 9 files changed, 111 insertions(+), 11 deletions(-) 17 | 18 | diff --git a/Sources/FoundationNetworking/URLProtectionSpace.swift b/Sources/FoundationNetworking/URLProtectionSpace.swift 19 | index 26dcc234..8fe61f60 100644 20 | --- a/Sources/FoundationNetworking/URLProtectionSpace.swift 21 | +++ b/Sources/FoundationNetworking/URLProtectionSpace.swift 22 | @@ -352,6 +352,8 @@ extension _HTTPURLProtocol._HTTPMessage._Challenge { 23 | return NSURLAuthenticationMethodHTTPBasic 24 | } else if authScheme.caseInsensitiveCompare(_HTTPURLProtocol._HTTPMessage._Challenge.AuthSchemeDigest) == .orderedSame { 25 | return NSURLAuthenticationMethodHTTPDigest 26 | + } else if authScheme.caseInsensitiveCompare(_HTTPURLProtocol._HTTPMessage._Challenge.AuthSchemeNTLM) == .orderedSame { 27 | + return NSURLAuthenticationMethodNTLM 28 | } else { 29 | return nil 30 | } 31 | diff --git a/Sources/FoundationNetworking/URLRequest.swift b/Sources/FoundationNetworking/URLRequest.swift 32 | index e87201e5..b2a104fe 100644 33 | --- a/Sources/FoundationNetworking/URLRequest.swift 34 | +++ b/Sources/FoundationNetworking/URLRequest.swift 35 | @@ -71,6 +71,9 @@ public struct URLRequest : ReferenceConvertible, Equatable, Hashable, Sendable { 36 | // to explicitly 60 then the precedence should be given to URLRequest.timeoutInterval. 37 | internal var isTimeoutIntervalSet = false 38 | 39 | + internal var authMethod: String? 40 | + internal var credential: URLCredential? 41 | + 42 | /// Returns the timeout interval of the receiver. 43 | /// - discussion: The timeout interval specifies the limit on the idle 44 | /// interval allotted to a request in the process of loading. The "idle 45 | diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPMessage.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPMessage.swift 46 | index d31b7a49..9c5dc1ba 100644 47 | --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPMessage.swift 48 | +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPMessage.swift 49 | @@ -111,6 +111,7 @@ extension _HTTPURLProtocol._HTTPMessage { 50 | struct _Challenge { 51 | static let AuthSchemeBasic = "basic" 52 | static let AuthSchemeDigest = "digest" 53 | + static let AuthSchemeNTLM = "ntlm" 54 | /// A single auth challenge parameter 55 | struct _AuthParameter { 56 | let name: String 57 | @@ -213,12 +214,27 @@ extension _HTTPURLProtocol._HTTPMessage._Challenge { 58 | break 59 | } 60 | let authScheme = String(authenticateView[authSchemeRange]) 61 | - if authScheme.caseInsensitiveCompare(AuthSchemeBasic) == .orderedSame { 62 | + let isBasic = authScheme.caseInsensitiveCompare(AuthSchemeBasic) == .orderedSame 63 | + let isDigest = authScheme.caseInsensitiveCompare(AuthSchemeDigest) == .orderedSame 64 | + let isNTLM = authScheme.caseInsensitiveCompare(AuthSchemeNTLM) == .orderedSame 65 | + 66 | + if isBasic || isDigest || isNTLM { 67 | let authDataView = authenticateView[authSchemeRange.upperBound...] 68 | let authParameters = _HTTPURLProtocol._HTTPMessage._Challenge._AuthParameter.parameters(from: authDataView) 69 | let challenge = _HTTPURLProtocol._HTTPMessage._Challenge(authScheme: authScheme, authParameters: authParameters) 70 | - // "realm" is the only mandatory parameter for Basic auth scheme. Otherwise consider parsed data invalid. 71 | - if challenge.parameter(withName: "realm") != nil { 72 | + 73 | + let isValidChallenge: Bool 74 | + 75 | + if isBasic || isDigest { 76 | + // "realm" is the only mandatory parameter for Basic auth scheme. Otherwise consider parsed data invalid. 77 | + isValidChallenge = challenge.parameter(withName: "realm") != nil 78 | + } else if isNTLM { 79 | + isValidChallenge = true 80 | + } else { 81 | + isValidChallenge = false 82 | + } 83 | + 84 | + if isValidChallenge { 85 | challenges.append(challenge) 86 | } 87 | } 88 | diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 89 | index dfe18758..18e56456 100644 90 | --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 91 | +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift 92 | @@ -442,6 +442,18 @@ internal class _HTTPURLProtocol: _NativeProtocol { 93 | easyHandle.set(requestMethod: request.httpMethod ?? "GET") 94 | // Always set the status as it may change if a HEAD is converted to a GET. 95 | easyHandle.set(noBody: request.httpMethod == "HEAD") 96 | + 97 | + if let authMethod = request.authMethod { 98 | + let authMethodWasSet = easyHandle.set(authMethod: authMethod) 99 | + if !authMethodWasSet { 100 | + NSLog("\(authMethod) is not supported") 101 | + } 102 | + if let credential = request.credential { 103 | + let username = credential.user ?? "" 104 | + let password = credential.password ?? "" 105 | + easyHandle.set(username: username, password: password) 106 | + } 107 | + } 108 | } 109 | 110 | /// What action to take 111 | diff --git a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 112 | index ad6836dc..965c24ab 100644 113 | --- a/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 114 | +++ b/Sources/FoundationNetworking/URLSession/NativeProtocol.swift 115 | @@ -684,6 +684,22 @@ extension _NativeProtocol { 116 | } 117 | } 118 | 119 | +extension _NativeProtocol { 120 | + 121 | + func prepareAuthenticationRequestReuse() { 122 | + // Without EasyHandle reuse we have bugs: 123 | + // 1. Digest: curl use 2 requests intead of 1 to authenticate 124 | + // 2. Digest + Post + urlRequest.setValue("chunked", forHTTPHeaderField: "Transfer-Encoding") 125 | + // curl doesn't send all data and as a result connection disconnects with timeout 126 | + // (TestURLSessionRealServer.test_dataTaskWithDigestAuth_InputStream) 127 | + // This allows to reuse _NativeProtocol and its EasyHandle 128 | + if case .taskCompleted = internalState { 129 | + self.internalState = .initial 130 | + } 131 | + } 132 | + 133 | +} 134 | + 135 | extension URLSession { 136 | static func printDebug(_ text: @autoclosure () -> String) { 137 | guard enableDebugOutput else { return } 138 | diff --git a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 139 | index cef77fac..c97ef4c2 100644 140 | --- a/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 141 | +++ b/Sources/FoundationNetworking/URLSession/URLSessionTask.swift 142 | @@ -1258,7 +1258,11 @@ extension _ProtocolClient : URLProtocolClient { 143 | } else { 144 | task._lastCredentialUsedFromStorageDuringAuthentication = nil 145 | } 146 | - task._protocolStorage = .existing(_HTTPURLProtocol(task: task, cachedResponse: nil, client: nil)) 147 | + if let urlProtocol = `protocol` as? _HTTPURLProtocol { 148 | + urlProtocol.prepareAuthenticationRequestReuse() 149 | + } else { 150 | + task._protocolStorage = .existing(_HTTPURLProtocol(task: task, cachedResponse: nil, client: nil)) 151 | + } 152 | } 153 | if case .stream(let stream) = task.knownBody, stream.streamStatus != .notOpen { 154 | task.knownBody = nil 155 | @@ -1299,7 +1303,12 @@ extension _ProtocolClient : URLProtocolClient { 156 | } 157 | } 158 | } else { 159 | - attemptProceedingWithDefaultCredential() 160 | + // This function is called from _NativeProtocol.completeTask. 161 | + // _NativeProtocol.internalState will be set to .taskCompleted after execution of this function. 162 | + // Seems like a bug. To fix it, I simulate same flow that we have with task delegate. 163 | + session.delegateQueue.addOperation { 164 | + attemptProceedingWithDefaultCredential() 165 | + } 166 | } 167 | } 168 | 169 | @@ -1401,7 +1410,8 @@ extension URLSessionTask { 170 | static func authHandler(for authScheme: String) -> _AuthHandler? { 171 | let handlers: [String : _AuthHandler] = [ 172 | NSURLAuthenticationMethodHTTPBasic : basicAuth, 173 | - NSURLAuthenticationMethodHTTPDigest: digestAuth 174 | + NSURLAuthenticationMethodHTTPDigest: digestAuth, 175 | + NSURLAuthenticationMethodNTLM : ntlmAuth, 176 | ] 177 | return handlers[authScheme] 178 | } 179 | @@ -1409,16 +1419,23 @@ extension URLSessionTask { 180 | //Authentication handlers 181 | static func basicAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { 182 | //TODO: Handle disposition. For now, we default to .useCredential 183 | - let user = credential?.user ?? "" 184 | - let password = credential?.password ?? "" 185 | - let encodedString = "\(user):\(password)".data(using: .utf8)?.base64EncodedString() 186 | task.authRequest = task.originalRequest 187 | - task.authRequest?.setValue("Basic \(encodedString!)", forHTTPHeaderField: "Authorization") 188 | + task.authRequest?.authMethod = NSURLAuthenticationMethodHTTPBasic 189 | + task.authRequest?.credential = credential 190 | } 191 | 192 | static func digestAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { 193 | - fatalError("The URLSession swift-corelibs-foundation implementation doesn't currently handle digest authentication.") 194 | + task.authRequest = task.originalRequest 195 | + task.authRequest?.authMethod = NSURLAuthenticationMethodHTTPDigest 196 | + task.authRequest?.credential = credential 197 | } 198 | + 199 | + static func ntlmAuth(_ task: URLSessionTask, _ disposition: URLSession.AuthChallengeDisposition, _ credential: URLCredential?) { 200 | + task.authRequest = task.originalRequest 201 | + task.authRequest?.authMethod = NSURLAuthenticationMethodNTLM 202 | + task.authRequest?.credential = credential 203 | + } 204 | + 205 | } 206 | 207 | extension URLProtocol { 208 | diff --git a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 209 | index cdf8875f..cdce2777 100644 210 | --- a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 211 | +++ b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 212 | @@ -359,6 +359,28 @@ extension _EasyHandle { 213 | CFURLSession_easy_getinfo_double(rawHandle, CFURLSessionInfoTOTAL_TIME, &timeSpent) 214 | return timeSpent / 1000 215 | } 216 | + 217 | + func set(authMethod: String) -> Bool { 218 | + var method = CFURLSessionAUTH_NONE 219 | + switch authMethod { 220 | + case NSURLAuthenticationMethodHTTPBasic: 221 | + method = CFURLSessionAUTH_BASIC 222 | + case NSURLAuthenticationMethodHTTPDigest: 223 | + method = CFURLSessionAUTH_DIGEST 224 | + case NSURLAuthenticationMethodNTLM: 225 | + method = CFURLSessionAUTH_NTLM 226 | + default: 227 | + return false 228 | + } 229 | + CFURLSession_easy_setopt_unsigned_long(rawHandle, CFURLSessionOptionHTTPAUTH, method) 230 | + return true 231 | + } 232 | + 233 | + func set(username: String, password: String) { 234 | + "\(username):\(password)".withCString { 235 | + CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionUSERPWD, UnsafeMutablePointer(mutating: $0)) 236 | + } 237 | + } 238 | } 239 | 240 | /// WebSocket support 241 | diff --git a/Sources/_CFURLSessionInterface/CFURLSessionInterface.c b/Sources/_CFURLSessionInterface/CFURLSessionInterface.c 242 | index e493ad77..f8d5a4ab 100644 243 | --- a/Sources/_CFURLSessionInterface/CFURLSessionInterface.c 244 | +++ b/Sources/_CFURLSessionInterface/CFURLSessionInterface.c 245 | @@ -90,6 +90,9 @@ CFURLSessionEasyCode CFURLSession_easy_setopt_int(CFURLSessionEasyHandle _Nonnul 246 | CFURLSessionEasyCode CFURLSession_easy_setopt_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, long a) { 247 | return MakeEasyCode(curl_easy_setopt(curl, option.value, a)); 248 | } 249 | +CFURLSessionEasyCode CFURLSession_easy_setopt_unsigned_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, unsigned long a) { 250 | + return MakeEasyCode(curl_easy_setopt(curl, option.value, a)); 251 | +} 252 | CFURLSessionEasyCode CFURLSession_easy_setopt_int64(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, long long a) { 253 | return MakeEasyCode(curl_easy_setopt(curl, option.value, (int64_t)a)); 254 | } 255 | @@ -651,6 +654,10 @@ CFURLSessionCurlVersion CFURLSessionCurlVersionInfo(void) { 256 | return v; 257 | } 258 | 259 | +unsigned long const CFURLSessionAUTH_NONE = CURLAUTH_NONE; 260 | +unsigned long const CFURLSessionAUTH_BASIC = CURLAUTH_BASIC; 261 | +unsigned long const CFURLSessionAUTH_DIGEST = CURLAUTH_DIGEST; 262 | +unsigned long const CFURLSessionAUTH_NTLM = CURLAUTH_NTLM; 263 | 264 | int const CFURLSessionWriteFuncPause = CURL_WRITEFUNC_PAUSE; 265 | int const CFURLSessionReadFuncPause = CURL_READFUNC_PAUSE; 266 | diff --git a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 267 | index 64c34955..e126b445 100644 268 | --- a/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 269 | +++ b/Sources/_CFURLSessionInterface/include/CFURLSessionInterface.h 270 | @@ -604,6 +604,10 @@ typedef struct CFURLSessionCurlVersion { 271 | } CFURLSessionCurlVersion; 272 | CF_EXPORT CFURLSessionCurlVersion CFURLSessionCurlVersionInfo(void); 273 | 274 | +CF_EXPORT unsigned long const CFURLSessionAUTH_NONE; 275 | +CF_EXPORT unsigned long const CFURLSessionAUTH_BASIC; 276 | +CF_EXPORT unsigned long const CFURLSessionAUTH_DIGEST; 277 | +CF_EXPORT unsigned long const CFURLSessionAUTH_NTLM; 278 | 279 | CF_EXPORT int const CFURLSessionWriteFuncPause; 280 | CF_EXPORT int const CFURLSessionReadFuncPause; 281 | @@ -635,6 +639,7 @@ CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_fptr(CFURLSessionEasyHan 282 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_ptr(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, void *_Nullable a); 283 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_int(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, int a); 284 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, long a); 285 | +CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_unsigned_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, unsigned long a); 286 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_int64(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, long long a); 287 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_wc(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, size_t(*_Nonnull a)(char *_Nonnull, size_t, size_t, void *_Nullable)); 288 | CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_fwc(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, size_t(*_Nonnull a)(char *_Nonnull, size_t, size_t, void *_Nullable)); 289 | -- 290 | 2.46.0 291 | 292 | -------------------------------------------------------------------------------- /.github/workflows/build-android-toolchain.yml: -------------------------------------------------------------------------------- 1 | name: Build Android Toolchain 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | build-swift-android-arm-64: 11 | runs-on: ubuntu-24.04 12 | container: ubuntu:noble 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Install dependencies 17 | run: ./build/000-install-dependencies-nobel.sh 18 | - name: Install NDK 19 | run: ./build/002-install-ndk.sh 20 | - name: Define build folders 21 | run: ./build/003-define-build-folders.sh 22 | - name: Clone Swift 23 | run: ./build/020-clone-swift.sh 24 | - name: Cache Swift Arm64 25 | id: cache-swift-arm-64 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/out/swift-android/lib 29 | key: ${{ runner.os }}-stdlib-arm64-api29-${{ hashFiles('.swift.sum') }} 30 | - name: Build Swift 31 | if: steps.cache-swift-arm-64.outputs.cache-hit != 'true' 32 | run: ./build/031-build-swift-arm64.sh 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: swift-android-arm64-v8a 36 | path: ~/out/swift-android/lib 37 | 38 | 39 | build-swift-android-armeabi-v7a: 40 | runs-on: ubuntu-24.04 41 | container: ubuntu:noble 42 | 43 | steps: 44 | - uses: actions/checkout@v1 45 | - name: Install dependencies 46 | run: ./build/000-install-dependencies-nobel.sh 47 | - name: Install NDK 48 | run: ./build/002-install-ndk.sh 49 | - name: Define build folders 50 | run: ./build/003-define-build-folders.sh 51 | - name: Clone Swift 52 | run: ./build/020-clone-swift.sh 53 | - name: Cache Swift Arm 32 54 | id: cache-swift-arm-32 55 | uses: actions/cache@v4 56 | with: 57 | path: ~/out/swift-android/lib 58 | key: ${{ runner.os }}-stdlib-arm32-api29-${{ hashFiles('.swift.sum') }} 59 | - name: Build Swift 60 | if: steps.cache-swift-arm-32.outputs.cache-hit != 'true' 61 | run: ./build/032-build-swift-arm.sh 62 | - uses: actions/upload-artifact@v4 63 | with: 64 | name: swift-android-armeabi-v7a 65 | path: ~/out/swift-android/lib 66 | 67 | 68 | build-swift-android-x86_64: 69 | runs-on: ubuntu-24.04 70 | container: ubuntu:noble 71 | 72 | steps: 73 | - uses: actions/checkout@v1 74 | - name: Install dependencies 75 | run: ./build/000-install-dependencies-nobel.sh 76 | - name: Install NDK 77 | run: ./build/002-install-ndk.sh 78 | - name: Define build folders 79 | run: ./build/003-define-build-folders.sh 80 | - name: Clone Swift 81 | run: ./build/020-clone-swift.sh 82 | - name: Cache Swift x86_64 83 | id: cache-swift-x86_64 84 | uses: actions/cache@v4 85 | with: 86 | path: ~/out/swift-android/lib 87 | key: ${{ runner.os }}-stdlib-x86_64-api29-${{ hashFiles('.swift.sum') }} 88 | - name: Build Swift 89 | if: steps.cache-swift-x86_64.outputs.cache-hit != 'true' 90 | run: ./build/033-build-swift-x86_64.sh 91 | - uses: actions/upload-artifact@v4 92 | with: 93 | name: swift-android-x86_64 94 | path: ~/out/swift-android/lib 95 | 96 | build-swift-android-x86: 97 | runs-on: ubuntu-24.04 98 | container: ubuntu:noble 99 | 100 | steps: 101 | - uses: actions/checkout@v1 102 | - name: Install dependencies 103 | run: ./build/000-install-dependencies-nobel.sh 104 | - name: Install NDK 105 | run: ./build/002-install-ndk.sh 106 | - name: Define build folders 107 | run: ./build/003-define-build-folders.sh 108 | - name: Clone Swift 109 | run: ./build/020-clone-swift.sh 110 | - name: Cache Swift x86 111 | id: cache-swift-x86 112 | uses: actions/cache@v4 113 | with: 114 | path: ~/out/swift-android/lib 115 | key: ${{ runner.os }}-stdlib-x86-api29-${{ hashFiles('.swift.sum') }} 116 | - name: Build Swift 117 | if: steps.cache-swift-x86.outputs.cache-hit != 'true' 118 | run: ./build/034-build-swift-x86.sh 119 | - uses: actions/upload-artifact@v4 120 | with: 121 | name: swift-android-x86 122 | path: ~/out/swift-android/lib 123 | 124 | 125 | build-openssl-android: 126 | runs-on: ubuntu-24.04 127 | container: ubuntu:noble 128 | 129 | steps: 130 | - uses: actions/checkout@v1 131 | - name: Cache OpenSSL 132 | id: cache-openssl 133 | uses: actions/cache@v4 134 | with: 135 | path: | 136 | ~/openssl/arm64 137 | ~/openssl/arm 138 | ~/openssl/x86_64 139 | ~/openssl/x86 140 | key: openssl-3.5.0-r1 141 | - name: Install dependencies 142 | if: steps.cache-openssl.outputs.cache-hit != 'true' 143 | run: ./build/000-install-dependencies-nobel.sh 144 | - name: Install NDK 145 | if: steps.cache-openssl.outputs.cache-hit != 'true' 146 | run: ./build/002-install-ndk.sh 147 | - name: Define build folders 148 | if: steps.cache-openssl.outputs.cache-hit != 'true' 149 | run: ./build/003-define-build-folders.sh 150 | - name: Build OpenSSL 151 | if: steps.cache-openssl.outputs.cache-hit != 'true' 152 | run: ./build/041-build-openssl.sh 153 | - uses: actions/upload-artifact@v4 154 | with: 155 | name: openssl-arm64-v8a 156 | path: ~/openssl/arm64 157 | - uses: actions/upload-artifact@v4 158 | with: 159 | name: openssl-armeabi-v7a 160 | path: ~/openssl/arm 161 | - uses: actions/upload-artifact@v4 162 | with: 163 | name: openssl-x86_64 164 | path: ~/openssl/x86_64 165 | - uses: actions/upload-artifact@v4 166 | with: 167 | name: openssl-x86 168 | path: ~/openssl/x86 169 | 170 | 171 | build-curl-android: 172 | needs: [build-openssl-android] 173 | runs-on: ubuntu-24.04 174 | container: ubuntu:noble 175 | 176 | steps: 177 | - uses: actions/checkout@v1 178 | - name: Cache curl 179 | id: cache-curl 180 | uses: actions/cache@v4 181 | with: 182 | path: | 183 | ~/curl/arm64 184 | ~/curl/arm 185 | ~/curl/x86_64 186 | ~/curl/x86 187 | key: curl-8.13.0-r1 188 | - name: Install dependencies 189 | if: steps.cache-curl.outputs.cache-hit != 'true' 190 | run: ./build/000-install-dependencies-nobel.sh 191 | - name: Install NDK 192 | if: steps.cache-curl.outputs.cache-hit != 'true' 193 | run: ./build/002-install-ndk.sh 194 | - name: Define build folders 195 | if: steps.cache-curl.outputs.cache-hit != 'true' 196 | run: ./build/003-define-build-folders.sh 197 | - uses: actions/download-artifact@v4 198 | if: steps.cache-curl.outputs.cache-hit != 'true' 199 | with: 200 | name: openssl-arm64-v8a 201 | path: ~/openssl/arm64 202 | - uses: actions/download-artifact@v4 203 | if: steps.cache-curl.outputs.cache-hit != 'true' 204 | with: 205 | name: openssl-armeabi-v7a 206 | path: ~/openssl/arm 207 | - uses: actions/download-artifact@v4 208 | if: steps.cache-curl.outputs.cache-hit != 'true' 209 | with: 210 | name: openssl-x86 211 | path: ~/openssl/x86 212 | - uses: actions/download-artifact@v4 213 | if: steps.cache-curl.outputs.cache-hit != 'true' 214 | with: 215 | name: openssl-x86_64 216 | path: ~/openssl/x86_64 217 | - name: Build cURL 218 | if: steps.cache-curl.outputs.cache-hit != 'true' 219 | run: ./build/042-build-curl.sh 220 | - uses: actions/upload-artifact@v4 221 | with: 222 | name: curl-arm64-v8a 223 | path: ~/curl/arm64 224 | - uses: actions/upload-artifact@v4 225 | with: 226 | name: curl-armeabi-v7a 227 | path: ~/curl/arm 228 | - uses: actions/upload-artifact@v4 229 | with: 230 | name: curl-x86_64 231 | path: ~/curl/x86_64 232 | - uses: actions/upload-artifact@v4 233 | with: 234 | name: curl-x86 235 | path: ~/curl/x86 236 | 237 | 238 | build-libxml-android: 239 | runs-on: ubuntu-24.04 240 | container: ubuntu:noble 241 | 242 | steps: 243 | - uses: actions/checkout@v1 244 | - name: Cache libxml 245 | id: cache-libxml 246 | uses: actions/cache@v4 247 | with: 248 | path: | 249 | ~/libxml/arm64 250 | ~/libxml/arm 251 | ~/libxml/x86_64 252 | ~/libxml/x86 253 | key: libxmlv2.14.3-r1 254 | - name: Install dependencies 255 | if: steps.cache-libxml.outputs.cache-hit != 'true' 256 | run: ./build/000-install-dependencies-nobel.sh 257 | - name: Install NDK 258 | if: steps.cache-libxml.outputs.cache-hit != 'true' 259 | run: ./build/002-install-ndk.sh 260 | - name: Define build folders 261 | if: steps.cache-libxml.outputs.cache-hit != 'true' 262 | run: ./build/003-define-build-folders.sh 263 | - name: Build libXML 264 | if: steps.cache-libxml.outputs.cache-hit != 'true' 265 | run: ./build/043-build-libxml.sh 266 | - uses: actions/upload-artifact@v4 267 | with: 268 | name: libxml-arm64-v8a 269 | path: ~/libxml/arm64 270 | - uses: actions/upload-artifact@v4 271 | with: 272 | name: libxml-armeabi-v7a 273 | path: ~/libxml/arm 274 | - uses: actions/upload-artifact@v4 275 | with: 276 | name: libxml-x86_64 277 | path: ~/libxml/x86_64 278 | - uses: actions/upload-artifact@v4 279 | with: 280 | name: libxml-x86 281 | path: ~/libxml/x86 282 | 283 | build-swift-corelibs-android-arm-64: 284 | needs: [build-openssl-android, build-curl-android, build-libxml-android, build-swift-android-arm-64] 285 | runs-on: ubuntu-24.04 286 | container: ubuntu:noble 287 | 288 | steps: 289 | - uses: actions/checkout@v1 290 | - name: Install dependencies 291 | run: ./build/000-install-dependencies-nobel.sh 292 | - name: Install NDK 293 | run: ./build/002-install-ndk.sh 294 | - name: Define build folders 295 | run: ./build/003-define-build-folders.sh 296 | - name: Clone Swift 297 | run: ./build/020-clone-swift.sh 298 | - uses: actions/download-artifact@v4 299 | with: 300 | name: openssl-arm64-v8a 301 | path: ~/openssl/arm64 302 | - uses: actions/download-artifact@v4 303 | with: 304 | name: curl-arm64-v8a 305 | path: ~/curl/arm64 306 | - uses: actions/download-artifact@v4 307 | with: 308 | name: libxml-arm64-v8a 309 | path: ~/libxml/arm64 310 | - uses: actions/download-artifact@v4 311 | with: 312 | name: swift-android-arm64-v8a 313 | path: ~/swift-android/lib 314 | - name: Build Corelibs 315 | run: ./build/052-build-corelibs.sh arm64 aarch64 aarch64-linux-android arm64-v8a aarch64-linux-android 316 | - uses: actions/upload-artifact@v4 317 | with: 318 | name: swift-android-arm64-v8a-libs 319 | path: ~/swift-toolchain/usr/lib/swift 320 | 321 | build-swift-corelibs-android-armeabi-v7a: 322 | needs: [build-openssl-android, build-curl-android, build-libxml-android, build-swift-android-armeabi-v7a] 323 | runs-on: ubuntu-24.04 324 | container: ubuntu:noble 325 | 326 | steps: 327 | - uses: actions/checkout@v1 328 | - name: Install dependencies 329 | run: ./build/000-install-dependencies-nobel.sh 330 | - name: Install NDK 331 | run: ./build/002-install-ndk.sh 332 | - name: Define build folders 333 | run: ./build/003-define-build-folders.sh 334 | - name: Clone Swift 335 | run: ./build/020-clone-swift.sh 336 | - uses: actions/download-artifact@v4 337 | with: 338 | name: openssl-armeabi-v7a 339 | path: ~/openssl/arm 340 | - uses: actions/download-artifact@v4 341 | with: 342 | name: curl-armeabi-v7a 343 | path: ~/curl/arm 344 | - uses: actions/download-artifact@v4 345 | with: 346 | name: libxml-armeabi-v7a 347 | path: ~/libxml/arm 348 | - uses: actions/download-artifact@v4 349 | with: 350 | name: swift-android-armeabi-v7a 351 | path: ~/swift-android/lib 352 | - name: Build Corelibs 353 | run: ./build/052-build-corelibs.sh arm armv7 arm-linux-androideabi armeabi-v7a armv7a-linux-androideabi 354 | - uses: actions/upload-artifact@v4 355 | with: 356 | name: swift-android-armeabi-v7a-libs 357 | path: ~/swift-toolchain/usr/lib/swift 358 | 359 | build-swift-corelibs-android-x86_64: 360 | needs: [build-openssl-android, build-curl-android, build-libxml-android, build-swift-android-x86_64] 361 | runs-on: ubuntu-24.04 362 | container: ubuntu:noble 363 | 364 | steps: 365 | - uses: actions/checkout@v1 366 | - name: Install dependencies 367 | run: ./build/000-install-dependencies-nobel.sh 368 | - name: Install NDK 369 | run: ./build/002-install-ndk.sh 370 | - name: Define build folders 371 | run: ./build/003-define-build-folders.sh 372 | - name: Clone Swift 373 | run: ./build/020-clone-swift.sh 374 | - uses: actions/download-artifact@v4 375 | with: 376 | name: openssl-x86_64 377 | path: ~/openssl/x86_64 378 | - uses: actions/download-artifact@v4 379 | with: 380 | name: curl-x86_64 381 | path: ~/curl/x86_64 382 | - uses: actions/download-artifact@v4 383 | with: 384 | name: libxml-x86_64 385 | path: ~/libxml/x86_64 386 | - uses: actions/download-artifact@v4 387 | with: 388 | name: swift-android-x86_64 389 | path: ~/swift-android/lib 390 | - name: Build Corelibs 391 | run: ./build/052-build-corelibs.sh x86_64 x86_64 x86_64-linux-android x86_64 x86_64-linux-android 392 | - uses: actions/upload-artifact@v4 393 | with: 394 | name: swift-android-x86_64-libs 395 | path: ~/swift-toolchain/usr/lib/swift 396 | 397 | build-swift-corelibs-android-x86: 398 | needs: [build-openssl-android, build-curl-android, build-libxml-android, build-swift-android-x86] 399 | runs-on: ubuntu-24.04 400 | container: ubuntu:noble 401 | 402 | steps: 403 | - uses: actions/checkout@v1 404 | - name: Install dependencies 405 | run: ./build/000-install-dependencies-nobel.sh 406 | - name: Install NDK 407 | run: ./build/002-install-ndk.sh 408 | - name: Define build folders 409 | run: ./build/003-define-build-folders.sh 410 | - name: Clone Swift 411 | run: ./build/020-clone-swift.sh 412 | - uses: actions/download-artifact@v4 413 | with: 414 | name: openssl-x86 415 | path: ~/openssl/x86 416 | - uses: actions/download-artifact@v4 417 | with: 418 | name: curl-x86 419 | path: ~/curl/x86 420 | - uses: actions/download-artifact@v4 421 | with: 422 | name: libxml-x86 423 | path: ~/libxml/x86 424 | - uses: actions/download-artifact@v4 425 | with: 426 | name: swift-android-x86 427 | path: ~/swift-android/lib 428 | - name: Build Corelibs 429 | run: ./build/052-build-corelibs.sh x86 i686 i686-linux-android x86 i686-linux-android 430 | - uses: actions/upload-artifact@v4 431 | with: 432 | name: swift-android-x86-libs 433 | path: ~/swift-toolchain/usr/lib/swift 434 | 435 | collect-toolchain: 436 | needs: [build-swift-corelibs-android-arm-64, build-swift-corelibs-android-armeabi-v7a, build-swift-corelibs-android-x86_64, build-swift-corelibs-android-x86] 437 | runs-on: ubuntu-24.04 438 | container: ubuntu:noble 439 | 440 | steps: 441 | - uses: actions/checkout@v1 442 | - name: Install dependencies 443 | run: ./build/000-install-dependencies-nobel.sh 444 | - name: Install NDK 445 | run: ./build/002-install-ndk.sh 446 | - uses: actions/download-artifact@v4 447 | with: 448 | name: swift-android-arm64-v8a-libs 449 | path: ~/lib/swift-aarch64 450 | - uses: actions/download-artifact@v4 451 | with: 452 | name: swift-android-armeabi-v7a-libs 453 | path: ~/lib/swift-armv7 454 | - uses: actions/download-artifact@v4 455 | with: 456 | name: swift-android-x86_64-libs 457 | path: ~/lib/swift-x86_64 458 | - uses: actions/download-artifact@v4 459 | with: 460 | name: swift-android-x86-libs 461 | path: ~/lib/swift-i686 462 | - uses: actions/download-artifact@v4 463 | with: 464 | name: swift-android-arm64-v8a 465 | path: ~/stdlib/swift-aarch64 466 | - uses: actions/download-artifact@v4 467 | with: 468 | name: swift-android-armeabi-v7a 469 | path: ~/stdlib/swift-armv7 470 | - uses: actions/download-artifact@v4 471 | with: 472 | name: swift-android-x86_64 473 | path: ~/stdlib/swift-x86_64 474 | - uses: actions/download-artifact@v4 475 | with: 476 | name: swift-android-x86 477 | path: ~/stdlib/swift-i686 478 | - name: Collect toolchain 479 | run: ./build/060-collect-toolchain.sh 480 | - uses: actions/upload-artifact@v4 481 | with: 482 | name: swift-android 483 | path: ~/out/swift-android.zip 484 | -------------------------------------------------------------------------------- /patches/swift-corelibs-foundation/0015-fix-websocket-buffered-read-fragment-support.patch: -------------------------------------------------------------------------------- 1 | From 860ce1e0f2fb76266c27afc1708ae01637a88192 Mon Sep 17 00:00:00 2001 2 | From: Andrew Druk 3 | Date: Mon, 4 Aug 2025 14:53:38 +0300 4 | Subject: [PATCH] Fix WebSocket buffered read Add support for fragmented 5 | messages 6 | 7 | Buffered socket reads could result in incomplete frame parsing due to incorrect assumptions about TCP delivery. This patch introduces proper accumulation of partial reads. 8 | 9 | Also adds handling for fragmented WebSocket messages split across multiple frames. 10 | 11 | (cherry picked from commit 940dd090506142e4198a11f24fe883bbe26166f0) 12 | --- 13 | .../WebSocket/WebSocketURLProtocol.swift | 25 +++- 14 | .../URLSession/libcurl/EasyHandle.swift | 4 +- 15 | Tests/Foundation/HTTPServer.swift | 99 ++++++++++++++-- 16 | Tests/Foundation/TestURLSession.swift | 107 +++++++++--------- 17 | 4 files changed, 169 insertions(+), 66 deletions(-) 18 | 19 | diff --git a/Sources/FoundationNetworking/URLSession/WebSocket/WebSocketURLProtocol.swift b/Sources/FoundationNetworking/URLSession/WebSocket/WebSocketURLProtocol.swift 20 | index 8216f23d..e35612dd 100644 21 | --- a/Sources/FoundationNetworking/URLSession/WebSocket/WebSocketURLProtocol.swift 22 | +++ b/Sources/FoundationNetworking/URLSession/WebSocket/WebSocketURLProtocol.swift 23 | @@ -17,6 +17,9 @@ import Foundation 24 | import Dispatch 25 | 26 | internal class _WebSocketURLProtocol: _HTTPURLProtocol { 27 | + 28 | + private var messageData = Data() 29 | + 30 | public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { 31 | super.init(task: task, cachedResponse: nil, client: client) 32 | } 33 | @@ -118,14 +121,14 @@ internal class _WebSocketURLProtocol: _HTTPURLProtocol { 34 | lastRedirectBody = redirectBody 35 | } 36 | 37 | - let flags = easyHandle.getWebSocketFlags() 38 | + let (offset, bytesLeft, flags) = easyHandle.getWebSocketMeta() 39 | 40 | - notifyTask(aboutReceivedData: data, flags: flags) 41 | + notifyTask(aboutReceivedData: data, offset: offset, bytesLeft: bytesLeft, flags: flags) 42 | internalState = .transferInProgress(ts) 43 | return .proceed 44 | } 45 | 46 | - fileprivate func notifyTask(aboutReceivedData data: Data, flags: _EasyHandle.WebSocketFlags) { 47 | + fileprivate func notifyTask(aboutReceivedData data: Data, offset: Int64, bytesLeft: Int64, flags: _EasyHandle.WebSocketFlags) { 48 | guard let t = self.task else { 49 | fatalError("Cannot notify") 50 | } 51 | @@ -159,10 +162,21 @@ internal class _WebSocketURLProtocol: _HTTPURLProtocol { 52 | } else if flags.contains(.pong) { 53 | task.noteReceivedPong() 54 | } else if flags.contains(.binary) { 55 | - let message = URLSessionWebSocketTask.Message.data(data) 56 | + if bytesLeft > 0 || flags.contains(.cont) { 57 | + messageData.append(data) 58 | + return 59 | + } 60 | + messageData.append(data) 61 | + let message = URLSessionWebSocketTask.Message.data(messageData) 62 | task.appendReceivedMessage(message) 63 | + messageData = Data() // Reset for the next message 64 | } else if flags.contains(.text) { 65 | - guard let utf8 = String(data: data, encoding: .utf8) else { 66 | + if bytesLeft > 0 || flags.contains(.cont) { 67 | + messageData.append(data) 68 | + return 69 | + } 70 | + messageData.append(data) 71 | + guard let utf8 = String(data: messageData, encoding: .utf8) else { 72 | NSLog("Invalid utf8 message received from server \(data)") 73 | let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorBadServerResponse, 74 | userInfo: [ 75 | @@ -175,6 +189,7 @@ internal class _WebSocketURLProtocol: _HTTPURLProtocol { 76 | } 77 | let message = URLSessionWebSocketTask.Message.string(utf8) 78 | task.appendReceivedMessage(message) 79 | + messageData = Data() // Reset for the next message 80 | } else { 81 | NSLog("Unexpected message received from server \(data) \(flags)") 82 | let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorBadServerResponse, 83 | diff --git a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 84 | index cfb9a9e5..c2ae72e6 100644 85 | --- a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 86 | +++ b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift 87 | @@ -402,10 +402,10 @@ extension _EasyHandle { 88 | } 89 | 90 | // Only valid to call within a didReceive(data:size:nmemb:) call 91 | - func getWebSocketFlags() -> WebSocketFlags { 92 | + func getWebSocketMeta() -> (Int64, Int64, WebSocketFlags) { 93 | let metadataPointer = CFURLSessionEasyHandleWebSocketsMetadata(rawHandle) 94 | let flags = WebSocketFlags(rawValue: metadataPointer.pointee.flags) 95 | - return flags 96 | + return (metadataPointer.pointee.offset, metadataPointer.pointee.bytesLeft, flags) 97 | } 98 | 99 | func receiveWebSocketsData() throws -> (Data, WebSocketFlags) { 100 | diff --git a/Tests/Foundation/HTTPServer.swift b/Tests/Foundation/HTTPServer.swift 101 | index ba5782b9..a4a4a38b 100644 102 | --- a/Tests/Foundation/HTTPServer.swift 103 | +++ b/Tests/Foundation/HTTPServer.swift 104 | @@ -919,6 +919,8 @@ public class TestURLSessionServer: CustomStringConvertible { 105 | "Connection: Upgrade"] 106 | 107 | let expectFullRequestResponseTests: Bool 108 | + let bufferedSendingTests: Bool 109 | + let fragmentedTests: Bool 110 | let sendClosePacket: Bool 111 | let completeUpgrade: Bool 112 | 113 | @@ -926,14 +928,32 @@ public class TestURLSessionServer: CustomStringConvertible { 114 | switch uri { 115 | case "/web-socket": 116 | expectFullRequestResponseTests = true 117 | + bufferedSendingTests = false 118 | + fragmentedTests = false 119 | + completeUpgrade = true 120 | + sendClosePacket = true 121 | + case "/web-socket/buffered-sending": 122 | + expectFullRequestResponseTests = true 123 | + bufferedSendingTests = true 124 | + fragmentedTests = false 125 | + completeUpgrade = true 126 | + sendClosePacket = true 127 | + case "/web-socket/fragmented": 128 | + expectFullRequestResponseTests = true 129 | + bufferedSendingTests = false 130 | + fragmentedTests = true 131 | completeUpgrade = true 132 | sendClosePacket = true 133 | case "/web-socket/semi-abrupt-close": 134 | expectFullRequestResponseTests = false 135 | + bufferedSendingTests = false 136 | + fragmentedTests = false 137 | completeUpgrade = true 138 | sendClosePacket = false 139 | case "/web-socket/abrupt-close": 140 | expectFullRequestResponseTests = false 141 | + bufferedSendingTests = false 142 | + fragmentedTests = false 143 | completeUpgrade = false 144 | sendClosePacket = false 145 | default: 146 | @@ -949,6 +969,8 @@ public class TestURLSessionServer: CustomStringConvertible { 147 | } 148 | responseHeaders.append("Sec-WebSocket-Protocol: \(expectedProtocol)") 149 | expectFullRequestResponseTests = false 150 | + bufferedSendingTests = false 151 | + fragmentedTests = false 152 | completeUpgrade = true 153 | sendClosePacket = true 154 | } 155 | @@ -983,10 +1005,41 @@ public class TestURLSessionServer: CustomStringConvertible { 156 | NSLog("Invalid string frame") 157 | throw InternalServerError.badBody 158 | } 159 | - 160 | - // Send a string message 161 | - let sendStringFrame = Data([0x81, UInt8(stringPayload.count)]) + stringPayload 162 | - try httpServer.tcpSocket.writeRawData(sendStringFrame) 163 | + 164 | + if bufferedSendingTests { 165 | + // Send a string message in chunks of 2 bytes 166 | + let sendStringFrame = Data([0x81, UInt8(stringPayload.count)]) + stringPayload 167 | + let bufferSize = 2 // Let's assume the server has a buffer size of 2 bytes 168 | + for i in stride(from: 0, to: sendStringFrame.count, by: bufferSize) { 169 | + let end = min(i + bufferSize, sendStringFrame.count) 170 | + let chunk = sendStringFrame.subdata(in: i.. Void { 301 | + let url = try XCTUnwrap(URL(string: urlString)) 302 | + let request = URLRequest(url: url) 303 | + 304 | + let delegate = SessionDelegate(with: expectation(description: "\(urlString): Connect")) 305 | + let task = delegate.runWebSocketTask(with: request, timeoutInterval: 4) 306 | + 307 | + // We interleave sending and receiving, as the test HTTPServer implementation is barebones, and can't handle receiving more than one frame at a time. So, this back-and-forth acts as a gating mechanism 308 | + try await task.send(.string("Hello")) 309 | + 310 | + let stringMessage = try await task.receive() 311 | + switch stringMessage { 312 | + case .string(let str): 313 | + XCTAssert(str == "Hello") 314 | + default: 315 | + XCTFail("Unexpected String Message") 316 | + } 317 | + 318 | + try await task.send(.data(Data([0x20, 0x22, 0x10, 0x03]))) 319 | + 320 | + let dataMessage = try await task.receive() 321 | + switch dataMessage { 322 | + case .data(let data): 323 | + XCTAssert(data == Data([0x20, 0x22, 0x10, 0x03])) 324 | + default: 325 | + XCTFail("Unexpected Data Message") 326 | + } 327 | + 328 | + do { 329 | + try await task.sendPing() 330 | + // Server hasn't closed the connection yet 331 | + } catch { 332 | + // Server closed the connection before we could process the pong 333 | + let urlError = try XCTUnwrap(error as? URLError) 334 | + XCTAssertEqual(urlError._nsError.code, NSURLErrorNetworkConnectionLost) 335 | + } 336 | + 337 | + await fulfillment(of: [delegate.expectation], timeout: 50) 338 | + 339 | + do { 340 | + _ = try await task.receive() 341 | + XCTFail("Expected to throw when receiving on closed task") 342 | + } catch { 343 | + let urlError = try XCTUnwrap(error as? URLError) 344 | + XCTAssertEqual(urlError._nsError.code, NSURLErrorNetworkConnectionLost) 345 | + } 346 | + 347 | + let callbacks = [ "urlSession(_:webSocketTask:didOpenWithProtocol:)", 348 | + "urlSession(_:webSocketTask:didCloseWith:reason:)", 349 | + "urlSession(_:task:didCompleteWithError:)" ] 350 | + XCTAssertEqual(delegate.callbacks.count, callbacks.count) 351 | + XCTAssertEqual(delegate.callbacks, callbacks, "Callbacks for \(#function)") 352 | } 353 | - 354 | - let callbacks = [ "urlSession(_:webSocketTask:didOpenWithProtocol:)", 355 | - "urlSession(_:webSocketTask:didCloseWith:reason:)", 356 | - "urlSession(_:task:didCompleteWithError:)" ] 357 | - XCTAssertEqual(delegate.callbacks.count, callbacks.count) 358 | - XCTAssertEqual(delegate.callbacks, callbacks, "Callbacks for \(#function)") 359 | + 360 | + try await testWebSocket(withURL: "ws://127.0.0.1:\(TestURLSession.serverPort)/web-socket") 361 | + try await testWebSocket(withURL: "ws://127.0.0.1:\(TestURLSession.serverPort)/web-socket/buffered-sending") 362 | + try await testWebSocket(withURL: "ws://127.0.0.1:\(TestURLSession.serverPort)/web-socket/fragmented") 363 | } 364 | 365 | func test_webSocketShared() async throws { 366 | -- 367 | 2.46.0 368 | 369 | --------------------------------------------------------------------------------