From f9d9954cf5771bbd0a00747a6e711cdf13cef75a Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 24 May 2024 18:22:16 +1000 Subject: [PATCH] Attempts to fix more crashes and fix unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fixed an index out of bounds issue with the mnemonic • Attempt to fix a crash due to failing to hash SignalAttachment • Attempt to fix a crash due to list data change inconsistencies • Ensuring we are shutting down the network when resetting app data • Updated the broken unit tests --- Session.xcodeproj/project.pbxproj | 8 +- .../Conversations/ConversationViewModel.swift | 9 +- Session/Home/HomeViewModel.swift | 4 +- .../MediaGalleryViewModel.swift | 2 +- Session/Meta/AppDelegate.swift | 2 +- Session/Meta/SessionApp.swift | 2 + .../Settings/BlockedContactsViewModel.swift | 2 +- Session/Settings/NukeDataModal.swift | 3 - .../Open Groups/OpenGroupManager.swift | 67 +++++---- .../Attachments/SignalAttachment.swift | 13 +- .../Pollers/OpenGroupPoller.swift | 7 +- .../LibSession/LibSessionUtilSpec.swift | 114 +++++++-------- ...ibSessionTypeConversionUtilitiesSpec.swift | 24 +--- .../Open Groups/OpenGroupManagerSpec.swift | 130 +++++------------- .../_TestUtilities/MockOGMCache.swift | 15 +- .../_TestUtilities/MockOGPoller.swift | 16 +++ SessionUtilitiesKit/Crypto/Mnemonic.swift | 11 +- .../Types/PagedDatabaseObserver.swift | 27 ++-- 18 files changed, 221 insertions(+), 235 deletions(-) create mode 100644 SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index ea176fdf4..086545ac3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -809,6 +809,7 @@ FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; }; FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; }; FDC0F0042BFECE12002CBFB9 /* TimeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */; }; + FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */; }; FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; }; FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; }; FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; }; @@ -1996,6 +1997,7 @@ FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = ""; }; FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = ""; }; FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUnit.swift; sourceTree = ""; }; + FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGPoller.swift; sourceTree = ""; }; FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = ""; }; FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = ""; }; @@ -4434,6 +4436,7 @@ children = ( FDC438BC27BB2AB400C60D73 /* Mockable.swift */, FD078E4C27E17156000769AF /* MockOGMCache.swift */, + FDC0F0072C00721A002CBFB9 /* MockOGPoller.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -6614,6 +6617,7 @@ FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */, FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */, FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */, + FDC0F0082C00721A002CBFB9 /* MockOGPoller.swift in Sources */, FDC290A627D860CE005DAE71 /* Mock.swift in Sources */, FDA1E83D29AC71A800C5C3BD /* LibSessionSpec.swift in Sources */, FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */, @@ -7981,7 +7985,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 446; + CURRENT_PROJECT_VERSION = 447; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -8059,7 +8063,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 446; + CURRENT_PROJECT_VERSION = 447; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index b91b9a744..c84169093 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -416,7 +416,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { initialUnreadInteractionId: self?.initialUnreadInteractionId ), currentDataRetriever: { self?.interactionData }, - onDataChange: self?.onInteractionChange, + onDataChangeRetriever: { self?.onInteractionChange }, onUnobservedDataChange: { updatedData, changeset in self?.unobservedInteractionDataChanges = (changeset.isEmpty ? nil : @@ -671,6 +671,11 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { } private func forceUpdateDataIfPossible() { + // Ensure this is on the main thread as we access properties that could be accessed on other threads + guard Thread.isMainThread else { + return DispatchQueue.main.async { [weak self] in self?.forceUpdateDataIfPossible() } + } + // If we can't get the current page data then don't bother trying to update (it's not going to work) guard let currentPageInfo: PagedData.PageInfo = self.pagedDataObserver?.pageInfo.wrappedValue else { return } @@ -685,7 +690,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { initialUnreadInteractionId: initialUnreadInteractionId ), currentDataRetriever: { [weak self] in self?.interactionData }, - onDataChange: self.onInteractionChange, + onDataChangeRetriever: { [weak self] in self?.onInteractionChange }, onUnobservedDataChange: { [weak self] updatedData, changeset in self?.unobservedInteractionDataChanges = (changeset.isEmpty ? nil : diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 9ce940a93..3c20a8194 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -207,7 +207,7 @@ public class HomeViewModel { PagedData.processAndTriggerUpdates( updatedData: self?.process(data: updatedData, for: updatedPageInfo), currentDataRetriever: { self?.threadData }, - onDataChange: self?.onThreadChange, + onDataChangeRetriever: { self?.onThreadChange }, onUnobservedDataChange: { updatedData, changeset in self?.unobservedThreadDataChanges = (changeset.isEmpty ? nil : @@ -286,7 +286,7 @@ public class HomeViewModel { PagedData.processAndTriggerUpdates( updatedData: updatedThreadData, currentDataRetriever: { [weak self] in (self?.unobservedThreadDataChanges?.0 ?? self?.threadData) }, - onDataChange: onThreadChange, + onDataChangeRetriever: { [weak self] in self?.onThreadChange }, onUnobservedDataChange: { [weak self] updatedData, changeset in self?.unobservedThreadDataChanges = (changeset.isEmpty ? nil : diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 75a7fdabd..7f0c4a007 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -103,7 +103,7 @@ public class MediaGalleryViewModel { PagedData.processAndTriggerUpdates( updatedData: self?.process(data: updatedData, for: updatedPageInfo), currentDataRetriever: { self?.galleryData }, - onDataChange: self?.onGalleryChange, + onDataChangeRetriever: { self?.onGalleryChange }, onUnobservedDataChange: { updatedData, changeset in self?.unobservedGalleryDataChanges = (changeset.isEmpty ? nil : diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index dca17e1fb..411bfaf15 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -493,7 +493,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD guard !Storage.isDatabasePasswordAccessible else { return } // All good - Logger.info("Exiting because we are in the background and the database password is not accessible.") + Log.info("Exiting because we are in the background and the database password is not accessible.") let notificationContent: UNMutableNotificationContent = UNMutableNotificationContent() notificationContent.body = String( diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index eb464e26b..b0b75c576 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -114,6 +114,8 @@ public struct SessionApp { public static func resetAppData(onReset: (() -> ())? = nil) { Log.flush() LibSession.clearMemoryState() + LibSession.clearSnodeCache() + LibSession.closeNetworkConnections() Storage.resetAllStorage() ProfileManager.resetProfileStorage() Attachment.resetAttachmentStorage() diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index 35a934c71..9be1c3079 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -64,7 +64,7 @@ public class BlockedContactsViewModel: SessionTableViewModel, NavigatableStateHo onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in PagedData.processAndTriggerUpdates( updatedData: self?.process(data: updatedData, for: updatedPageInfo) - .mapToSessionTableViewData(for: self), + .mapToSessionTableViewData(for: self), // Update the cell positions for background rounding currentDataRetriever: { self?.tableData }, valueSubject: self?.pendingTableDataSubject ) diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 16ed7a9b8..48c9e70a6 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -282,9 +282,6 @@ final class NukeDataModal: Modal { $0.recentReactionTimestamps = [] } - // Clear the Snode pool - LibSession.clearSnodeCache() - // Stop any pollers (UIApplication.shared.delegate as? AppDelegate)?.stopPollers() diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 1816a95cd..5f50758d6 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -41,23 +41,21 @@ public final class OpenGroupManager { // Update the cache state and re-create all of the pollers dependencies.caches.mutate(cache: .openGroupManager) { cache in cache.isPolling = true - cache.pollers = servers - .reduce(into: [:]) { result, server in - result[server.lowercased()]?.stop() // Should never occur - result[server.lowercased()] = OpenGroupAPI.Poller(for: server.lowercased()) + + servers + .map { server -> OpenGroupAPI.PollerType in cache.getOrCreatePoller(for: server.lowercased()) } + .forEach { poller in + // Now that the pollers have been created actually start them + poller.stop() // Should never be an issue + poller.startIfNeeded(using: dependencies) } } - - // Now that the pollers have been created actually start them - dependencies.caches[.openGroupManager].pollers - .forEach { _, poller in poller.startIfNeeded(using: dependencies) } } } public func stopPolling(using dependencies: Dependencies = Dependencies()) { dependencies.caches.mutate(cache: .openGroupManager) { - $0.pollers.forEach { _, openGroupPoller in openGroupPoller.stop() } - $0.pollers.removeAll() + $0.stopAndRemoveAllPollers() $0.isPolling = false } } @@ -142,7 +140,7 @@ public final class OpenGroupManager { } // First check if there is no poller for the specified server - if Set(dependencies.caches[.openGroupManager].pollers.keys).intersection(serverOptions).isEmpty { + if Set(dependencies.caches[.openGroupManager].serversBeingPolled).intersection(serverOptions).isEmpty { return false } @@ -295,16 +293,8 @@ public final class OpenGroupManager { OpenGroupAPI.workQueue.async(using: dependencies) { // (Re)start the poller if needed (want to force it to poll immediately in the next // run loop to avoid a big delay before the next poll) - let poller: OpenGroupAPI.Poller = dependencies.caches.mutate(cache: .openGroupManager) { - // Don't create a new poller instance if one already exists so we don't - // double up on pollers - guard let poller: OpenGroupAPI.Poller = $0.pollers[server.lowercased()] else { - let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server.lowercased()) - $0.pollers[server.lowercased()] = poller - return poller - } - - return poller + let poller: OpenGroupAPI.PollerType = dependencies.caches.mutate(cache: .openGroupManager) { + $0.getOrCreatePoller(for: server.lowercased()) } poller.stop() @@ -356,9 +346,9 @@ public final class OpenGroupManager { .defaulting(to: 1) if numActiveRooms == 1, let server: String = server?.lowercased() { - let poller = dependencies.caches[.openGroupManager].pollers[server] - poller?.stop() - dependencies.caches.mutate(cache: .openGroupManager) { $0.pollers[server] = nil } + dependencies.caches.mutate(cache: .openGroupManager) { + $0.stopAndRemovePoller(for: server) + } } // Remove all the data (everything should cascade delete) @@ -1230,12 +1220,31 @@ public extension OpenGroupManager { public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>? public var groupImagePublishers: [String: AnyPublisher] = [:] - public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server public var isPolling: Bool = false + public var serversBeingPolled: Set { return Set(_pollers.keys) } /// Server URL to value public var hasPerformedInitialPoll: [String: Bool] = [:] public var timeSinceLastPoll: [String: TimeInterval] = [:] + + private var _pollers: [String: OpenGroupAPI.PollerType] = [:] // One for each server + public func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType { + guard let poller: OpenGroupAPI.PollerType = _pollers[server.lowercased()] else { + let poller: OpenGroupAPI.Poller = OpenGroupAPI.Poller(for: server.lowercased()) + _pollers[server.lowercased()] = poller + return poller + } + + return poller + } + public func stopAndRemovePoller(for server: String) { + _pollers[server.lowercased()]?.stop() + _pollers[server.lowercased()] = nil + } + public func stopAndRemoveAllPollers() { + _pollers.forEach { _, poller in poller.stop() } + _pollers.removeAll() + } fileprivate var _timeSinceLastOpen: TimeInterval? public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { @@ -1272,8 +1281,8 @@ public protocol OGMImmutableCacheType: ImmutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get } var groupImagePublishers: [String: AnyPublisher] { get } - var pollers: [String: OpenGroupAPI.Poller] { get } var isPolling: Bool { get } + var serversBeingPolled: Set { get } var hasPerformedInitialPoll: [String: Bool] { get } var timeSinceLastPoll: [String: TimeInterval] { get } @@ -1285,13 +1294,17 @@ public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType { var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get set } var groupImagePublishers: [String: AnyPublisher] { get set } - var pollers: [String: OpenGroupAPI.Poller] { get set } var isPolling: Bool { get set } + var serversBeingPolled: Set { get } var hasPerformedInitialPoll: [String: Bool] { get set } var timeSinceLastPoll: [String: TimeInterval] { get set } var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType + func stopAndRemovePoller(for server: String) + func stopAndRemoveAllPollers() + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval } diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index 45ebb30d1..dec044d86 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -1133,14 +1133,21 @@ public class SignalAttachment: Equatable, Hashable { // MARK: - Hashable public func hash(into hasher: inout Hasher) { - dataSource.hash(into: &hasher) dataUTI.hash(into: &hasher) captionText.hash(into: &hasher) linkPreviewDraft.hash(into: &hasher) isConvertibleToTextMessage.hash(into: &hasher) isConvertibleToContactShare.hash(into: &hasher) - cachedImage.hash(into: &hasher) - cachedVideoPreview.hash(into: &hasher) isVoiceMessage.hash(into: &hasher) + + /// There was a crash in `AttachmentApprovalViewController when trying to generate the hash + /// value to store in a dictionary, I'm guessing it's due to either `dataSource`, `cachedImage` or `cachedVideoPreview` + /// so, instead of trying to hash them directly which involves unknown behaviours due to `NSObject` & `UIImage` types, this + /// has been reworked to use primitives + dataSource.sourceFilename.hash(into: &hasher) + cachedImage?.size.width.hash(into: &hasher) + cachedImage?.size.height.hash(into: &hasher) + cachedVideoPreview?.size.width.hash(into: &hasher) + cachedVideoPreview?.size.height.hash(into: &hasher) } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 7269e13e3..7bfc56073 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -7,7 +7,12 @@ import SessionSnodeKit import SessionUtilitiesKit extension OpenGroupAPI { - public final class Poller { + public protocol PollerType { + func startIfNeeded(using dependencies: Dependencies) + func stop() + } + + public final class Poller: PollerType { typealias PollResponse = (info: ResponseInfoType, data: [OpenGroupAPI.Endpoint: Decodable]) private let server: String diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift index 55cd0f556..6e192174e 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift @@ -305,7 +305,7 @@ fileprivate extension LibSessionUtilSpec { // Empty contacts shouldn't have an existing contact let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)! let contactPtr: UnsafeMutablePointer? = nil expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse()) @@ -374,7 +374,7 @@ fileprivate extension LibSessionUtilSpec { // Pretend we uploaded it let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)! config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) expect(config_needs_push(conf)).to(beFalse()) expect(config_needs_dump(conf)).to(beTrue()) @@ -415,7 +415,7 @@ fileprivate extension LibSessionUtilSpec { expect(contact4.created).to(equal(createdTs)) let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + var cAnotherId: [CChar] = anotherId.cString(using: .utf8)! var contact5: contacts_contact = contacts_contact() expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) expect(String(libSessionVal: contact5.name)).to(beEmpty()) @@ -435,8 +435,8 @@ fileprivate extension LibSessionUtilSpec { // Check the merging let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)! + var mergeHashes: [UnsafePointer?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? []) var mergeData: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] var mergeSize: [Int] = [pushData4.pointee.config_len] let mergedHashes: UnsafeMutablePointer? = config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1) @@ -481,7 +481,7 @@ fileprivate extension LibSessionUtilSpec { // Client 2 adds a new friend: let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222" - var cThirdId: [CChar] = thirdId.cArray.nullTerminated() + var cThirdId: [CChar] = thirdId.cString(using: .utf8)! var contact7: contacts_contact = contacts_contact() expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) contact7.nickname = "Nickname 3".toLibSession() @@ -509,13 +509,13 @@ fileprivate extension LibSessionUtilSpec { .to(equal([fakeHash2])) let fakeHash3a: String = "fakehash3a" - var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated() + var cFakeHash3a: [CChar] = fakeHash3a.cString(using: .utf8)! let fakeHash3b: String = "fakehash3b" - var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated() + var cFakeHash3b: [CChar] = fakeHash3b.cString(using: .utf8)! config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a) config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b) - var mergeHashes2: [UnsafePointer?] = [cFakeHash3b].unsafeCopy() + var mergeHashes2: [UnsafePointer?] = ((try? [cFakeHash3b].unsafeCopyCStringArray()) ?? []) var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData7.pointee.config)] var mergeSize2: [Int] = [pushData7.pointee.config_len] let mergedHashes2: UnsafeMutablePointer? = config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1) @@ -526,7 +526,7 @@ fileprivate extension LibSessionUtilSpec { mergedHashes2?.deallocate() pushData7.deallocate() - var mergeHashes3: [UnsafePointer?] = [cFakeHash3a].unsafeCopy() + var mergeHashes3: [UnsafePointer?] = ((try? [cFakeHash3a].unsafeCopyCStringArray()) ?? []) var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData6.pointee.config)] var mergeSize3: [Int] = [pushData6.pointee.config_len] let mergedHashes3: UnsafeMutablePointer? = config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1) @@ -552,7 +552,7 @@ fileprivate extension LibSessionUtilSpec { .to(equal([fakeHash3a, fakeHash3b])) let fakeHash4: String = "fakeHash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)! config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4) config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4) pushData8.deallocate() @@ -594,7 +594,7 @@ fileprivate extension LibSessionUtilSpec { ) throws -> contacts_contact { let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())" let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count)) - var cSessionId: [CChar] = sessionId.cArray.nullTerminated() + var cSessionId: [CChar] = sessionId.cString(using: .utf8)! var contact: contacts_contact = contacts_contact() guard contacts_get_or_construct(conf, &contact, &cSessionId) else { @@ -717,7 +717,7 @@ fileprivate extension LibSessionUtilSpec { let pushData2: UnsafeMutablePointer = config_push(conf) expect(pushData2.pointee.seqno).to(equal(1)) - let expPush1Encrypted: [UInt8] = Data(hex: [ + let expPush1Encrypted: [UInt8] = Array(Data(hex: [ "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f", "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84", "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868", @@ -725,7 +725,7 @@ fileprivate extension LibSessionUtilSpec { "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656", "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea", "49bf122762d7bc1d6d9c02f6d54f8384" - ].joined()).bytes + ].joined())) // We haven't dumped, so still need to dump: expect(config_needs_dump(conf)).to(beTrue()) @@ -743,7 +743,7 @@ fileprivate extension LibSessionUtilSpec { // So now imagine we got back confirmation from the swarm that the push has been stored: let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)! config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) pushData2.deallocate() @@ -768,8 +768,8 @@ fileprivate extension LibSessionUtilSpec { // Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into // conf2: - var mergeHashes: [UnsafePointer?] = [cFakeHash1].unsafeCopy() - var mergeData: [UnsafePointer?] = [expPush1Encrypted].unsafeCopy() + var mergeHashes: [UnsafePointer?] = ((try? [cFakeHash1].unsafeCopyCStringArray()) ?? []) + var mergeData: [UnsafePointer?] = ((try? [expPush1Encrypted].unsafeCopyUInt8Array()) ?? []) var mergeSize: [Int] = [expPush1Encrypted.count] let mergedHashes: UnsafeMutablePointer? = config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1) expect([String](pointer: mergedHashes?.pointee.value, count: mergedHashes?.pointee.len)) @@ -820,13 +820,13 @@ fileprivate extension LibSessionUtilSpec { expect(config_needs_push(conf2)).to(beTrue()) let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)! let pushData3: UnsafeMutablePointer = config_push(conf) expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2) let fakeHash3: String = "fakehash3" - var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() + var cFakeHash3: [CChar] = fakeHash3.cString(using: .utf8)! let pushData4: UnsafeMutablePointer = config_push(conf2) expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3) @@ -852,7 +852,7 @@ fileprivate extension LibSessionUtilSpec { // Feed the new config into each other. (This array could hold multiple configs if we pulled // down more than one). - var mergeHashes2: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeHashes2: [UnsafePointer?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? []) var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData3.pointee.config)] var mergeSize2: [Int] = [pushData3.pointee.config_len] let mergedHashes2: UnsafeMutablePointer? = config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1) @@ -862,7 +862,7 @@ fileprivate extension LibSessionUtilSpec { mergedHashes2?.deallocate() pushData3.deallocate() - var mergeHashes3: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeHashes3: [UnsafePointer?] = ((try? [cFakeHash3].unsafeCopyCStringArray()) ?? []) var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] var mergeSize3: [Int] = [pushData4.pointee.config_len] let mergedHashes3: UnsafeMutablePointer? = config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1) @@ -909,9 +909,9 @@ fileprivate extension LibSessionUtilSpec { expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1)) let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)! let fakeHash5: String = "fakehash5" - var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated() + var cFakeHash5: [CChar] = fakeHash5.cString(using: .utf8)! config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4) config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5) pushData5.deallocate() @@ -967,7 +967,7 @@ fileprivate extension LibSessionUtilSpec { // Empty contacts shouldn't have an existing contact let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)! var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1() expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse()) expect(convo_info_volatile_size(conf)).to(equal(0)) @@ -1001,7 +1001,7 @@ fileprivate extension LibSessionUtilSpec { expect(config_needs_dump(conf)).to(beTrue()) let openGroupBaseUrl: String = "http://Example.ORG:5678" - var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated() + var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cString(using: .utf8)! let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased() // ("http://Example.ORG:5678" // .lowercased() @@ -1009,7 +1009,7 @@ fileprivate extension LibSessionUtilSpec { // [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count)) // ) let openGroupRoom: String = "SudokuRoom" - var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated() + var cOpenGroupRoom: [CChar] = openGroupRoom.cString(using: .utf8)! let openGroupRoomResult: String = openGroupRoom.lowercased() // ("SudokuRoom" // .lowercased() @@ -1036,7 +1036,7 @@ fileprivate extension LibSessionUtilSpec { // Pretend we uploaded it let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)! config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1) expect(config_needs_dump(conf)).to(beTrue()) expect(config_needs_push(conf)).to(beFalse()) @@ -1070,14 +1070,14 @@ fileprivate extension LibSessionUtilSpec { community2.unread = true let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + var cAnotherId: [CChar] = anotherId.cString(using: .utf8)! var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1() expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue()) oneToOne5.unread = true convo_info_volatile_set_1to1(conf2, &oneToOne5) let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - var cThirdId: [CChar] = thirdId.cArray.nullTerminated() + var cThirdId: [CChar] = thirdId.cString(using: .utf8)! var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue()) legacyGroup2.last_read = (nowTimestampMs - 50) @@ -1089,8 +1089,8 @@ fileprivate extension LibSessionUtilSpec { // Check the merging let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)! + var mergeHashes: [UnsafePointer?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? []) var mergeData: [UnsafePointer?] = [UnsafePointer(pushData2.pointee.config)] var mergeSize: [Int] = [pushData2.pointee.config_len] let mergedHashes: UnsafeMutablePointer? = config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1) @@ -1141,7 +1141,7 @@ fileprivate extension LibSessionUtilSpec { } let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000" - var cFourthId: [CChar] = fourthId.cArray.nullTerminated() + var cFourthId: [CChar] = fourthId.cString(using: .utf8)! expect(config_needs_push(conf)).to(beFalse()) convo_info_volatile_erase_1to1(conf, &cFourthId) expect(config_needs_push(conf)).to(beFalse()) @@ -1231,7 +1231,7 @@ fileprivate extension LibSessionUtilSpec { // Empty contacts shouldn't have an existing contact let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + var cDefinitelyRealId: [CChar] = definitelyRealId.cString(using: .utf8)! let legacyGroup1: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) expect(legacyGroup1?.pointee).to(beNil()) expect(user_groups_size(conf)).to(equal(0)) @@ -1285,7 +1285,7 @@ fileprivate extension LibSessionUtilSpec { "055555555555555555555555555555555555555555555555555555555555555555", "056666666666666666666666666666666666666666666666666666666666666666" ] - var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() } + var cUsers: [[CChar]] = users.map { $0.cString(using: .utf8)! } legacyGroup2.pointee.name = "Englishmen".toLibSession() legacyGroup2.pointee.disappearing_timer = 60 legacyGroup2.pointee.joined_at = createdTs @@ -1348,9 +1348,9 @@ fileprivate extension LibSessionUtilSpec { ugroups_legacy_group_free(legacyGroup3) let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray - var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated() - var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated() + var cCommunityPubkey: [UInt8] = Array(Data(hex: communityPubkey)) + var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cString(using: .utf8)! + var cCommunityRoom: [CChar] = "SudokuRoom".cString(using: .utf8)! var community1: ugroups_community_info = ugroups_community_info() expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) .to(beTrue()) @@ -1373,7 +1373,7 @@ fileprivate extension LibSessionUtilSpec { // Pretend we uploaded it let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + var cFakeHash1: [CChar] = fakeHash1.cString(using: .utf8)! config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) expect(config_needs_dump(conf)).to(beTrue()) expect(config_needs_push(conf)).to(beFalse()) @@ -1492,8 +1492,8 @@ fileprivate extension LibSessionUtilSpec { ])) } - var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated() + var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cString(using: .utf8)! + var cCommunity2Room: [CChar] = "sudokuRoom".cString(using: .utf8)! var community2: ugroups_community_info = ugroups_community_info() expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) .to(beTrue()) @@ -1518,7 +1518,7 @@ fileprivate extension LibSessionUtilSpec { expect(config_needs_dump(conf2)).to(beTrue()) let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var cFakeHash2: [CChar] = fakeHash2.cString(using: .utf8)! let pushData7: UnsafeMutablePointer = config_push(conf2) expect(pushData7.pointee.seqno).to(equal(2)) config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2) @@ -1542,7 +1542,7 @@ fileprivate extension LibSessionUtilSpec { config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2) expect(config_needs_dump(conf2)).to(beFalse()) - var mergeHashes1: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeHashes1: [UnsafePointer?] = ((try? [cFakeHash2].unsafeCopyCStringArray()) ?? []) var mergeData1: [UnsafePointer?] = [UnsafePointer(pushData8.pointee.config)] var mergeSize1: [Int] = [pushData8.pointee.config_len] let mergedHashes1: UnsafeMutablePointer? = config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1) @@ -1552,8 +1552,8 @@ fileprivate extension LibSessionUtilSpec { mergedHashes1?.deallocate() pushData8.deallocate() - var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated() + var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cString(using: .utf8)! + var cCommunity3Room: [CChar] = "SudokuRoom".cString(using: .utf8)! var community3: ugroups_community_info = ugroups_community_info() expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) .to(beTrue()) @@ -1581,12 +1581,12 @@ fileprivate extension LibSessionUtilSpec { expect(config_needs_push(conf2)).to(beTrue()) expect(config_needs_dump(conf2)).to(beTrue()) - var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated() - var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated() + var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cString(using: .utf8)! + var cCommunity4Room: [CChar] = "sudokuROOM".cString(using: .utf8)! user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room) let fakeHash3: String = "fakehash3" - var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() + var cFakeHash3: [CChar] = fakeHash3.cString(using: .utf8)! let pushData10: UnsafeMutablePointer = config_push(conf2) config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3) @@ -1599,7 +1599,7 @@ fileprivate extension LibSessionUtilSpec { .to(equal([fakeHash3])) currentHashes4?.deallocate() - var mergeHashes2: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeHashes2: [UnsafePointer?] = ((try? [cFakeHash3].unsafeCopyCStringArray()) ?? []) var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData10.pointee.config)] var mergeSize2: [Int] = [pushData10.pointee.config_len] let mergedHashes2: UnsafeMutablePointer? = config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1) @@ -1613,13 +1613,13 @@ fileprivate extension LibSessionUtilSpec { expect(user_groups_size_legacy_groups(conf)).to(equal(1)) var prio: Int32 = 0 - var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated() - var cBeanstalkPubkey: [UInt8] = Data( + var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cString(using: .utf8)! + var cBeanstalkPubkey: [UInt8] = Array(Data( hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" - ).cArray + )) ["fee", "fi", "fo", "fum"].forEach { room in - var cRoom: [CChar] = room.cArray.nullTerminated() + var cRoom: [CChar] = room.cString(using: .utf8)! prio += 1 var community4: ugroups_community_info = ugroups_community_info() @@ -1634,7 +1634,7 @@ fileprivate extension LibSessionUtilSpec { expect(user_groups_size_legacy_groups(conf)).to(equal(1)) let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + var cFakeHash4: [CChar] = fakeHash4.cString(using: .utf8)! let pushData11: UnsafeMutablePointer = config_push(conf) config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4) expect(pushData11.pointee.seqno).to(equal(4)) @@ -1643,12 +1643,12 @@ fileprivate extension LibSessionUtilSpec { // Load some obsolete ones in just to check that they get immediately obsoleted let fakeHash10: String = "fakehash10" - let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated() + let cFakeHash10: [CChar] = fakeHash10.cString(using: .utf8)! let fakeHash11: String = "fakehash11" - let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated() + let cFakeHash11: [CChar] = fakeHash11.cString(using: .utf8)! let fakeHash12: String = "fakehash12" - let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated() - var mergeHashes3: [UnsafePointer?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy() + let cFakeHash12: [CChar] = fakeHash12.cString(using: .utf8)! + var mergeHashes3: [UnsafePointer?] = ((try? [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopyCStringArray()) ?? []) var mergeData3: [UnsafePointer?] = [ UnsafePointer(pushData10.pointee.config), UnsafePointer(pushData2.pointee.config), diff --git a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift b/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift index 313372b5f..c1624604e 100644 --- a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift +++ b/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift @@ -12,11 +12,6 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { override class func spec() { // MARK: - a String describe("a String") { - // MARK: -- can convert to a cArray - it("can convert to a cArray") { - expect("Test123".cArray).to(equal([84, 101, 115, 116, 49, 50, 51])) - } - // MARK: -- can contain emoji it("can contain emoji") { let original: String = "Hi 👋" @@ -187,11 +182,6 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { // MARK: - Data describe("Data") { - // MARK: -- can convert to a cArray - it("can convert to a cArray") { - expect(Data([1, 2, 3]).cArray).to(equal([1, 2, 3])) - } - // MARK: -- when initialised with a libSession value context("when initialised with a libSession value") { // MARK: ---- returns truncated data when given the wrong length @@ -297,9 +287,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { // MARK: ---- returns the correct array it("returns the correct array") { var test: [CChar] = ( - "Test1".cArray.nullTerminated() + - "Test2".cArray.nullTerminated() + - "Test3AndExtra".cArray.nullTerminated() + "Test1".cString(using: .utf8)! + + "Test2".cString(using: .utf8)! + + "Test3AndExtra".cString(using: .utf8)! ) let result = test.withUnsafeMutableBufferPointer { ptr in var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) @@ -325,9 +315,9 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { // MARK: ---- handles empty strings without issues it("handles empty strings without issues") { var test: [CChar] = ( - "Test1".cArray.nullTerminated() + - "".cArray.nullTerminated() + - "Test2".cArray.nullTerminated() + "Test1".cString(using: .utf8)! + + "".cString(using: .utf8)! + + "Test2".cString(using: .utf8)! ) let result = test.withUnsafeMutableBufferPointer { ptr in var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) @@ -345,7 +335,7 @@ class LibSessionTypeConversionUtilitiesSpec: QuickSpec { // MARK: ---- returns null when given a null count it("returns null when given a null count") { - var test: [CChar] = "Test1".cArray.nullTerminated() + var test: [CChar] = "Test1".cString(using: .utf8)! let result = test.withUnsafeMutableBufferPointer { ptr in var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index af505b931..65948b162 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -174,11 +174,20 @@ class OpenGroupManagerSpec: QuickSpec { cache.when { $0.encodedPublicKey }.thenReturn("05\(TestConstants.publicKey)") } ) + @TestState var mockPoller: MockOGPoller! = MockOGPoller( + initialSetup: { poller in + poller.when { $0.stop() }.thenReturn(()) + poller.when { $0.startIfNeeded(using: any()) }.thenReturn(()) + } + ) @TestState var mockOGMCache: MockOGMCache! = MockOGMCache( initialSetup: { cache in cache.when { $0.pendingChanges }.thenReturn([]) - cache.when { $0.pollers = any() }.thenReturn(()) cache.when { $0.isPolling = any() }.thenReturn(()) + cache.when { $0.serversBeingPolled }.thenReturn([]) + cache.when { $0.hasPerformedInitialPoll }.thenReturn([:]) + cache.when { $0.timeSinceLastPoll }.thenReturn([:]) + cache.when { $0.getTimeSinceLastOpen(using: any()) }.thenReturn(0) cache .when { $0.defaultRoomsPublisher = any(type: [OpenGroupManager.DefaultRoomInfo].self) } .thenReturn(()) @@ -188,6 +197,9 @@ class OpenGroupManagerSpec: QuickSpec { cache .when { $0.pendingChanges = any(type: OpenGroupAPI.PendingChange.self) } .thenReturn(()) + cache.when { $0.getOrCreatePoller(for: any()) }.thenReturn(mockPoller) + cache.when { $0.stopAndRemovePoller(for: any()) }.thenReturn(()) + cache.when { $0.stopAndRemoveAllPollers() }.thenReturn(()) } ) @TestState var mockCaches: MockCaches! = MockCaches() @@ -209,13 +221,6 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: - an OpenGroupManager describe("an OpenGroupManager") { - - afterEach { - // Just in case the shared instance had pollers created we should stop them - OpenGroupManager.shared.stopPolling() - openGroupManager.stopPolling() - } - // MARK: -- cache data context("cache data") { // MARK: ---- defaults the time since last open to greatestFiniteMagnitude @@ -293,7 +298,6 @@ class OpenGroupManagerSpec: QuickSpec { } .thenReturn(0) mockOGMCache.when { $0.isPolling }.thenReturn(false) - mockOGMCache.when { $0.pollers }.thenReturn([:]) mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -308,10 +312,11 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.pollers = [ - "testserver": OpenGroupAPI.Poller(for: "testserver"), - "testserver1": OpenGroupAPI.Poller(for: "testserver1") - ] + $0.getOrCreatePoller(for: "testserver") + }) + expect(mockOGMCache) + .to(call(matchingParameters: true) { + $0.getOrCreatePoller(for: "testserver1") }) } @@ -329,7 +334,7 @@ class OpenGroupManagerSpec: QuickSpec { openGroupManager.startPolling(using: dependencies) - expect(mockOGMCache).toNot(call { $0.pollers }) + expect(mockOGMCache).toNot(call { $0.getOrCreatePoller(for: any()) }) } } @@ -352,14 +357,13 @@ class OpenGroupManagerSpec: QuickSpec { } mockOGMCache.when { $0.isPolling }.thenReturn(true) - mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) } // MARK: ---- removes all pollers it("removes all pollers") { openGroupManager.stopPolling(using: dependencies) - expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] }) + expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemoveAllPollers() }) } // MARK: ---- updates the isPolling flag @@ -443,13 +447,12 @@ class OpenGroupManagerSpec: QuickSpec { context("when checking it has an existing open group") { // MARK: ---- when there is a thread for the room and the cache has a poller context("when there is a thread for the room and the cache has a poller") { + beforeEach { + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) + } + // MARK: ------ for the no-scheme variant context("for the no-scheme variant") { - beforeEach { - mockOGMCache.when { $0.pollers } - .thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) - } - // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( @@ -501,11 +504,6 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ for the http variant context("for the http variant") { - beforeEach { - mockOGMCache.when { $0.pollers } - .thenReturn(["http://testserver": OpenGroupAPI.Poller(for: "http://testserver")]) - } - // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( @@ -557,11 +555,6 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ------ for the https variant context("for the https variant") { - beforeEach { - mockOGMCache.when { $0.pollers } - .thenReturn(["https://testserver": OpenGroupAPI.Poller(for: "https://testserver")]) - } - // MARK: -------- returns true when no scheme is provided it("returns true when no scheme is provided") { expect( @@ -616,8 +609,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when given the legacy DNS host and there is a cached poller for the default server") { // MARK: ------ returns true it("returns true") { - mockOGMCache.when { $0.pollers } - .thenReturn(["http://116.203.70.33": OpenGroupAPI.Poller(for: "http://116.203.70.33")]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://116.203.70.33"]) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://116.203.70.33"), @@ -651,7 +643,7 @@ class OpenGroupManagerSpec: QuickSpec { context("when given the default server and there is a cached poller for the legacy DNS host") { // MARK: ------ returns true it("returns true") { - mockOGMCache.when { $0.pollers }.thenReturn(["http://open.getsession.org": OpenGroupAPI.Poller(for: "http://open.getsession.org")]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["http://open.getsession.org"]) mockStorage.write { db in try SessionThread( id: OpenGroup.idFor(roomToken: "testRoom", server: "http://open.getsession.org"), @@ -683,8 +675,6 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- returns false when given an invalid server it("returns false when given an invalid server") { - mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) - expect( mockStorage.read { db -> Bool in openGroupManager @@ -701,7 +691,7 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- returns false if there is not a poller for the server in the cache it("returns false if there is not a poller for the server in the cache") { - mockOGMCache.when { $0.pollers }.thenReturn([:]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn([]) expect( mockStorage.read { db -> Bool in @@ -719,7 +709,6 @@ class OpenGroupManagerSpec: QuickSpec { // MARK: ---- returns false if there is a poller for the server in the cache but no thread for the room it("returns false if there is a poller for the server in the cache but no thread for the room") { - mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) mockStorage.write { db in try SessionThread.deleteAll(db) } @@ -749,7 +738,6 @@ class OpenGroupManagerSpec: QuickSpec { mockNetwork .when { $0.send(.onionRequest(any(), to: any(), with: any()), using: dependencies) } .thenReturn(Network.BatchResponse.mockCapabilitiesAndRoomResponse) - mockOGMCache.when { $0.pollers }.thenReturn([:]) mockUserDefaults .when { (defaults: inout any UserDefaultsType) -> Any? in @@ -824,15 +812,18 @@ class OpenGroupManagerSpec: QuickSpec { expect(mockOGMCache) .to(call(matchingParameters: true) { - $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")] + $0.getOrCreatePoller(for: "testserver") + }) + expect(mockPoller) + .to(call(matchingParameters: true) { [dependencies = dependencies!] in + $0.startIfNeeded(using: dependencies) }) } // MARK: ---- an existing room context("an existing room") { beforeEach { - mockOGMCache.when { $0.pollers } - .thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) + mockOGMCache.when { $0.serversBeingPolled }.thenReturn(["testserver"]) mockStorage.write { db in try testOpenGroup.insert(db) } @@ -954,8 +945,6 @@ class OpenGroupManagerSpec: QuickSpec { .set(to: OpenGroup.idFor(roomToken: "testRoom", server: "testServer")) ) } - - mockOGMCache.when { $0.pollers }.thenReturn([:]) } // MARK: ---- removes all interactions for the thread @@ -994,8 +983,6 @@ class OpenGroupManagerSpec: QuickSpec { context("and there is only one open group for this server") { // MARK: ------ stops the poller it("stops the poller") { - mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) - mockStorage.write { db in openGroupManager .delete( @@ -1006,7 +993,7 @@ class OpenGroupManagerSpec: QuickSpec { ) } - expect(mockOGMCache).to(call(matchingParameters: true) { $0.pollers = [:] }) + expect(mockOGMCache).to(call(matchingParameters: true) { $0.stopAndRemovePoller(for: "testserver") }) } // MARK: ------ removes the open group @@ -1175,7 +1162,6 @@ class OpenGroupManagerSpec: QuickSpec { try testOpenGroup.insert(db) } - mockOGMCache.when { $0.pollers }.thenReturn([:]) mockOGMCache.when { $0.hasPerformedInitialPoll }.thenReturn([:]) mockOGMCache.when { $0.timeSinceLastPoll }.thenReturn([:]) mockOGMCache @@ -1575,54 +1561,6 @@ class OpenGroupManagerSpec: QuickSpec { } } - // MARK: ---- when checking to start polling - context("when checking to start polling") { - // MARK: ------ starts a new poller when not already polling - it("starts a new poller when not already polling") { - mockOGMCache.when { $0.pollers }.thenReturn([:]) - - mockStorage.write { db in - try OpenGroupManager.handlePollInfo( - db, - pollInfo: testPollInfo, - publicKey: TestConstants.publicKey, - for: "testRoom", - on: "testServer", - using: dependencies - ) - } - - expect(mockOGMCache) - .to(call(matchingParameters: true) { - $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")] - }) - } - - // MARK: ------ restarts the poller if there already is one - it("restarts the poller if there already is one") { - mockOGMCache.when { $0.pollers }.thenReturn(["testserver": OpenGroupAPI.Poller(for: "testserver")]) - mockNetwork - .when { $0.send(.onionRequest(any(), to: any(), with: any()), using: dependencies) } - .thenReturn(Network.BatchResponse.mockBlindedPollResponse) - - mockStorage.write { db in - try OpenGroupManager.handlePollInfo( - db, - pollInfo: testPollInfo, - publicKey: TestConstants.publicKey, - for: "testRoom", - on: "testServer", - using: dependencies - ) - } - - expect(mockOGMCache) - .to(call(matchingParameters: true) { - $0.pollers = ["testserver": OpenGroupAPI.Poller(for: "testserver")] - }) - } - } - // MARK: ---- when trying to get the room image context("when trying to get the room image") { beforeEach { diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index d326d0a15..e22ff41a3 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -17,16 +17,15 @@ class MockOGMCache: Mock, OGMCacheType { set { accept(args: [newValue]) } } - var pollers: [String: OpenGroupAPI.Poller] { - get { return accept() as! [String: OpenGroupAPI.Poller] } - set { accept(args: [newValue]) } - } - var isPolling: Bool { get { return accept() as! Bool } set { accept(args: [newValue]) } } + var serversBeingPolled: Set { + get { return accept() as! Set } + } + var hasPerformedInitialPoll: [String: Bool] { get { return accept() as! [String: Bool] } set { accept(args: [newValue]) } @@ -42,6 +41,12 @@ class MockOGMCache: Mock, OGMCacheType { set { accept(args: [newValue]) } } + func getOrCreatePoller(for server: String) -> OpenGroupAPI.PollerType { + return accept(args: [server]) as! OpenGroupAPI.PollerType + } + func stopAndRemovePoller(for server: String) { accept(args: [server]) } + func stopAndRemoveAllPollers() { accept() } + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { return accept(args: [dependencies]) as! TimeInterval } diff --git a/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift b/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift new file mode 100644 index 000000000..4c7358b5d --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/MockOGPoller.swift @@ -0,0 +1,16 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +@testable import SessionMessagingKit + +class MockOGPoller: Mock, OpenGroupAPI.PollerType { + func startIfNeeded(using dependencies: Dependencies) { + accept() + } + + func stop() { + accept() + } +} diff --git a/SessionUtilitiesKit/Crypto/Mnemonic.swift b/SessionUtilitiesKit/Crypto/Mnemonic.swift index a9dbcfd0c..9dd184e5e 100644 --- a/SessionUtilitiesKit/Crypto/Mnemonic.swift +++ b/SessionUtilitiesKit/Crypto/Mnemonic.swift @@ -118,7 +118,9 @@ public enum Mnemonic { } public static func decode(mnemonic: String, language: Language = .english) throws -> String { - var words: [String] = mnemonic.components(separatedBy: .whitespacesAndNewlines) + var words: [String] = mnemonic + .components(separatedBy: .whitespacesAndNewlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } let truncatedWordSet: [String] = language.loadTruncatedWordSet() let prefixLength: Int = language.prefixLength var result = "" @@ -131,6 +133,13 @@ public enum Mnemonic { // Get checksum word let checksumWord = words.popLast()! + // Limit the words to a multiple of 3 to avoid index-out-of-bounds issues + let remainder: Int = (words.count % 3) + + if remainder > 0 { + words.removeLast(remainder) + } + // Decode for chunkStartIndex in stride(from: 0, to: words.count, by: 3) { guard diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index 7fa6df7e0..9b6f6dd3a 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -45,6 +45,9 @@ public class PagedDatabaseObserver: TransactionObserver where // MARK: - Initialization + /// Create a `PagedDatabaseObserver` which triggers the callback whenever changes occur + /// + /// **Note:** The `onChangeUnsorted` could be run on any logic may need to be shifted to the UI thread public init( pagedTable: ObservedTable.Type, pageSize: Int, @@ -766,18 +769,10 @@ public class PagedDatabaseObserver: TransactionObserver where self.dataCache.mutate { $0 = $0.upserting(items: associatedLoadedData.values) } self.pageInfo.mutate { $0 = updatedPageInfo } - let triggerUpdates: () -> () = { [weak self, dataCache = self.dataCache.wrappedValue] in - self?.onChangeUnsorted(dataCache.values, updatedPageInfo) - self?.isLoadingMoreData.mutate { $0 = false } - } - - // Make sure the updates run on the main thread - guard Thread.isMainThread else { - DispatchQueue.main.async { triggerUpdates() } - return - } - - triggerUpdates() + // Trigger the unsorted change callback (the actual UI update triggering should eventually be run on + // the main thread via the `PagedData.processAndTriggerUpdates` function) + self.onChangeUnsorted(self.dataCache.wrappedValue.values, updatedPageInfo) + self.isLoadingMoreData.mutate { $0 = false } } public func reload() { @@ -1022,7 +1017,7 @@ public enum PagedData { public static func processAndTriggerUpdates( updatedData: [SectionModel]?, currentDataRetriever: @escaping (() -> [SectionModel]?), - onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ())?, + onDataChangeRetriever: @escaping (() -> (([SectionModel], StagedChangeset<[SectionModel]>) -> ())?), onUnobservedDataChange: @escaping (([SectionModel], StagedChangeset<[SectionModel]>) -> Void) ) { guard let updatedData: [SectionModel] = updatedData else { return } @@ -1045,7 +1040,7 @@ public enum PagedData { /// /// **Note:** We do this even if the 'changeset' is empty because if this change reverts a previous change we /// need to ensure the `onUnobservedDataChange` gets cleared so it doesn't end up in an invalid state - guard let onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ()) = onDataChange else { + guard let onDataChange: (([SectionModel], StagedChangeset<[SectionModel]>) -> ()) = onDataChangeRetriever() else { onUnobservedDataChange(updatedData, changeset) return } @@ -1056,7 +1051,7 @@ public enum PagedData { onDataChange(updatedData, changeset) } - // No need to dispatch to the next run loop if we are alread on the main thread + // No need to dispatch to the next run loop if we are already on the main thread guard !Thread.isMainThread else { performUpdates() return @@ -1096,7 +1091,7 @@ public enum PagedData { valueSubject?.send((updatedData, StagedChangeset())) } - // No need to dispatch to the next run loop if we are alread on the main thread + // No need to dispatch to the next run loop if we are already on the main thread guard !Thread.isMainThread else { performUpdates() return