From 13a432b9de25f1469afbf00587635a42eba15f3b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 13 Apr 2018 11:58:59 -0400 Subject: [PATCH] Limit attachment caption length to 2k bytes // FREEBIE --- Signal.xcodeproj/project.pbxproj | 12 ++- Signal/test/util/StringAdditionsTest.swift | 100 ++++++++++++++++++ .../AttachmentApprovalViewController.swift | 20 ++-- SignalMessaging/categories/String+OWS.swift | 49 +++++++++ 4 files changed, 169 insertions(+), 12 deletions(-) create mode 100644 Signal/test/util/StringAdditionsTest.swift create mode 100644 SignalMessaging/categories/String+OWS.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index acd652c0d..52564a344 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -48,9 +48,9 @@ 34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; }; 34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; }; 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; - 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; 34386A51207D0C01009F5D9C /* HomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A4D207D0C01009F5D9C /* HomeViewController.m */; }; 34386A52207D0C01009F5D9C /* HomeViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 34386A50207D0C01009F5D9C /* HomeViewCell.m */; }; + 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; 343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */; }; 343A65981FC4CFE7000477A1 /* ConversationScrollButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */; }; 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; }; @@ -281,6 +281,8 @@ 4523D016206EDC2B00A2AB51 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523D015206EDC2B00A2AB51 /* LRUCache.swift */; }; 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; 452C7CA72037628B003D51A5 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; }; + 452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */; }; + 452D1AF320810B6F00A67F7F /* String+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1AF220810B6F00A67F7F /* String+OWS.swift */; }; 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; }; 452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; }; 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */; }; @@ -616,11 +618,11 @@ 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ElegantIcons.ttf; sourceTree = ""; }; 34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = ""; }; 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; - 34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = ""; }; 34386A4D207D0C01009F5D9C /* HomeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewController.m; sourceTree = ""; }; 34386A4E207D0C01009F5D9C /* HomeViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewCell.h; sourceTree = ""; }; 34386A4F207D0C01009F5D9C /* HomeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HomeViewController.h; sourceTree = ""; }; 34386A50207D0C01009F5D9C /* HomeViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HomeViewCell.m; sourceTree = ""; }; + 34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = ""; }; 343A65931FC47D5D000477A1 /* DebugUISyncMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISyncMessages.h; sourceTree = ""; }; 343A65941FC47D5E000477A1 /* DebugUISyncMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISyncMessages.m; sourceTree = ""; }; 343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConversationScrollButton.m; sourceTree = ""; }; @@ -889,6 +891,8 @@ 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionalPanGestureRecognizer.swift; sourceTree = ""; }; 4523D015206EDC2B00A2AB51 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = ""; }; + 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringAdditionsTest.swift; sourceTree = ""; }; + 452D1AF220810B6F00A67F7F /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = ""; }; 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = ""; }; 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = ""; }; 452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewController.swift; sourceTree = ""; }; @@ -1434,6 +1438,7 @@ 34480B601FD0A98800BC14EF /* UIView+OWS.m */, 346129D41FD20ADC00532771 /* UIViewController+OWS.h */, 346129D31FD20ADB00532771 /* UIViewController+OWS.m */, + 452D1AF220810B6F00A67F7F /* String+OWS.swift */, ); path = categories; sourceTree = ""; @@ -2189,6 +2194,7 @@ 45360B8F1F9527DA00FA666C /* SearcherTest.swift */, B660F6B31C29868000687D6E /* UtilTest.h */, B660F6B41C29868000687D6E /* UtilTest.m */, + 452D1AF02081059C00A67F7F /* StringAdditionsTest.swift */, ); path = util; sourceTree = ""; @@ -3103,6 +3109,7 @@ 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */, 450998651FD8A34D00D89EB3 /* DeviceSleepManager.swift in Sources */, 3478506B1FD9B78A007B8332 /* NoopCallMessageHandler.swift in Sources */, + 452D1AF320810B6F00A67F7F /* String+OWS.swift in Sources */, 451F8A3D1FD713CA005CB9DA /* ThreadViewHelper.m in Sources */, 346129AD1FD1F34E00532771 /* ImageCache.swift in Sources */, 452C7CA72037628B003D51A5 /* Weak.swift in Sources */, @@ -3349,6 +3356,7 @@ B660F6DA1C29868000687D6E /* ExceptionsTest.m in Sources */, B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */, 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */, + 452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */, B660F6BB1C29868000687D6E /* OWSContactsManagerTest.m in Sources */, B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */, diff --git a/Signal/test/util/StringAdditionsTest.swift b/Signal/test/util/StringAdditionsTest.swift new file mode 100644 index 000000000..f0c785f94 --- /dev/null +++ b/Signal/test/util/StringAdditionsTest.swift @@ -0,0 +1,100 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import XCTest + +class StringAdditionsTest: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testASCII() { + let originalString = "Hello World" + + var truncatedString = originalString.truncated(toByteCount: 8) + XCTAssertEqual("Hello Wo", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 0) + XCTAssertEqual("", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 11) + XCTAssertEqual("Hello World", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 12) + XCTAssertEqual("Hello World", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 100) + XCTAssertEqual("Hello World", truncatedString) + } + + func testMultiByte() { + let originalString = "🇨🇦🇨🇦🇨🇦🇨🇦" + + var truncatedString = originalString.truncated(toByteCount: 0) + XCTAssertEqual("", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 1) + XCTAssertEqual("", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 7) + XCTAssertEqual("", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 8) + XCTAssertEqual("🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 9) + XCTAssertEqual("🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 15) + XCTAssertEqual("🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 16) + XCTAssertEqual("🇨🇦🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 17) + XCTAssertEqual("🇨🇦🇨🇦", truncatedString) + } + + func testMixed() { + let originalString = "Oh🇨🇦Canada🇨🇦" + + var truncatedString = originalString.truncated(toByteCount: 0) + XCTAssertEqual("", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 1) + XCTAssertEqual("O", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 7) + XCTAssertEqual("Oh", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 9) + XCTAssertEqual("Oh", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 10) + XCTAssertEqual("Oh🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 11) + XCTAssertEqual("Oh🇨🇦C", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 23) + XCTAssertEqual("Oh🇨🇦Canada", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 24) + XCTAssertEqual("Oh🇨🇦Canada🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 25) + XCTAssertEqual("Oh🇨🇦Canada🇨🇦", truncatedString) + + truncatedString = originalString.truncated(toByteCount: 100) + XCTAssertEqual("Oh🇨🇦Canada🇨🇦", truncatedString) + } + +} diff --git a/SignalMessaging/attachments/AttachmentApprovalViewController.swift b/SignalMessaging/attachments/AttachmentApprovalViewController.swift index ccaa6464d..e7aa712e1 100644 --- a/SignalMessaging/attachments/AttachmentApprovalViewController.swift +++ b/SignalMessaging/attachments/AttachmentApprovalViewController.swift @@ -634,19 +634,19 @@ class CaptioningToolbar: UIView, UITextViewDelegate { public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - // Limit caption character count. We do this in characters, not bytes. - // This character limit will be safely below our byte limit (16k) for almost all uses. - // Because the captioning interface doesn't allow newlines, in practice design pressures users to leave relatively short captions. - let maxCharacterCount = 2000 - guard textView.text.count + text.count - range.length <= maxCharacterCount else { + let existingText: String = textView.text ?? "" + let proposedText: String = (existingText as NSString).replacingCharacters(in: range, with: text) + + guard proposedText.utf8.count <= kOversizeTextMessageSizeThreshold else { + Logger.debug("\(self.logTag) in \(#function) long text was truncated") self.lengthLimitLabel.isHidden = false + // Accept as much of the input as we can - let remainingSpace = maxCharacterCount - textView.text.count - if (remainingSpace) > 0 { - let acceptableAddition = text.substring(to: text.startIndex.advanced(by: remainingSpace)) - textView.text = "\(textView.text ?? "")\(acceptableAddition)" - updateHeight(textView: textView) + let byteBudget = kOversizeTextMessageSizeThreshold + range.length - existingText.utf8.count + if byteBudget >= 0, let acceptableNewText = text.truncated(toByteCount: byteBudget) { + textView.text = (existingText as NSString).replacingCharacters(in: range, with: acceptableNewText) } + return false } self.lengthLimitLabel.isHidden = true diff --git a/SignalMessaging/categories/String+OWS.swift b/SignalMessaging/categories/String+OWS.swift new file mode 100644 index 000000000..275a318e1 --- /dev/null +++ b/SignalMessaging/categories/String+OWS.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation + +public extension String { + + // Truncates string to be less than or equal to byteCount, while ensuring we never truncate partial characters for multibyte characters. + public func truncated(toByteCount byteCount: UInt) -> String? { + var lowerBoundCharCount = 0 + var upperBoundCharCount = self.count + + while (lowerBoundCharCount < upperBoundCharCount) { + guard let upperBoundData = self.prefix(upperBoundCharCount).data(using: .utf8) else { + owsFail("in \(#function) upperBoundData was unexpectedly nil") + return nil + } + + if upperBoundData.count <= byteCount { + break + } + + // converge + if upperBoundCharCount - lowerBoundCharCount == 1 { + upperBoundCharCount = lowerBoundCharCount + break + } + + let midpointCharCount = (lowerBoundCharCount + upperBoundCharCount) / 2 + let midpointString = self.prefix(midpointCharCount) + + guard let midpointData = midpointString.data(using: .utf8) else { + owsFail("in \(#function) midpointData was unexpectedly nil") + return nil + } + let midpointByteCount = midpointData.count + + if midpointByteCount < byteCount { + lowerBoundCharCount = midpointCharCount + } else { + upperBoundCharCount = midpointCharCount + } + } + + return String(self.prefix(upperBoundCharCount)) + } + +}