From bb67b92062c8c6a2c3652eedeee7a66b815bee77 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 9 Feb 2023 09:50:18 +0200 Subject: [PATCH] Fixes #464 - Incorrect regex usage on extended grapheme clusters --- ElementX.xcodeproj/project.pbxproj | 20 ++++++++-- .../Other/Extensions/NSRegularExpresion.swift | 40 +++++++++++++++++++ .../HTMLParsing/AttributedStringBuilder.swift | 11 +++-- .../Sources/Other/MatrixEntityRegex.swift | 10 ++--- ElementX/Sources/Other/PermalinkBuilder.swift | 8 ++-- .../Sources/TestMeasurementParser.swift | 2 +- changelog.d/464.bugfix | 1 + 7 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 ElementX/Sources/Other/Extensions/NSRegularExpresion.swift create mode 100644 changelog.d/464.bugfix diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0a77903cc5..18e8f44274 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -346,7 +346,6 @@ A663FE6704CB500EBE782AE1 /* AnalyticsPromptCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */; }; A69A54FF11A3F9EA0660E6BF /* NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0D8F620C8B314840D8602E3F /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; - A7CC2102298ACB1700DBE1C7 /* ProgressTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7CC2101298ACB1700DBE1C7 /* ProgressTracker.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; @@ -468,6 +467,7 @@ E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; E8AB8D16E6D8E8E501F29BD9 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; }; E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; }; + EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; @@ -493,6 +493,7 @@ F32B271F60531BE92C6E62A1 /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */; }; F425C3F85BFF28C9AC593F52 /* MockNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; + F587A9AF25A262DE5A7B0369 /* ProgressTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28551E81CE3700E5F1EC9B5 /* ProgressTracker.swift */; }; F61AFA8BF2E739FBC30472F5 /* NotificationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */; }; F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; @@ -864,6 +865,7 @@ 93B21E72926FACB13A186689 /* ml */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ml; path = ml.lproj/Localizable.stringsdict; sourceTree = ""; }; 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemPlainStylerView.swift; sourceTree = ""; }; + 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNotificationManager.swift; sourceTree = ""; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; 9772C1D2223108EB3131AEE4 /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = ""; }; @@ -907,7 +909,6 @@ A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; - A7CC2101298ACB1700DBE1C7 /* ProgressTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressTracker.swift; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; A8F48EB9B52E70285A4BCB07 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ur; path = ur.lproj/Localizable.strings; sourceTree = ""; }; A9873374E72AA53260AE90A2 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -1086,6 +1087,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; + F28551E81CE3700E5F1EC9B5 /* ProgressTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressTracker.swift; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationToastView.swift; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; @@ -1478,7 +1480,12 @@ E26747B3154A5DBC3A7E24A5 /* Image.swift */, 4E2245243369B99216C7D84E /* ImageCache.swift */, 2AFEF3AC64B1358083F76B8B /* List.swift */, +<<<<<<< HEAD 7310D8DFE01AF45F0689C3AA /* Publisher.swift */, +||||||| parent of 829ba5a (Fixes #464 - Incorrect regex usage on extended grapheme clusters) +======= + 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */, +>>>>>>> 829ba5a (Fixes #464 - Incorrect regex usage on extended grapheme clusters) 40B21E611DADDEF00307E7AC /* String.swift */, A9FDA5344F7C4C6E4E863E13 /* Swipe.swift */, A40C19719687984FD9478FBE /* Task.swift */, @@ -2265,7 +2272,7 @@ 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */, C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */, F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */, - A7CC2101298ACB1700DBE1C7 /* ProgressTracker.swift */, + F28551E81CE3700E5F1EC9B5 /* ProgressTracker.swift */, 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */, BB3073CCD77D906B330BC1D6 /* Tests.swift */, 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */, @@ -3105,7 +3112,6 @@ B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 33D630461FC4562CC767EE9F /* FileCache.swift in Sources */, - A7CC2102298ACB1700DBE1C7 /* ProgressTracker.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, 6C67774E8387D44426718BD9 /* FilePreviewCoordinator.swift in Sources */, 6C9F6C7F2B35288C4230EF3F /* FilePreviewModels.swift in Sources */, @@ -3171,6 +3177,7 @@ C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */, FE8D76708280968F7A670852 /* MockUserNotificationController.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, + EA01A06EEDFEF4AE7652E5F3 /* NSRegularExpresion.swift in Sources */, FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */, 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */, B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */, @@ -3198,7 +3205,12 @@ 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */, 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, +<<<<<<< HEAD 2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */, +||||||| parent of 829ba5a (Fixes #464 - Incorrect regex usage on extended grapheme clusters) +======= + F587A9AF25A262DE5A7B0369 /* ProgressTracker.swift in Sources */, +>>>>>>> 829ba5a (Fixes #464 - Incorrect regex usage on extended grapheme clusters) 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */, 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, diff --git a/ElementX/Sources/Other/Extensions/NSRegularExpresion.swift b/ElementX/Sources/Other/Extensions/NSRegularExpresion.swift new file mode 100644 index 0000000000..8d7fd38f60 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/NSRegularExpresion.swift @@ -0,0 +1,40 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// NSRegularExpressions work internally on NSStrings, we need to be careful how we build the ranges for extended grapheme clusters https://stackoverflow.com/a/27880748/730924 +extension NSRegularExpression { + func enumerateMatches(in string: String, options: NSRegularExpression.MatchingOptions = [], using block: (NSTextCheckingResult?, NSRegularExpression.MatchingFlags, UnsafeMutablePointer) -> Void) { + enumerateMatches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length), using: block) + } + + func matches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] { + matches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func numberOfMatches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> Int { + numberOfMatches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func firstMatch(in string: String, options: NSRegularExpression.MatchingOptions = []) -> NSTextCheckingResult? { + firstMatch(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } + + func rangeOfFirstMatch(in string: String, options: NSRegularExpression.MatchingOptions = []) -> NSRange { + rangeOfFirstMatch(in: string, options: options, range: .init(location: 0, length: (string as NSString).length)) + } +} diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index e03dedd87b..af20a106ce 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -132,14 +132,13 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private func addLinks(_ attributedString: NSMutableAttributedString) { let string = attributedString.string - let range = NSRange(location: 0, length: attributedString.string.count) - var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: [], range: range) - matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: [], range: range)) + var matches = MatrixEntityRegex.userIdentifierRegex.matches(in: string, options: []) + matches.append(contentsOf: MatrixEntityRegex.roomIdentifierRegex.matches(in: string, options: [])) // As of right now we do not handle event id links in any way so there is no need to add them as links -// matches.append(contentsOf: MatrixEntityRegex.eventIdentifierRegex.matches(in: string, options: [], range: range)) - matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: [], range: range)) - matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: [], range: range)) +// matches.append(contentsOf: MatrixEntityRegex.eventIdentifierRegex.matches(in: string, options: [])) + matches.append(contentsOf: MatrixEntityRegex.roomAliasRegex.matches(in: string, options: [])) + matches.append(contentsOf: MatrixEntityRegex.linkRegex.matches(in: string, options: [])) guard matches.count > 0 else { return diff --git a/ElementX/Sources/Other/MatrixEntityRegex.swift b/ElementX/Sources/Other/MatrixEntityRegex.swift index 9b66cef872..2f9e28f900 100644 --- a/ElementX/Sources/Other/MatrixEntityRegex.swift +++ b/ElementX/Sources/Other/MatrixEntityRegex.swift @@ -49,7 +49,7 @@ enum MatrixEntityRegex: String { // swiftlint:enable force_try static func isMatrixHomeserver(_ homeserver: String) -> Bool { - guard let match = homeserverRegex.firstMatch(in: homeserver, range: .init(location: 0, length: homeserver.count)) else { + guard let match = homeserverRegex.firstMatch(in: homeserver) else { return false } @@ -57,7 +57,7 @@ enum MatrixEntityRegex: String { } static func isMatrixUserIdentifier(_ identifier: String) -> Bool { - guard let match = userIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + guard let match = userIdentifierRegex.firstMatch(in: identifier) else { return false } @@ -65,7 +65,7 @@ enum MatrixEntityRegex: String { } static func isMatrixRoomAlias(_ alias: String) -> Bool { - guard let match = roomAliasRegex.firstMatch(in: alias, range: .init(location: 0, length: alias.count)) else { + guard let match = roomAliasRegex.firstMatch(in: alias) else { return false } @@ -73,7 +73,7 @@ enum MatrixEntityRegex: String { } static func isMatrixRoomIdentifier(_ identifier: String) -> Bool { - guard let match = roomIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + guard let match = roomIdentifierRegex.firstMatch(in: identifier) else { return false } @@ -81,7 +81,7 @@ enum MatrixEntityRegex: String { } static func isMatrixEventIdentifier(_ identifier: String) -> Bool { - guard let match = eventIdentifierRegex.firstMatch(in: identifier, range: .init(location: 0, length: identifier.count)) else { + guard let match = eventIdentifierRegex.firstMatch(in: identifier) else { return false } diff --git a/ElementX/Sources/Other/PermalinkBuilder.swift b/ElementX/Sources/Other/PermalinkBuilder.swift index 5a54a77fc9..e468f928cb 100644 --- a/ElementX/Sources/Other/PermalinkBuilder.swift +++ b/ElementX/Sources/Other/PermalinkBuilder.swift @@ -56,18 +56,18 @@ enum PermalinkBuilder { fragment = String(fragment.dropFirst(1)) } - if let userIdentifierRange = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + if let userIdentifierRange = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: fragment)?.range { return .userIdentifier((fragment as NSString).substring(with: userIdentifierRange)) } - if let roomAliasRange = MatrixEntityRegex.roomAliasRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + if let roomAliasRange = MatrixEntityRegex.roomAliasRegex.firstMatch(in: fragment)?.range { return .roomAlias((fragment as NSString).substring(with: roomAliasRange)) } - if let roomIdentifierRange = MatrixEntityRegex.roomIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + if let roomIdentifierRange = MatrixEntityRegex.roomIdentifierRegex.firstMatch(in: fragment)?.range { let roomIdentifier = (fragment as NSString).substring(with: roomIdentifierRange) - if let eventIdentifierRange = MatrixEntityRegex.eventIdentifierRegex.firstMatch(in: fragment, range: .init(location: 0, length: fragment.count))?.range { + if let eventIdentifierRange = MatrixEntityRegex.eventIdentifierRegex.firstMatch(in: fragment)?.range { let eventIdentifier = (fragment as NSString).substring(with: eventIdentifierRange) return .event(roomIdentifier: roomIdentifier, eventIdentifier: eventIdentifier) } diff --git a/IntegrationTests/Sources/TestMeasurementParser.swift b/IntegrationTests/Sources/TestMeasurementParser.swift index 0e43cc05cd..36e96dfc99 100644 --- a/IntegrationTests/Sources/TestMeasurementParser.swift +++ b/IntegrationTests/Sources/TestMeasurementParser.swift @@ -89,7 +89,7 @@ class TestMeasurementParser { } let string = String(data: handle.availableData, encoding: .utf8) ?? "\n" - self.regex.matches(in: string, options: .reportCompletion, range: NSRange(location: 0, length: string.count)).forEach { + self.regex.matches(in: string, options: .reportCompletion).forEach { if let nameIndex = Range($0.range(at: 1), in: string), let averageIndex = Range($0.range(at: 3), in: string) { let name = String(string[nameIndex]) diff --git a/changelog.d/464.bugfix b/changelog.d/464.bugfix new file mode 100644 index 0000000000..e3c7054f9b --- /dev/null +++ b/changelog.d/464.bugfix @@ -0,0 +1 @@ +Fixed incorrect link detection on messages containing emojis \ No newline at end of file