From ae174d4a8760af635a6d253c70aa427fc5b2801e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 1 Sep 2017 16:49:03 -0400 Subject: [PATCH] proper handling for multibyte characters // FREEBIE --- Signal.xcodeproj/project.pbxproj | 6 ++ Signal/src/Profiles/OWSProfileManager.h | 2 +- .../ViewControllers/ProfileViewController.m | 46 ++------------- Signal/src/util/TextFieldHelper.swift | 56 +++++++++++++++++++ 4 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 Signal/src/util/TextFieldHelper.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ac4c1d9a7..cf1c71618 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -132,6 +132,8 @@ 451DE9FE1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */; }; 452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */ = {isa = PBXBuildFile; fileRef = 452037D01EE84975004E4CDF /* DebugUISessionState.m */; }; 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; }; + 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; }; + 4521C3C11F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; }; 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; 452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; }; @@ -593,6 +595,7 @@ 452037CF1EE84975004E4CDF /* DebugUISessionState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISessionState.h; sourceTree = ""; }; 452037D01EE84975004E4CDF /* DebugUISessionState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISessionState.m; sourceTree = ""; }; 4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = ""; }; 4526BD481CA61C8D00166BC8 /* OWSMessageEditing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageEditing.h; sourceTree = ""; }; 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = ""; }; 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = ""; }; @@ -1456,6 +1459,7 @@ 76EB04FB18170B33006006FC /* Util.h */, 45F170D51E315310003FC1F2 /* Weak.swift */, 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, + 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */, ); path = util; sourceTree = ""; @@ -2370,6 +2374,7 @@ 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */, 34B3F8721E8DF1700035BE1A /* AdvancedSettingsTableViewController.m in Sources */, 45F170D61E315310003FC1F2 /* Weak.swift in Sources */, + 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34B3F8891E8DF1700035BE1A /* OWSConversationSettingsViewController.m in Sources */, 34C42D671F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m in Sources */, 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */, @@ -2421,6 +2426,7 @@ 45BB93391E688E14001E3939 /* UIDevice+featureSupport.swift in Sources */, B660F7211C29988E00687D6E /* SignalKeyingStorage.m in Sources */, B660F7221C29988E00687D6E /* VersionMigrations.m in Sources */, + 4521C3C11F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 45D231781DC7E8F10034FA89 /* SessionResetJob.swift in Sources */, 458967111DC117CC00E9DD21 /* AccountManagerTest.swift in Sources */, 45F659831E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */, diff --git a/Signal/src/Profiles/OWSProfileManager.h b/Signal/src/Profiles/OWSProfileManager.h index 8a7bd3e74..d6d070a56 100644 --- a/Signal/src/Profiles/OWSProfileManager.h +++ b/Signal/src/Profiles/OWSProfileManager.h @@ -13,8 +13,8 @@ extern NSString *const kNSNotificationName_ProfileWhitelistDidChange; extern NSString *const kNSNotificationKey_ProfileRecipientId; extern NSString *const kNSNotificationKey_ProfileGroupId; -extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter; extern const NSUInteger kOWSProfileManager_NameDataLength; +extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter; @class TSThread; @class OWSAES256Key; diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 8d09af086..c31fd9ca7 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -452,48 +452,10 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat replacementString:(NSString *)insertionText { // TODO: Possibly filter invalid input. - - // Prevent crashing undo bug - if (editingRange.length + editingRange.location > textField.text.length) { - return NO; - } - - NSUInteger (^byteLength)(NSString *) = ^NSUInteger(NSString *string) { - if (string == nil) { - return 0; - } - - return [string dataUsingEncoding:NSUTF8StringEncoding].length; - }; - - NSUInteger lengthOfRemainingExistingString - = byteLength(textField.text) - byteLength([textField.text substringWithRange:editingRange]); - NSUInteger newLength = lengthOfRemainingExistingString + byteLength(insertionText); - - DDLogVerbose(@"%@ newLength: %lu", self.tag, (unsigned long)newLength); - if (newLength <= kOWSProfileManager_NameDataLength) { - return YES; - } - - // Don't allow any change if inserting a single char (typically this means typing) - if (insertionText.length < 2) { - return NO; - } - - // However if pasting, accept as much of the string as possible. - NSUInteger availableSpace = kOWSProfileManager_NameDataLength - lengthOfRemainingExistingString; - - NSString *acceptableSubstring = @""; - for (NSUInteger i = 0; i <= insertionText.length; i++) { - NSString *maybeAcceptableSubstring = [insertionText substringWithRange:NSMakeRange(0, i)]; - if (byteLength(maybeAcceptableSubstring) <= availableSpace) { - acceptableSubstring = maybeAcceptableSubstring; - } - } - textField.text = [textField.text stringByReplacingCharactersInRange:editingRange withString:acceptableSubstring]; - - // We've already handled any valid editing manually, so prevent further changes. - return NO; + return [TextFieldHelper textField:textField + shouldChangeCharactersInRange:editingRange + replacementString:insertionText + byteLimit:kOWSProfileManager_NameDataLength]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField diff --git a/Signal/src/util/TextFieldHelper.swift b/Signal/src/util/TextFieldHelper.swift new file mode 100644 index 000000000..6819aff95 --- /dev/null +++ b/Signal/src/util/TextFieldHelper.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation +import UIKit + +@objc class TextFieldHelper: NSObject { + + // Used to implement the UITextFieldDelegate method: `textField:shouldChangeCharactersInRange:replacementString` + // Takes advantage of Swift's superior unicode handling to append partial pasted text without splitting multi-byte characters. + class func textField(_ textField: UITextField, shouldChangeCharactersInRange editingRange: NSRange, replacementString: String, byteLimit: UInt) -> Bool { + + let byteLength = { (string: String) -> UInt in + return UInt(string.utf8.count) + } + + let existingString = textField.text ?? "" + + // Given an NSRange, we need to interact with the NS flavor of substring + let removedString = (existingString as NSString).substring(with: editingRange) + + let lengthOfRemainingExistingString = byteLength(existingString) - byteLength(removedString) + + let newLength = lengthOfRemainingExistingString + byteLength(replacementString) + + if (newLength <= byteLimit) { + return true + } + + // Don't allow any change if inserting a single char is already over the limit (typically this means typing) + if (replacementString.characters.count < 2) { + return false + } + + // However if pasting, accept as much of the string as possible. + let availableSpace = byteLimit - lengthOfRemainingExistingString + + var acceptableSubstring = "" + + for (_, char) in replacementString.characters.enumerated() { + var maybeAcceptableSubstring = acceptableSubstring + maybeAcceptableSubstring.append(char) + if (byteLength(maybeAcceptableSubstring) <= availableSpace) { + acceptableSubstring = maybeAcceptableSubstring + } else { + break + } + } + + textField.text = (existingString as NSString).replacingCharacters(in: editingRange, with:acceptableSubstring) + + // We've already handled any valid editing manually, so prevent further changes. + return false + } +}