From 22e7c5bed465043ba78dd13b7980aa1494605277 Mon Sep 17 00:00:00 2001 From: Oleg Korshul Date: Fri, 6 Mar 2026 23:25:00 +0300 Subject: [PATCH 1/4] Change download method --- .../src/FileTransporter_mac.mm | 248 +++++++++++++----- 1 file changed, 182 insertions(+), 66 deletions(-) diff --git a/Common/Network/FileTransporter/src/FileTransporter_mac.mm b/Common/Network/FileTransporter/src/FileTransporter_mac.mm index a670af625c..ddda7d7315 100644 --- a/Common/Network/FileTransporter/src/FileTransporter_mac.mm +++ b/Common/Network/FileTransporter/src/FileTransporter_mac.mm @@ -105,11 +105,48 @@ void Create() { static NSString* StringWToNSString ( const std::wstring& Str ) { + // Returns +1 retained NSString (alloc/init). Caller must release in non-ARC. NSString* pString = [ [ NSString alloc ] - initWithBytes : (char*)Str.data() length : Str.size() * sizeof(wchar_t) - encoding : CFStringConvertEncodingToNSStringEncoding ( kCFStringEncodingUTF32LE ) ]; + initWithBytes : (char*)Str.data() length : Str.size() * sizeof(wchar_t) + encoding : CFStringConvertEncodingToNSStringEncoding ( kCFStringEncodingUTF32LE ) ]; return pString; } + + // Safely creates an NSURL from a string that may already be percent-encoded or not. + // + // Problem with [NSURL URLWithString:]: it accepts malformed URLs silently on older iOS + // versions but returns nil on newer ones (iOS 15+), causing inconsistent behaviour. + // + // Problem with stringByAddingPercentEncodingWithAllowedCharacters + URLQueryAllowedCharacterSet: + // URLQueryAllowedCharacterSet is designed only for the query portion of a URL. + // Applied to a full URL it double-encodes already-encoded sequences (e.g. %20 -> %2520) + // and may encode characters that are legal in other URL components, breaking the URL. + // + // NSURLComponents is the correct API: it validates the full URL structure, does not + // re-encode already-encoded sequences, and returns nil for truly malformed input. + // Returns an autoreleased NSURL*, or nil if the string cannot be made into a valid URL. + static NSURL* SafeURLFromString(NSString* urlString) + { + if (!urlString || urlString.length == 0) + return nil; + + // Primary path: NSURLComponents validates and parses without breaking existing encoding. + NSURLComponents* components = [NSURLComponents componentsWithString:urlString]; + NSURL* url = components.URL; + if (url) + return url; + + // Fallback: the string contains unencoded characters (spaces, cyrillic, brackets, etc.). + // URLFragmentAllowedCharacterSet covers all URL components, unlike URLQueryAllowedCharacterSet. + NSString* encoded = [urlString stringByAddingPercentEncodingWithAllowedCharacters: + [NSCharacterSet URLFragmentAllowedCharacterSet]]; + if (!encoded) + return nil; + + components = [NSURLComponents componentsWithString:encoded]; + return components.URL; + } + class CFileTransporterBaseCocoa : public CFileTransporterBase { public : @@ -145,83 +182,162 @@ virtual int DownloadFile() override return 0; #endif + // stringURL: +1 retained (alloc/init). Must be released on all exit paths in non-ARC. NSString* stringURL = StringWToNSString(m_sDownloadFileUrl); - NSString* escapedURL = [stringURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + NSURL* url = SafeURLFromString(stringURL); - int nResult = 1; - - if (m_pSession) + if (!url) { - NSURLRequest* urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:escapedURL]]; - - __block NSData* result = nil; - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - - NSURLSession* _session = ((CSessionMAC*)m_pSession->m_pInternal)->m_session; - if (nil == _session) - _session = [NSURLSession sharedSession]; - - [[_session dataTaskWithRequest:urlRequest - completionHandler:^(NSData *data, NSURLResponse* response, NSError *error) { - if (error == nil) - result = data; - - dispatch_semaphore_signal(sem); - }] resume]; - - dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); - - if (result) - { - NSString* filePath = StringWToNSString(m_sDownloadFilePath); - [result writeToFile:filePath atomically:YES]; - - nResult = 0; - } - else - { - nResult = 1; - } - - return nResult; + NSLog(@"[DownloadFile] Invalid URL: %@", stringURL); +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + [stringURL release]; +#endif +#endif + return 1; } - else + + // Select session. If the caller provided one, prefer it; fall back to the shared + // singleton. [NSURLSession sharedSession] is a singleton -- never retain/release it. + NSURLSession* session = nil; + if (m_pSession && m_pSession->m_pInternal) { - NSURL* url = [NSURL URLWithString:escapedURL]; - NSData* urlData = [[NSData alloc] initWithContentsOfURL:url]; - if ( urlData ) - { - NSString* filePath = StringWToNSString(m_sDownloadFilePath); - [urlData writeToFile:filePath atomically:YES]; - - #if defined(_IOS) - return 0; - #else - #ifndef _ASC_USE_ARC_ - if (!GetARCEnabled()) + CSessionMAC* sessionInternal = (CSessionMAC*)m_pSession->m_pInternal; + session = sessionInternal->m_session; + } + if (!session) + session = [NSURLSession sharedSession]; + + // NSData initWithContentsOfURL / dataWithContentsOfURL must NOT be used for HTTP: + // - Fails silently on CDN responses with Transfer-Encoding: chunked + // (returns NSCocoaErrorDomain Code=256 on some iOS versions) + // - Does not follow HTTP redirects reliably + // - Ignores Content-Encoding (gzip, br) + // - Has no timeout, can block indefinitely + // NSURLSession handles all of the above correctly on all supported iOS/macOS versions. + NSURLRequest* urlRequest = [NSURLRequest requestWithURL:url]; + + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + + // In non-ARC, __block variables are NOT automatically retained by the block. + // |data| passed to the completion handler is autoreleased; without an explicit + // retain it may be deallocated before semaphore_wait returns on the calling thread. + // We therefore retain inside the block and release after use below. + __block NSData* resultData = nil; + + NSURLSessionDataTask* task = + [session dataTaskWithRequest:urlRequest + completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) { - [stringURL release]; - //[url release]; - [urlData release]; - } - #endif - #endif - return 0; - } + if (error) + { + NSLog(@"[DownloadFile] Network error: %@, URL: %@", error, url); + } + else if ([response isKindOfClass:[NSHTTPURLResponse class]]) + { + NSInteger status = [(NSHTTPURLResponse*)response statusCode]; + if (status >= 200 && status < 300) + { +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + resultData = [data retain]; + else +#endif +#endif + resultData = data; + } + else + { + NSLog(@"[DownloadFile] HTTP %ld, URL: %@", (long)status, url); + } + } + else + { + // Non-HTTP scheme (e.g. file://) -- accept unconditionally. +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + resultData = [data retain]; + else +#endif +#endif + resultData = data; + } + + dispatch_semaphore_signal(sem); + }]; + + [task resume]; + + // 60-second hard timeout. DISPATCH_TIME_FOREVER must not be used: if the network + // stack silently drops the connection the completion handler may never fire, + // leaving this thread suspended indefinitely (deadlock). + const int64_t kTimeoutSeconds = 60; + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, kTimeoutSeconds * NSEC_PER_SEC); + long waitResult = dispatch_semaphore_wait(sem, timeout); + + // dispatch_semaphore_create returns a +1 object. Release it in non-ARC. + // In ARC (including all iOS targets) GCD objects are managed automatically. +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + dispatch_release(sem); +#endif +#endif - #if defined(_IOS) - return 1; - #else - #ifndef _ASC_USE_ARC_ + if (waitResult != 0) + { + // Timeout expired. Cancel the in-flight task so that the completion handler + // is called with NSURLErrorCancelled (data == nil), preventing it from writing + // to resultData after we return and corrupting memory in non-ARC. + [task cancel]; + NSLog(@"[DownloadFile] Timeout after %llds, URL: %@", (long long)kTimeoutSeconds, url); +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ if (!GetARCEnabled()) { + // resultData could theoretically be non-nil if the completion handler + // raced with the timeout check on a multicore device before cancel ran. + [resultData release]; [stringURL release]; - //[url release]; } - #endif - #endif +#endif +#endif + return 1; + } + + if (!resultData) + { +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + [stringURL release]; +#endif +#endif return 1; } + + // filePath: +1 retained (alloc/init). Must be released in non-ARC. + NSString* filePath = StringWToNSString(m_sDownloadFilePath); + BOOL written = [resultData writeToFile:filePath atomically:YES]; + + if (!written) + NSLog(@"[DownloadFile] Failed to write file: %@", filePath); + +#if !defined(_IOS) +#ifndef _ASC_USE_ARC_ + if (!GetARCEnabled()) + { + [resultData release]; + [filePath release]; + [stringURL release]; + } +#endif +#endif + + return written ? 0 : 1; } virtual int UploadData() override From de9709ea50d7ad268b612d70078e2c259e597060 Mon Sep 17 00:00:00 2001 From: "Oleg.Korshul" Date: Mon, 9 Mar 2026 13:35:02 +0300 Subject: [PATCH 2/4] Fix fromUnicode on ios --- UnicodeConverter/UnicodeConverter_internal_ios.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnicodeConverter/UnicodeConverter_internal_ios.mm b/UnicodeConverter/UnicodeConverter_internal_ios.mm index dc9254cd81..fb5a987b7b 100644 --- a/UnicodeConverter/UnicodeConverter_internal_ios.mm +++ b/UnicodeConverter/UnicodeConverter_internal_ios.mm @@ -130,7 +130,7 @@ static std::string fromUnicode(const wchar_t* sInput, const unsigned int& nInputLen, const char* converterName) { - if (nInputLen > 0) + if (nInputLen <= 0) return ""; NSString* sEncodingCF = StringAToNSString(std::string(converterName)); From 0101774675f0c53eb106bfc98fe7e51a4e9a6258 Mon Sep 17 00:00:00 2001 From: "Oleg.Korshul" Date: Mon, 9 Mar 2026 13:35:40 +0300 Subject: [PATCH 3/4] Fix safeUrl method --- .../src/FileTransporter_mac.mm | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/Common/Network/FileTransporter/src/FileTransporter_mac.mm b/Common/Network/FileTransporter/src/FileTransporter_mac.mm index ddda7d7315..95331f7b62 100644 --- a/Common/Network/FileTransporter/src/FileTransporter_mac.mm +++ b/Common/Network/FileTransporter/src/FileTransporter_mac.mm @@ -112,39 +112,28 @@ void Create() return pString; } - // Safely creates an NSURL from a string that may already be percent-encoded or not. - // - // Problem with [NSURL URLWithString:]: it accepts malformed URLs silently on older iOS - // versions but returns nil on newer ones (iOS 15+), causing inconsistent behaviour. - // - // Problem with stringByAddingPercentEncodingWithAllowedCharacters + URLQueryAllowedCharacterSet: - // URLQueryAllowedCharacterSet is designed only for the query portion of a URL. - // Applied to a full URL it double-encodes already-encoded sequences (e.g. %20 -> %2520) - // and may encode characters that are legal in other URL components, breaking the URL. - // - // NSURLComponents is the correct API: it validates the full URL structure, does not - // re-encode already-encoded sequences, and returns nil for truly malformed input. - // Returns an autoreleased NSURL*, or nil if the string cannot be made into a valid URL. + // NSURLComponents correctly handles mixed input (already-encoded + raw characters) + // without double-encoding, but normalises unreserved percent-sequences (%5F → _). + // If preserving the exact encoding is required, use [NSURL URLWithString:] instead. static NSURL* SafeURLFromString(NSString* urlString) { if (!urlString || urlString.length == 0) return nil; - // Primary path: NSURLComponents validates and parses without breaking existing encoding. - NSURLComponents* components = [NSURLComponents componentsWithString:urlString]; - NSURL* url = components.URL; + NSURL* url = [NSURL URLWithString:urlString]; if (url) return url; - // Fallback: the string contains unencoded characters (spaces, cyrillic, brackets, etc.). - // URLFragmentAllowedCharacterSet covers all URL components, unlike URLQueryAllowedCharacterSet. - NSString* encoded = [urlString stringByAddingPercentEncodingWithAllowedCharacters: - [NSCharacterSet URLFragmentAllowedCharacterSet]]; + // Fallback: the string contains unencoded characters (spaces, cyrillic, + // brackets, etc.). URLFragmentAllowedCharacterSet includes '%', so + // existing percent-sequences won't be double-encoded. + NSString* encoded = + [urlString stringByAddingPercentEncodingWithAllowedCharacters: + [NSCharacterSet URLFragmentAllowedCharacterSet]]; if (!encoded) return nil; - components = [NSURLComponents componentsWithString:encoded]; - return components.URL; + return [NSURL URLWithString:encoded]; } class CFileTransporterBaseCocoa : public CFileTransporterBase From 33c4b5130ec8e7aa5aba91bdb32d13b5d08a9753 Mon Sep 17 00:00:00 2001 From: Oleg Korshul Date: Mon, 9 Mar 2026 21:14:26 +0300 Subject: [PATCH 4/4] Revert missed file --- Common/3dParty/socketio/patches/sio_client_impl_encode.patch | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Common/3dParty/socketio/patches/sio_client_impl_encode.patch diff --git a/Common/3dParty/socketio/patches/sio_client_impl_encode.patch b/Common/3dParty/socketio/patches/sio_client_impl_encode.patch new file mode 100644 index 0000000000..d7021d6b59 --- /dev/null +++ b/Common/3dParty/socketio/patches/sio_client_impl_encode.patch @@ -0,0 +1,5 @@ +<<<<<<< + if((c >= 'a' && c <= 'z') || (c>= 'A' && c<= 'Z') || (c >= '0' && c<= '9')){ +======= + if((c >= 'a' && c <= 'z') || (c>= 'A' && c<= 'Z') || (c >= '0' && c<= '9') || ('-' == c) || ('_' == c) || ('.' == c) || ('~' == c)){ +>>>>>>>