You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUtilitiesKit/General/String+Utilities.swift

509 lines
21 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CoreText
public extension String {
var bytes: [UInt8] { Array(self.utf8) }
var nullIfEmpty: String? {
guard isEmpty else { return self }
return nil
}
var glyphCount: Int {
let richText = NSAttributedString(string: self)
let line = CTLineCreateWithAttributedString(richText)
return CTLineGetGlyphCount(line)
}
var isSingleAlphabet: Bool {
return (glyphCount == 1 && isAlphabetic)
}
var isAlphabetic: Bool {
return !isEmpty && range(of: "[^a-zA-Z]", options: .regularExpression) == nil
}
var isSingleEmoji: Bool {
return (glyphCount == 1 && containsEmoji)
}
var containsEmoji: Bool {
return unicodeScalars.contains { $0.isEmoji }
}
var containsOnlyEmoji: Bool {
return (
!isEmpty &&
!unicodeScalars.contains(where: {
!$0.isEmoji &&
!$0.isZeroWidthJoiner
})
)
}
func localized() -> String {
// If the localized string matches the key provided then the localisation failed
let localizedString = NSLocalizedString(self, comment: "")
Log.assert(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings")
return localizedString
}
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
var ranges: [Range<Index>] = []
while
(ranges.last.map({ $0.upperBound < self.endIndex }) ?? true),
let range = self.range(
of: substring,
options: options,
range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex,
locale: locale
)
{
ranges.append(range)
}
return ranges
}
static func filterNotificationText(_ text: String?) -> String? {
guard let text = text?.filteredForDisplay else { return nil }
// iOS strips anything that looks like a printf formatting character from
// the notification body, so if we want to dispay a literal "%" in a notification
// it must be escaped.
// see https://developer.apple.com/documentation/uikit/uilocalnotification/1616646-alertbody
// for more details.
return text.replacingOccurrences(of: "%", with: "%%")
}
func appending(_ other: String?) -> String {
guard let value: String = other else { return self }
return self.appending(value)
}
}
// MARK: - Formatting
public extension String.StringInterpolation {
mutating func appendInterpolation(plural value: Int) {
appendInterpolation(value == 1 ? "" : "s") // stringlint:disable
}
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
mutating func appendInterpolation(pluralES value: Int) {
appendInterpolation(value == 1 ? "" : "es") // stringlint:disable
}
mutating func appendInterpolation(_ value: String, number: Int, singular: String = "", plural: String = "s") {
appendInterpolation("\(value)\(number == 1 ? singular : plural)") // stringlint:disable
}
mutating func appendInterpolation(_ value: TimeUnit, unit: TimeUnit.Unit, resolution: Int = 2) {
appendLiteral("\(TimeUnit(value, unit: unit, resolution: resolution))") // stringlint:disable
}
mutating func appendInterpolation(_ value: Int, format: String) {
let result: String = String(format: "%\(format)d", value)
appendLiteral(result)
}
mutating func appendInterpolation(_ value: Double, format: String, omitZeroDecimal: Bool = false) {
guard !omitZeroDecimal || Int(exactly: value) == nil else {
appendLiteral("\(Int(exactly: value)!)")
return
}
let result: String = String(format: "%\(format)f", value)
appendLiteral(result)
}
}
public extension String {
static func formattedDuration(_ duration: TimeInterval, format: TimeInterval.DurationFormat = .short) -> String {
let secondsPerMinute: TimeInterval = 60
let secondsPerHour: TimeInterval = (secondsPerMinute * 60)
let secondsPerDay: TimeInterval = (secondsPerHour * 24)
let secondsPerWeek: TimeInterval = (secondsPerDay * 7)
switch format {
case .videoDuration:
let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60))
let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let hours: Int = Int(duration / 3600)
guard hours > 0 else { return String(format: "%02ld:%02ld", minutes, seconds) }
return String(format: "%ld:%02ld:%02ld", hours, minutes, seconds)
case .hoursMinutesSeconds:
let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60))
let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let hours: Int = Int(duration / 3600)
guard hours > 0 else { return String(format: "%ld:%02ld", minutes, seconds) }
return String(format: "%ld:%02ld:%02ld", hours, minutes, seconds)
case .short:
switch duration {
case 0..<secondsPerMinute: // Seconds
return String(
format: "TIME_AMOUNT_SECONDS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration)),
number: .none
)
)
case secondsPerMinute..<secondsPerHour: // Minutes
return String(
format: "TIME_AMOUNT_MINUTES_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerMinute)),
number: .none
)
)
case secondsPerHour..<secondsPerDay: // Hours
return String(
format: "TIME_AMOUNT_HOURS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerHour)),
number: .none
)
)
case secondsPerDay..<secondsPerWeek: // Days
return String(
format: "TIME_AMOUNT_DAYS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerDay)),
number: .none
)
)
default: // Weeks
return String(
format: "TIME_AMOUNT_WEEKS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerWeek)),
number: .none
)
)
}
case .long:
switch duration {
case 0..<secondsPerMinute: // XX Seconds
return String(
format: "TIME_AMOUNT_SECONDS".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration)),
number: .none
)
)
case secondsPerMinute..<(secondsPerMinute * 1.5): // 1 Minute
return String(
format: "TIME_AMOUNT_SINGLE_MINUTE".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerMinute)),
number: .none
)
)
case (secondsPerMinute * 1.5)..<secondsPerHour: // Multiple Minutes
return String(
format: "TIME_AMOUNT_MINUTES".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerMinute)),
number: .none
)
)
case secondsPerHour..<(secondsPerHour * 1.5): // 1 Hour
return String(
format: "TIME_AMOUNT_SINGLE_HOUR".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerHour)),
number: .none
)
)
case (secondsPerHour * 1.5)..<secondsPerDay: // Multiple Hours
return String(
format: "TIME_AMOUNT_HOURS".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerHour)),
number: .none
)
)
case secondsPerDay..<(secondsPerDay * 1.5): // 1 Day
return String(
format: "TIME_AMOUNT_SINGLE_DAY".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerDay)),
number: .none
)
)
case (secondsPerDay * 1.5)..<secondsPerWeek: // Multiple Days
return String(
format: "TIME_AMOUNT_DAYS".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerDay)),
number: .none
)
)
case secondsPerWeek..<(secondsPerWeek * 1.5): // 1 Week
return String(
format: "TIME_AMOUNT_SINGLE_WEEK".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerWeek)),
number: .none
)
)
default: // Multiple Weeks
return String(
format: "TIME_AMOUNT_WEEKS".localized(),
NumberFormatter.localizedString(
from: NSNumber(floatLiteral: floor(duration / secondsPerWeek)),
number: .none
)
)
}
case .twoUnits:
let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60))
let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let hours: Int = Int((duration / 3600).truncatingRemainder(dividingBy: 24))
let days: Int = Int((duration / 3600 / 24).truncatingRemainder(dividingBy: 7))
let weeks: Int = Int(duration / 3600 / 24 / 7)
guard weeks == 0 else {
return String(
format: "TIME_AMOUNT_WEEKS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: weeks),
number: .none
)
) + " " + String(
format: "TIME_AMOUNT_DAYS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: days),
number: .none
)
)
}
guard days == 0 else {
return String(
format: "TIME_AMOUNT_DAYS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: days),
number: .none
)
) + " " + String(
format: "TIME_AMOUNT_HOURS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: hours),
number: .none
)
)
}
guard hours == 0 else {
return String(
format: "TIME_AMOUNT_HOURS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: hours),
number: .none
)
) + " " + String(
format: "TIME_AMOUNT_MINUTES_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: minutes),
number: .none
)
)
}
guard minutes == 0 else {
return String(
format: "TIME_AMOUNT_MINUTES_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: minutes),
number: .none
)
) + " " + String(
format: "TIME_AMOUNT_SECONDS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: seconds),
number: .none
)
)
}
return String(
format: "TIME_AMOUNT_SECONDS_SHORT_FORMAT".localized(),
NumberFormatter.localizedString(
from: NSNumber(integerLiteral: seconds),
number: .none
)
)
}
}
}
// MARK: - Unicode Handling
private extension CharacterSet {
static let bidiLeftToRightIsolate: String.UTF16View.Element = 0x2066
static let bidiRightToLeftIsolate: String.UTF16View.Element = 0x2067
static let bidiFirstStrongIsolate: String.UTF16View.Element = 0x2068
static let bidiLeftToRightEmbedding: String.UTF16View.Element = 0x202A
static let bidiRightToLeftEmbedding: String.UTF16View.Element = 0x202B
static let bidiLeftToRightOverride: String.UTF16View.Element = 0x202D
static let bidiRightToLeftOverride: String.UTF16View.Element = 0x202E
static let bidiPopDirectionalFormatting: String.UTF16View.Element = 0x202C
static let bidiPopDirectionalIsolate: String.UTF16View.Element = 0x2069
static let bidiControlCharacterSet: CharacterSet = {
return CharacterSet(charactersIn: "\(bidiLeftToRightIsolate)\(bidiRightToLeftIsolate)\(bidiFirstStrongIsolate)\(bidiLeftToRightEmbedding)\(bidiRightToLeftEmbedding)\(bidiLeftToRightOverride)\(bidiRightToLeftOverride)\(bidiPopDirectionalFormatting)\(bidiPopDirectionalIsolate)")
}()
static let unsafeFilenameCharacterSet: CharacterSet = CharacterSet(charactersIn: "\u{202D}\u{202E}")
static let nonPrintingCharacterSet: CharacterSet = {
var result: CharacterSet = .whitespacesAndNewlines
result.formUnion(.controlCharacters)
result.formUnion(bidiControlCharacterSet)
// Left-to-right and Right-to-left marks.
result.formUnion(CharacterSet(charactersIn: "\u{200E}\u{200f}"))
return result;
}()
}
public extension String {
var filteredForDisplay: String {
self.stripped
.filterForExcessiveDiacriticals
.ensureBalancedBidiControlCharacters
}
var filteredFilename: String {
self.stripped
.filterForExcessiveDiacriticals
.filterUnsafeFilenameCharacters
}
var stripped: String {
// If string has no printing characters, consider it empty
guard self.trimmingCharacters(in: .nonPrintingCharacterSet).count > 0 else {
return ""
}
return self.trimmingCharacters(in: .whitespacesAndNewlines)
}
private var hasExcessiveDiacriticals: Bool {
for char in self.enumerated() {
let scalarCount = String(char.element).unicodeScalars.count
if scalarCount > 8 {
return true
}
}
return false
}
private var filterForExcessiveDiacriticals: String {
guard hasExcessiveDiacriticals else { return self }
return self.folding(options: .diacriticInsensitive, locale: .current)
}
private var ensureBalancedBidiControlCharacters: String {
var isolateStartsCount: Int = 0
var isolatePopCount: Int = 0
var formattingStartsCount: Int = 0
var formattingPopCount: Int = 0
self.utf16.forEach { char in
switch char {
case CharacterSet.bidiLeftToRightIsolate, CharacterSet.bidiRightToLeftIsolate,
CharacterSet.bidiFirstStrongIsolate:
isolateStartsCount += 1
case CharacterSet.bidiPopDirectionalIsolate: isolatePopCount += 1
case CharacterSet.bidiLeftToRightEmbedding, CharacterSet.bidiRightToLeftEmbedding,
CharacterSet.bidiLeftToRightOverride, CharacterSet.bidiRightToLeftOverride:
formattingStartsCount += 1
case CharacterSet.bidiPopDirectionalFormatting: formattingPopCount += 1
default: break
}
}
var balancedString: String = ""
// If we have too many isolate pops, prepend FSI to balance
while isolatePopCount > isolateStartsCount {
balancedString.append("\(CharacterSet.bidiFirstStrongIsolate)")
isolateStartsCount += 1
}
// If we have too many formatting pops, prepend LRE to balance
while formattingPopCount > formattingStartsCount {
balancedString.append("\(CharacterSet.bidiLeftToRightEmbedding)")
formattingStartsCount += 1
}
balancedString.append(self)
// If we have too many formatting starts, append PDF to balance
while formattingStartsCount > formattingPopCount {
balancedString.append("\(CharacterSet.bidiPopDirectionalFormatting)")
formattingPopCount += 1
}
// If we have too many isolate starts, append PDI to balance
while isolateStartsCount > isolatePopCount {
balancedString.append("\(CharacterSet.bidiPopDirectionalIsolate)")
isolatePopCount += 1
}
return balancedString
}
private var filterUnsafeFilenameCharacters: String {
Merge remote-tracking branch 'origin/feature/swift-package-manager' into feature/groups-rebuild # Conflicts: # Podfile # Podfile.lock # Session.xcodeproj/project.pbxproj # Session/Calls/Call Management/SessionCall.swift # Session/Calls/Call Management/SessionCallManager.swift # Session/Calls/CallVC.swift # Session/Conversations/ConversationVC+Interaction.swift # Session/Conversations/ConversationVC.swift # Session/Conversations/ConversationViewModel.swift # Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift # Session/Conversations/Settings/ThreadSettingsViewModel.swift # Session/Emoji/Emoji+Available.swift # Session/Home/GlobalSearch/GlobalSearchViewController.swift # Session/Home/HomeVC.swift # Session/Home/HomeViewModel.swift # Session/Home/New Conversation/NewDMVC.swift # Session/Media Viewing & Editing/DocumentTitleViewController.swift # Session/Media Viewing & Editing/GIFs/GifPickerCell.swift # Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift # Session/Media Viewing & Editing/ImagePickerController.swift # Session/Media Viewing & Editing/MediaTileViewController.swift # Session/Media Viewing & Editing/PhotoCapture.swift # Session/Media Viewing & Editing/PhotoCaptureViewController.swift # Session/Media Viewing & Editing/PhotoLibrary.swift # Session/Media Viewing & Editing/SendMediaNavigationController.swift # Session/Meta/AppDelegate.swift # Session/Meta/AppEnvironment.swift # Session/Meta/MainAppContext.swift # Session/Meta/SessionApp.swift # Session/Notifications/NotificationPresenter.swift # Session/Notifications/PushRegistrationManager.swift # Session/Notifications/SyncPushTokensJob.swift # Session/Notifications/UserNotificationsAdaptee.swift # Session/Onboarding/LandingVC.swift # Session/Onboarding/LinkDeviceVC.swift # Session/Onboarding/Onboarding.swift # Session/Onboarding/RegisterVC.swift # Session/Onboarding/RestoreVC.swift # Session/Settings/HelpViewModel.swift # Session/Settings/NukeDataModal.swift # Session/Shared/FullConversationCell.swift # Session/Shared/OWSBezierPathView.m # Session/Utilities/BackgroundPoller.swift # Session/Utilities/MockDataGenerator.swift # SessionMessagingKit/Configuration.swift # SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift # SessionMessagingKit/Database/Migrations/_004_RemoveLegacyYDB.swift # SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift # SessionMessagingKit/Database/Migrations/_015_BlockCommunityMessageRequests.swift # SessionMessagingKit/Database/Migrations/_018_DisappearingMessagesConfiguration.swift # SessionMessagingKit/Database/Models/Attachment.swift # SessionMessagingKit/Database/Models/DisappearingMessageConfiguration.swift # SessionMessagingKit/Database/Models/Interaction.swift # SessionMessagingKit/Database/Models/Profile.swift # SessionMessagingKit/Database/Models/SessionThread.swift # SessionMessagingKit/File Server/FileServerAPI.swift # SessionMessagingKit/Jobs/AttachmentDownloadJob.swift # SessionMessagingKit/Jobs/CheckForAppUpdatesJob.swift # SessionMessagingKit/Jobs/DisappearingMessagesJob.swift # SessionMessagingKit/Jobs/FailedMessageSendsJob.swift # SessionMessagingKit/Jobs/MessageSendJob.swift # SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift # SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift # SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift # SessionMessagingKit/Messages/Message.swift # SessionMessagingKit/Open Groups/Crypto/Crypto+OpenGroupAPI.swift # SessionMessagingKit/Open Groups/Models/SOGSMessage.swift # SessionMessagingKit/Open Groups/OpenGroupAPI.swift # SessionMessagingKit/Open Groups/OpenGroupManager.swift # SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ExpirationTimers.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift # SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+LegacyClosedGroups.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Sending & Receiving/MessageSender.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/SubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/Models/UnsubscribeRequest.swift # SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift # SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift # SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupAPI+Poller.swift # SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift # SessionMessagingKit/Utilities/ProfileManager.swift # SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift # SessionMessagingKitTests/LibSession/LibSessionSpec.swift # SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift # SessionMessagingKitTests/Open Groups/Models/SOGSMessageSpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift # SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift # SessionMessagingKitTests/Utilities/CryptoSMKSpec.swift # SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift # SessionNotificationServiceExtension/NSENotificationPresenter.swift # SessionNotificationServiceExtension/NotificationServiceExtension.swift # SessionShareExtension/ShareAppExtensionContext.swift # SessionShareExtension/ShareNavController.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/Crypto/Crypto+SessionSnodeKit.swift # SessionSnodeKit/Models/DeleteAllBeforeResponse.swift # SessionSnodeKit/Models/DeleteAllMessagesResponse.swift # SessionSnodeKit/Models/DeleteMessagesResponse.swift # SessionSnodeKit/Models/RevokeSubkeyRequest.swift # SessionSnodeKit/Models/RevokeSubkeyResponse.swift # SessionSnodeKit/Models/SendMessageResponse.swift # SessionSnodeKit/Models/SnodeAuthenticatedRequestBody.swift # SessionSnodeKit/Models/UpdateExpiryAllResponse.swift # SessionSnodeKit/Models/UpdateExpiryResponse.swift # SessionSnodeKit/Networking/SnodeAPI.swift # SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift # SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift # SessionTests/Database/DatabaseSpec.swift # SessionTests/Settings/NotificationContentViewModelSpec.swift # SessionUIKit/Components/ToastController.swift # SessionUIKit/Style Guide/Values.swift # SessionUtilitiesKit/Crypto/Crypto+SessionUtilitiesKit.swift # SessionUtilitiesKit/Crypto/Crypto.swift # SessionUtilitiesKit/Database/Models/Identity.swift # SessionUtilitiesKit/Database/Models/Job.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Database/Types/Migration.swift # SessionUtilitiesKit/General/AppContext.swift # SessionUtilitiesKit/General/Data+Utilities.swift # SessionUtilitiesKit/General/Logging.swift # SessionUtilitiesKit/General/SNUserDefaults.swift # SessionUtilitiesKit/General/String+Trimming.swift # SessionUtilitiesKit/General/String+Utilities.swift # SessionUtilitiesKit/General/TimestampUtils.swift # SessionUtilitiesKit/General/UIEdgeInsets.swift # SessionUtilitiesKit/JobRunner/JobRunner.swift # SessionUtilitiesKit/LibSession/LibSessionError.swift # SessionUtilitiesKit/Media/DataSource.swift # SessionUtilitiesKit/Meta/SessionUtilitiesKit.h # SessionUtilitiesKit/Networking/NetworkType.swift # SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift # SessionUtilitiesKit/Utilities/BackgroundTaskManager.swift # SessionUtilitiesKit/Utilities/BencodeResponse.swift # SessionUtilitiesKit/Utilities/CExceptionHelper.mm # SessionUtilitiesKit/Utilities/FileManagerType.swift # SessionUtilitiesKit/Utilities/KeychainStorageType.swift # SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift # SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift # SessionUtilitiesKitTests/General/SessionIdSpec.swift # SessionUtilitiesKitTests/JobRunner/JobRunnerSpec.swift # SignalUtilitiesKit/Configuration.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCropViewController.swift # SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorModel.swift # SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift # SignalUtilitiesKit/Meta/SignalUtilitiesKit.h # SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift # SignalUtilitiesKit/Shared Views/CircleView.swift # SignalUtilitiesKit/Shared Views/TappableView.swift # SignalUtilitiesKit/Utilities/AppSetup.swift # SignalUtilitiesKit/Utilities/Bench.swift # SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift # _SharedTestUtilities/CommonMockedExtensions.swift # _SharedTestUtilities/MockCrypto.swift # _SharedTestUtilities/Mocked.swift # _SharedTestUtilities/SynchronousStorage.swift
10 months ago
let unsafeCharacterSet: CharacterSet = CharacterSet.unsafeFilenameCharacterSet
guard self.rangeOfCharacter(from: unsafeCharacterSet) != nil else { return self }
var filtered = ""
var remainder = self
while let range = remainder.rangeOfCharacter(from: unsafeCharacterSet) {
if range.lowerBound != remainder.startIndex {
filtered += remainder[..<range.lowerBound]
}
// The "replacement" code point.
filtered += "\u{FFFD}"
remainder = String(remainder[range.upperBound...])
}
filtered += remainder
return filtered
}
}