From 91b3110cd52e728657af99d60da95a49aaf5b472 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 24 Apr 2025 16:21:16 -0700 Subject: [PATCH] Delete no-longer-needed Unicode support for URLs. --- Mac/Browser.swift | 2 +- .../WebFeedInspectorViewController.swift | 4 +- .../AddFeed/AddWebFeedWindowController.swift | 2 +- ...idebarViewController+ContextualMenus.swift | 6 +- .../LocalAccount/LocalAccountRefresher.swift | 2 +- Modules/RSWeb/Package.swift | 14 +- .../RSWeb/HTMLMetadataDownloader.swift | 2 +- .../Sources/RSWeb/UTS46/Data+Extensions.swift | 21 - .../RSWeb/UTS46/Scanner+Extensions.swift | 54 -- .../Sources/RSWeb/UTS46/String+Punycode.swift | 596 ------------------ .../Sources/RSWeb/UTS46/UTS46+Loading.swift | 227 ------- Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift | 189 ------ Modules/RSWeb/Sources/RSWeb/UTS46/uts46 | Bin 15368 -> 0 bytes iOS/Add/AddFeedViewController.swift | 2 +- .../WebFeedInspectorViewController.swift | 4 +- 15 files changed, 17 insertions(+), 1108 deletions(-) delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift delete mode 100644 Modules/RSWeb/Sources/RSWeb/UTS46/uts46 diff --git a/Mac/Browser.swift b/Mac/Browser.swift index d574bc7ad..e2558667a 100644 --- a/Mac/Browser.swift +++ b/Mac/Browser.swift @@ -43,7 +43,7 @@ struct Browser { /// - Note: Some browsers (specifically Chromium-derived ones) will ignore the request /// to open in the background. static func open(_ urlString: String, inBackground: Bool) { - guard let url = URL(unicodeString: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } + guard let url = URL(string: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } let configuration = NSWorkspace.OpenConfiguration() configuration.requiresUniversalLinks = true diff --git a/Mac/Inspector/WebFeedInspectorViewController.swift b/Mac/Inspector/WebFeedInspectorViewController.swift index 39d82a9dc..27ef8ab2c 100644 --- a/Mac/Inspector/WebFeedInspectorViewController.swift +++ b/Mac/Inspector/WebFeedInspectorViewController.swift @@ -162,11 +162,11 @@ private extension WebFeedInspectorViewController { } func updateHomePageURL() { - homePageURLTextField?.stringValue = feed?.homePageURL?.decodedURLString ?? "" + homePageURLTextField?.stringValue = feed?.homePageURL ?? "" } func updateFeedURL() { - urlTextField?.stringValue = feed?.url.decodedURLString ?? "" + urlTextField?.stringValue = feed?.url ?? "" } func updateNotifyAboutNewArticles() { diff --git a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift index 16c8a4d72..207a05a60 100644 --- a/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift +++ b/Mac/MainWindow/AddFeed/AddWebFeedWindowController.swift @@ -91,7 +91,7 @@ class AddWebFeedWindowController : NSWindowController, AddFeedWindowController { cancelSheet() return; } - guard let url = URL(unicodeString: normalizedURLString) else { + guard let url = URL(string: normalizedURLString) else { cancelSheet() return } diff --git a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift index 3d9d4256b..3944c1bfd 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController+ContextualMenus.swift @@ -216,16 +216,16 @@ private extension SidebarViewController { } if let homePageURL = webFeed.homePageURL, let _ = URL(string: homePageURL) { - let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL) + let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL) menu.addItem(item) menu.addItem(NSMenuItem.separator()) } - let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url.decodedURLString ?? webFeed.url) + let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), webFeed.url) menu.addItem(copyFeedURLItem) if let homePageURL = webFeed.homePageURL { - let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL.decodedURLString ?? homePageURL) + let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL) menu.addItem(item) } menu.addItem(NSMenuItem.separator()) diff --git a/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift b/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift index 42a942b37..220192583 100644 --- a/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift +++ b/Modules/Account/Sources/Account/LocalAccount/LocalAccountRefresher.swift @@ -216,7 +216,7 @@ private extension LocalAccountRefresher { if let url = urlCache[urlString] { return url } - if let url = URL(unicodeString: urlString) { + if let url = URL(string: urlString) { urlCache[urlString] = url return url } diff --git a/Modules/RSWeb/Package.swift b/Modules/RSWeb/Package.swift index b06f12331..71a511d0e 100644 --- a/Modules/RSWeb/Package.swift +++ b/Modules/RSWeb/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library( name: "RSWeb", type: .dynamic, - targets: ["RSWeb"]), + targets: ["RSWeb"]) ], dependencies: [ .package(path: "../RSParser"), @@ -20,15 +20,11 @@ let package = Package( dependencies: [ "RSParser", "RSCore" - ], - resources: [.copy("UTS46/uts46")], - swiftSettings: [.define("SWIFT_PACKAGE")]), + ] + //swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] + ), .testTarget( name: "RSWebTests", - dependencies: [ - "RSWeb", - "RSParser", - "RSCore" - ]), + dependencies: ["RSWeb"]) ] ) diff --git a/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift b/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift index c59f0b7a9..c59489aeb 100644 --- a/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift +++ b/Modules/RSWeb/Sources/RSWeb/HTMLMetadataDownloader.swift @@ -76,7 +76,7 @@ private extension HTMLMetadataDownloader { func downloadMetadata(_ url: String) { - guard let actualURL = URL(unicodeString: url) else { + guard let actualURL = URL(string: url) else { if Self.debugLoggingEnabled { Self.logger.debug("HTMLMetadataDownloader skipping download for \(url) because it couldn’t construct a URL.") } diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift deleted file mode 100644 index 140b6498b..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/Data+Extensions.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Data+Extensions.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-04-12. -// - -import Foundation -import zlib - -extension Data { - - var crc32: UInt32 { - return self.withUnsafeBytes { - let buffer = $0.bindMemory(to: UInt8.self) - let initial = zlib.crc32(0, nil, 0) - return UInt32(zlib.crc32(initial, buffer.baseAddress, numericCast(buffer.count))) - } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift deleted file mode 100644 index 0ffb0c425..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/Scanner+Extensions.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Scanner+Extensions.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-04-20. -// - -import Foundation - -// Wrapper functions for < 10.15 compatibility -// TODO: Remove when support for < 10.15 is dropped. -extension Scanner { - - func shimScanUpToCharacters(from set: CharacterSet) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanUpToCharacters(from: set) - } else { - var str: NSString? - self.scanUpToCharacters(from: set, into: &str) - return str as String? - } - } - - func shimScanCharacters(from set: CharacterSet) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanCharacters(from: set) - } else { - var str: NSString? - self.scanCharacters(from: set, into: &str) - return str as String? - } - } - - func shimScanUpToString(_ substring: String) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanUpToString(substring) - } else { - var str: NSString? - self.scanUpTo(substring, into: &str) - return str as String? - } - } - - func shimScanString(_ searchString: String) -> String? { - if #available(macOS 10.15, iOS 13.0, *) { - return self.scanString(searchString) - } else { - var str: NSString? - self.scanString(searchString, into: &str) - return str as String? - } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift deleted file mode 100644 index a6afd15b0..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/String+Punycode.swift +++ /dev/null @@ -1,596 +0,0 @@ -// -// String+Punycode.swift -// Punycode -// -// Created by Nate Weaver on 2020-03-16. -// - -import Foundation - -public extension String { - - /// The IDNA-encoded representation of a Unicode domain. - /// - /// This will properly split domains on periods; e.g., - /// "www.bücher.ch" becomes "www.xn--bcher-kva.ch". - var idnaEncoded: String? { - guard let mapped = try? self.mapUTS46() else { return nil } - - let nonASCII = CharacterSet(charactersIn: UnicodeScalar(0)...UnicodeScalar(127)).inverted - var result = "" - - let s = Scanner(string: mapped.precomposedStringWithCanonicalMapping) - let dotAt = CharacterSet(charactersIn: ".@") - - while !s.isAtEnd { - if let input = s.shimScanUpToCharacters(from: dotAt) { - if !input.isValidLabel { return nil } - - if input.rangeOfCharacter(from: nonASCII) != nil { - result.append("xn--") - - if let encoded = input.punycodeEncoded { - result.append(encoded) - } - } else { - result.append(input) - } - } - - if let input = s.shimScanCharacters(from: dotAt) { - result.append(input) - } - } - - return result - } - - /// The Unicode representation of an IDNA-encoded domain. - /// - /// This will properly split domains on periods; e.g., - /// "www.xn--bcher-kva.ch" becomes "www.bücher.ch". - var idnaDecoded: String? { - var result = "" - let s = Scanner(string: self) - let dotAt = CharacterSet(charactersIn: ".@") - - while !s.isAtEnd { - if let input = s.shimScanUpToCharacters(from: dotAt) { - if input.lowercased().hasPrefix("xn--") { - let start = input.index(input.startIndex, offsetBy: 4) - guard let substr = input[start...].punycodeDecoded else { return nil } - guard substr.isValidLabel else { return nil } - result.append(substr) - } else { - result.append(input) - } - } - - if let input = s.shimScanCharacters(from: dotAt) { - result.append(input) - } - } - - return result - } - - /// The IDNA- and percent-encoded representation of a URL string. - var encodedURLString: String? { - let urlParts = self.urlParts - var pathAndQuery = urlParts.pathAndQuery - - var allowedCharacters = CharacterSet.urlPathAllowed - allowedCharacters.insert(charactersIn: "%?") - pathAndQuery = pathAndQuery.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? "" - - var result = "\(urlParts.scheme)\(urlParts.delim)" - - if let username = urlParts.username?.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) { - if let password = urlParts.password?.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) { - result.append("\(username):\(password)@") - } else { - result.append("\(username)@") - } - } - - guard let host = urlParts.host.idnaEncoded else { return nil } - - result.append("\(host)\(pathAndQuery)") - - if var fragment = urlParts.fragment { - var fragmentAlloweCharacters = CharacterSet.urlFragmentAllowed - fragmentAlloweCharacters.insert(charactersIn: "%") - fragment = fragment.addingPercentEncoding(withAllowedCharacters: fragmentAlloweCharacters) ?? "" - - result.append("#\(fragment)") - } - - return result - } - - /// The Unicode representation of an IDNA- and percent-encoded URL string. - var decodedURLString: String? { - let urlParts = self.urlParts - var usernamePassword = "" - - if let username = urlParts.username?.removingPercentEncoding { - if let password = urlParts.password?.removingPercentEncoding { - usernamePassword = "\(username):\(password)@" - } else { - usernamePassword = "\(username)@" - } - } - - guard let host = urlParts.host.idnaDecoded else { return nil } - - var result = "\(urlParts.scheme)\(urlParts.delim)\(usernamePassword)\(host)\(urlParts.pathAndQuery.removingPercentEncoding ?? "")" - - if let fragment = urlParts.fragment?.removingPercentEncoding { - result.append("#\(fragment)") - } - - return result - } - -} - -public extension URL { - - /// Initializes a URL with a Unicode URL string. - /// - /// If `unicodeString` can be successfully encoded, equivalent to - /// - /// ``` - /// URL(string: unicodeString.encodedURLString!) - /// ``` - /// - /// - Parameter unicodeString: The unicode URL string with which to create a URL. - init?(unicodeString: String) { - if let url = URL(string: unicodeString) { - self = url - return - } - - guard let encodedString = unicodeString.encodedURLString else { return nil } - self.init(string: encodedString) - } - - /// The IDNA- and percent-decoded representation of the URL. - /// - /// Equivalent to - /// - /// ``` - /// self.absoluteString.decodedURLString - /// ``` - var decodedURLString: String? { - return self.absoluteString.decodedURLString - } - - /// Initializes a URL from a relative Unicode string and a base URL. - /// - Parameters: - /// - unicodeString: The URL string with which to initialize the NSURL object. `unicodeString` is interpreted relative to `baseURL`. - /// - url: The base URL for the URL object - init?(unicodeString: String, relativeTo url: URL?) { - if let url = URL(string: unicodeString, relativeTo: url) { - self = url - return - } - - let parts = unicodeString.urlParts - - if !parts.host.isEmpty { - guard let encodedString = unicodeString.encodedURLString else { return nil } - self.init(string: encodedString, relativeTo: url) - } else { - var allowedCharacters = CharacterSet.urlPathAllowed - allowedCharacters.insert(charactersIn: "%?#") - guard let encoded = unicodeString.addingPercentEncoding(withAllowedCharacters: allowedCharacters) else { return nil } - self.init(string: encoded, relativeTo: url) - } - } - -} - -private extension StringProtocol { - - /// Punycode-encodes a string. - /// - /// Returns `nil` on error. - /// - Todo: Throw errors on failure instead of returning `nil`. - var punycodeEncoded: String? { - var result = "" - let scalars = self.unicodeScalars - let inputLength = scalars.count - - var n = Punycode.initialN - var delta: UInt32 = 0 - var outLen: UInt32 = 0 - var bias = Punycode.initialBias - - for scalar in scalars where scalar.isASCII { - result.unicodeScalars.append(scalar) - outLen += 1 - } - - let b: UInt32 = outLen - var h: UInt32 = outLen - - if b > 0 { - result.append(Punycode.delimiter) - } - - // Main encoding loop: - - while h < inputLength { - var m = UInt32.max - - for c in scalars { - if c.value >= n && c.value < m { - m = c.value - } - } - - if m - n > (UInt32.max - delta) / (h + 1) { - return nil // overflow - } - - delta += (m - n) * (h + 1) - n = m - - for c in scalars { - - if c.value < n { - delta += 1 - - if delta == 0 { - return nil // overflow - } - } - - if c.value == n { - var q = delta - var k = Punycode.base - - while true { - let t = k <= bias ? Punycode.tmin : - k >= bias + Punycode.tmax ? Punycode.tmax : k - bias - - if q < t { - break - } - - let encodedDigit = Punycode.encodeDigit(t + (q - t) % (Punycode.base - t), flag: false) - - result.unicodeScalars.append(UnicodeScalar(encodedDigit)!) - q = (q - t) / (Punycode.base - t) - - k += Punycode.base - } - - result.unicodeScalars.append(UnicodeScalar(Punycode.encodeDigit(q, flag: false))!) - bias = Punycode.adapt(delta: delta, numPoints: h + 1, firstTime: h == b) - delta = 0 - h += 1 - } - } - - delta += 1 - n += 1 - } - - return result - } - - /// Punycode-decodes a string. - /// - /// Returns `nil` on error. - /// - Todo: Throw errors on failure instead of returning `nil`. - var punycodeDecoded: String? { - var result = "" - let scalars = self.unicodeScalars - - let endIndex = scalars.endIndex - var n = Punycode.initialN - var outLen: UInt32 = 0 - var i: UInt32 = 0 - var bias = Punycode.initialBias - - var b = scalars.startIndex - - for j in scalars.indices { - if Character(self.unicodeScalars[j]) == Punycode.delimiter { - b = j - break - } - } - - for j in scalars.indices { - if j >= b { - break - } - - let scalar = scalars[j] - - if !scalar.isASCII { - return nil // bad input - } - - result.unicodeScalars.append(scalar) - outLen += 1 - - } - - var inPos = b > scalars.startIndex ? scalars.index(after: b) : scalars.startIndex - - while inPos < endIndex { - - var k = Punycode.base - var w: UInt32 = 1 - let oldi = i - - while true { - if inPos >= endIndex { - return nil // bad input - } - - let digit = Punycode.decodeDigit(scalars[inPos].value) - - inPos = scalars.index(after: inPos) - - if digit >= Punycode.base { return nil } // bad input - if digit > (UInt32.max - i) / w { return nil } // overflow - - i += digit * w - let t = k <= bias ? Punycode.tmin : - k >= bias + Punycode.tmax ? Punycode.tmax : k - bias - - if digit < t { - break - } - - if w > UInt32.max / (Punycode.base - t) { return nil } // overflow - - w *= Punycode.base - t - - k += Punycode.base - } - - bias = Punycode.adapt(delta: i - oldi, numPoints: outLen + 1, firstTime: oldi == 0) - - if i / (outLen + 1) > UInt32.max - n { return nil } // overflow - - n += i / (outLen + 1) - i %= outLen + 1 - - let index = result.unicodeScalars.index(result.unicodeScalars.startIndex, offsetBy: Int(i)) - result.unicodeScalars.insert(UnicodeScalar(n)!, at: index) - - outLen += 1 - i += 1 - } - - return result - } - -} - -private extension String { - - var urlParts: URLParts { - let colonSlash = CharacterSet(charactersIn: ":/") - let slashQuestion = CharacterSet(charactersIn: "/?") - let s = Scanner(string: self) - var scheme = "" - var delim = "" - var host = "" - var path = "" - var username: String? - var password: String? - var fragment: String? - - if let hostOrScheme = s.shimScanUpToCharacters(from: colonSlash) { - let maybeDelim = s.shimScanCharacters(from: colonSlash) ?? "" - - if maybeDelim.hasPrefix(":") { - delim = maybeDelim - scheme = hostOrScheme - host = s.shimScanUpToCharacters(from: slashQuestion) ?? "" - } else { - path.append(hostOrScheme) - path.append(maybeDelim) - } - } else if let maybeDelim = s.shimScanString("//") { - delim = maybeDelim - - if let maybeHost = s.shimScanUpToCharacters(from: slashQuestion) { - host = maybeHost - } - } - - path.append(s.shimScanUpToString("#") ?? "") - - if s.shimScanString("#") != nil { - fragment = s.shimScanUpToCharacters(from: .newlines) ?? "" - } - - let usernamePasswordHostPort = host.components(separatedBy: "@") - - switch usernamePasswordHostPort.count { - case 1: - host = usernamePasswordHostPort[0] - case 0: - break // error - default: - let usernamePassword = usernamePasswordHostPort[0].components(separatedBy: ":") - username = usernamePassword[0] - password = usernamePassword.count > 1 ? usernamePassword[1] : nil - host = usernamePasswordHostPort[1] - } - - return URLParts(scheme: scheme, delim: delim, host: host, pathAndQuery: path, username: username, password: password, fragment: fragment) - } - - enum UTS46MapError: Error { - /// A disallowed codepoint was found in the string. - case disallowedCodepoint(scalar: UnicodeScalar) - } - - /// Perform a single-pass mapping using UTS #46. - /// - /// - Returns: The mapped string. - /// - Throws: `UTS46Error`. - func mapUTS46() throws -> String { - try UTS46.loadIfNecessary() - - var result = "" - - for scalar in self.unicodeScalars { - if UTS46.disallowedCharacters.contains(scalar) { - throw UTS46MapError.disallowedCodepoint(scalar: scalar) - } - - if UTS46.ignoredCharacters.contains(scalar) { - continue - } - - if let mapped = UTS46.characterMap[scalar.value] { - result.append(mapped) - } else { - result.unicodeScalars.append(scalar) - } - } - - return result - } - - var isValidLabel: Bool { - guard self.precomposedStringWithCanonicalMapping.unicodeScalars.elementsEqual(self.unicodeScalars) else { return false } - - guard (try? self.mapUTS46()) != nil else { return false } - - if let category = self.unicodeScalars.first?.properties.generalCategory { - if category == .nonspacingMark || category == .spacingMark || category == .enclosingMark { return false } - } - - return self.hasValidJoiners - } - - /// Whether a string's joiners (if any) are valid according to IDNA 2008 ContextJ. - /// - /// See [RFC 5892, Appendix A.1 and A.2](https://tools.ietf.org/html/rfc5892#appendix-A). - var hasValidJoiners: Bool { - try! UTS46.loadIfNecessary() - - let scalars = self.unicodeScalars - - for index in scalars.indices { - let scalar = scalars[index] - - if scalar.value == 0x200C { // Zero-width non-joiner - if index == scalars.indices.first { return false } - - var subindex = scalars.index(before: index) - var previous = scalars[subindex] - - if previous.properties.canonicalCombiningClass == .virama { continue } - - while true { - guard let joiningType = UTS46.joiningTypes[previous.value] else { return false } - - if joiningType == .transparent { - if subindex == scalars.startIndex { - return false - } - - subindex = scalars.index(before: subindex) - previous = scalars[subindex] - } else if joiningType == .dual || joiningType == .left { - break - } else { - return false - } - } - - subindex = scalars.index(after: index) - var next = scalars[subindex] - - while true { - if subindex == scalars.endIndex { - return false - } - - guard let joiningType = UTS46.joiningTypes[next.value] else { return false } - - if joiningType == .transparent { - subindex = scalars.index(after: index) - next = scalars[subindex] - } else if joiningType == .right || joiningType == .dual { - break - } else { - return false - } - } - } else if scalar.value == 0x200D { // Zero-width joiner - if index == scalars.startIndex { return false } - - let subindex = scalars.index(before: index) - let previous = scalars[subindex] - - if previous.properties.canonicalCombiningClass != .virama { return false } - } - } - - return true - } - -} - -private enum Punycode { - static let base = UInt32(36) - static let tmin = UInt32(1) - static let tmax = UInt32(26) - static let skew = UInt32(38) - static let damp = UInt32(700) - static let initialBias = UInt32(72) - static let initialN = UInt32(0x80) - static let delimiter: Character = "-" - - static func decodeDigit(_ cp: UInt32) -> UInt32 { - return cp &- 48 < 10 ? cp &- 22 : cp &- 65 < 26 ? cp &- 65 : - cp &- 97 < 26 ? cp &- 97 : Self.base - } - - static func encodeDigit(_ d: UInt32, flag: Bool) -> UInt32 { - return d + 22 + 75 * UInt32(d < 26 ? 1 : 0) - ((flag ? 1 : 0) << 5) - } - - static let maxint = UInt32.max - - static func adapt(delta: UInt32, numPoints: UInt32, firstTime: Bool) -> UInt32 { - - var delta = delta - - delta = firstTime ? delta / Self.damp : delta >> 1 - delta += delta / numPoints - - var k: UInt32 = 0 - - while delta > ((Self.base - Self.tmin) * Self.tmax) / 2 { - delta /= Self.base - Self.tmin - k += Self.base - } - - return k + (Self.base - Self.tmin + 1) * delta / (delta + Self.skew) - } -} - -private struct URLParts { - var scheme: String - var delim: String - var host: String - var pathAndQuery: String - - var username: String? - var password: String? - var fragment: String? -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift deleted file mode 100644 index 3c6f98144..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46+Loading.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// UTS46+Loading.swift -// icumap2code -// -// Created by Nate Weaver on 2020-05-08. -// - -import Foundation -import Compression - -extension UTS46 { - - private static func parseHeader(from data: Data) throws -> Header? { - let headerData = data.prefix(8) - - guard headerData.count == 8 else { throw UTS46Error.badSize } - - return Header(rawValue: headerData) - } - - static func load(from url: URL) throws { - let fileData = try Data(contentsOf: url) - - guard let header = try? parseHeader(from: fileData) else { return } - - guard header.version == 1 else { throw UTS46Error.unknownVersion } - - let offset = header.dataOffset - - guard fileData.count > offset else { throw UTS46Error.badSize } - - let compressedData = fileData[offset...] - - guard let data = self.decompress(data: compressedData, algorithm: header.compression) else { - throw UTS46Error.decompressionError - } - - var index = 0 - - while index < data.count { - let marker = data[index] - - index += 1 - - switch marker { - case Marker.characterMap: - index = parseCharacterMap(from: data, start: index) - case Marker.ignoredCharacters: - index = parseIgnoredCharacters(from: data, start: index) - case Marker.disallowedCharacters: - index = parseDisallowedCharacters(from: data, start: index) - case Marker.joiningTypes: - index = parseJoiningTypes(from: data, start: index) - default: - throw UTS46Error.badMarker - } - } - - isLoaded = true - } - - static var bundle: Bundle { - #if SWIFT_PACKAGE - return Bundle.module - #else - return Bundle(for: Self.self) - #endif - } - - static func loadIfNecessary() throws { - guard !isLoaded else { return } - guard let url = Self.bundle.url(forResource: "uts46", withExtension: nil) else { throw CocoaError(.fileNoSuchFile) } - - try load(from: url) - } - - private static func decompress(data: Data, algorithm: CompressionAlgorithm?) -> Data? { - - guard let rawAlgorithm = algorithm?.rawAlgorithm else { return data } - - let capacity = 131_072 // 128 KB - let destinationBuffer = UnsafeMutablePointer.allocate(capacity: capacity) - - let decompressed = data.withUnsafeBytes { (rawBuffer) -> Data? in - let bound = rawBuffer.bindMemory(to: UInt8.self) - let decodedCount = compression_decode_buffer(destinationBuffer, capacity, bound.baseAddress!, rawBuffer.count, nil, rawAlgorithm) - - if decodedCount == 0 || decodedCount == capacity { - return nil - } - - return Data(bytes: destinationBuffer, count: decodedCount) - } - - return decompressed - } - - private static func parseCharacterMap(from data: Data, start: Int) -> Int { - characterMap.removeAll() - var index = start - - main: while index < data.count { - var accumulator = Data() - - while data[index] != Marker.sequenceTerminator { - if data[index] > Marker.min { break main } - - accumulator.append(data[index]) - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - // FIXME: throw an error here. - guard str.count > 0 else { continue } - - let codepoint = str.unicodeScalars.first!.value - - characterMap[codepoint] = String(str.unicodeScalars.dropFirst()) - - index += 1 - } - - return index - } - - private static func parseRanges(from: String) -> [ClosedRange]? { - guard from.unicodeScalars.count % 2 == 0 else { return nil } - - var ranges = [ClosedRange]() - var first: UnicodeScalar? - - for (index, scalar) in from.unicodeScalars.enumerated() { - if index % 2 == 0 { - first = scalar - } else if let first = first { - ranges.append(first...scalar) - } - } - - return ranges - } - - static func parseCharacterSet(from data: Data, start: Int) -> (index: Int, charset: CharacterSet?) { - var index = start - var accumulator = Data() - - while index < data.count, data[index] < Marker.min { - accumulator.append(data[index]) - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - guard let ranges = parseRanges(from: str) else { - return (index: index, charset: nil) - } - - var charset = CharacterSet() - - for range in ranges { - charset.insert(charactersIn: range) - } - - return (index: index, charset: charset) - } - - static func parseIgnoredCharacters(from data: Data, start: Int) -> Int { - let (index, charset) = parseCharacterSet(from: data, start: start) - - if let charset = charset { - ignoredCharacters = charset - } - - return index - } - - static func parseDisallowedCharacters(from data: Data, start: Int) -> Int { - let (index, charset) = parseCharacterSet(from: data, start: start) - - if let charset = charset { - disallowedCharacters = charset - } - - return index - } - - static func parseJoiningTypes(from data: Data, start: Int) -> Int { - var index = start - joiningTypes.removeAll() - - main: while index < data.count, data[index] < Marker.min { - var accumulator = Data() - - while index < data.count { - if data[index] > Marker.min { break main } - accumulator.append(data[index]) - - index += 1 - } - - let str = String(data: accumulator, encoding: .utf8)! - - var type: JoiningType? - var first: UnicodeScalar? - - for scalar in str.unicodeScalars { - if scalar.isASCII { - type = JoiningType(rawValue: Character(scalar)) - } else if let type = type { - if first == nil { - first = scalar - } else { - for value in first!.value...scalar.value { - joiningTypes[value] = type - } - - first = nil - } - } - } - } - - return index - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift b/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift deleted file mode 100644 index fa7a2faaa..000000000 --- a/Modules/RSWeb/Sources/RSWeb/UTS46/UTS46.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// UTS46.swift -// PunyCocoa Swift -// -// Created by Nate Weaver on 2020-03-29. -// - -import Foundation -import Compression - -/// UTS46 mapping. -/// -/// Storage file format. Codepoints are stored UTF-8-encoded. -/// -/// All multibyte integers are little-endian. -/// -/// Header: -/// -/// +--------------+---------+---------+---------+ -/// | 6 bytes | 1 byte | 1 byte | 4 bytes | -/// +--------------+---------+---------+---------+ -/// | magic number | version | flags | crc32 | -/// +--------------+---------+---------+---------+ -/// -/// - `magic number`: `"UTS#46"` (`0x55 0x54 0x53 0x23 0x34 0x36`). -/// - `version`: format version (1 byte; currently `0x01`). -/// - `flags`: Bitfield: -/// -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// | currently unused | crc | compression | -/// +-----+-----+-----+-----+-----+-----+-----+-----+ -/// -/// - `crc`: Contains a CRC32 of the data after the header. -/// - `compression`: compression mode of the data. -/// Currently identical to NSData's compression constants + 1: -/// -/// - 0: no compression -/// - 1: LZFSE -/// - 2: LZ4 -/// - 3: LZMA -/// - 4: ZLIB -/// -/// - `crc32`: CRC32 of the (possibly compressed) data. Implementations can skip -/// parsing this unless data integrity is an issue. -/// -/// The data section is a collection of data blocks of the format -/// -/// [marker][section data] ... -/// -/// Section data formats: -/// -/// If marker is `characterMap`: -/// -/// [codepoint][mapped-codepoint ...][null] ... -/// -/// If marker is `disallowedCharacters` or `ignoredCharacters`: -/// -/// [codepoint-range] ... -/// -/// If marker is `joiningTypes`: -/// -/// [type][[codepoint-range] ...] -/// -/// where `type` is one of `C`, `D`, `L`, `R`, or `T`. -/// -/// `codepoint-range`: two codepoints, marking the first and last codepoints of a -/// closed range. Single-codepoint ranges have the same start and end codepoint. -/// -class UTS46 { - - static var characterMap: [UInt32: String] = [:] - static var ignoredCharacters: CharacterSet = [] - static var disallowedCharacters: CharacterSet = [] - static var joiningTypes = [UInt32: JoiningType]() - - static var isLoaded = false - - enum Marker { - static let characterMap = UInt8.max - static let ignoredCharacters = UInt8.max - 1 - static let disallowedCharacters = UInt8.max - 2 - static let joiningTypes = UInt8.max - 3 - - static let min = UInt8.max - 10 // No valid UTF-8 byte can fall here. - - static let sequenceTerminator: UInt8 = 0 - } - - enum JoiningType: Character { - case causing = "C" - case dual = "D" - case right = "R" - case left = "L" - case transparent = "T" - } - - enum UTS46Error: Error { - case badSize - case compressionError - case decompressionError - case badMarker - case unknownVersion - } - - /// Identical values to `NSData.CompressionAlgorithm + 1`. - enum CompressionAlgorithm: UInt8 { - case none = 0 - case lzfse = 1 - case lz4 = 2 - case lzma = 3 - case zlib = 4 - - var rawAlgorithm: compression_algorithm? { - switch self { - case .lzfse: - return COMPRESSION_LZFSE - case .lz4: - return COMPRESSION_LZ4 - case .lzma: - return COMPRESSION_LZMA - case .zlib: - return COMPRESSION_ZLIB - default: - return nil - } - } - } - - struct Header: RawRepresentable, CustomDebugStringConvertible { - typealias RawValue = [UInt8] - - var rawValue: [UInt8] { - let value = Self.signature + [version, flags.rawValue] - assert(value.count == 8) - return value - } - - private static let compressionMask: UInt8 = 0x07 - private static let signature: [UInt8] = Array("UTS#46".utf8) - - private struct Flags: RawRepresentable { - var rawValue: UInt8 { - return (hasCRC ? hasCRCMask : 0) | compression.rawValue - } - - var hasCRC: Bool - var compression: CompressionAlgorithm - - private let hasCRCMask: UInt8 = 1 << 3 - private let compressionMask: UInt8 = 0x7 - - init(rawValue: UInt8) { - hasCRC = rawValue & hasCRCMask != 0 - let compressionBits = rawValue & compressionMask - - compression = CompressionAlgorithm(rawValue: compressionBits) ?? .none - } - - init(compression: CompressionAlgorithm = .none, hasCRC: Bool = false) { - self.compression = compression - self.hasCRC = hasCRC - } - } - - let version: UInt8 - private var flags: Flags - var hasCRC: Bool { flags.hasCRC } - var compression: CompressionAlgorithm { flags.compression } - var dataOffset: Int { 8 + (flags.hasCRC ? 4 : 0) } - - init?(rawValue: T) where T.Index == Int { - guard rawValue.count == 8 else { return nil } - guard rawValue.prefix(Self.signature.count).elementsEqual(Self.signature) else { return nil } - - version = rawValue[rawValue.index(rawValue.startIndex, offsetBy: 6)] - flags = Flags(rawValue: rawValue[rawValue.index(rawValue.startIndex, offsetBy: 7)]) - } - - init(compression: CompressionAlgorithm = .none, hasCRC: Bool = false) { - self.version = 1 - self.flags = Flags(compression: compression, hasCRC: hasCRC) - } - - var debugDescription: String { "has CRC: \(hasCRC); compression: \(String(describing: compression))" } - } - -} diff --git a/Modules/RSWeb/Sources/RSWeb/UTS46/uts46 b/Modules/RSWeb/Sources/RSWeb/UTS46/uts46 deleted file mode 100644 index 101001987a758fd1755c9ed24fab6afa8bb16611..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15368 zcmV+jJom#@R8u1~HUSHSO?8(2H+ooF0003064^lmz{q#rjsqb9761U6;nYLm$6Y(X zT>yWONFV|c6^uZZ*g7k?sQVczd2%FLD!6K)el9a3qJrPcUbCC4`;Q5*Dk}B%ZeR2m z5j86+a~`5L_O*>AhH?R8kYwU*NZYbkWakn9Bu@O zuS((~zh~ItT=$opbhe|y{|?E}?%j(P)@GblIbLoi7&K?C2V)tAJeIP(M~#hjN(~IN z23K(ox7*X&O354^2_OQ1iHV7DjzRlQoRr5sV`|0lIGuE-B9+=m_9 z8kcgOIqE6U(Uz|jhZ&@lw{fWNHr~rRRm%N7)AXDKq$u<+Gpz>|;LkyK5NC}FAE?$I z4xAY})&G)8OqL(x0GNpkrZtwc|v)qf`C^-xseS(uL@ z!F!4SHa=TKOdr_*QFU072sW^F`RE6ElPvjZlmwKK^h(N?<=C;$`|C6NXamKrb4%gu zA?eTF7pmqZnJWYNo_)?)=-i(XM2&?v+=y8qHK%jswT&dJ^u_@xiZ@ze_E>w78FP6B zcQChdxgQFrOi3N7u|29LZN)P?<-~#Bjq2ogtpmTd@cBO(wR|^bN{$X?mSZsVyUir; z&7eCq03+yibFVPWmDkh&J@b`l%iWN+{DH=|vl&&>+x4r;lMh_oWgrmGgQdFNlpciA zs5f6LkyMbzZ;84TL7MLcR*B%cVnYHkPmbZNw2gu2piqGt*A$0D;oh;(ZvVi|s-;Nc zlJ@C7f1D-oeNzCSP{2jOGAL!dfH}I4ipM8J*C`j?Vd`3wZ-79Bn4+mbH&Z4>Y}-mc z5B(hlaR~lDU?d1hyb%K|!x{M?lvQ4awFV92r*Dz{dW=}EclJ9K#_9^8eM`9+nun3a zDk3TcVu9^p0Uf&iKNjSPeT?4CNF$TRMjtO<{of* zHgvG7MxnUr5zY`zkbd5@t!TAmwx<&0qR_o!m+cpbbHR!Odnv=e%_v1F5A&frl{i^W zVy%lLdF^6+&Rb$GA8BOAKN;(Ji(#3gvw#A zyEz3f;~z^f!Wq&T3N`4cG6yV<8-Y-fS*R@knnxUJB0^=myUYe5ZQ|6fr)yuNZyfS* zYW)gm_CqY(jbc)jg9Lq2b-aX_b4$C9Mp6CyVsEVt9{U-Euk7%&`f8CL3&~sv`woBP1XmQ1r zf*0$-rcZ;gqd-t%$E74)+>%yB%8s5+F=a64Qy^l)#)$~1OIRThzLK;84}m|H<{cX7 zjt7kM`q)&c8kXi-Kwt~`5h!e8soo#H&t|vMBMnH2+K0x4sQU(e5~y}#m>ha2=`dC6 zz8Be^DtCn8%z(z&La}n~=)9m~+{<2e_4e3-%lzq>q15K-uX6kLo{5azE21pnXt!Tv zXeWww%4*6R+xE^n`Io zLy+e=V!g0z-7LZ$2ryIxazsLu#s9rpZePFyq;2>W!E`oh^nzrQHSBq@gt^-oisv$| ztP_+D_`2i?L+?PCi1W#j9b2j6MP(j4$?G6~cxDJqqcrqRyHY!;Fg`!IerQk4Os`Dx zqLo>iCzlCSu1hAp6Fu>Zv|n6l4m7=v3S)HhiAg<-N8t-^#=77vYw)5?bzWE`3Qqr7 z8fd)QJ_}?ex9)I?J_t<#zWJV!u`=jufua^)y(KH~LAd_PctiBNrkaXf(Z*CixNA!2 z9{h+M@(=DZVw}`9>SvE-Ksab06*Ml??(v^TU_vEdi>}kWS_#rs$p4C6kKtlRA0So< zg2f70n&Iakf~8%C_Ru;xmP(Yx8|K-^{1=ME7Fq`Ch`%}Q_qgVWj9C9>t@Vl6l#LS& z?%0AD-Z?g^_!qkicHSggtt)MZ_wq3HNpHG>NrTkZP|qDQeZq z!1)E%Hy|q40zNfjP4qM1#XdDEKa8H}E?_k^X?uCY70?$CjN9$Bv4c@M7s}suf)!8h zJc$Mu7yX8Ce5c;-_%Rcr6gWruL`%}AjRzm9gCq)*EZpnPJ*lm!eR&W!PRcWC%s#jc zjM)$Ds%!i7Ln0@3mk3LUZ45jCemkasGkeJB7JUfTL$FPaCMIFX zeD}+clr88d0p|u|eSieEypfAa_8o&!W)?ooBhAZ9{v9ep3sWTp27&wAh^OOMzsS~W z<_4n>_@N^peCEd9@a!IfT{qvEP9harqeRBf@tYPCBm=u-sWHfJ*GuVl<)?ssr6Zof z-}UxN%K=8kM`-vR5nRCkzeh(i`upvZV>Zn}wR)%(DT^g_x|*SLo#iz@rt2zjivQnb z(B>WD65W!{Flt>=Ev}4itEK}=L1*~Xr(H|yxTf0LdO67LYqMof`U4eTLwp0O@!YvH z@l5YFE=;!m+uECAOMS=TuJkk7hQvC{xP$y?;il9z)C=dP%c-a$%#Y&A|`4L zGM@jK|UjanQMd{Q5 zoUIGw^tKi0GOk|R!NIuDvBBebv+Q@;qFg6AMt%oUXx?qRSHo?C_d=zCZP>(JdHW;Y ze6y7&6EnO&g@)AYZn`A%S~VQgp0Z~8Hp2|we-m?-bN3Je}!O(u?sCUf9UD?7@r+ae**I8io3tDa{%vDFBPBG~F4F>}z*$np!U%miew z4|EQjaiqv=jsI}0DhLjKW}hJ)p}Q8muVcW(7SUF1zqz7??iMQq1?l{e7KD8dN=_{N z;>^O*mvGOr!4$r~a5s8FrfRUai_=pMj3twG@9i5TpIwW8ahubo_uw7Y2Pbm#3Un(9 zuLTDALRMB@bTO&6D_cqG^!+opUg?!_;;;?F?%{~G@j~?~mGnNx7rOuL&EuR)3H;__ zg5cFB&hHpK)=gWM>v{q=TYX_z58DKsrr6YP@Q6py3A{zRp((1sPD1?$OBC;kU)Hzv z!D6UotigNck`h;K9A34MmHjrQFj6Mes(k5t;UMv8DEUM%-dwZ-=w-=vtwo_m=2}#) z4`pCSaMu_X z>L8liqz^7Z=grCcI%JYX5fK)D7eF#F@(w*BBrKv^bY>%75{82fQ^{e3KEl-50=VR< zvBl0)pD{sT8U#JXK>34}GYHM)-GOdbZs4b2BpgRoX-0OlvmLMmlhs5FMCgIM*i!kw z+Fn$_&DaXefAkol5)yuR?M`GKqh#!i3x(aF4J4FE12lZo_^rk%le44;QW38LAMes6 zF~R4RPVk!7YFw&7eQ{diBFs!9UdOW2s?GAj&Ade_3^ca-w`GMfox8yANUXKz(Fk5b#4rr$W; znI^p8*1#(r{3wf<^=^+eOn*B?&70|zcc;y8_W zz*vnhSj2Orj~k7bBt&&2QVklr3n|hwtx~eV54_CZDo%#1qh3p?ocp(ttv&Bnu?G1I znz|(`=aAB3nouOyw(utn3&ZAN7a@hxVrgO-h2F#o1-jtSf=ty z#)f2AIV>z3`+`*%3|RuKO_PxZQH(?0N9Ve={*gq++s{<`k_oNdcLB7tV&e#=&~RiE zylQ-{3UG_S1+p4k@77kCOtsTWQU|ZKgIT~`f<9<(Pgm!xU+P2!Na zB);;B^4dqc{1NJ1Qw^giR>QV)la@Vg6IeN#r+NagJg8jR(z_FFkf2Fo64eSY+3l4ehx4<3Wu8J7s_}94wyHqD z#hnB;JU-|T<`htu&fJ04A~6V-mIW~AFGi6$WS3&Ne*Y*DdS)c7TzTcc1qmpr*nYWI zv~vxt7(p?w$|C3y{-p{r;uxiA=X5z6=v%GjQ&V~)EJ;IgvZc`St_@aLN2UmgkUrSy>VrcEV|f| zY^Mvc0Aece4nD40$A5XMaEb94WWX?E0N2JeIS56W<5K|Y9`vq3v14Q*{2*Rx>bL;q1<4 zguOl|4NGVHLVC_-K6cA2>pgcv14K-!649(KP}gTDRhy4Ay!|OSS_Cbu9?RPXAV$A+hVoe$5rjbNp@tP0zlC%=f~U89qy7=bh$;C7Xq#)91w(LVr7DmF z#fqunkA{Go}@e|&_8A@#oIpn&5uS$$k1oU*UZ50iRD(+$Y`mVRE`S zUc!?k6>;jt@RBUYji>K*)W~Avc|2&<{AxC7^{XHw(KRi6#U2-K#G$ya$%uGx{{1=C zx#}M?krO0Rh7{lPpqZ?-wFp5t%6Zkp`fe%Pls)6|;@PlJIUCAxv=J|UGHhb)Xfi77 zg<-G9T(7bku#Br0;}cHqJ{^T6`D@A8*t9yK{W2=FsEqcxkCt%L4Z&=ta~raw96F7+ z2~F?PpAu7T8Cg@_JUD$n=hC4OVH8$D7P>34a@WDTg+vr&0ffM?1kM>%D#l1jEimag z1geVCWtJ^8su()K7GR1%Kj0AWk%o$szk-*8v#daxlMWJr~UqDr% z>$)H!`ubpyq=z(;2}jkiYPvti`qYQ4hhImcHr^BN9(lLk34h`8yg;e`yWq_ z)QD%~^qGJ{ooy3Wk7piD<#w#hKxzE$TyxW7qJ_Z+>`SjmV)#bW?;<*B6p8u?U&%vN z;ysV=M8^+AR)2ROZ!5S9-fkgyeO+@EUv|EK172Bx80uWt+vjCy>0QQIPr#hvvfa&I zX7Tz^)(EMzYH;NL%em<@;3-0ne4lWonPgJKs;>Ywq=3QUH-LOBRUg*xl}uu%H{S~+ z5thCT#ahy8txQMZB)h?Ut~t!_7kQ1hxsy>o4T?ZCwS`d$cW`duMV1ZQm)9NFbJHMztjp*s7kxGH@JtQ%I|e98x+-~hFo?;P=0%%hEJ#}uC!o** zq+93jbk7J2>Y73WumbH`j0sXD1ihgXf|Z4VwH!b;I8U+JSN6Y5&D{3G^a$Kv53j$SET>tpMS}~y=B2ZyS$AGR#%%g) z7mi>DO;Sif#ONdx3APjseXmPByGlH1?Fmamo2>RZ@Z&CP&G?4w82;gki>b&AuL1Lp zJG{u&JoY3F!3HS9zcPVE*kPdt5}y5?42q!Z+26TZ3M({6y;-a5qCEEgGl^k_15u>@ zp%m1{vSi4kBv)6>1O<6Rft4y$5&A(-b-+44szl^XEVv1YJy$=Ft!K){;8W#Tr^!kBU>5U=HcOKwnEgS--Qf__)I# zT5@I{GOi@Q$^H@iS>Mq$&!ldJ#Th+YV)J?KQP%IX&B&G44_4Bx15ujUJn&~r2u z8Pko-8emPrq$Mj9o@I(=JN?+SA@dxoWKFp&x)t7jLrj=M8oTswa~gWMQ?lsJ4;qyp z+KOnmMRw-sxFk{&sj{Sc=Sd>(soWngWphOoXtVKmOuFmEG~V-&6UGV7c1>yy#wO1T zT&r{VqO_|3M}$WPuAd7j$`sGU$R0Z`s~eA@K4RPj@AO>2+^bm)^7w9wH;|Cyj)9qI zN>qOJcAux1fd822gh2+40aQA*pZm5?8nOt1I>VlHc0WaRv@$KA`|~}tv#JeG2E1=l z(lDHAk>lQLuGGEh!~@58pG}k=7UuU9F08~M4B-LV7`&072NZ;7Mz2fiSMCWqY|N+< zQ61$H%v=a=B_iB{)c@=dZ;_OgeV=v$lOIUAd$O1})F6|A)g;rQuKcFlL}m4N*9S0) z<}zkL9z?6{{(vHj*ZwNEcz+r2$C()_O-8AYQVVWdNEQ$ zp|NN$A>NzrF$3q7HJnt^r+fS`cbo_rz z7D#E)1pqEdwR|44}zb8DUbYWGzPi?bd z>$&Qg?d2fbI5EWKL++JT3ab&3#01zXGnCY*dFenDt&eQmT}s%RVyVD03XQs4X2h*V zBskBHPjk_ zJ)8`sov<9&%a@8?^IC5mqn#30hcg~nw-O$T)dI2s`Q5lAQ@^yeo{76ATrx|vda$LU z%ZnWyIJGSPVs??$uD<1>?C)&WvL0Iz)Ymbvr5CYHDf=;S3swr{yxHzgt|7N7B~TD@ zBT#NyhyQ?w>9I0v!ls6X&+`eRI4iFk>|Pznub=BoBF83iW+FU)+4U4GaYztaOJrAi z*PKnrghRwo`QmG8O9^kAp)&po!_JzY$ARvN{d6d)ShDKB7O2t_vEj&ZM(?iFP2v+P z-EqpUJFAOCdXB4Jv@8l-hm7h2c9_sm6Ys#Z7i?8RP2wysWH_XMEsOz5i+6H*1VS7q zw6f9gNf}s{o<{_g21hO|3`_N1Wig^vHa{){mlDIHZ_LsSNjJb&uXm@Dln20{mcoD@ zXZ9KqIDJs*{A}k%;mLvtg#s)2eeBzw^c|v`?wa;uQh6&UzY>VSAF>cXRanxzLepw! zlIfASAS*gZ+mK>2kf%Zc(sU;?{&Rh2KlmVK46@Znl(XuYIm;7~P&^x5Sd%^R+QhH3 zUIur>fppn^aL`}7EMd3UNq>8^HVTk`u|X2Fjl(o6jAwa3-H+k`4}utCJZm_%f24D- zLNEePO!$IR*Trt0b6kKgZ?tg_a*X6e`9K@W)C}oy;BnuFxi|F zmtG1}8Fgsuem} zi%^d~IHO{GODNHEwG7?kcxRv#+k5#;I|BPCknVcu4FT_Z^~KR(b~>@);Y2*$=0MsX z#H{QRf-pJ~hd--Z|Uj3gqh%gpEqv z*O8}x-CCJU=f%i-9+b5vWn8N*p|P*=(x4%g;0N|$>|zUIKAo8nwpV?Z38IiXt3j%A zWt5B&sP4=en4c~d4-GINLQ;vq0JGhj){92uE#dbV99{ywTp2o?aa~Y@VF$<#rerrW zO%BZaVJ5(Ut3#LIT;qC2^*)xKe-C5l+|dURG~QE>qLyL^{?3Y?jdI*G=Xm!UkuP=2 zeq+KRn`m?McnnoJ+R)91|1MbC^Hy493?2-GOgQqUS{N02Q!PkSzxU7Rn_;k;_f3(a zah0U2+Eb)S*p6@Y%Qy!*n;>M-0<~PD6It@Th8C# zVi1Oo<(IV!T(eh24CuX~;3O-+khlR`QuZ@d|iMZB}gLH$ntKhpd-S%VeRnx$oA;G)pH-IT4G|{o1?@hF-^5@f9460 zx}G?*g~lCUy`ziQLNo0;6CLjTgP<~ratxhT6U0HNO^@HN9^rB}xZYA%H@1CuEs>9y z(CmUo1D9YEoLuR2qyPhwjHLO#sz#qkF8^l$N~35!ze;vBn_d1Rz=inRQg$@| zXDS55+2|A?1y6jEV4$p@QNB$>sAqwa>E<_Ace$=EBC|4h^br3Wma(n<#M=gP7mv>; z(f6xRquQ*+n$kK4^Lrd*qt7Ah-&c2CF=epl|Xv0d4Y%IknNpqo zg4d98E46~=n-gY4J+}w7NB$z1Yo^HT6eoOlZUOxUH8|Gz5=jMy)Vscy^jX-yu??>M z*xtC;ho4=dnzj)t4so7(dsvV0 zow{DcST;PP{$buSRm&{C-Y%lXxCdPZV1Z0XIYD$=Y5%b)Q2rca%7-U@=)xzlk2dK=A5k+ld6Yn-cd`E-t%Q2VLShfX&f z1%-i^y}L3mB0#aCz8viN$fxDWd=tw6qJs6W2Z-Op8Z`5!?cM-=_1eCy(v?c){jhQ_^FEd&Zn=rTp8BUviT&Rs9@mzt3pp zz2WEO{Su%LsVJ=Ep*(2^b~Ykq#|N#wcE`shId>mmY{yFLE*v#2c(TomU0VOIx9;LH{i0tXIG~1$)Q6ofmvki=_@s1yL605-{*A&58;(<(PzJWz~}hPngTR<{VA9!(1I#=Ct+Rs1(i zW$+hDrpS3PYM0;AesmVKv2Y+r%3wi^?&{5R*JpK*g8)gd)ac~WD&b?)5C>Re!i|@I zLkXE&*>Fxg^v_88vhz%UD)yCtP!JMV(+&%{)6-+q=Q*Dw+ozY#m3LxJevK%p7Cy;S z1w1I;-J3+|ogI3H^Le}5jjH<$bBnMzBRG>DPMulNeT(=HxKO}_=X<>1f^YMIWR%0gt09a;*azNiEL^% z;dkbw{l%Fkr5jbaFzbUGpo`RLs=G`QS^}lgFZ`RmY}f)(RGm&AGUWrZ>w8J#2F%4P zL*66Gv&dj873B2YTp9~^T?w%}|KgV(7R9XW8EPy(FCg{w3luCKqwyJU3T!t8D@Hhe zHQfB0>?ASuL*yE~rWw(P!XvsRrb3}OC6}V#MlYz%i1HRhGFkztE()<=X!f7TC(Q8- z=o>X%4+~DLQqMkTj#o7BMHP5bBC^{2DTKSsS(^&kpkuM>D1$bnaeVQ2Eq)En)I32PUus>i{pdFw+qF1$7M2_7VdT)Z20uC<_t z5VK10OwF3M^g{9y?uo)eaWPMrCG`;R2rA5bV`6bOf>u;J zU-gd+H3L>=fA4~W6C8yZvC)&WD?kfw=M5mwP6K>sMFZeY4Pc)VXFo)SYNh1r97wvy z;h%6CY4vIZGp94mVIJ@sylHWv7>G2bZ8QybGy24b2W%(rx>6CRtui_?og}+L&EOUm>SDBd9)hBmr}bR zbw^$k#_5MOb&^o#nZ|sKd0>h^Iq__6ZcXHm2<_Vl)~SIl>q?ICdoSlcGz|KrBarC* zQX?<5xdukr)BI{+o$-R)}l^0feW_yVIZa)MP{o5TzRXNSbt-sRHCgBz>*SN38 zH6B~qvn4}g<0y0<+cgI6ZWBG_J*ndlp^M|Joi(}YEZpO$HY0m%|&It}Er zjo$K`SDJ`Q1<=-E1SUSIXaI0kKghJ0KdaJu@mOdEMHuE_W85{8(4d})-~S9UZiKkC z{!n!&uN@JJPb>H-_eBcb3Q6Z`h&8n-JguFIba2J|l?Mvp{`6;mMJoF3_H)pGg zkW??F@&>=}Cz~b@CW~FI9F^fzwh{Br0(x!c7qiABMlF&ZISDL6{r1!nQiDi*x8UX) zA~q9OJvfZ7KkMe`e$Ym26t&HK~QxA7dbYV{3u zLqUsiEcMkABB-vQ%Zg1{%)iF=&po;S-nM4Yli~;B+qQcg{0a+&%^8KK z9+{hV?P67FbN>`eI=drBv7#z@LCtY39|44hTy}5{60MC|L{f+19Zb*x`pQva~xqhLbj!7d!5fcJs(qfWtnc3oVh#mcs;}w``zkXQhp5L zPK)MiM(yJpk+oLgtLk(JO2cva71}N4sjz8}2PDw*mvTlh`iz_ws9NRdu*y`i5qWEg z^wZi;v}MeW(KY44M6{*^J($PYRnZMrOLabFqmP`@ubzFlZz(ZIJgpV|k_d#hEQ`uh z$@{}>5vn!i3L|`RL&jzS!63toji7Qb-nD9rp-U@q=r$0om8OG0+&BY^=4&ACE-&Md zTLu0-+={XZ_l0!fa}C*(Y#_ase8EQmXhCYkr0(aQdy$)IB5?(;O*~L+5nJgJ_q)bC z%Wg1;x9(pt#;QIpFCl%jt2FjkfScd@hpF9x9Y4Mo0S3L10p@%swKNaX@Qfiy71e(GE%m)krh=phc@qxF;vu@=HZH_>@_G zyS&&SL18300OB{(_t%^ZwT3xYzZoR*d@xu!Qw@`RzKr><3Z16(FxVb%BZd)t06weJ z#0*)Z1vu;fy`+7+(PQD8P@@40&noU}Q!3ze61Zg*&~9ALi4GPi^P&$xjSy3GUaJht zTqyuAtqxfB8w7I(s?N{=PJqNjryT445fynaFt+;JUcsAU z20C1KPjnqcm0QXWV)UlNpn%_Ubwe;K@O@}_HAeHO#^dP965HI2|EbukU3zXYR=4$zg)HGShIZOQHi*V zKkZ^fxBw4u`>@FOI>+7_pU|l1XXII4`m1u&mEBkTjsaE6e^NWLWQ$B+enyrS00!M2$K`i;F@GCd2Xo4W#nKaCCXtiLl#)~*`J-nf$=iIS`H zg{=~?ec!W*@Un$1M$=)W`Ybbefv{;PC`|&0Y1V$!6(b(IVg`y;>{9|i+K0_JzQy{A z`P}mHtnl~p5*vK|Abx{^6P06OJi@=52 zUkR+GUNfT$|GoeRIxRSfB+>vEIX`F$NQ(d7* zQ?}1G3eR*V?;XO9bP&%F6Th}E#rAZnPf$_?zej-FOFV#|99}=9++7=ygrer3Hct{4 z=Q>?VK^;}t48X~El(G6lv3*8fm`lqL4lu)tDFvW9dwoDPH|%S9JKNHCcvMD^CWa>F zc7XO(s;CUZv$g+(2nGrUEVJMQsrh83(5DrzG*4D$mDHUxbt6rCEc6~lKlZWgXHM7m z#q$VMMyaCT7*}jUB4g=lmK_D?y@^X;BJ15zZ4Y%-QmLtnGBmo{r?I$>Ud#p9|EOv~ z;zh!fdY{^M%M2@*v%-BvREh7n@#37k28)8z`lNgZRL=HwwqF310;w+p(M(jJEL5p& z@#VNh3?n6ecm^C!a=lY<7~p87zVLF}oZ-8g5qMS)t?02a#Ek=Sb45Uo92B5j#%1zh zwPH1!CFEE`>X#1C3P>ZmSx#%o<^5ITzPu%=YEQD20t6^jr*J$CCS)qDAk>8uH&Km_ zn4ho;oOXUh=V|9VeAGYGwGp;ow=w1V0#@cXDWWG3=qHWTMl9w&Rw6K)XU1L_0x#p- z@Ba7M-2gEAAzS55E9Wmb^UDLs0A{8^Z_vDRa!0HQ*n1|M*0B@Q?*@; z^hFvs%VH_8H82(`8sU?U!X1i02g)&ZaLp!vJstyPt&Il?H<YjIF|_E#wV}dx~NBJVxY?-RyF1Y-#K zFrh{863s_*(|ra_76TJcm)U{bQ$>F+{#U@N2ae^4{m)v}G(Du1L5ae#6Yjwqv5&j? z>I9{!uoo|~so=jP&{vUoXNWf@{ABd6Hw7R58w=~JCHT|r>|rSa+Dd;{>Cij zzheE^LPJJ-1Ts&q3ehre>V~ zYjGvi*elyNa125m$vSz|+cJ94 zW5dM#Fni3q(kUD*j@V=_{L{k#EoUbSp6Y-JF)tKfib7m+k8tj_wFHX%|COXPGF+@- zJSNIm-mX;`UeK^FHC9s%yd;?l5Q^`M8a2*rI@(%!+b=k6b}~6pZ?qeFExga%qbRHK zzt9f`(P;bIk2}@qL`y;@QD_AMdoS&c#%NJ{JAzd#J#dOZ`Tfn*GOZCWbrS1?=u9d} zPW5M16t=Xgx^x}3LVCgUQLp_*QyECqy_rlrarD1sZLXcdPg@1^Wef8z&EfY64dsI!fanYqG$>kjDf@?VFX_>B`7;^OC^Av!H4I&FN1+kdjV zf|FlmU+yak&aZ8jZHU$(sMDGOiO0<#i6?=6I-1U1T>f?pG)syuPZ$u)?}~-k6Y;vH z4&9VWAOk0FvE>S=XtG-Ae5#V7$i&=$G=kHeJ{f-fNLi=9;e-p*n9{JFxf0BIdy9M< zT`er2G$&%EF|ZI^vrNR>3ljUE@K4&uV|0kBtF6#8*LZ(X@@9^I@+qOP5^!8o#7GY6 zYI(t0eVDDqscAqLTH}gg^dJGHTarTD7xBe54dv1+ZEMs{W|{`~r5htB&Aqn=vsc1_ zC@VOQU_Jr^m7w%`ZnLxnKc!m;2O$|c8w94~<31pLe1o4O9dL#EvA49IgR??oVs))m zREe-EeRz|$I_>78QotwFfle*4)Dr%h+@GbO6m=0f9NQjzE=KT9n3)ByKP8?R9)alI zZ~RR@rJP!{uSDEF?p5$% zxP1KyCH$V$;^*G4_tXUux_D75-jGGW-&&nfaf~_lut}nS_bxaNF3+?4O?f(ASNU5l zFYDtKdu9V>4cAJ~xT;LroVnbh4m)%*OIBp1$l8dqhslewH<4+Pb{30KlJvY49@uB* z%9O|VSR1+O`Cj8CDwX$hK#AAceod-6!L9dwsOq@(+zooLe78@IOjwNIzKc>i@bT{( zOaPif6&olSfV{)Y+R4z_S$rK#TO5Q9DX-hu`Ba?N*h|zP47n?{q~hH_lun zpievXc7UfDR^Fd{C~XzjC%j7VG}Ztf~hvcL461xa+Bi<7QQ z{76>P4#!9W;|2UhcEkOP-7@z>{Fq#>hQjBZUzQ2IkB=Y*F1W4{nnwgwfb4HvnWh?5 zq9*?lbLclZ?bjg%sh>*N6f57)kbSISfl~5j`4mY3=Uh*rKRE8)G@7XhZV#-x${SUX z`~*r~CHx1>iA`K(p;Y&JD!8obe}v;>6WEy>3djHU&QqH@3gk&nqpKdGMLN-g3K_HY zhz*fjgOdz?g8=hhQ-U_->6yJ9m$bx^{|hU6mQZcf6xGPOzH6>h5)M6n1qaYS=?qzn z-(nu?!CA#cM~vU*m!cGq@h7FasR|Zd`u(W*Yv4WTRg8}gOG;%py0EmQ1t6wd)#{pu=jm&mjRz*q$Atz8r14hCgecjNY%Z_U;WB9&H9FZ3c1w){` zu{6`A;!vslL^On@&+`hLJClbdaR)>qSspX=AwS)Gl6bB=s=D4Hgf%jS_n446x&j7U z`O5{ab#c!MkbKv!G5h=6JBAl&r2ifj+h>K4Oe{s`iu|8vd5Om(6DU0~y%`%((a709 z0C6ryGKp`K6*ln*F8X=~rnN=`j34YW=C4IC@E%QoC#K53)w0)V^>IudnN1_pNb@hG zuOs2`6jqCCSol{csaWI&@Z)k0E&8#K%o*Ew#^uCfc1%7K1Tr`c1nvCr8cX3bP9^Se zD3CCi(qs7S8UVxjd=u{cRgHR)8SXAWI6kc8xd3!_a)^N~pNSI5EOJ8bbG>z{YBh6) z)xp$UTH%dYZ(ur$lv-$2_k7UxubN{|&>!Gy> zhr_Xwh(n4ClkH)6Mm2Zp4o(b3nA;OVnv6BXU!UKoJF^x;py&Kn^T*~GwwLiK>wbRsNRJi6#eO=Sb!1wbm%Ji3^Qv;fyF9PJ|mcO1%~6s<&UtJ z1O=7Lv>>QKf+ccjAP^z5GnI*Zs^Kb9cK=;`m8}{wO~KmfxN5hqTqheeChXN58JNzQ9>+