From 1b8213328c23952649b5e43afdaba14be839b38f Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:23:27 +0800 Subject: [PATCH 1/3] fix: correctly handle url containing hash but without search --- xcode/Ext-Safari/Functions.swift | 74 ++------ xcode/Shared/UrlPolyfill.swift | 96 +++++++++++ xcode/Tests-Mac/UrlCodecTests.swift | 182 ++++++++++++++++++++ xcode/Tests-Mac/UserscriptsTests.swift | 22 ++- xcode/Userscripts.xcodeproj/project.pbxproj | 12 ++ 5 files changed, 311 insertions(+), 75 deletions(-) create mode 100644 xcode/Shared/UrlPolyfill.swift create mode 100644 xcode/Tests-Mac/UrlCodecTests.swift diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index ae4a931e..738fe244 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -77,29 +77,12 @@ func openSaveLocation() -> Bool { } func validateUrl(_ urlString: String) -> Bool { - var urlChecked = urlString - // if the url is already encoded, decode it - if isEncoded(urlChecked) { - if let decodedUrl = urlChecked.removingPercentEncoding { - urlChecked = decodedUrl - } else { - logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(urlString, privacy: .public)") - return false - } - } - // encode all urls strings - if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - urlChecked = encodedUrl - } else { - logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(urlString, privacy: .public)") - return false - } guard - let parts = getUrlProps(urlChecked), + let parts = jsLikeURL(urlString), let ptcl = parts["protocol"], let path = parts["pathname"] else { - logger?.error("\(#function, privacy: .public) - failed at (3) for \(urlString, privacy: .public)") + logger?.error("\(#function, privacy: .public) - Invalid URL: \(urlString, privacy: .public)") return false } if @@ -129,7 +112,10 @@ func isVersionNewer(_ oldVersion: String, _ newVersion: String) -> Bool { } func isEncoded(_ str: String) -> Bool { - return str.removingPercentEncoding != str + if let decoded = str.removingPercentEncoding { + return decoded != str + } + return false } // parser @@ -888,25 +874,9 @@ func getRemoteFileContents(_ url: String) -> String? { urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:") logger?.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https") } - // if the url is already encoded, decode it - if isEncoded(urlChecked) { - if let decodedUrl = urlChecked.removingPercentEncoding { - urlChecked = decodedUrl - } else { - logger?.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(url, privacy: .public)") - return nil - } - } - // encode all urls strings - if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - urlChecked = encodedUrl - } else { - logger?.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(url, privacy: .public)") - return nil - } // convert url string to url - guard let solidURL = URL(string: urlChecked) else { - logger?.error("\(#function, privacy: .public) - failed at (3), couldn't convert string to url, \(url, privacy: .public)") + guard let solidURL = fixedURL(string: urlChecked) else { + logger?.error("\(#function, privacy: .public) - failed at (1), invalid URL: \(url, privacy: .public)") return nil } var contents = "" @@ -929,7 +899,7 @@ func getRemoteFileContents(_ url: String) -> String? { // if made it to this point and contents still an empty string, something went wrong with the request if contents.isEmpty { - logger?.error("\(#function, privacy: .public) - failed at (4), contents empty, \(url, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (2), contents empty: \(url, privacy: .public)") return nil } logger?.info("\(#function, privacy: .public) - completed for \(url, privacy: .public)") @@ -1032,28 +1002,6 @@ func checkDefaultDirectories() -> Bool { } // matching -func getUrlProps(_ url: String) -> [String: String]? { - guard - let parts = URLComponents(string: url), - let ptcl = parts.scheme, - let host = parts.host - else { - logger?.error("\(#function, privacy: .public) - failed to parse url") - return nil - } - var search = "" - if let query = parts.query { - search = "?" + query - } - return [ - "protocol": "\(ptcl):", - "host": host, - "pathname": parts.path, - "search": search, - "href": url - ] -} - func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { let pattern = #"[\.|\?|\^|\$|\+|\{|\}|\[|\]|\||\\(|\)|\/]"# var patternReplace = "^\(stringPattern.replacingOccurrences(of: pattern, with: #"\\$0"#, options: .regularExpression))$" @@ -1066,9 +1014,9 @@ func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { func match(_ url: String, _ matchPattern: String) -> Bool { guard - let parts = getUrlProps(url), + let parts = jsLikeURL(url), let ptcl = parts["protocol"], - let host = parts["host"], + let host = parts["hostname"], var path = parts["pathname"] else { logger?.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)") diff --git a/xcode/Shared/UrlPolyfill.swift b/xcode/Shared/UrlPolyfill.swift new file mode 100644 index 00000000..f0110d67 --- /dev/null +++ b/xcode/Shared/UrlPolyfill.swift @@ -0,0 +1,96 @@ +import Foundation + +extension CharacterSet { + // https://developer.apple.com/documentation/foundation/characterset#2902136 + public static let urlAllowed_ = CharacterSet(charactersIn: "#") + .union(.urlFragmentAllowed) + .union(.urlHostAllowed) + .union(.urlPasswordAllowed) + .union(.urlPathAllowed) + .union(.urlQueryAllowed) + .union(.urlUserAllowed) +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI +func encodeURI(_ uri: String) -> String { + // https://developer.apple.com/documentation/foundation/characterset#2902136 +// var urlAllowed = CharacterSet(charactersIn: "#") +// urlAllowed.formUnion(.urlFragmentAllowed) +// urlAllowed.formUnion(.urlHostAllowed) +// urlAllowed.formUnion(.urlPasswordAllowed) +// urlAllowed.formUnion(.urlPathAllowed) +// urlAllowed.formUnion(.urlQueryAllowed) +// urlAllowed.formUnion(.urlUserAllowed) + return uri.addingPercentEncoding(withAllowedCharacters: .urlAllowed_) ?? uri +} + +func fixedURL(string urlString: String) -> URL? { + let rawUrlString = urlString.removingPercentEncoding ?? urlString + var url: URL? + if #available(macOS 14.0, iOS 17.0, *) { + url = URL(string: rawUrlString, encodingInvalidCharacters: true) + } else { + url = URL(string: encodeURI(rawUrlString)) + } + return url +} + +// https://developer.mozilla.org/en-US/docs/Web/API/URL +func jsLikeURL(_ urlString: String, baseString: String? = nil) -> [String: String]? { + var _url: URL? + if let baseString = baseString { + guard let baseUrl = fixedURL(string: baseString) else { + return nil + } + _url = URL(string: urlString, relativeTo: baseUrl) + } else { + _url = fixedURL(string: urlString) + } + guard let url = _url else { + return nil + } + + guard let scheme = url.scheme else { + return nil + } + var port = (url.port == nil) ? "" : String(url.port!) + if (scheme == "http" && port == "80") { port = "" } + if (scheme == "https" && port == "443") { port = "" } + if #available(macOS 13.0, iOS 16.0, *) { + let hostname = url.host(percentEncoded: true) ?? "" + let host = (port == "") ? hostname : "\(hostname):\(port)" + let query = url.query(percentEncoded: true) ?? "" + let fragment = url.fragment(percentEncoded: true) ?? "" + return [ + "hash": fragment == "" ? "" : "#\(fragment)", + "host": host, + "hostname": hostname, +// "href": url.absoluteString, + "origin": "\(scheme)://\(host)", + "password": url.password(percentEncoded: true) ?? "", + "pathname": url.path(percentEncoded: true), + "port": port, + "protocol": "\(scheme):", + "search": query == "" ? "" : "?\(query)", + "username": url.user(percentEncoded: true) ?? "" + ] + } else { + let hostname = url.host ?? "" + let host = (port == "") ? hostname : "\(hostname):\(port)" + let query = url.query ?? "" + let fragment = url.fragment ?? "" + return [ + "hash": fragment == "" ? "" : "#\(fragment)", + "host": host, + "hostname": hostname, +// "href": url.absoluteString, + "origin": "\(scheme)://\(host)", + "password": url.password ?? "", + "pathname": url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path, + "port": port, + "protocol": "\(scheme):", + "search": query == "" ? "" : "?\(query)", + "username": url.user?.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? "" + ] + } +} diff --git a/xcode/Tests-Mac/UrlCodecTests.swift b/xcode/Tests-Mac/UrlCodecTests.swift new file mode 100644 index 00000000..52a08bb3 --- /dev/null +++ b/xcode/Tests-Mac/UrlCodecTests.swift @@ -0,0 +1,182 @@ +import XCTest + +final class UrlCodecTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + + func testEncodeURI() throws { + + func check(_ urlString: String, _ res: String) -> Bool { + let url = encodeURI(urlString) + if (url != res) { + print(urlString, url) + return false + } + return true + } + + XCTAssert(check("http://user:password@host:80/path?q=1#id", + "http://user:password@host:80/path?q=1#id")) + + XCTAssert(check("https://用户名:密码@中.文:80/path/中文/?a=中文#中文", + "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@%E4%B8%AD.%E6%96%87:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD%E6%96%87#%E4%B8%AD%E6%96%87")) + + XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", + "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@%E4%B8%AD.%E6%96%87:80/path/%E4%B8%AD%25E6%2596%2587/?a=%E4%B8%AD&b=%25E6%2596%2587&c#%E4%B8%AD%E6%96%87")) + + } + + func testJsLikeURL() throws { + + func diffPrint(_ dA: Dictionary?, _ dB: Dictionary) { + guard let dA = dA else { + return print("dA is nil") + } + for k in dA.keys { + if dA[k] != dB[k] { + print(k, dA[k] ?? "nil", dB[k] ?? "nil") + } + } + } + + func check(_ urlString: String, _ res: Dictionary) -> Bool { + let url = jsLikeURL(urlString) + if (url != res) { + print(urlString) + diffPrint(url, res) + return false + } + return true + } + + /** javascript get res + url = new URL("http://user:password@host:80/path?q=1#id") + res = {} + for(let k of ["hash","host","hostname","href","origin","password","pathname","port","protocol","search","username"]){ + res[k] = url[k]; + } + JSON.stringify(res) + */ + + XCTAssert(check("http://user:password@host:80/path?q=1#id", [ + "hash": "#id", + "host": "host", + "hostname": "host", +// "href": "http://user:password@host/path?q=1#id", + "origin": "http://host", + "password": "password", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "?q=1", + "username": "user" + ])) + + XCTAssert(check("http://host.test:8080/path?#id", [ + "hash": "#id", + "host": "host.test:8080", + "hostname": "host.test", +// "href": "http://host.test:8080/path?#id", + "origin": "http://host.test:8080", + "password": "", + "pathname": "/path", + "port": "8080", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test:8080/path?", [ + "hash": "", + "host": "host.test:8080", + "hostname": "host.test", +// "href": "http://host.test:8080/path?", + "origin": "http://host.test:8080", + "password": "", + "pathname": "/path", + "port": "8080", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/path#id", [ + "hash": "#id", + "host": "host.test", + "hostname": "host.test", +// "href": "http://host.test/path#id", + "origin": "http://host.test", + "password": "", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/path", [ + "hash": "", + "host": "host.test", + "hostname": "host.test", +// "href": "http://host.test/path", + "origin": "http://host.test", + "password": "", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/", [ + "hash": "", + "host": "host.test", + "hostname": "host.test", +// "href": "http://host.test/", + "origin": "http://host.test", + "password": "", + "pathname": "/", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ + "hash": "#%E4%B8%AD%E6%96%87", + "host": "xn--fiq.xn--7dv:80", + "hostname": "xn--fiq.xn--7dv", +// "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@xn--fiq.xn--7dv:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", + "origin": "https://xn--fiq.xn--7dv:80", + "password": "%E5%AF%86%E7%A0%81", + "pathname": "/path/%E4%B8%AD%E6%96%87/", + "port": "80", + "protocol": "https:", + "search": "?a=%E4%B8%AD&b=%E6%96%87&c", + "username": "%E7%94%A8%E6%88%B7%E5%90%8D" + ])) + + } + +} diff --git a/xcode/Tests-Mac/UserscriptsTests.swift b/xcode/Tests-Mac/UserscriptsTests.swift index 12c91bd0..c8911bb1 100644 --- a/xcode/Tests-Mac/UserscriptsTests.swift +++ b/xcode/Tests-Mac/UserscriptsTests.swift @@ -60,6 +60,7 @@ class UserscriptsTests: XCTestCase { "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", "https://cdn.frankerfacez.com/static/ffz_injector.user.js", "http://www.k21p.com/example.user.js", // add http protocol + "https://example.test/%%%test.user.js", // removingPercentEncoding -> nil "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js" ] var result = [String]() @@ -76,17 +77,17 @@ class UserscriptsTests: XCTestCase { let urls = [ "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", + "https://update.greasyfork.org/scripts/460897/1277476/gbCookies.js#sha256-Sv+EuBerch8z/6LvAU0m/ufvjmqB1Q/kbQrX7zAvOPk=", "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", "https://cdn.frankerfacez.com/static/ffz_injector.user.js", "http://www.k21p.com/example.user.js" // add http protocol ] - var result = [String]() for url in urls { - if let contents = getRemoteFileContents(url) { - result.append(contents) + if getRemoteFileContents(url) == nil { + print(#function, url) + XCTAssert(false) } } - XCTAssert(result.count == urls.count) } func testFileRemoteUpdate() throws { @@ -118,8 +119,6 @@ class UserscriptsTests: XCTestCase { } func testMatching() throws { - var count = 0 - var result = [String]() let patternDict = [ "*://*/*": [ "https://www.bing.com/", @@ -174,22 +173,21 @@ class UserscriptsTests: XCTestCase { ] ] for (pattern, urls) in patternDict { - count = count + urls.count for url in urls { - if match(url, pattern) { - result.append("1") + if !match(url, pattern) { + print(#function, "patternDict", url, pattern) + XCTAssert(false) } } } for (pattern, urls) in patternDictFails { - // don't increment count since these tests should fail for url in urls { if match(url, pattern) { - result.removeLast() + print(#function, "patternDictFails", url, pattern) + XCTAssert(false) } } } - XCTAssert(result.count == count) } func testPerformanceExample() throws { diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 5b6feb25..5ddbcdad 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 030C64F52AC62CC900548FBD /* Settings.bundle */; }; 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030C64FB2AD0B24C00548FBD /* Initialization.swift */; }; + 030FCAD42B182008004D13CD /* UrlCodecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD32B182008004D13CD /* UrlCodecTests.swift */; }; + 030FCADC2B189AAB004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; + 030FCADD2B18BF70004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; + 030FCADF2B18BF72004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; 039F59532AD9591C002E9977 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 039F59522AD9591C002E9977 /* dist */; }; @@ -102,6 +106,8 @@ /* Begin PBXFileReference section */ 030C64F52AC62CC900548FBD /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 030C64FB2AD0B24C00548FBD /* Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; + 030FCAD32B182008004D13CD /* UrlCodecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlCodecTests.swift; sourceTree = ""; }; + 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlPolyfill.swift; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; 039F59522AD9591C002E9977 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; @@ -208,6 +214,7 @@ isa = PBXGroup; children = ( 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */, + 030FCAD32B182008004D13CD /* UrlCodecTests.swift */, ); path = "Tests-Mac"; sourceTree = ""; @@ -313,6 +320,7 @@ children = ( 78B25582299D4C27000B2E9B /* Utilities.swift */, 03C24F962ABD2CBB00F130F9 /* Preferences.swift */, + 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */, ); path = Shared; sourceTree = ""; @@ -595,6 +603,8 @@ buildActionMask = 2147483647; files = ( 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADC2B189AAB004D13CD /* UrlPolyfill.swift in Sources */, + 030FCAD42B182008004D13CD /* UrlCodecTests.swift in Sources */, 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */, 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 78B25587299D4C27000B2E9B /* Utilities.swift in Sources */, @@ -622,6 +632,7 @@ files = ( 78B25586299D4C27000B2E9B /* Utilities.swift in Sources */, 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADD2B18BF70004D13CD /* UrlPolyfill.swift in Sources */, 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */, ); @@ -645,6 +656,7 @@ files = ( 78B25584299D4C27000B2E9B /* Utilities.swift in Sources */, 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADF2B18BF72004D13CD /* UrlPolyfill.swift in Sources */, 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4AED6492268CDB58001794BF /* Functions.swift in Sources */, ); From d4e74e3c1fa01d2d18ec094859395cfdd41a2795 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 1 Dec 2023 12:31:30 +0800 Subject: [PATCH 2/3] refactor(dev): improve xcode unit tests --- xcode/Shared/UrlPolyfill.swift | 4 +-- xcode/Tests-Mac/UrlCodecTests.swift | 40 ++++++++++++++++++-------- xcode/Tests-Mac/UserscriptsTests.swift | 4 ++- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/xcode/Shared/UrlPolyfill.swift b/xcode/Shared/UrlPolyfill.swift index f0110d67..96cebed5 100644 --- a/xcode/Shared/UrlPolyfill.swift +++ b/xcode/Shared/UrlPolyfill.swift @@ -65,7 +65,7 @@ func jsLikeURL(_ urlString: String, baseString: String? = nil) -> [String: Strin "hash": fragment == "" ? "" : "#\(fragment)", "host": host, "hostname": hostname, -// "href": url.absoluteString, + // "href": url.absoluteString, "origin": "\(scheme)://\(host)", "password": url.password(percentEncoded: true) ?? "", "pathname": url.path(percentEncoded: true), @@ -83,7 +83,7 @@ func jsLikeURL(_ urlString: String, baseString: String? = nil) -> [String: Strin "hash": fragment == "" ? "" : "#\(fragment)", "host": host, "hostname": hostname, -// "href": url.absoluteString, + // "href": url.absoluteString, "origin": "\(scheme)://\(host)", "password": url.password ?? "", "pathname": url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path, diff --git a/xcode/Tests-Mac/UrlCodecTests.swift b/xcode/Tests-Mac/UrlCodecTests.swift index 52a08bb3..a11ac25a 100644 --- a/xcode/Tests-Mac/UrlCodecTests.swift +++ b/xcode/Tests-Mac/UrlCodecTests.swift @@ -83,7 +83,7 @@ final class UrlCodecTests: XCTestCase { "hash": "#id", "host": "host", "hostname": "host", -// "href": "http://user:password@host/path?q=1#id", + // "href": "http://user:password@host/path?q=1#id", "origin": "http://host", "password": "password", "pathname": "/path", @@ -97,7 +97,7 @@ final class UrlCodecTests: XCTestCase { "hash": "#id", "host": "host.test:8080", "hostname": "host.test", -// "href": "http://host.test:8080/path?#id", + // "href": "http://host.test:8080/path?#id", "origin": "http://host.test:8080", "password": "", "pathname": "/path", @@ -111,7 +111,7 @@ final class UrlCodecTests: XCTestCase { "hash": "", "host": "host.test:8080", "hostname": "host.test", -// "href": "http://host.test:8080/path?", + // "href": "http://host.test:8080/path?", "origin": "http://host.test:8080", "password": "", "pathname": "/path", @@ -125,7 +125,7 @@ final class UrlCodecTests: XCTestCase { "hash": "#id", "host": "host.test", "hostname": "host.test", -// "href": "http://host.test/path#id", + // "href": "http://host.test/path#id", "origin": "http://host.test", "password": "", "pathname": "/path", @@ -139,7 +139,7 @@ final class UrlCodecTests: XCTestCase { "hash": "", "host": "host.test", "hostname": "host.test", -// "href": "http://host.test/path", + // "href": "http://host.test/path", "origin": "http://host.test", "password": "", "pathname": "/path", @@ -153,7 +153,7 @@ final class UrlCodecTests: XCTestCase { "hash": "", "host": "host.test", "hostname": "host.test", -// "href": "http://host.test/", + // "href": "http://host.test/", "origin": "http://host.test", "password": "", "pathname": "/", @@ -163,12 +163,12 @@ final class UrlCodecTests: XCTestCase { "username": "" ])) - XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ + XCTAssert(check("https://用户名:密码@host.test:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ "hash": "#%E4%B8%AD%E6%96%87", - "host": "xn--fiq.xn--7dv:80", - "hostname": "xn--fiq.xn--7dv", -// "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@xn--fiq.xn--7dv:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", - "origin": "https://xn--fiq.xn--7dv:80", + "host": "host.test:80", + "hostname": "host.test", + // "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@host.test:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", + "origin": "https://host.test:80", "password": "%E5%AF%86%E7%A0%81", "pathname": "/path/%E4%B8%AD%E6%96%87/", "port": "80", @@ -176,7 +176,23 @@ final class UrlCodecTests: XCTestCase { "search": "?a=%E4%B8%AD&b=%E6%96%87&c", "username": "%E7%94%A8%E6%88%B7%E5%90%8D" ])) + + if #available(macOS 14.0, iOS 17.0, *) { + XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ + "hash": "#%E4%B8%AD%E6%96%87", + "host": "xn--fiq.xn--7dv:80", + "hostname": "xn--fiq.xn--7dv", + // "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@xn--fiq.xn--7dv:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", + "origin": "https://xn--fiq.xn--7dv:80", + "password": "%E5%AF%86%E7%A0%81", + "pathname": "/path/%E4%B8%AD%E6%96%87/", + "port": "80", + "protocol": "https:", + "search": "?a=%E4%B8%AD&b=%E6%96%87&c", + "username": "%E7%94%A8%E6%88%B7%E5%90%8D" + ])) + } - } + } // testJsLikeURL() -> END } diff --git a/xcode/Tests-Mac/UserscriptsTests.swift b/xcode/Tests-Mac/UserscriptsTests.swift index c8911bb1..bdb07d38 100644 --- a/xcode/Tests-Mac/UserscriptsTests.swift +++ b/xcode/Tests-Mac/UserscriptsTests.swift @@ -80,6 +80,7 @@ class UserscriptsTests: XCTestCase { "https://update.greasyfork.org/scripts/460897/1277476/gbCookies.js#sha256-Sv+EuBerch8z/6LvAU0m/ufvjmqB1Q/kbQrX7zAvOPk=", "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", "https://cdn.frankerfacez.com/static/ffz_injector.user.js", + "https://☁️.com/", // punycode domian "http://www.k21p.com/example.user.js" // add http protocol ] for url in urls { @@ -123,7 +124,8 @@ class UserscriptsTests: XCTestCase { "*://*/*": [ "https://www.bing.com/", "https://example.org/foo/bar.html", - "https://a.org/some/path/" + "https://a.org/some/path/", + "https://☁️.com/" ], "*://*.mozilla.org/*": [ "http://mozilla.org/", From a992ef3222f976f92b9b3a075ebc20f90527117f Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 2 Dec 2023 04:10:52 +0800 Subject: [PATCH 3/3] refactor(dev): improve tests and error logging --- xcode/Ext-Safari/Functions.swift | 6 ++++++ xcode/Tests-Mac/UserscriptsTests.swift | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 738fe244..03d3397a 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -887,14 +887,20 @@ func getRemoteFileContents(_ url: String) -> String? { if let r = response as? HTTPURLResponse, data != nil, error == nil { if r.statusCode == 200 { contents = String(data: data!, encoding: .utf8) ?? "" + } else { + logger?.error("\(#function, privacy: .public) - http statusCode (\(r.statusCode, privacy: .public)): \(url, privacy: .public)") } } + if let error = error { + logger?.error("\(#function, privacy: .public) - task error: \(error.localizedDescription, privacy: .public) (\(url, privacy: .public))") + } semaphore.signal() } task?.resume() // wait 30 seconds before timing out if semaphore.wait(timeout: .now() + 30) == .timedOut { task?.cancel() + logger?.error("\(#function, privacy: .public) - 30 seconds timeout: \(url, privacy: .public)") } // if made it to this point and contents still an empty string, something went wrong with the request diff --git a/xcode/Tests-Mac/UserscriptsTests.swift b/xcode/Tests-Mac/UserscriptsTests.swift index bdb07d38..24d70009 100644 --- a/xcode/Tests-Mac/UserscriptsTests.swift +++ b/xcode/Tests-Mac/UserscriptsTests.swift @@ -74,15 +74,19 @@ class UserscriptsTests: XCTestCase { } func testGetRemoteFileContents() throws { - let urls = [ + var urls:[String] = [ "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", "https://update.greasyfork.org/scripts/460897/1277476/gbCookies.js#sha256-Sv+EuBerch8z/6LvAU0m/ufvjmqB1Q/kbQrX7zAvOPk=", "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", "https://cdn.frankerfacez.com/static/ffz_injector.user.js", - "https://☁️.com/", // punycode domian "http://www.k21p.com/example.user.js" // add http protocol ] + if #available(macOS 14.0, iOS 17.0, *) { + urls += [ + "https://☁️.com/", // IDN / Non-Punycode-encoded domain name + ] + } for url in urls { if getRemoteFileContents(url) == nil { print(#function, url)