From d2c82cb9156fbc05c860d6168fdbfe8eabd65386 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 16 Jun 2023 19:38:14 +1000 Subject: [PATCH] Started working on some config contact pruning logic Added support for a ConfirmationModal with an input field Added a mechanism on Debug builds to export the database and it's key Added logic to catch exceptions thrown within libSession (need to actually plug it in) Added a debug-only mechanism to export the users database and (encrypted) database key Added a few unit tests to check the CONTACTS config message size constraints --- Scripts/DecryptExportedKey.swift | 20 + Session.xcodeproj/project.pbxproj | 8 + Session/Settings/HelpViewModel.swift | 158 +++ .../Migrations/_013_SessionUtilChanges.swift | 17 +- .../Database/Models/Contact.swift | 5 +- .../Database/Models/SessionThread.swift | 2 +- .../SessionUtil+Contacts.swift | 238 ++++- .../SessionUtil+ConvoInfoVolatile.swift | 138 +-- .../Configs/ConfigContactsSpec.swift | 871 +++++++++++----- .../Configs/ConfigConvoInfoVolatileSpec.swift | 480 ++++----- .../Configs/ConfigUserGroupsSpec.swift | 972 +++++++++--------- .../Configs/ConfigUserProfileSpec.swift | 748 +++++++------- .../LibSessionUtil/LibSessionSpec.swift | 10 +- .../ThreadSettingsViewModelSpec.swift | 8 +- .../Components/ConfirmationModal.swift | 141 ++- SessionUtilitiesKit/Database/Storage.swift | 36 + .../Meta/SessionUtilitiesKit.h | 1 + .../Utilities/CExceptionHelper.h | 16 + .../Utilities/CExceptionHelper.mm | 36 + 19 files changed, 2375 insertions(+), 1530 deletions(-) create mode 100644 Scripts/DecryptExportedKey.swift create mode 100644 SessionUtilitiesKit/Utilities/CExceptionHelper.h create mode 100644 SessionUtilitiesKit/Utilities/CExceptionHelper.mm diff --git a/Scripts/DecryptExportedKey.swift b/Scripts/DecryptExportedKey.swift new file mode 100644 index 000000000..d17e6ef2e --- /dev/null +++ b/Scripts/DecryptExportedKey.swift @@ -0,0 +1,20 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import CryptoKit + +let arguments = CommandLine.arguments + +// First argument is the file name +if arguments.count == 3 { + let encryptedData = Data(base64Encoded: arguments[1].data(using: .utf8)!)! + let hash: SHA256.Digest = SHA256.hash(data: arguments[2].data(using: .utf8)!) + let key: SymmetricKey = SymmetricKey(data: Data(hash.makeIterator())) + let sealedBox = try! ChaChaPoly.SealedBox(combined: encryptedData) + let decryptedData = try! ChaChaPoly.open(sealedBox, using: key) + + print(Array(decryptedData).map { String(format: "%02x", $0) }.joined()) +} +else { + print("Please provide the base64 encoded 'encrypted key' and plain text 'password' as arguments") +} diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f6e415d2b..44dae29d6 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -588,6 +588,8 @@ FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */; }; FD2B4B042949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */; }; FD3003662A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */; }; + FD30036A2A3ADEC100B5A5FB /* CExceptionHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */; }; FD368A6829DE8F9C000DBF1E /* _012_AddFTSIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */; }; FD368A6A29DE9E30000DBF1E /* UIContextualAction+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */; }; FD37E9C328A1C6F3003AE748 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */; }; @@ -1727,6 +1729,8 @@ FD2B4AFE2946C93200AB4848 /* ConfigurationSyncJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationSyncJob.swift; sourceTree = ""; }; FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = ""; }; FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMessageReceiveJob.swift; sourceTree = ""; }; + FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CExceptionHelper.h; sourceTree = ""; }; + FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CExceptionHelper.mm; sourceTree = ""; }; FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = ""; }; FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = ""; }; FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; @@ -3589,6 +3593,8 @@ isa = PBXGroup; children = ( FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, + FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */, + FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */, FD09796A27F6C67500936362 /* Failable.swift */, FD09797127FAA2F500936362 /* Optional+Utilities.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, @@ -4457,6 +4463,7 @@ B8856E1A256F1700001CE70E /* OWSMath.h in Headers */, C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */, C32C5B51256DC219003C73A2 /* NSNotificationCenter+OWS.h in Headers */, + FD30036A2A3ADEC100B5A5FB /* CExceptionHelper.h in Headers */, C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */, C32C6018256E07F9003C73A2 /* NSUserDefaults+OWS.h in Headers */, B8856D8D256F1502001CE70E /* UIView+OWS.h in Headers */, @@ -5639,6 +5646,7 @@ C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */, + FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 1f8d6f89c..e97b2e13e 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import CryptoKit import GRDB import DifferenceKit import SessionUIKit @@ -9,6 +10,10 @@ import SessionUtilitiesKit import SignalCoreKit class HelpViewModel: SessionTableViewModel { +#if DEBUG + private var databaseKeyEncryptionPassword: String = "" +#endif + // MARK: - Section public enum Section: SessionTableSection { @@ -17,6 +22,9 @@ class HelpViewModel: SessionTableViewModel= 6 else { + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Error", + body: .text("Password must be at least 6 characters") + ) + ), + transitionType: .present + ) + return + } + + do { + let exportInfo = try Storage.shared.exportInfo(password: password) + let shareVC = UIActivityViewController( + activityItems: [ + URL(fileURLWithPath: exportInfo.dbPath), + URL(fileURLWithPath: exportInfo.keyPath) + ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { [weak self] _, completed, _, _ in + guard + completed && + generatedPassword == self?.databaseKeyEncryptionPassword + else { return } + + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Password", + body: .text(""" + The generated password was: + \(generatedPassword) + + Avoid sending this via the same means as the database + """), + confirmTitle: "Share", + dismissOnConfirm: false, + onConfirm: { [weak self] modal in + modal.dismiss(animated: true) { + let passwordShareVC = UIActivityViewController( + activityItems: [generatedPassword], + applicationActivities: nil + ) + if UIDevice.current.isIPad { + passwordShareVC.excludedActivityTypes = [] + passwordShareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + passwordShareVC.popoverPresentationController?.sourceView = targetView + passwordShareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(passwordShareVC, transitionType: .present) + } + } + ) + ), + transitionType: .present + ) + } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = targetView + shareVC.popoverPresentationController?.sourceRect = (targetView?.bounds ?? .zero) + } + + self?.transitionToScreen(shareVC, transitionType: .present) + } + catch { + let message: String = { + switch error { + case CryptoKitError.incorrectKeySize: + return "The password must be between 6 and 32 characters (padded to 32 bytes)" + + default: return "Failed to export database" + } + }() + + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "Error", + body: .text(message) + ) + ), + transitionType: .present + ) + } + } + } + ) + ), + transitionType: .present + ) + } +#endif } diff --git a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift index ea8c28885..f70169fca 100644 --- a/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift +++ b/SessionMessagingKit/Database/Migrations/_013_SessionUtilChanges.swift @@ -55,11 +55,9 @@ enum _013_SessionUtilChanges: Migration { // shares the same 'id' as the 'groupId') so we can cascade delete automatically t.column(.groupId, .text) .notNull() - .indexed() // Quicker querying .references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted t.column(.profileId, .text) .notNull() - .indexed() // Quicker querying t.column(.role, .integer).notNull() t.column(.isHidden, .boolean) .notNull() @@ -80,6 +78,11 @@ enum _013_SessionUtilChanges: Migration { try db.drop(table: GroupMember.self) try db.rename(table: TmpGroupMember.databaseTableName, to: GroupMember.databaseTableName) + // Need to create the indexes separately from creating 'TmpGroupMember' to ensure they + // have the correct names + try db.createIndex(on: GroupMember.self, columns: [.groupId]) + try db.createIndex(on: GroupMember.self, columns: [.profileId]) + // SQLite doesn't support removing unique constraints so we need to create a new table with // the setup we want, copy data from the old table over, drop the old table and rename the new table struct TmpClosedGroupKeyPair: Codable, TableRecord, FetchableRecord, PersistableRecord, ColumnExpressible { @@ -107,18 +110,16 @@ enum _013_SessionUtilChanges: Migration { try db.create(table: TmpClosedGroupKeyPair.self) { t in t.column(.threadId, .text) .notNull() - .indexed() // Quicker querying .references(ClosedGroup.self, onDelete: .cascade) // Delete if ClosedGroup deleted t.column(.publicKey, .blob).notNull() t.column(.secretKey, .blob).notNull() t.column(.receivedTimestamp, .double) .notNull() - .indexed() // Quicker querying t.column(.threadKeyPairHash, .integer) .notNull() .unique() - .indexed() } + // Insert into the new table, drop the old table and rename the new table to be the old one try ClosedGroupKeyPair .fetchAll(db) @@ -144,6 +145,12 @@ enum _013_SessionUtilChanges: Migration { try db.rename(table: TmpClosedGroupKeyPair.databaseTableName, to: ClosedGroupKeyPair.databaseTableName) // Add an index for the 'ClosedGroupKeyPair' so we can lookup existing keys more easily + // + // Note: Need to create the indexes separately from creating 'TmpClosedGroupKeyPair' to ensure they + // have the correct names + try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadId]) + try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.receivedTimestamp]) + try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadKeyPairHash]) try db.createIndex( on: ClosedGroupKeyPair.self, columns: [.threadId, .threadKeyPairHash] diff --git a/SessionMessagingKit/Database/Models/Contact.swift b/SessionMessagingKit/Database/Models/Contact.swift index 6cc3b4fc5..6f3b05c47 100644 --- a/SessionMessagingKit/Database/Models/Contact.swift +++ b/SessionMessagingKit/Database/Models/Contact.swift @@ -54,12 +54,13 @@ public struct Contact: Codable, Identifiable, Equatable, FetchableRecord, Persis isApproved: Bool = false, isBlocked: Bool = false, didApproveMe: Bool = false, - hasBeenBlocked: Bool = false + hasBeenBlocked: Bool = false, + dependencies: Dependencies = Dependencies() ) { self.id = id self.isTrusted = ( isTrusted || - id == getUserHexEncodedPublicKey() // Always trust ourselves + id == getUserHexEncodedPublicKey(dependencies: dependencies) // Always trust ourselves ) self.isApproved = isApproved self.isBlocked = isBlocked diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index a8ef201fe..0e5df42e5 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -36,7 +36,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, case pinnedPriority } - public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible { + public enum Variant: Int, Codable, Hashable, DatabaseValueConvertible, CaseIterable { case contact case legacyGroup case community diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift index 66fe34ec3..96b185793 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift @@ -34,63 +34,16 @@ internal extension SessionUtil { mergeNeedsDump: Bool, latestConfigSentTimestampMs: Int64 ) throws { - typealias ContactData = [ - String: ( - contact: Contact, - profile: Profile, - priority: Int32, - created: TimeInterval - ) - ] - guard mergeNeedsDump else { return } guard conf != nil else { throw SessionUtilError.nilConfigObject } - var contactData: ContactData = [:] - var contact: contacts_contact = contacts_contact() - let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) - - while !contacts_iterator_done(contactIterator, &contact) { - let contactId: String = String(cString: withUnsafeBytes(of: contact.session_id) { [UInt8]($0) } - .map { CChar($0) } - .nullTerminated() - ) - let contactResult: Contact = Contact( - id: contactId, - isApproved: contact.approved, - isBlocked: contact.blocked, - didApproveMe: contact.approved_me - ) - let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true) - let profileResult: Profile = Profile( - id: contactId, - name: String(libSessionVal: contact.name), - lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000), - nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true), - profilePictureUrl: profilePictureUrl, - profileEncryptionKey: (profilePictureUrl == nil ? nil : - Data( - libSessionVal: contact.profile_pic.key, - count: ProfileManager.avatarAES256KeyByteLength - ) - ), - lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000) - ) - - contactData[contactId] = ( - contactResult, - profileResult, - contact.priority, - TimeInterval(contact.created) - ) - contacts_iterator_advance(contactIterator) - } - contacts_iterator_free(contactIterator) // Need to free the iterator - // The current users contact data is handled separately so exclude it if it's present (as that's // actually a bug) let userPublicKey: String = getUserHexEncodedPublicKey(db) - let targetContactData: ContactData = contactData.filter { $0.key != userPublicKey } + let targetContactData: [String: ContactData] = extractContacts( + from: conf, + latestConfigSentTimestampMs: latestConfigSentTimestampMs + ).filter { $0.key != userPublicKey } // Since we don't sync 100% of the data stored against the contact and profile objects we // need to only update the data we do have to ensure we don't overwrite anything that doesn't @@ -490,6 +443,121 @@ internal extension SessionUtil { return updated } + + // MARK: - Pruning + + static func pruningIfNeeded( + _ db: Database, + conf: UnsafeMutablePointer? + ) throws { + // First make sure we are actually thowing the correct size constraint error (don't want to prune contacts + // as a result of some other random error + do { + try CExceptionHelper.performSafely { config_push(conf).deallocate() } + return // If we didn't error then no need to prune + } + catch { + guard (error as NSError).userInfo["NSLocalizedDescription"] as? String == "Config data is too large" else { + throw error + } + } + + // Extract the contact data from the config + var allContactData: [String: ContactData] = extractContacts( + from: conf, + latestConfigSentTimestampMs: 0 + ) + + // Remove the current user profile info (shouldn't be in there but just in case) + let userPublicKey: String = getUserHexEncodedPublicKey(db) + var cUserPublicKey: [CChar] = userPublicKey.cArray.nullTerminated() + contacts_erase(conf, &cUserPublicKey) + + /// Do the following in stages (we want to prune as few contacts as possible because we are essentially deleting data and removing these + /// contacts will result in not just contact data but also associated conversation data for the contact being removed from linked devices + /// + /// + /// **Step 1** First of all we want to try to detect spam-attacks (ie. if someone creates a bunch of accounts and messages you, and you + /// systematically block every one of those accounts - this can quickly add up) + /// + /// We will do this by filtering the contact data to only include blocked contacts, grouping those contacts into contacts created within the + /// same hour and then only including groups that have more than 10 contacts (ie. if you blocked 20 users within an hour we expect those + /// contacts were spammers) + let blockSpamBatchingResolution: TimeInterval = (60 * 60) + // TODO: Do we want to only do this case for contacts that were created over X time ago? (to avoid unintentionally unblocking accounts that were recently blocked + let likelySpammerContacts: [ContactData] = allContactData + .values + .filter { $0.contact.isBlocked } + .grouped(by: { $0.created / blockSpamBatchingResolution }) + .filter { _, values in values.count > 20 } + .values + .flatMap { $0 } + + if !likelySpammerContacts.isEmpty { + likelySpammerContacts.forEach { contact in + var cSessionId: [CChar] = contact.contact.id.cArray.nullTerminated() + contacts_erase(conf, &cSessionId) + + allContactData.removeValue(forKey: contact.contact.id) + } + + // If we are no longer erroring then we can stop here + do { return try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch {} + } + + /// We retrieve the `CONVO_INFO_VOLATILE` records and one-to-one conversation message counts as they will be relevant for subsequent checks + let volatileThreadInfo: [String: VolatileThreadInfo] = SessionUtil + .config(for: .convoInfoVolatile, publicKey: userPublicKey) + .wrappedValue + .map { SessionUtil.extractConvoVolatileInfo(from: $0) } + .defaulting(to: []) + .reduce(into: [:]) { result, next in result[next.threadId] = next } + let conversationMessageCounts: [String: Int] = try SessionThread + .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) + .select(.id) + .annotated(with: SessionThread.interactions.count) + .asRequest(of: ThreadCount.self) + .fetchAll(db) + .reduce(into: [:]) { result, next in result[next.id] = next.interactionCount } + + /// **Step 2** Next up we want to remove contact records which are likely to be invalid, this means contacts which: + /// - Aren't blocked + /// - Have no `name` value + /// - Have no `CONVO_INFO_VOLATILE` record + /// - Have no messages in their one-to-one conversations + /// + /// Any contacts that meet the above criteria are likely either invalid contacts or are contacts which the user hasn't seen or interacted + /// with for 30+ days + let likelyInvalidContacts: [ContactData] = allContactData + .values + .filter { !$0.contact.isBlocked } + .filter { $0.profile.name.isEmpty } + .filter { volatileThreadInfo[$0.contact.id] == nil } + .filter { (conversationMessageCounts[$0.contact.id] ?? 0) == 0 } + + if !likelyInvalidContacts.isEmpty { + likelyInvalidContacts.forEach { contact in + var cSessionId: [CChar] = contact.contact.id.cArray.nullTerminated() + contacts_erase(conf, &cSessionId) + + allContactData.removeValue(forKey: contact.contact.id) + } + + // If we are no longer erroring then we can stop here + do { return try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch {} + } + + + // TODO: Exclude contacts that have no profile info(?) + // TODO: Exclude contacts that have a CONVO_INFO_VOLATILE record + // TODO: Exclude contacts that have a conversation with messages in the database (ie. only delete "empty" contacts) + + // TODO: Start pruning valid contacts which have really old conversations... + + print("RAWR") + } } // MARK: - External Outgoing Changes @@ -555,3 +623,71 @@ extension SessionUtil { } } } + +// MARK: - ContactData + +private struct ContactData { + let contact: Contact + let profile: Profile + let priority: Int32 + let created: TimeInterval +} + +// MARK: - ThreadCount + +private struct ThreadCount: Codable, FetchableRecord { + let id: String + let interactionCount: Int +} + +// MARK: - Convenience + +private extension SessionUtil { + static func extractContacts( + from conf: UnsafeMutablePointer?, + latestConfigSentTimestampMs: Int64 + ) -> [String: ContactData] { + var result: [String: ContactData] = [:] + var contact: contacts_contact = contacts_contact() + let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) + + while !contacts_iterator_done(contactIterator, &contact) { + let contactId: String = String(cString: withUnsafeBytes(of: contact.session_id) { [UInt8]($0) } + .map { CChar($0) } + .nullTerminated() + ) + let contactResult: Contact = Contact( + id: contactId, + isApproved: contact.approved, + isBlocked: contact.blocked, + didApproveMe: contact.approved_me + ) + let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true) + let profileResult: Profile = Profile( + id: contactId, + name: String(libSessionVal: contact.name), + lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000), + nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true), + profilePictureUrl: profilePictureUrl, + profileEncryptionKey: (profilePictureUrl == nil ? nil : + Data( + libSessionVal: contact.profile_pic.key, + count: ProfileManager.avatarAES256KeyByteLength + ) + ), + lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000) + ) + + result[contactId] = ContactData( + contact: contactResult, + profile: profileResult, + priority: contact.priority, + created: TimeInterval(contact.created) + ) + contacts_iterator_advance(contactIterator) + } + contacts_iterator_free(contactIterator) // Need to free the iterator + + return result + } +} diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift index c114816f2..47964c504 100644 --- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift @@ -22,71 +22,8 @@ internal extension SessionUtil { guard mergeNeedsDump else { return } guard conf != nil else { throw SessionUtilError.nilConfigObject } - var volatileThreadInfo: [VolatileThreadInfo] = [] - var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1() - var community: convo_info_volatile_community = convo_info_volatile_community() - var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf) - - while !convo_info_volatile_iterator_done(convoIterator) { - if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) { - volatileThreadInfo.append( - VolatileThreadInfo( - threadId: String(libSessionVal: oneToOne.session_id), - variant: .contact, - changes: [ - .markedAsUnread(oneToOne.unread), - .lastReadTimestampMs(oneToOne.last_read) - ] - ) - ) - } - else if convo_info_volatile_it_is_community(convoIterator, &community) { - let server: String = String(libSessionVal: community.base_url) - let roomToken: String = String(libSessionVal: community.room) - let publicKey: String = Data( - libSessionVal: community.pubkey, - count: OpenGroup.pubkeyByteLength - ).toHexString() - - volatileThreadInfo.append( - VolatileThreadInfo( - threadId: OpenGroup.idFor(roomToken: roomToken, server: server), - variant: .community, - openGroupUrlInfo: OpenGroupUrlInfo( - threadId: OpenGroup.idFor(roomToken: roomToken, server: server), - server: server, - roomToken: roomToken, - publicKey: publicKey - ), - changes: [ - .markedAsUnread(community.unread), - .lastReadTimestampMs(community.last_read) - ] - ) - ) - } - else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) { - volatileThreadInfo.append( - VolatileThreadInfo( - threadId: String(libSessionVal: legacyGroup.group_id), - variant: .legacyGroup, - changes: [ - .markedAsUnread(legacyGroup.unread), - .lastReadTimestampMs(legacyGroup.last_read) - ] - ) - ) - } - else { - SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update") - } - - convo_info_volatile_iterator_advance(convoIterator) - } - convo_info_volatile_iterator_free(convoIterator) // Need to free the iterator - - // Get the local volatile thread info from all conversations + // Get the volatile thread info from the conf and local conversations + let volatileThreadInfo: [VolatileThreadInfo] = extractConvoVolatileInfo(from: conf) let localVolatileThreadInfo: [String: VolatileThreadInfo] = VolatileThreadInfo.fetchAll(db) .reduce(into: [:]) { result, next in result[next.threadId] = next } @@ -572,6 +509,76 @@ public extension SessionUtil { } } } + + internal static func extractConvoVolatileInfo( + from conf: UnsafeMutablePointer? + ) -> [VolatileThreadInfo] { + var result: [VolatileThreadInfo] = [] + var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1() + var community: convo_info_volatile_community = convo_info_volatile_community() + var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + let convoIterator: OpaquePointer = convo_info_volatile_iterator_new(conf) + + while !convo_info_volatile_iterator_done(convoIterator) { + if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) { + result.append( + VolatileThreadInfo( + threadId: String(libSessionVal: oneToOne.session_id), + variant: .contact, + changes: [ + .markedAsUnread(oneToOne.unread), + .lastReadTimestampMs(oneToOne.last_read) + ] + ) + ) + } + else if convo_info_volatile_it_is_community(convoIterator, &community) { + let server: String = String(libSessionVal: community.base_url) + let roomToken: String = String(libSessionVal: community.room) + let publicKey: String = Data( + libSessionVal: community.pubkey, + count: OpenGroup.pubkeyByteLength + ).toHexString() + + result.append( + VolatileThreadInfo( + threadId: OpenGroup.idFor(roomToken: roomToken, server: server), + variant: .community, + openGroupUrlInfo: OpenGroupUrlInfo( + threadId: OpenGroup.idFor(roomToken: roomToken, server: server), + server: server, + roomToken: roomToken, + publicKey: publicKey + ), + changes: [ + .markedAsUnread(community.unread), + .lastReadTimestampMs(community.last_read) + ] + ) + ) + } + else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) { + result.append( + VolatileThreadInfo( + threadId: String(libSessionVal: legacyGroup.group_id), + variant: .legacyGroup, + changes: [ + .markedAsUnread(legacyGroup.unread), + .lastReadTimestampMs(legacyGroup.last_read) + ] + ) + ) + } + else { + SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update") + } + + convo_info_volatile_iterator_advance(convoIterator) + } + convo_info_volatile_iterator_free(convoIterator) // Need to free the iterator + + return result + } } fileprivate extension [SessionUtil.VolatileThreadInfo.Change] { @@ -597,3 +604,4 @@ fileprivate extension [SessionUtil.VolatileThreadInfo.Change] { return nil } } + diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift index ad31995a5..a363422b1 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import Sodium import SessionUtil import SessionUtilitiesKit @@ -8,299 +9,601 @@ import SessionUtilitiesKit import Quick import Nimble +@testable import SessionMessagingKit + /// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches class ConfigContactsSpec { + enum ContactProperty: CaseIterable { + case name + case nickname + case approved + case approved_me + case blocked + case profile_pic + case created + case notifications + case mute_until + } + // MARK: - Spec static func spec() { - it("generates Contact configs correctly") { - let createdTs: Int64 = 1680064059 - let nowTs: Int64 = Int64(Date().timeIntervalSince1970) - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - let contactPtr: UnsafeMutablePointer? = nil - expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse()) - - expect(contacts_size(conf)).to(equal(0)) - - var contact2: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact2.name)).to(beEmpty()) - expect(String(libSessionVal: contact2.nickname)).to(beEmpty()) - expect(contact2.approved).to(beFalse()) - expect(contact2.approved_me).to(beFalse()) - expect(contact2.blocked).to(beFalse()) - expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty()) - expect(contact2.created).to(equal(0)) - expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) - expect(contact2.mute_until).to(equal(0)) - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(0)) - pushData1.deallocate() - - // Update the contact data - contact2.name = "Joe".toLibSession() - contact2.nickname = "Joey".toLibSession() - contact2.approved = true - contact2.approved_me = true - contact2.created = createdTs - contact2.notifications = CONVO_NOTIFY_ALL - contact2.mute_until = nowTs + 1800 - - // Update the contact - contacts_set(conf, &contact2) - - // Ensure the contact details were updated - var contact3: contacts_contact = contacts_contact() - expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact3.name)).to(equal("Joe")) - expect(String(libSessionVal: contact3.nickname)).to(equal("Joey")) - expect(contact3.approved).to(beTrue()) - expect(contact3.approved_me).to(beTrue()) - expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty()) - expect(contact3.blocked).to(beFalse()) - expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId)) - expect(contact3.created).to(equal(createdTs)) - expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(contact2.mute_until).to(equal(nowTs + 1800)) - - - // Since we've made changes, we should need to push new config to the swarm, *and* should need - // to dump the updated state: - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed multiple fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed multiple fields here). - expect(pushData2.pointee.seqno).to(equal(1)) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beTrue()) - pushData2.deallocate() - - // NB: Not going to check encrypted data and decryption here because that's general (not - // specific to contacts) and is covered already in the user profile tests. - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData3: UnsafeMutablePointer = config_push(conf2) - expect(pushData3.pointee.seqno).to(equal(1)) - pushData3.deallocate() - - // Because we just called dump() above, to load up contacts2 - expect(config_needs_dump(conf)).to(beFalse()) - - // Ensure the contact details were updated - var contact4: contacts_contact = contacts_contact() - expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact4.name)).to(equal("Joe")) - expect(String(libSessionVal: contact4.nickname)).to(equal("Joey")) - expect(contact4.approved).to(beTrue()) - expect(contact4.approved_me).to(beTrue()) - expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty()) - expect(contact4.blocked).to(beFalse()) - expect(contact4.created).to(equal(createdTs)) - - let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() - var contact5: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) - expect(String(libSessionVal: contact5.name)).to(beEmpty()) - expect(String(libSessionVal: contact5.nickname)).to(beEmpty()) - expect(contact5.approved).to(beFalse()) - expect(contact5.approved_me).to(beFalse()) - expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty()) - expect(contact5.blocked).to(beFalse()) - - // We're not setting any fields, but we should still keep a record of the session id - contacts_set(conf2, &contact5) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData4: UnsafeMutablePointer = config_push(conf2) - expect(pushData4.pointee.seqno).to(equal(2)) - - // Check the merging - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] - var mergeSize: [Int] = [pushData4.pointee.config_len] - expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2) - mergeHashes.forEach { $0?.deallocate() } - pushData4.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - - let pushData5: UnsafeMutablePointer = config_push(conf) - expect(pushData5.pointee.seqno).to(equal(2)) - pushData5.deallocate() - - // Iterate through and make sure we got everything we expected - var sessionIds: [String] = [] - var nicknames: [String] = [] - expect(contacts_size(conf)).to(equal(2)) - - var contact6: contacts_contact = contacts_contact() - let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) - while !contacts_iterator_done(contactIterator, &contact6) { - sessionIds.append(String(libSessionVal: contact6.session_id)) - nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)") - contacts_iterator_advance(contactIterator) + context("CONTACTS") { + // MARK: - when checking error catching + context("when checking error catching") { + var seed: Data! + var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! + var edSK: [UInt8]! + var error: UnsafeMutablePointer? + var conf: UnsafeMutablePointer? + + beforeEach { + seed = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + identity = try! Identity.generate(from: seed) + edSK = identity.ed25519KeyPair.secretKey + + // Initialize a brand new, empty config because we have no dump data to deal with. + error = nil + conf = nil + _ = contacts_init(&conf, &edSK, nil, 0, error) + error?.deallocate() + } + + // MARK: -- it can catch size limit errors thrown when pushing + it("can catch size limit errors thrown when pushing") { + try (0..<10000).forEach { index in + var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + contacts_set(conf, &contact) + } + + expect(contacts_size(conf)).to(equal(10000)) + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + expect { + try CExceptionHelper.performSafely { config_push(conf).deallocate() } + } + .to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"]))) + } + + // MARK: -- can catch size limit errors thrown when dumping + it("can catch size limit errors thrown when dumping") { + try (0..<10000).forEach { index in + var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + contacts_set(conf, &contact) + } + + expect(contacts_size(conf)).to(equal(10000)) + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + expect { + try CExceptionHelper.performSafely { + var dump: UnsafeMutablePointer? = nil + var dumpLen: Int = 0 + config_dump(conf, &dump, &dumpLen) + dump?.deallocate() + } + } + .to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"]))) + } } - contacts_iterator_free(contactIterator) // Need to free the iterator - expect(sessionIds.count).to(equal(2)) - expect(sessionIds.count).to(equal(contacts_size(conf))) - expect(sessionIds.first).to(equal(definitelyRealId)) - expect(sessionIds.last).to(equal(anotherId)) - expect(nicknames.first).to(equal("Joey")) - expect(nicknames.last).to(equal("(N/A)")) - - // Conflict! Oh no! - - // On client 1 delete a contact: - contacts_erase(conf, definitelyRealId) - - // Client 2 adds a new friend: - let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222" - var cThirdId: [CChar] = thirdId.cArray.nullTerminated() - var contact7: contacts_contact = contacts_contact() - expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) - contact7.nickname = "Nickname 3".toLibSession() - contact7.approved = true - contact7.approved_me = true - contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession() - contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() - contacts_set(conf2, &contact7) - - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData6: UnsafeMutablePointer = config_push(conf) - expect(pushData6.pointee.seqno).to(equal(3)) - - let pushData7: UnsafeMutablePointer = config_push(conf2) - expect(pushData7.pointee.seqno).to(equal(3)) - - let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)! - let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)! - expect(pushData6Str).toNot(equal(pushData7Str)) - expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len)) - .to(equal([fakeHash2])) - expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) - .to(equal([fakeHash2])) - - let fakeHash3a: String = "fakehash3a" - var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated() - let fakeHash3b: String = "fakehash3b" - var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated() - config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a) - config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b) - - var mergeHashes2: [UnsafePointer?] = [cFakeHash3b].unsafeCopy() - var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData7.pointee.config)] - var mergeSize2: [Int] = [pushData7.pointee.config_len] - expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - expect(config_needs_push(conf)).to(beTrue()) - - var mergeHashes3: [UnsafePointer?] = [cFakeHash3a].unsafeCopy() - var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData6.pointee.config)] - var mergeSize3: [Int] = [pushData6.pointee.config_len] - expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) - expect(config_needs_push(conf2)).to(beTrue()) - mergeHashes2.forEach { $0?.deallocate() } - mergeHashes3.forEach { $0?.deallocate() } - pushData6.deallocate() - pushData7.deallocate() - - let pushData8: UnsafeMutablePointer = config_push(conf) - expect(pushData8.pointee.seqno).to(equal(4)) - - let pushData9: UnsafeMutablePointer = config_push(conf2) - expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno)) - - let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)! - let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)! - expect(pushData8Str).to(equal(pushData9Str)) - expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len)) - .to(equal([fakeHash3b, fakeHash3a])) - expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len)) - .to(equal([fakeHash3a, fakeHash3b])) - - let fakeHash4: String = "fakeHash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4) - config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4) - pushData8.deallocate() - pushData9.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - // Validate the changes - var sessionIds2: [String] = [] - var nicknames2: [String] = [] - expect(contacts_size(conf)).to(equal(2)) - - var contact8: contacts_contact = contacts_contact() - let contactIterator2: UnsafeMutablePointer = contacts_iterator_new(conf) - while !contacts_iterator_done(contactIterator2, &contact8) { - sessionIds2.append(String(libSessionVal: contact8.session_id)) - nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)") - contacts_iterator_advance(contactIterator2) + // MARK: - when checking size limits + context("when checking size limits") { + var numRecords: Int! + var seed: Data! + var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! + var edSK: [UInt8]! + var error: UnsafeMutablePointer? + var conf: UnsafeMutablePointer? + + beforeEach { + numRecords = 0 + seed = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + identity = try! Identity.generate(from: seed) + edSK = identity.ed25519KeyPair.secretKey + + // Initialize a brand new, empty config because we have no dump data to deal with. + error = nil + conf = nil + _ = contacts_init(&conf, &edSK, nil, 0, error) + error?.deallocate() + } + + // MARK: -- has not changed the max empty records + it("has not changed the max empty records") { + for index in (0..<10000) { + var contact: contacts_contact = try createContact(for: index, in: conf) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(1775)) + } + + // MARK: -- has not changed the max name only records + it("has not changed the max name only records") { + for index in (0..<10000) { + var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name]) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(526)) + } + + // MARK: -- has not changed the max name and profile pic only records + it("has not changed the max name and profile pic only records") { + for index in (0..<10000) { + var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name, .profile_pic]) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(184)) + } + + // MARK: -- has not changed the max filled records + it("has not changed the max filled records") { + for index in (0..<10000) { + var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + contacts_set(conf, &contact) + + do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } + catch { break } + + // We successfully inserted a contact and didn't hit the limit so increment the counter + numRecords += 1 + } + + // Check that the record count matches the maximum when we last checked + expect(numRecords).to(equal(134)) + } } - contacts_iterator_free(contactIterator2) // Need to free the iterator - expect(sessionIds2.count).to(equal(2)) - expect(sessionIds2.first).to(equal(anotherId)) - expect(sessionIds2.last).to(equal(thirdId)) - expect(nicknames2.first).to(equal("(N/A)")) - expect(nicknames2.last).to(equal("Nickname 3")) + // MARK: - when pruning + context("when pruning") { + var mockStorage: Storage! + var seed: Data! + var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)! + var edSK: [UInt8]! + var error: UnsafeMutablePointer? + var conf: UnsafeMutablePointer? + + beforeEach { + mockStorage = Storage( + customWriter: try! DatabaseQueue(), + customMigrations: [ + SNUtilitiesKit.migrations(), + SNMessagingKit.migrations() + ] + ) + seed = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + identity = try! Identity.generate(from: seed) + edSK = identity.ed25519KeyPair.secretKey + + // Initialize a brand new, empty config because we have no dump data to deal with. + error = nil + conf = nil + _ = contacts_init(&conf, &edSK, nil, 0, error) + error?.deallocate() + } + + it("does something") { + mockStorage.write { db in + try SessionThread.fetchOrCreate(db, id: "1", variant: .contact, shouldBeVisible: true) + try SessionThread.fetchOrCreate(db, id: "2", variant: .contact, shouldBeVisible: true) + try SessionThread.fetchOrCreate(db, id: "3", variant: .contact, shouldBeVisible: true) + _ = try Interaction( + threadId: "1", + authorId: "1", + variant: .standardIncoming, + body: "Test1" + ).inserted(db) + _ = try Interaction( + threadId: "1", + authorId: "2", + variant: .standardIncoming, + body: "Test2" + ).inserted(db) + _ = try Interaction( + threadId: "3", + authorId: "3", + variant: .standardIncoming, + body: "Test3" + ).inserted(db) + + try SessionUtil.pruningIfNeeded( + db, + conf: conf + ) + + expect(contacts_size(conf)).to(equal(0)) + } + } + } + + // MARK: - generates config correctly + + it("generates config correctly") { + let createdTs: Int64 = 1680064059 + let nowTs: Int64 = Int64(Date().timeIntervalSince1970) + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + let contactPtr: UnsafeMutablePointer? = nil + expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse()) + + expect(contacts_size(conf)).to(equal(0)) + + var contact2: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact2.name)).to(beEmpty()) + expect(String(libSessionVal: contact2.nickname)).to(beEmpty()) + expect(contact2.approved).to(beFalse()) + expect(contact2.approved_me).to(beFalse()) + expect(contact2.blocked).to(beFalse()) + expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty()) + expect(contact2.created).to(equal(0)) + expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) + expect(contact2.mute_until).to(equal(0)) + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(0)) + pushData1.deallocate() + + // Update the contact data + contact2.name = "Joe".toLibSession() + contact2.nickname = "Joey".toLibSession() + contact2.approved = true + contact2.approved_me = true + contact2.created = createdTs + contact2.notifications = CONVO_NOTIFY_ALL + contact2.mute_until = nowTs + 1800 + + // Update the contact + contacts_set(conf, &contact2) + + // Ensure the contact details were updated + var contact3: contacts_contact = contacts_contact() + expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact3.name)).to(equal("Joe")) + expect(String(libSessionVal: contact3.nickname)).to(equal("Joey")) + expect(contact3.approved).to(beTrue()) + expect(contact3.approved_me).to(beTrue()) + expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty()) + expect(contact3.blocked).to(beFalse()) + expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId)) + expect(contact3.created).to(equal(createdTs)) + expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL)) + expect(contact2.mute_until).to(equal(nowTs + 1800)) + + + // Since we've made changes, we should need to push new config to the swarm, *and* should need + // to dump the updated state: + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed multiple fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed multiple fields here). + expect(pushData2.pointee.seqno).to(equal(1)) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beTrue()) + pushData2.deallocate() + + // NB: Not going to check encrypted data and decryption here because that's general (not + // specific to contacts) and is covered already in the user profile tests. + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData3: UnsafeMutablePointer = config_push(conf2) + expect(pushData3.pointee.seqno).to(equal(1)) + pushData3.deallocate() + + // Because we just called dump() above, to load up contacts2 + expect(config_needs_dump(conf)).to(beFalse()) + + // Ensure the contact details were updated + var contact4: contacts_contact = contacts_contact() + expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue()) + expect(String(libSessionVal: contact4.name)).to(equal("Joe")) + expect(String(libSessionVal: contact4.nickname)).to(equal("Joey")) + expect(contact4.approved).to(beTrue()) + expect(contact4.approved_me).to(beTrue()) + expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty()) + expect(contact4.blocked).to(beFalse()) + expect(contact4.created).to(equal(createdTs)) + + let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" + var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + var contact5: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) + expect(String(libSessionVal: contact5.name)).to(beEmpty()) + expect(String(libSessionVal: contact5.nickname)).to(beEmpty()) + expect(contact5.approved).to(beFalse()) + expect(contact5.approved_me).to(beFalse()) + expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently + expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty()) + expect(contact5.blocked).to(beFalse()) + + // We're not setting any fields, but we should still keep a record of the session id + contacts_set(conf2, &contact5) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData4: UnsafeMutablePointer = config_push(conf2) + expect(pushData4.pointee.seqno).to(equal(2)) + + // Check the merging + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] + var mergeSize: [Int] = [pushData4.pointee.config_len] + expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2) + mergeHashes.forEach { $0?.deallocate() } + pushData4.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + + let pushData5: UnsafeMutablePointer = config_push(conf) + expect(pushData5.pointee.seqno).to(equal(2)) + pushData5.deallocate() + + // Iterate through and make sure we got everything we expected + var sessionIds: [String] = [] + var nicknames: [String] = [] + expect(contacts_size(conf)).to(equal(2)) + + var contact6: contacts_contact = contacts_contact() + let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) + while !contacts_iterator_done(contactIterator, &contact6) { + sessionIds.append(String(libSessionVal: contact6.session_id)) + nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)") + contacts_iterator_advance(contactIterator) + } + contacts_iterator_free(contactIterator) // Need to free the iterator + + expect(sessionIds.count).to(equal(2)) + expect(sessionIds.count).to(equal(contacts_size(conf))) + expect(sessionIds.first).to(equal(definitelyRealId)) + expect(sessionIds.last).to(equal(anotherId)) + expect(nicknames.first).to(equal("Joey")) + expect(nicknames.last).to(equal("(N/A)")) + + // Conflict! Oh no! + + // On client 1 delete a contact: + contacts_erase(conf, definitelyRealId) + + // Client 2 adds a new friend: + let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222" + var cThirdId: [CChar] = thirdId.cArray.nullTerminated() + var contact7: contacts_contact = contacts_contact() + expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) + contact7.nickname = "Nickname 3".toLibSession() + contact7.approved = true + contact7.approved_me = true + contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession() + contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() + contacts_set(conf2, &contact7) + + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData6: UnsafeMutablePointer = config_push(conf) + expect(pushData6.pointee.seqno).to(equal(3)) + + let pushData7: UnsafeMutablePointer = config_push(conf2) + expect(pushData7.pointee.seqno).to(equal(3)) + + let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)! + let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)! + expect(pushData6Str).toNot(equal(pushData7Str)) + expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len)) + .to(equal([fakeHash2])) + expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) + .to(equal([fakeHash2])) + + let fakeHash3a: String = "fakehash3a" + var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated() + let fakeHash3b: String = "fakehash3b" + var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated() + config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a) + config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b) + + var mergeHashes2: [UnsafePointer?] = [cFakeHash3b].unsafeCopy() + var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData7.pointee.config)] + var mergeSize2: [Int] = [pushData7.pointee.config_len] + expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + expect(config_needs_push(conf)).to(beTrue()) + + var mergeHashes3: [UnsafePointer?] = [cFakeHash3a].unsafeCopy() + var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData6.pointee.config)] + var mergeSize3: [Int] = [pushData6.pointee.config_len] + expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) + expect(config_needs_push(conf2)).to(beTrue()) + mergeHashes2.forEach { $0?.deallocate() } + mergeHashes3.forEach { $0?.deallocate() } + pushData6.deallocate() + pushData7.deallocate() + + let pushData8: UnsafeMutablePointer = config_push(conf) + expect(pushData8.pointee.seqno).to(equal(4)) + + let pushData9: UnsafeMutablePointer = config_push(conf2) + expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno)) + + let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)! + let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)! + expect(pushData8Str).to(equal(pushData9Str)) + expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len)) + .to(equal([fakeHash3b, fakeHash3a])) + expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len)) + .to(equal([fakeHash3a, fakeHash3b])) + + let fakeHash4: String = "fakeHash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4) + config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4) + pushData8.deallocate() + pushData9.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + // Validate the changes + var sessionIds2: [String] = [] + var nicknames2: [String] = [] + expect(contacts_size(conf)).to(equal(2)) + + var contact8: contacts_contact = contacts_contact() + let contactIterator2: UnsafeMutablePointer = contacts_iterator_new(conf) + while !contacts_iterator_done(contactIterator2, &contact8) { + sessionIds2.append(String(libSessionVal: contact8.session_id)) + nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)") + contacts_iterator_advance(contactIterator2) + } + contacts_iterator_free(contactIterator2) // Need to free the iterator + + expect(sessionIds2.count).to(equal(2)) + expect(sessionIds2.first).to(equal(anotherId)) + expect(sessionIds2.last).to(equal(thirdId)) + expect(nicknames2.first).to(equal("(N/A)")) + expect(nicknames2.last).to(equal("Nickname 3")) + } } } + + // MARK: - Convenience + + private static func createContact( + for index: Int, + in conf: UnsafeMutablePointer?, + maxing properties: [ContactProperty] = [] + ) throws -> contacts_contact { + let postPrefixId: String = "050000000000000000000000000000000000000000000000000000000000000000" + let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count)) + var cSessionId: [CChar] = sessionId.cArray.nullTerminated() + var contact: contacts_contact = contacts_contact() + + guard contacts_get_or_construct(conf, &contact, &cSessionId) else { + throw SessionUtilError.getOrConstructFailedUnexpectedly + } + + // Set the values to the maximum data that can fit + properties.forEach { property in + switch property { + case .approved: contact.approved = true + case .approved_me: contact.approved_me = true + case .blocked: contact.blocked = true + case .created: contact.created = Int64.max + case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY + case .mute_until: contact.mute_until = Int64.max + + case .name: + contact.name = String( + data: Data( + repeating: "A".data(using: .utf8)![0], + count: SessionUtil.libSessionMaxNameByteLength + ), + encoding: .utf8 + ).toLibSession() + + case .nickname: + contact.nickname = String( + data: Data( + repeating: "A".data(using: .utf8)![0], + count: SessionUtil.libSessionMaxNameByteLength + ), + encoding: .utf8 + ).toLibSession() + + case .profile_pic: + contact.profile_pic = user_profile_pic( + url: String( + data: Data( + repeating: "A".data(using: .utf8)![0], + count: SessionUtil.libSessionMaxProfileUrlByteLength + ), + encoding: .utf8 + ).toLibSession(), + key: "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() + ) + } + } + + return contact + } +} + +fileprivate extension Array where Element == ConfigContactsSpec.ContactProperty { + static var allProperties: [ConfigContactsSpec.ContactProperty] = ConfigContactsSpec.ContactProperty.allCases } diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift index 668d54d1e..86325c4ad 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigConvoInfoVolatileSpec.swift @@ -11,255 +11,257 @@ import Nimble /// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches class ConfigConvoInfoVolatileSpec { // MARK: - Spec - + static func spec() { - it("generates ConvoInfoVolatile configs correctly") { - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - 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)) - - var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId)) - .to(beTrue()) - expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId)) - expect(oneToOne2.last_read).to(equal(0)) - expect(oneToOne2.unread).to(beFalse()) - - // No need to sync a conversation with a default state - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - // Update the last read - let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) - oneToOne2.last_read = nowTimestampMs - - // The new data doesn't get stored until we call this: - convo_info_volatile_set_1to1(conf, &oneToOne2) - - var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId)) - .to(beFalse()) - expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue()) - expect(oneToOne3.last_read).to(equal(nowTimestampMs)) - - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - let openGroupBaseUrl: String = "http://Example.ORG:5678" - var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated() - let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased() - // ("http://Example.ORG:5678" - // .lowercased() - // .cArray + - // [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count)) - // ) - let openGroupRoom: String = "SudokuRoom" - var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated() - let openGroupRoomResult: String = openGroupRoom.lowercased() - // ("SudokuRoom" - // .lowercased() - // .cArray + - // [CChar](repeating: 0, count: (65 - openGroupRoom.count)) - // ) - var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .bytes - var community1: convo_info_volatile_community = convo_info_volatile_community() - expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue()) - expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community1.unread = true - - // The new data doesn't get stored until we call this: - convo_info_volatile_set_community(conf, &community1); - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(1)) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1) - expect(config_needs_dump(conf)).to(beTrue()) - expect(config_needs_push(conf)).to(beFalse()) - pushData1.deallocate() - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_dump(conf2)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1() - expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true)) - expect(oneToOne4.last_read).to(equal(nowTimestampMs)) - expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId)) - expect(oneToOne4.unread).to(beFalse()) - - var community2: convo_info_volatile_community = convo_info_volatile_community() - expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community2.unread = true - - let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" - var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() - 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 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) - convo_info_volatile_set_legacy_group(conf2, &legacyGroup2) - expect(config_needs_push(conf2)).to(beTrue()) - - let pushData2: UnsafeMutablePointer = config_push(conf2) - expect(pushData2.pointee.seqno).to(equal(2)) - - // Check the merging - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData: [UnsafePointer?] = [UnsafePointer(pushData2.pointee.config)] - var mergeSize: [Int] = [pushData2.pointee.config_len] - expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2) - pushData2.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - - for targetConf in [conf, conf2] { - // Iterate through and make sure we got everything we expected - var seen: [String] = [] - expect(convo_info_volatile_size(conf)).to(equal(4)) - expect(convo_info_volatile_size_1to1(conf)).to(equal(2)) - expect(convo_info_volatile_size_communities(conf)).to(equal(1)) - expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1)) + context("CONVO_INFO_VOLATILE") { + it("generates config correctly") { + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() - var c2: convo_info_volatile_community = convo_info_volatile_community() - var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf) + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - while !convo_info_volatile_iterator_done(it) { - if convo_info_volatile_it_is_1to1(it, &c1) { - seen.append("1-to-1: \(String(libSessionVal: c1.session_id))") - } - else if convo_info_volatile_it_is_community(it, &c2) { - seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else if convo_info_volatile_it_is_legacy_group(it, &c3) { - seen.append("cl: \(String(libSessionVal: c3.group_id))") + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + 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)) + + var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId)) + .to(beTrue()) + expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId)) + expect(oneToOne2.last_read).to(equal(0)) + expect(oneToOne2.unread).to(beFalse()) + + // No need to sync a conversation with a default state + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + // Update the last read + let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000)) + oneToOne2.last_read = nowTimestampMs + + // The new data doesn't get stored until we call this: + convo_info_volatile_set_1to1(conf, &oneToOne2) + + var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId)) + .to(beFalse()) + expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue()) + expect(oneToOne3.last_read).to(equal(nowTimestampMs)) + + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + let openGroupBaseUrl: String = "http://Example.ORG:5678" + var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated() + let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased() + // ("http://Example.ORG:5678" + // .lowercased() + // .cArray + + // [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count)) + // ) + let openGroupRoom: String = "SudokuRoom" + var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated() + let openGroupRoomResult: String = openGroupRoom.lowercased() + // ("SudokuRoom" + // .lowercased() + // .cArray + + // [CChar](repeating: 0, count: (65 - openGroupRoom.count)) + // ) + var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + .bytes + var community1: convo_info_volatile_community = convo_info_volatile_community() + expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue()) + expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult)) + expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult)) + expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community1.unread = true + + // The new data doesn't get stored until we call this: + convo_info_volatile_set_community(conf, &community1); + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(1)) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1) + expect(config_needs_dump(conf)).to(beTrue()) + expect(config_needs_push(conf)).to(beFalse()) + pushData1.deallocate() + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_dump(conf2)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1() + expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true)) + expect(oneToOne4.last_read).to(equal(nowTimestampMs)) + expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId)) + expect(oneToOne4.unread).to(beFalse()) + + var community2: convo_info_volatile_community = convo_info_volatile_community() + expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue()) + expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult)) + expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult)) + expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community2.unread = true + + let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111" + var cAnotherId: [CChar] = anotherId.cArray.nullTerminated() + 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 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) + convo_info_volatile_set_legacy_group(conf2, &legacyGroup2) + expect(config_needs_push(conf2)).to(beTrue()) + + let pushData2: UnsafeMutablePointer = config_push(conf2) + expect(pushData2.pointee.seqno).to(equal(2)) + + // Check the merging + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + var mergeHashes: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData: [UnsafePointer?] = [UnsafePointer(pushData2.pointee.config)] + var mergeSize: [Int] = [pushData2.pointee.config_len] + expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2) + pushData2.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + expect(convo_info_volatile_size(conf)).to(equal(4)) + expect(convo_info_volatile_size_1to1(conf)).to(equal(2)) + expect(convo_info_volatile_size_communities(conf)).to(equal(1)) + expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1)) + + var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() + var c2: convo_info_volatile_community = convo_info_volatile_community() + var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf) + + while !convo_info_volatile_iterator_done(it) { + if convo_info_volatile_it_is_1to1(it, &c1) { + seen.append("1-to-1: \(String(libSessionVal: c1.session_id))") + } + else if convo_info_volatile_it_is_community(it, &c2) { + seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else if convo_info_volatile_it_is_legacy_group(it, &c3) { + seen.append("cl: \(String(libSessionVal: c3.group_id))") + } + + convo_info_volatile_iterator_advance(it) } - convo_info_volatile_iterator_advance(it) + convo_info_volatile_iterator_free(it) + + expect(seen).to(equal([ + "1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", + "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", + "og: http://example.org:5678/r/sudokuroom", + "cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ])) } - convo_info_volatile_iterator_free(it) + let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000" + var cFourthId: [CChar] = fourthId.cArray.nullTerminated() + expect(config_needs_push(conf)).to(beFalse()) + convo_info_volatile_erase_1to1(conf, &cFourthId) + expect(config_needs_push(conf)).to(beFalse()) + convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId) + expect(config_needs_push(conf)).to(beTrue()) + expect(convo_info_volatile_size(conf)).to(equal(3)) + expect(convo_info_volatile_size_1to1(conf)).to(equal(1)) - expect(seen).to(equal([ - "1-to-1: 051111111111111111111111111111111111111111111111111111111111111111", - "1-to-1: 055000000000000000000000000000000000000000000000000000000000000000", - "og: http://example.org:5678/r/sudokuroom", - "cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + // Check the single-type iterators: + var seen1: [String?] = [] + var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() + let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf) + + while !convo_info_volatile_iterator_done(it1) { + expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue()) + + seen1.append(String(libSessionVal: c1.session_id)) + convo_info_volatile_iterator_advance(it1) + } + + convo_info_volatile_iterator_free(it1) + expect(seen1).to(equal([ + "051111111111111111111111111111111111111111111111111111111111111111" + ])) + + var seen2: [String?] = [] + var c2: convo_info_volatile_community = convo_info_volatile_community() + let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf) + + while !convo_info_volatile_iterator_done(it2) { + expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue()) + + seen2.append(String(libSessionVal: c2.base_url)) + convo_info_volatile_iterator_advance(it2) + } + + convo_info_volatile_iterator_free(it2) + expect(seen2).to(equal([ + "http://example.org:5678" + ])) + + var seen3: [String?] = [] + var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() + let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf) + + while !convo_info_volatile_iterator_done(it3) { + expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue()) + + seen3.append(String(libSessionVal: c3.group_id)) + convo_info_volatile_iterator_advance(it3) + } + + convo_info_volatile_iterator_free(it3) + expect(seen3).to(equal([ + "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" ])) } - - let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000" - var cFourthId: [CChar] = fourthId.cArray.nullTerminated() - expect(config_needs_push(conf)).to(beFalse()) - convo_info_volatile_erase_1to1(conf, &cFourthId) - expect(config_needs_push(conf)).to(beFalse()) - convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId) - expect(config_needs_push(conf)).to(beTrue()) - expect(convo_info_volatile_size(conf)).to(equal(3)) - expect(convo_info_volatile_size_1to1(conf)).to(equal(1)) - - // Check the single-type iterators: - var seen1: [String?] = [] - var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1() - let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf) - - while !convo_info_volatile_iterator_done(it1) { - expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue()) - - seen1.append(String(libSessionVal: c1.session_id)) - convo_info_volatile_iterator_advance(it1) - } - - convo_info_volatile_iterator_free(it1) - expect(seen1).to(equal([ - "051111111111111111111111111111111111111111111111111111111111111111" - ])) - - var seen2: [String?] = [] - var c2: convo_info_volatile_community = convo_info_volatile_community() - let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf) - - while !convo_info_volatile_iterator_done(it2) { - expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue()) - - seen2.append(String(libSessionVal: c2.base_url)) - convo_info_volatile_iterator_advance(it2) - } - - convo_info_volatile_iterator_free(it2) - expect(seen2).to(equal([ - "http://example.org:5678" - ])) - - var seen3: [String?] = [] - var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf) - - while !convo_info_volatile_iterator_done(it3) { - expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue()) - - seen3.append(String(libSessionVal: c3.group_id)) - convo_info_volatile_iterator_advance(it3) - } - - convo_info_volatile_iterator_free(it3) - expect(seen3).to(equal([ - "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ])) } } } diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift index 6ace1db75..926cf74f6 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserGroupsSpec.swift @@ -82,505 +82,507 @@ class ConfigUserGroupsSpec { expect(result8?.publicKey) .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) } - - it("generates UserGroup configs correctly") { - let createdTs: Int64 = 1680064059 - let nowTs: Int64 = Int64(Date().timeIntervalSince1970) - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // Empty contacts shouldn't have an existing contact - let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" - var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() - let legacyGroup1: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup1?.pointee).to(beNil()) - expect(user_groups_size(conf)).to(equal(0)) - - let legacyGroup2: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup2.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup2.pointee.session_id)) - .to(equal(definitelyRealId)) - expect(legacyGroup2.pointee.disappearing_timer).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal("")) - expect(legacyGroup2.pointee.priority).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal("")) - expect(legacyGroup2.pointee.joined_at).to(equal(0)) - expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) - expect(legacyGroup2.pointee.mute_until).to(equal(0)) - - // Iterate through and make sure we got everything we expected - var membersSeen1: [String: Bool] = [:] - var memberSessionId1: UnsafePointer? = nil - var memberAdmin1: Bool = false - let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) - - while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) { - membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1 - } - - ugroups_legacy_members_free(membersIt1) - - expect(membersSeen1).to(beEmpty()) - - // No need to sync a conversation with a default state - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beFalse()) - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee.seqno).to(equal(0)) - expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len)) - .to(beEmpty()) - expect(pushData1.pointee.config_len).to(equal(256)) - pushData1.deallocate() - - let users: [String] = [ - "050000000000000000000000000000000000000000000000000000000000000000", - "051111111111111111111111111111111111111111111111111111111111111111", - "052222222222222222222222222222222222222222222222222222222222222222", - "053333333333333333333333333333333333333333333333333333333333333333", - "054444444444444444444444444444444444444444444444444444444444444444", - "055555555555555555555555555555555555555555555555555555555555555555", - "056666666666666666666666666666666666666666666666666666666666666666" - ] - var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() } - legacyGroup2.pointee.name = "Englishmen".toLibSession() - legacyGroup2.pointee.disappearing_timer = 60 - legacyGroup2.pointee.joined_at = createdTs - legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL - legacyGroup2.pointee.mute_until = (nowTs + 3600) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse()) - - // Flip to and from admin - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue()) - - expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue()) - expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue()) - - var membersSeen2: [String: Bool] = [:] - var memberSessionId2: UnsafePointer? = nil - var memberAdmin2: Bool = false - let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) - - while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) { - membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2 - } - - ugroups_legacy_members_free(membersIt2) - - expect(membersSeen2).to(equal([ - "050000000000000000000000000000000000000000000000000000000000000000": false, - "051111111111111111111111111111111111111111111111111111111111111111": false, - "052222222222222222222222222222222222222222222222222222222222222222": true - ])) - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff") - let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)! - let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)! - - // Note: this isn't exactly what Session actually does here for legacy closed - // groups (rather it uses X25519 keys) but for this test the distinction doesn't matter. - legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession() - legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession() - legacyGroup2.pointee.priority = 3 - - expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString()) - .to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e")) - expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString()) - .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) - - // The new data doesn't get stored until we call this: - user_groups_set_free_legacy_group(conf, legacyGroup2) - - let legacyGroup3: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) - expect(legacyGroup3?.pointee).toNot(beNil()) - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - 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 community1: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) - .to(beTrue()) - - expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case - expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - community1.priority = 14 - - // The new data doesn't get stored until we call this: - user_groups_set_community(conf, &community1) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed two fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - expect(pushData2.pointee.seqno).to(equal(1)) - expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len)) - .to(beEmpty()) - - // Pretend we uploaded it - let fakeHash1: String = "fakehash1" - var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - expect(config_needs_dump(conf)).to(beTrue()) - expect(config_needs_push(conf)).to(beFalse()) - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - config_dump(conf, &dump1, &dump1Len) - - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) - error2?.deallocate() - dump1?.deallocate() - - expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2 - expect(config_needs_push(conf)).to(beFalse()) - - let pushData3: UnsafeMutablePointer = config_push(conf) - expect(pushData3.pointee.seqno).to(equal(1)) - expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len)) - .to(beEmpty()) - pushData3.deallocate() - - let currentHashes1: UnsafeMutablePointer? = config_current_hashes(conf) - expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len)) - .to(equal(["fakehash1"])) - currentHashes1?.deallocate() - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData4: UnsafeMutablePointer = config_push(conf2) - expect(pushData4.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len)) - .to(beEmpty()) - pushData4.deallocate() - - let currentHashes2: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len)) - .to(equal(["fakehash1"])) - currentHashes2?.deallocate() - - expect(user_groups_size(conf2)).to(equal(2)) - expect(user_groups_size_communities(conf2)).to(equal(1)) - expect(user_groups_size_legacy_groups(conf2)).to(equal(1)) - - let legacyGroup4: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) - expect(legacyGroup4?.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal("")) - expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60)) - expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId)) - expect(legacyGroup4?.pointee.priority).to(equal(3)) - expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen")) - expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs)) - expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600)) - - var membersSeen3: [String: Bool] = [:] - var memberSessionId3: UnsafePointer? = nil - var memberAdmin3: Bool = false - let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4) - - while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) { - membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3 - } - - ugroups_legacy_members_free(membersIt3) - ugroups_legacy_group_free(legacyGroup4) - - expect(membersSeen3).to(equal([ - "050000000000000000000000000000000000000000000000000000000000000000": false, - "051111111111111111111111111111111111111111111111111111111111111111": false, - "052222222222222222222222222222222222222222222222222222222222222222": true - ])) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData5: UnsafeMutablePointer = config_push(conf2) - expect(pushData5.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData5.deallocate() - - for targetConf in [conf, conf2] { + + context("USER_GROUPS") { + it("generates config correctly") { + let createdTs: Int64 = 1680064059 + let nowTs: Int64 = Int64(Date().timeIntervalSince1970) + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // Empty contacts shouldn't have an existing contact + let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000" + var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated() + let legacyGroup1: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup1?.pointee).to(beNil()) + expect(user_groups_size(conf)).to(equal(0)) + + let legacyGroup2: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup2.pointee).toNot(beNil()) + expect(String(libSessionVal: legacyGroup2.pointee.session_id)) + .to(equal(definitelyRealId)) + expect(legacyGroup2.pointee.disappearing_timer).to(equal(0)) + expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) + expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup2.pointee.priority).to(equal(0)) + expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal("")) + expect(legacyGroup2.pointee.joined_at).to(equal(0)) + expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) + expect(legacyGroup2.pointee.mute_until).to(equal(0)) + // Iterate through and make sure we got everything we expected - var seen: [String] = [] + var membersSeen1: [String: Bool] = [:] + var memberSessionId1: UnsafePointer? = nil + var memberAdmin1: Bool = false + let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) - var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() - var c2: ugroups_community_info = ugroups_community_info() - let it: OpaquePointer = user_groups_iterator_new(targetConf) - - while !user_groups_iterator_done(it) { - if user_groups_it_is_legacy_group(it, &c1) { - var memberCount: Int = 0 - var adminCount: Int = 0 - ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") - } - else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else { - seen.append("unknown") - } - - user_groups_iterator_advance(it) + while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) { + membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1 } - user_groups_iterator_free(it) + ugroups_legacy_members_free(membersIt1) - expect(seen).to(equal([ - "community: http://example.org:5678/r/SudokuRoom", - "legacy: Englishmen, 1 admins, 2 members" + expect(membersSeen1).to(beEmpty()) + + // No need to sync a conversation with a default state + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beFalse()) + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee.seqno).to(equal(0)) + expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len)) + .to(beEmpty()) + expect(pushData1.pointee.config_len).to(equal(256)) + pushData1.deallocate() + + let users: [String] = [ + "050000000000000000000000000000000000000000000000000000000000000000", + "051111111111111111111111111111111111111111111111111111111111111111", + "052222222222222222222222222222222222222222222222222222222222222222", + "053333333333333333333333333333333333333333333333333333333333333333", + "054444444444444444444444444444444444444444444444444444444444444444", + "055555555555555555555555555555555555555555555555555555555555555555", + "056666666666666666666666666666666666666666666666666666666666666666" + ] + var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() } + legacyGroup2.pointee.name = "Englishmen".toLibSession() + legacyGroup2.pointee.disappearing_timer = 60 + legacyGroup2.pointee.joined_at = createdTs + legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL + legacyGroup2.pointee.mute_until = (nowTs + 3600) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse()) + + // Flip to and from admin + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue()) + + expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue()) + expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue()) + + var membersSeen2: [String: Bool] = [:] + var memberSessionId2: UnsafePointer? = nil + var memberAdmin2: Bool = false + let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2) + + while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) { + membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2 + } + + ugroups_legacy_members_free(membersIt2) + + expect(membersSeen2).to(equal([ + "050000000000000000000000000000000000000000000000000000000000000000": false, + "051111111111111111111111111111111111111111111111111111111111111111": false, + "052222222222222222222222222222222222222222222222222222222222222222": true ])) - } - - var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated() - var community2: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) - .to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678")) - expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) - .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) - expect(community2.priority).to(equal(14)) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData6: UnsafeMutablePointer = config_push(conf2) - expect(pushData6.pointee.seqno).to(equal(1)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData6.deallocate() - - community2.room = "sudokuRoom".toLibSession() // Change capitalization - user_groups_set_community(conf2, &community2) - - expect(config_needs_push(conf2)).to(beTrue()) - expect(config_needs_dump(conf2)).to(beTrue()) - - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - let pushData7: UnsafeMutablePointer = config_push(conf2) - expect(pushData7.pointee.seqno).to(equal(2)) - config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2) - expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) - .to(equal([fakeHash1])) - - let currentHashes3: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len)) - .to(equal([fakeHash2])) - currentHashes3?.deallocate() - - var dump2: UnsafeMutablePointer? = nil - var dump2Len: Int = 0 - config_dump(conf2, &dump2, &dump2Len) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData8: UnsafeMutablePointer = config_push(conf2) - expect(pushData8.pointee.seqno).to(equal(2)) - config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2) - expect(config_needs_dump(conf2)).to(beFalse()) - - var mergeHashes1: [UnsafePointer?] = [cFakeHash2].unsafeCopy() - var mergeData1: [UnsafePointer?] = [UnsafePointer(pushData8.pointee.config)] - var mergeSize1: [Int] = [pushData8.pointee.config_len] - expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1)) - pushData8.deallocate() - - var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() - var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated() - var community3: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) - .to(beTrue()) - expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change - - expect(user_groups_size(conf)).to(equal(2)) - expect(user_groups_size_communities(conf)).to(equal(1)) - expect(user_groups_size_legacy_groups(conf)).to(equal(1)) - - let legacyGroup5: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue()) - expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue()) - expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue()) - - expect(config_needs_push(conf2)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - - let pushData9: UnsafeMutablePointer = config_push(conf2) - expect(pushData9.pointee.seqno).to(equal(2)) - expect(config_needs_dump(conf2)).to(beFalse()) - pushData9.deallocate() - - user_groups_set_free_legacy_group(conf2, legacyGroup5) - 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() - user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room) - - let fakeHash3: String = "fakehash3" - var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() - let pushData10: UnsafeMutablePointer = config_push(conf2) - config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3) - - expect(pushData10.pointee.seqno).to(equal(3)) - expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len)) - .to(equal([fakeHash2])) - - let currentHashes4: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len)) - .to(equal([fakeHash3])) - currentHashes4?.deallocate() - - var mergeHashes2: [UnsafePointer?] = [cFakeHash3].unsafeCopy() - var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData10.pointee.config)] - var mergeSize2: [Int] = [pushData10.pointee.config_len] - expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - - expect(user_groups_size(conf)).to(equal(1)) - expect(user_groups_size_communities(conf)).to(equal(0)) - 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( - hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" - ).cArray - - ["fee", "fi", "fo", "fum"].forEach { room in - var cRoom: [CChar] = room.cArray.nullTerminated() - prio += 1 - var community4: ugroups_community_info = ugroups_community_info() - expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey)) + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff") + let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)! + let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)! + + // Note: this isn't exactly what Session actually does here for legacy closed + // groups (rather it uses X25519 keys) but for this test the distinction doesn't matter. + legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession() + legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession() + legacyGroup2.pointee.priority = 3 + + expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString()) + .to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e")) + expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString()) + .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) + + // The new data doesn't get stored until we call this: + user_groups_set_free_legacy_group(conf, legacyGroup2) + + let legacyGroup3: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cDefinitelyRealId) + expect(legacyGroup3?.pointee).toNot(beNil()) + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + 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 community1: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) .to(beTrue()) - community4.priority = prio - user_groups_set_community(conf, &community4) - } - - expect(user_groups_size(conf)).to(equal(5)) - expect(user_groups_size_communities(conf)).to(equal(4)) - expect(user_groups_size_legacy_groups(conf)).to(equal(1)) - - let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - let pushData11: UnsafeMutablePointer = config_push(conf) - config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4) - expect(pushData11.pointee.seqno).to(equal(4)) - expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len)) - .to(equal([fakeHash3, fakeHash2, fakeHash1])) - - // Load some obsolete ones in just to check that they get immediately obsoleted - let fakeHash10: String = "fakehash10" - let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated() - let fakeHash11: String = "fakehash11" - let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated() - let fakeHash12: String = "fakehash12" - let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated() - var mergeHashes3: [UnsafePointer?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy() - var mergeData3: [UnsafePointer?] = [ - UnsafePointer(pushData10.pointee.config), - UnsafePointer(pushData2.pointee.config), - UnsafePointer(pushData7.pointee.config), - UnsafePointer(pushData11.pointee.config) - ] - var mergeSize3: [Int] = [ - pushData10.pointee.config_len, - pushData2.pointee.config_len, - pushData7.pointee.config_len, - pushData11.pointee.config_len - ] - expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4)) - expect(config_needs_dump(conf2)).to(beTrue()) - expect(config_needs_push(conf2)).to(beFalse()) - pushData2.deallocate() - pushData7.deallocate() - pushData10.deallocate() - pushData11.deallocate() - - let currentHashes5: UnsafeMutablePointer? = config_current_hashes(conf2) - expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len)) - .to(equal([fakeHash4])) - currentHashes5?.deallocate() - - let pushData12: UnsafeMutablePointer = config_push(conf2) - expect(pushData12.pointee.seqno).to(equal(4)) - expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len)) - .to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3])) - pushData12.deallocate() - - for targetConf in [conf, conf2] { - // Iterate through and make sure we got everything we expected - var seen: [String] = [] - var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() - var c2: ugroups_community_info = ugroups_community_info() - let it: OpaquePointer = user_groups_iterator_new(targetConf) + expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case + expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving + expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + community1.priority = 14 - while !user_groups_iterator_done(it) { - if user_groups_it_is_legacy_group(it, &c1) { - var memberCount: Int = 0 - var adminCount: Int = 0 - ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") - } - else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") - } - else { - seen.append("unknown") - } - - user_groups_iterator_advance(it) + // The new data doesn't get stored until we call this: + user_groups_set_community(conf, &community1) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed two fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + expect(pushData2.pointee.seqno).to(equal(1)) + expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len)) + .to(beEmpty()) + + // Pretend we uploaded it + let fakeHash1: String = "fakehash1" + var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + expect(config_needs_dump(conf)).to(beTrue()) + expect(config_needs_push(conf)).to(beFalse()) + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + config_dump(conf, &dump1, &dump1Len) + + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0)) + error2?.deallocate() + dump1?.deallocate() + + expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2 + expect(config_needs_push(conf)).to(beFalse()) + + let pushData3: UnsafeMutablePointer = config_push(conf) + expect(pushData3.pointee.seqno).to(equal(1)) + expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len)) + .to(beEmpty()) + pushData3.deallocate() + + let currentHashes1: UnsafeMutablePointer? = config_current_hashes(conf) + expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len)) + .to(equal(["fakehash1"])) + currentHashes1?.deallocate() + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData4: UnsafeMutablePointer = config_push(conf2) + expect(pushData4.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len)) + .to(beEmpty()) + pushData4.deallocate() + + let currentHashes2: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len)) + .to(equal(["fakehash1"])) + currentHashes2?.deallocate() + + expect(user_groups_size(conf2)).to(equal(2)) + expect(user_groups_size_communities(conf2)).to(equal(1)) + expect(user_groups_size_legacy_groups(conf2)).to(equal(1)) + + let legacyGroup4: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) + expect(legacyGroup4?.pointee).toNot(beNil()) + expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) + expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60)) + expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId)) + expect(legacyGroup4?.pointee.priority).to(equal(3)) + expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen")) + expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs)) + expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL)) + expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600)) + + var membersSeen3: [String: Bool] = [:] + var memberSessionId3: UnsafePointer? = nil + var memberAdmin3: Bool = false + let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4) + + while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) { + membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3 } - user_groups_iterator_free(it) + ugroups_legacy_members_free(membersIt3) + ugroups_legacy_group_free(legacyGroup4) - expect(seen).to(equal([ - "community: http://jacksbeanstalk.org/r/fee", - "community: http://jacksbeanstalk.org/r/fi", - "community: http://jacksbeanstalk.org/r/fo", - "community: http://jacksbeanstalk.org/r/fum", - "legacy: Englishmen, 3 admins, 2 members" + expect(membersSeen3).to(equal([ + "050000000000000000000000000000000000000000000000000000000000000000": false, + "051111111111111111111111111111111111111111111111111111111111111111": false, + "052222222222222222222222222222222222222222222222222222222222222222": true ])) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData5: UnsafeMutablePointer = config_push(conf2) + expect(pushData5.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData5.deallocate() + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + + var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() + var c2: ugroups_community_info = ugroups_community_info() + let it: OpaquePointer = user_groups_iterator_new(targetConf) + + while !user_groups_iterator_done(it) { + if user_groups_it_is_legacy_group(it, &c1) { + var memberCount: Int = 0 + var adminCount: Int = 0 + ugroups_legacy_members_count(&c1, &memberCount, &adminCount) + seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + } + else if user_groups_it_is_community(it, &c2) { + seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else { + seen.append("unknown") + } + + user_groups_iterator_advance(it) + } + + user_groups_iterator_free(it) + + expect(seen).to(equal([ + "community: http://example.org:5678/r/SudokuRoom", + "legacy: Englishmen, 1 admins, 2 members" + ])) + } + + var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() + var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated() + var community2: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) + .to(beTrue()) + expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678")) + expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value + expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) + expect(community2.priority).to(equal(14)) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData6: UnsafeMutablePointer = config_push(conf2) + expect(pushData6.pointee.seqno).to(equal(1)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData6.deallocate() + + community2.room = "sudokuRoom".toLibSession() // Change capitalization + user_groups_set_community(conf2, &community2) + + expect(config_needs_push(conf2)).to(beTrue()) + expect(config_needs_dump(conf2)).to(beTrue()) + + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + let pushData7: UnsafeMutablePointer = config_push(conf2) + expect(pushData7.pointee.seqno).to(equal(2)) + config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2) + expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len)) + .to(equal([fakeHash1])) + + let currentHashes3: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len)) + .to(equal([fakeHash2])) + currentHashes3?.deallocate() + + var dump2: UnsafeMutablePointer? = nil + var dump2Len: Int = 0 + config_dump(conf2, &dump2, &dump2Len) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData8: UnsafeMutablePointer = config_push(conf2) + expect(pushData8.pointee.seqno).to(equal(2)) + config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2) + expect(config_needs_dump(conf2)).to(beFalse()) + + var mergeHashes1: [UnsafePointer?] = [cFakeHash2].unsafeCopy() + var mergeData1: [UnsafePointer?] = [UnsafePointer(pushData8.pointee.config)] + var mergeSize1: [Int] = [pushData8.pointee.config_len] + expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1)) + pushData8.deallocate() + + var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated() + var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated() + var community3: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) + .to(beTrue()) + expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change + + expect(user_groups_size(conf)).to(equal(2)) + expect(user_groups_size_communities(conf)).to(equal(1)) + expect(user_groups_size_legacy_groups(conf)).to(equal(1)) + + let legacyGroup5: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue()) + expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue()) + expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue()) + + expect(config_needs_push(conf2)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + + let pushData9: UnsafeMutablePointer = config_push(conf2) + expect(pushData9.pointee.seqno).to(equal(2)) + expect(config_needs_dump(conf2)).to(beFalse()) + pushData9.deallocate() + + user_groups_set_free_legacy_group(conf2, legacyGroup5) + 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() + user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room) + + let fakeHash3: String = "fakehash3" + var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated() + let pushData10: UnsafeMutablePointer = config_push(conf2) + config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3) + + expect(pushData10.pointee.seqno).to(equal(3)) + expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len)) + .to(equal([fakeHash2])) + + let currentHashes4: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len)) + .to(equal([fakeHash3])) + currentHashes4?.deallocate() + + var mergeHashes2: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeData2: [UnsafePointer?] = [UnsafePointer(pushData10.pointee.config)] + var mergeSize2: [Int] = [pushData10.pointee.config_len] + expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + + expect(user_groups_size(conf)).to(equal(1)) + expect(user_groups_size_communities(conf)).to(equal(0)) + 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( + hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" + ).cArray + + ["fee", "fi", "fo", "fum"].forEach { room in + var cRoom: [CChar] = room.cArray.nullTerminated() + prio += 1 + + var community4: ugroups_community_info = ugroups_community_info() + expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey)) + .to(beTrue()) + community4.priority = prio + user_groups_set_community(conf, &community4) + } + + expect(user_groups_size(conf)).to(equal(5)) + expect(user_groups_size_communities(conf)).to(equal(4)) + expect(user_groups_size_legacy_groups(conf)).to(equal(1)) + + let fakeHash4: String = "fakehash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + let pushData11: UnsafeMutablePointer = config_push(conf) + config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4) + expect(pushData11.pointee.seqno).to(equal(4)) + expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len)) + .to(equal([fakeHash3, fakeHash2, fakeHash1])) + + // Load some obsolete ones in just to check that they get immediately obsoleted + let fakeHash10: String = "fakehash10" + let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated() + let fakeHash11: String = "fakehash11" + let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated() + let fakeHash12: String = "fakehash12" + let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated() + var mergeHashes3: [UnsafePointer?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy() + var mergeData3: [UnsafePointer?] = [ + UnsafePointer(pushData10.pointee.config), + UnsafePointer(pushData2.pointee.config), + UnsafePointer(pushData7.pointee.config), + UnsafePointer(pushData11.pointee.config) + ] + var mergeSize3: [Int] = [ + pushData10.pointee.config_len, + pushData2.pointee.config_len, + pushData7.pointee.config_len, + pushData11.pointee.config_len + ] + expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4)) + expect(config_needs_dump(conf2)).to(beTrue()) + expect(config_needs_push(conf2)).to(beFalse()) + pushData2.deallocate() + pushData7.deallocate() + pushData10.deallocate() + pushData11.deallocate() + + let currentHashes5: UnsafeMutablePointer? = config_current_hashes(conf2) + expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len)) + .to(equal([fakeHash4])) + currentHashes5?.deallocate() + + let pushData12: UnsafeMutablePointer = config_push(conf2) + expect(pushData12.pointee.seqno).to(equal(4)) + expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len)) + .to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3])) + pushData12.deallocate() + + for targetConf in [conf, conf2] { + // Iterate through and make sure we got everything we expected + var seen: [String] = [] + + var c1: ugroups_legacy_group_info = ugroups_legacy_group_info() + var c2: ugroups_community_info = ugroups_community_info() + let it: OpaquePointer = user_groups_iterator_new(targetConf) + + while !user_groups_iterator_done(it) { + if user_groups_it_is_legacy_group(it, &c1) { + var memberCount: Int = 0 + var adminCount: Int = 0 + ugroups_legacy_members_count(&c1, &memberCount, &adminCount) + + seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + } + else if user_groups_it_is_community(it, &c2) { + seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + } + else { + seen.append("unknown") + } + + user_groups_iterator_advance(it) + } + + user_groups_iterator_free(it) + + expect(seen).to(equal([ + "community: http://jacksbeanstalk.org/r/fee", + "community: http://jacksbeanstalk.org/r/fi", + "community: http://jacksbeanstalk.org/r/fo", + "community: http://jacksbeanstalk.org/r/fum", + "legacy: Englishmen, 3 admins, 2 members" + ])) + } } } } diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift index 15cafd306..ce0ba7a6e 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigUserProfileSpec.swift @@ -12,386 +12,388 @@ import Nimble /// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches class ConfigUserProfileSpec { // MARK: - Spec - + static func spec() { - it("generates UserProfile configs correctly") { - let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") - - // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately - let identity = try! Identity.generate(from: seed) - var edSK: [UInt8] = identity.ed25519KeyPair.secretKey - expect(edSK.toHexString().suffix(64)) - .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) - expect(identity.x25519KeyPair.publicKey.toHexString()) - .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) - expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) - - // Initialize a brand new, empty config because we have no dump data to deal with. - let error: UnsafeMutablePointer? = nil - var conf: UnsafeMutablePointer? = nil - expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) - error?.deallocate() - - // We don't need to push anything, since this is an empty config - expect(config_needs_push(conf)).to(beFalse()) - // And we haven't changed anything so don't need to dump to db - expect(config_needs_dump(conf)).to(beFalse()) - - // Since it's empty there shouldn't be a name. - let namePtr: UnsafePointer? = user_profile_get_name(conf) - expect(namePtr).to(beNil()) - - // We don't need to push since we haven't changed anything, so this call is mainly just for - // testing: - let pushData1: UnsafeMutablePointer = config_push(conf) - expect(pushData1.pointee).toNot(beNil()) - expect(pushData1.pointee.seqno).to(equal(0)) - expect(pushData1.pointee.config_len).to(equal(256)) - - let encDomain: [CChar] = "UserProfile" - .bytes - .map { CChar(bitPattern: $0) } - expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile")) - - var toPushDecSize: Int = 0 - let toPushDecrypted: UnsafeMutablePointer? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize) - let prefixPadding: String = (0..<193) - .map { _ in "\0" } - .joined() - expect(toPushDecrypted).toNot(beNil()) - expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead - expect(String(pointer: toPushDecrypted, length: toPushDecSize)) - .to(equal("\(prefixPadding)d1:#i0e1:&de1:? = user_profile_get_name(conf) - expect(namePtr2).toNot(beNil()) - expect(String(cString: namePtr2!)).to(equal("Kallie")) - - let pic2: user_profile_pic = user_profile_get_pic(conf); - expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp")) - expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength)) - .to(equal("secret78901234567890123456789012".data(using: .utf8))) - expect(user_profile_get_nts_priority(conf)).to(equal(9)) - - // Since we've made changes, we should need to push new config to the swarm, *and* should need - // to dump the updated state: - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_dump(conf)).to(beTrue()) - - // incremented since we made changes (this only increments once between - // dumps; even though we changed two fields here). - let pushData2: UnsafeMutablePointer = config_push(conf) - expect(pushData2.pointee.seqno).to(equal(1)) - - // Note: This hex value differs from the value in the library tests because - // it looks like the library has an "end of cell mark" character added at the - // end (0x07 or '0007') so we need to manually add it to work - let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965") - .bytes - // The data to be actually pushed, expanded like this to make it somewhat human-readable: - let expPush1Decrypted: [UInt8] = [""" - d - 1:#i1e - 1:& d - 1:+ i9e - 1:n 6:Kallie - 1:p 34:http://example.org/omg-pic-123.bmp - 1:q 32:secret78901234567890123456789012 - e - 1:< l - l i0e 32: - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability - .bytes, - expHash0, - """ - de e - e - 1:= d - 1:+ 0: - 1:n 0: - 1:p 0: - 1:q 0: - e - e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability - .bytes - ].flatMap { $0 } - let expPush1Encrypted: [UInt8] = Data(hex: [ - "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f", - "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84", - "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868", - "2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d", - "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656", - "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea", - "49bf122762d7bc1d6d9c02f6d54f8384" - ].joined()).bytes - - let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)! - let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)! - expect(pushData2Str).to(equal(expPush1EncryptedStr)) - - // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) - var pushData2DecSize: Int = 0 - let pushData2Decrypted: UnsafeMutablePointer? = config_decrypt( - pushData2.pointee.config, - pushData2.pointee.config_len, - edSK, - encDomain, - &pushData2DecSize - ) - let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count)) - .map { _ in "\0" } - .joined() - expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead - - let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)! - let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii) - .map { "\(prefixPadding2)\($0)" }! - expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr)) - pushData2Decrypted?.deallocate() - - // We haven't dumped, so still need to dump: - expect(config_needs_dump(conf)).to(beTrue()) - // We did call push, but we haven't confirmed it as stored yet, so this will still return true: - expect(config_needs_push(conf)).to(beTrue()) - - var dump1: UnsafeMutablePointer? = nil - var dump1Len: Int = 0 - - config_dump(conf, &dump1, &dump1Len) - // (in a real client we'd now store this to disk) - - expect(config_needs_dump(conf)).to(beFalse()) - - let expDump1: [CChar] = [ - """ - d - 1:! i2e - 1:$ \(expPush1Decrypted.count): - """ - .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) }, - expPush1Decrypted - .map { CChar(bitPattern: $0) }, - """ - 1:(0: - 1:)le - e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + context("USER_PROFILE") { + it("generates config correctly") { + let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef") + + // FIXME: Would be good to move these into the libSession-util instead of using Sodium separately + let identity = try! Identity.generate(from: seed) + var edSK: [UInt8] = identity.ed25519KeyPair.secretKey + expect(edSK.toHexString().suffix(64)) + .to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7")) + expect(identity.x25519KeyPair.publicKey.toHexString()) + .to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72")) + expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString())) + + // Initialize a brand new, empty config because we have no dump data to deal with. + let error: UnsafeMutablePointer? = nil + var conf: UnsafeMutablePointer? = nil + expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0)) + error?.deallocate() + + // We don't need to push anything, since this is an empty config + expect(config_needs_push(conf)).to(beFalse()) + // And we haven't changed anything so don't need to dump to db + expect(config_needs_dump(conf)).to(beFalse()) + + // Since it's empty there shouldn't be a name. + let namePtr: UnsafePointer? = user_profile_get_name(conf) + expect(namePtr).to(beNil()) + + // We don't need to push since we haven't changed anything, so this call is mainly just for + // testing: + let pushData1: UnsafeMutablePointer = config_push(conf) + expect(pushData1.pointee).toNot(beNil()) + expect(pushData1.pointee.seqno).to(equal(0)) + expect(pushData1.pointee.config_len).to(equal(256)) + + let encDomain: [CChar] = "UserProfile" .bytes .map { CChar(bitPattern: $0) } - ].flatMap { $0 } - expect(String(pointer: dump1, length: dump1Len, encoding: .ascii)) - .to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii))) - dump1?.deallocate() - - // 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() - config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) - pushData2.deallocate() - - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump - - var dump2: UnsafeMutablePointer? = nil - var dump2Len: Int = 0 - config_dump(conf, &dump2, &dump2Len) - - let expDump2: [CChar] = [ - """ + expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile")) + + var toPushDecSize: Int = 0 + let toPushDecrypted: UnsafeMutablePointer? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize) + let prefixPadding: String = (0..<193) + .map { _ in "\0" } + .joined() + expect(toPushDecrypted).toNot(beNil()) + expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead + expect(String(pointer: toPushDecrypted, length: toPushDecSize)) + .to(equal("\(prefixPadding)d1:#i0e1:&de1:? = user_profile_get_name(conf) + expect(namePtr2).toNot(beNil()) + expect(String(cString: namePtr2!)).to(equal("Kallie")) + + let pic2: user_profile_pic = user_profile_get_pic(conf); + expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp")) + expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength)) + .to(equal("secret78901234567890123456789012".data(using: .utf8))) + expect(user_profile_get_nts_priority(conf)).to(equal(9)) + + // Since we've made changes, we should need to push new config to the swarm, *and* should need + // to dump the updated state: + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_dump(conf)).to(beTrue()) + + // incremented since we made changes (this only increments once between + // dumps; even though we changed two fields here). + let pushData2: UnsafeMutablePointer = config_push(conf) + expect(pushData2.pointee.seqno).to(equal(1)) + + // Note: This hex value differs from the value in the library tests because + // it looks like the library has an "end of cell mark" character added at the + // end (0x07 or '0007') so we need to manually add it to work + let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965") + .bytes + // The data to be actually pushed, expanded like this to make it somewhat human-readable: + let expPush1Decrypted: [UInt8] = [""" d - 1:! i0e - 1:$ \(expPush1Decrypted.count): + 1:#i1e + 1:& d + 1:+ i9e + 1:n 6:Kallie + 1:p 34:http://example.org/omg-pic-123.bmp + 1:q 32:secret78901234567890123456789012 + e + 1:< l + l i0e 32: + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability + .bytes, + expHash0, """ - .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) - .bytes - .map { CChar(bitPattern: $0) }, - expPush1Decrypted - .map { CChar(bitPattern: $0) }, - """ - 1:(9:fakehash1 - 1:)le + de e + e + 1:= d + 1:+ 0: + 1:n 0: + 1:p 0: + 1:q 0: + e e - """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability .bytes - .map { CChar(bitPattern: $0) } - ].flatMap { $0 } - expect(String(pointer: dump2, length: dump2Len, encoding: .ascii)) - .to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii))) - dump2?.deallocate() - expect(config_needs_dump(conf)).to(beFalse()) - - // Now we're going to set up a second, competing config object (in the real world this would be - // another Session client somewhere). - - // Start with an empty config, as above: - let error2: UnsafeMutablePointer? = nil - var conf2: UnsafeMutablePointer? = nil - expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0)) - expect(config_needs_dump(conf2)).to(beFalse()) - error2?.deallocate() - - // 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 mergeSize: [Int] = [expPush1Encrypted.count] - expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) - mergeHashes.forEach { $0?.deallocate() } - mergeData.forEach { $0?.deallocate() } - - // Our state has changed, so we need to dump: - expect(config_needs_dump(conf2)).to(beTrue()) - var dump3: UnsafeMutablePointer? = nil - var dump3Len: Int = 0 - config_dump(conf2, &dump3, &dump3Len) - // (store in db) - dump3?.deallocate() - expect(config_needs_dump(conf2)).to(beFalse()) - - // We *don't* need to push: even though we updated, all we did is update to the merged data (and - // didn't have any sort of merge conflict needed): - expect(config_needs_push(conf2)).to(beFalse()) - - // Now let's create a conflicting update: - - // Change the name on both clients: - user_profile_set_name(conf, "Nibbler") - user_profile_set_name(conf2, "Raz") - - // And, on conf2, we're also going to change the profile pic: - let p2: user_profile_pic = user_profile_pic( - url: "http://new.example.com/pic".toLibSession(), - key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession() - ) - user_profile_set_pic(conf2, p2) - - // Both have changes, so push need a push - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - - let fakeHash2: String = "fakehash2" - var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() - 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() - 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) - - var dump4: UnsafeMutablePointer? = nil - var dump4Len: Int = 0 - config_dump(conf, &dump4, &dump4Len); - var dump5: UnsafeMutablePointer? = nil - var dump5Len: Int = 0 - config_dump(conf2, &dump5, &dump5Len); - // (store in db) - dump4?.deallocate() - dump5?.deallocate() - - // Since we set different things, we're going to get back different serialized data to be - // pushed: - let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii) - let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii) - expect(pushData3Str).toNot(equal(pushData4Str)) - - // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client - // also fetches new messages and pulls down the other client's `seqno=2` value. - - // 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 mergeData2: [UnsafePointer?] = [UnsafePointer(pushData3.pointee.config)] - var mergeSize2: [Int] = [pushData3.pointee.config_len] - expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) - pushData3.deallocate() - var mergeHashes3: [UnsafePointer?] = [cFakeHash3].unsafeCopy() - var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] - var mergeSize3: [Int] = [pushData4.pointee.config_len] - expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) - pushData4.deallocate() - - // Now after the merge we *will* want to push from both client, since both will have generated a - // merge conflict update (with seqno = 3). - expect(config_needs_push(conf)).to(beTrue()) - expect(config_needs_push(conf2)).to(beTrue()) - let pushData5: UnsafeMutablePointer = config_push(conf) - let pushData6: UnsafeMutablePointer = config_push(conf2) - expect(pushData5.pointee.seqno).to(equal(3)) - expect(pushData6.pointee.seqno).to(equal(3)) - - // They should have resolved the conflict to the same thing: - expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) - expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) - // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized - // message just happens to have a higher hash -- and thus gets priority -- for this particular - // test). - - // Since only one of them set a profile pic there should be no conflict there: - let pic3: user_profile_pic = user_profile_get_pic(conf) - expect(pic3.url).toNot(beNil()) - expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic")) - expect(pic3.key).toNot(beNil()) - expect(Data(libSessionVal: pic3.key, count: 32).toHexString()) - .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) - let pic4: user_profile_pic = user_profile_get_pic(conf2) - expect(pic4.url).toNot(beNil()) - expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic")) - expect(pic4.key).toNot(beNil()) - expect(Data(libSessionVal: pic4.key, count: 32).toHexString()) - .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) - expect(user_profile_get_nts_priority(conf)).to(equal(9)) - expect(user_profile_get_nts_priority(conf2)).to(equal(9)) - - let fakeHash4: String = "fakehash4" - var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() - let fakeHash5: String = "fakehash5" - var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated() - config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4) - config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5) - pushData5.deallocate() - pushData6.deallocate() - - var dump6: UnsafeMutablePointer? = nil - var dump6Len: Int = 0 - config_dump(conf, &dump6, &dump6Len); - var dump7: UnsafeMutablePointer? = nil - var dump7Len: Int = 0 - config_dump(conf2, &dump7, &dump7Len); - // (store in db) - dump6?.deallocate() - dump7?.deallocate() - - expect(config_needs_dump(conf)).to(beFalse()) - expect(config_needs_dump(conf2)).to(beFalse()) - expect(config_needs_push(conf)).to(beFalse()) - expect(config_needs_push(conf2)).to(beFalse()) - - // Wouldn't do this in a normal session but doing it here to properly clean up - // after the test - conf?.deallocate() - conf2?.deallocate() + ].flatMap { $0 } + let expPush1Encrypted: [UInt8] = Data(hex: [ + "9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f", + "1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84", + "aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868", + "2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d", + "e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656", + "056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea", + "49bf122762d7bc1d6d9c02f6d54f8384" + ].joined()).bytes + + let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)! + let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)! + expect(pushData2Str).to(equal(expPush1EncryptedStr)) + + // Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data) + var pushData2DecSize: Int = 0 + let pushData2Decrypted: UnsafeMutablePointer? = config_decrypt( + pushData2.pointee.config, + pushData2.pointee.config_len, + edSK, + encDomain, + &pushData2DecSize + ) + let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count)) + .map { _ in "\0" } + .joined() + expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead + + let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)! + let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii) + .map { "\(prefixPadding2)\($0)" }! + expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr)) + pushData2Decrypted?.deallocate() + + // We haven't dumped, so still need to dump: + expect(config_needs_dump(conf)).to(beTrue()) + // We did call push, but we haven't confirmed it as stored yet, so this will still return true: + expect(config_needs_push(conf)).to(beTrue()) + + var dump1: UnsafeMutablePointer? = nil + var dump1Len: Int = 0 + + config_dump(conf, &dump1, &dump1Len) + // (in a real client we'd now store this to disk) + + expect(config_needs_dump(conf)).to(beFalse()) + + let expDump1: [CChar] = [ + """ + d + 1:! i2e + 1:$ \(expPush1Decrypted.count): + """ + .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) }, + expPush1Decrypted + .map { CChar(bitPattern: $0) }, + """ + 1:(0: + 1:)le + e + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) } + ].flatMap { $0 } + expect(String(pointer: dump1, length: dump1Len, encoding: .ascii)) + .to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii))) + dump1?.deallocate() + + // 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() + config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1) + pushData2.deallocate() + + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump + + var dump2: UnsafeMutablePointer? = nil + var dump2Len: Int = 0 + config_dump(conf, &dump2, &dump2Len) + + let expDump2: [CChar] = [ + """ + d + 1:! i0e + 1:$ \(expPush1Decrypted.count): + """ + .removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) }, + expPush1Decrypted + .map { CChar(bitPattern: $0) }, + """ + 1:(9:fakehash1 + 1:)le + e + """.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) + .bytes + .map { CChar(bitPattern: $0) } + ].flatMap { $0 } + expect(String(pointer: dump2, length: dump2Len, encoding: .ascii)) + .to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii))) + dump2?.deallocate() + expect(config_needs_dump(conf)).to(beFalse()) + + // Now we're going to set up a second, competing config object (in the real world this would be + // another Session client somewhere). + + // Start with an empty config, as above: + let error2: UnsafeMutablePointer? = nil + var conf2: UnsafeMutablePointer? = nil + expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0)) + expect(config_needs_dump(conf2)).to(beFalse()) + error2?.deallocate() + + // 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 mergeSize: [Int] = [expPush1Encrypted.count] + expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1)) + mergeHashes.forEach { $0?.deallocate() } + mergeData.forEach { $0?.deallocate() } + + // Our state has changed, so we need to dump: + expect(config_needs_dump(conf2)).to(beTrue()) + var dump3: UnsafeMutablePointer? = nil + var dump3Len: Int = 0 + config_dump(conf2, &dump3, &dump3Len) + // (store in db) + dump3?.deallocate() + expect(config_needs_dump(conf2)).to(beFalse()) + + // We *don't* need to push: even though we updated, all we did is update to the merged data (and + // didn't have any sort of merge conflict needed): + expect(config_needs_push(conf2)).to(beFalse()) + + // Now let's create a conflicting update: + + // Change the name on both clients: + user_profile_set_name(conf, "Nibbler") + user_profile_set_name(conf2, "Raz") + + // And, on conf2, we're also going to change the profile pic: + let p2: user_profile_pic = user_profile_pic( + url: "http://new.example.com/pic".toLibSession(), + key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession() + ) + user_profile_set_pic(conf2, p2) + + // Both have changes, so push need a push + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + + let fakeHash2: String = "fakehash2" + var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated() + 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() + 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) + + var dump4: UnsafeMutablePointer? = nil + var dump4Len: Int = 0 + config_dump(conf, &dump4, &dump4Len); + var dump5: UnsafeMutablePointer? = nil + var dump5Len: Int = 0 + config_dump(conf2, &dump5, &dump5Len); + // (store in db) + dump4?.deallocate() + dump5?.deallocate() + + // Since we set different things, we're going to get back different serialized data to be + // pushed: + let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii) + let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii) + expect(pushData3Str).toNot(equal(pushData4Str)) + + // Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client + // also fetches new messages and pulls down the other client's `seqno=2` value. + + // 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 mergeData2: [UnsafePointer?] = [UnsafePointer(pushData3.pointee.config)] + var mergeSize2: [Int] = [pushData3.pointee.config_len] + expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1)) + pushData3.deallocate() + var mergeHashes3: [UnsafePointer?] = [cFakeHash3].unsafeCopy() + var mergeData3: [UnsafePointer?] = [UnsafePointer(pushData4.pointee.config)] + var mergeSize3: [Int] = [pushData4.pointee.config_len] + expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1)) + pushData4.deallocate() + + // Now after the merge we *will* want to push from both client, since both will have generated a + // merge conflict update (with seqno = 3). + expect(config_needs_push(conf)).to(beTrue()) + expect(config_needs_push(conf2)).to(beTrue()) + let pushData5: UnsafeMutablePointer = config_push(conf) + let pushData6: UnsafeMutablePointer = config_push(conf2) + expect(pushData5.pointee.seqno).to(equal(3)) + expect(pushData6.pointee.seqno).to(equal(3)) + + // They should have resolved the conflict to the same thing: + expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler")) + expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler")) + // (Note that they could have also both resolved to "Raz" here, but the hash of the serialized + // message just happens to have a higher hash -- and thus gets priority -- for this particular + // test). + + // Since only one of them set a profile pic there should be no conflict there: + let pic3: user_profile_pic = user_profile_get_pic(conf) + expect(pic3.url).toNot(beNil()) + expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic")) + expect(pic3.key).toNot(beNil()) + expect(Data(libSessionVal: pic3.key, count: 32).toHexString()) + .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) + let pic4: user_profile_pic = user_profile_get_pic(conf2) + expect(pic4.url).toNot(beNil()) + expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic")) + expect(pic4.key).toNot(beNil()) + expect(Data(libSessionVal: pic4.key, count: 32).toHexString()) + .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) + expect(user_profile_get_nts_priority(conf)).to(equal(9)) + expect(user_profile_get_nts_priority(conf2)).to(equal(9)) + + let fakeHash4: String = "fakehash4" + var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated() + let fakeHash5: String = "fakehash5" + var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated() + config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4) + config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5) + pushData5.deallocate() + pushData6.deallocate() + + var dump6: UnsafeMutablePointer? = nil + var dump6Len: Int = 0 + config_dump(conf, &dump6, &dump6Len); + var dump7: UnsafeMutablePointer? = nil + var dump7Len: Int = 0 + config_dump(conf2, &dump7, &dump7Len); + // (store in db) + dump6?.deallocate() + dump7?.deallocate() + + expect(config_needs_dump(conf)).to(beFalse()) + expect(config_needs_dump(conf2)).to(beFalse()) + expect(config_needs_push(conf)).to(beFalse()) + expect(config_needs_push(conf2)).to(beFalse()) + + // Wouldn't do this in a normal session but doing it here to properly clean up + // after the test + conf?.deallocate() + conf2?.deallocate() + } } } } diff --git a/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift b/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift index be670ad5d..42d0746d7 100644 --- a/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/LibSessionSpec.swift @@ -12,9 +12,11 @@ class LibSessionSpec: QuickSpec { // MARK: - Spec override func spec() { - ConfigContactsSpec.spec() - ConfigUserProfileSpec.spec() - ConfigConvoInfoVolatileSpec.spec() - ConfigUserGroupsSpec.spec() + describe("libSession") { + ConfigContactsSpec.spec() + ConfigUserProfileSpec.spec() + ConfigConvoInfoVolatileSpec.spec() + ConfigUserGroupsSpec.spec() + } } } diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index da4ca0e8f..5d2b08362 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -53,12 +53,16 @@ class ThreadSettingsViewModelSpec: QuickSpec { try Profile( id: "05\(TestConstants.publicKey)", - name: "TestMe" + name: "TestMe", + lastNameUpdate: 0, + lastProfilePictureUpdate: 0 ).insert(db) try Profile( id: "TestId", - name: "TestUser" + name: "TestUser", + lastNameUpdate: 0, + lastProfilePictureUpdate: 0 ).insert(db) } viewModel = ThreadSettingsViewModel( diff --git a/SessionUIKit/Components/ConfirmationModal.swift b/SessionUIKit/Components/ConfirmationModal.swift index 27cf08520..a7f4088d7 100644 --- a/SessionUIKit/Components/ConfirmationModal.swift +++ b/SessionUIKit/Components/ConfirmationModal.swift @@ -4,15 +4,38 @@ import UIKit import SessionUtilitiesKit // FIXME: Refactor as part of the Groups Rebuild -public class ConfirmationModal: Modal { +public class ConfirmationModal: Modal, UITextFieldDelegate { private static let closeSize: CGFloat = 24 private var internalOnConfirm: ((ConfirmationModal) -> ())? = nil private var internalOnCancel: ((ConfirmationModal) -> ())? = nil private var internalOnBodyTap: (() -> ())? = nil + private var internalOnTextChanged: ((String) -> ())? = nil // MARK: - Components + private lazy var contentTapGestureRecognizer: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(contentViewTapped) + ) + contentView.addGestureRecognizer(result) + result.isEnabled = false + + return result + }() + + private lazy var imageViewTapGestureRecognizer: UITapGestureRecognizer = { + let result: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(imageViewTapped) + ) + imageViewContainer.addGestureRecognizer(result) + result.isEnabled = false + + return result + }() + private lazy var titleLabel: UILabel = { let result: UILabel = UILabel() result.font = .boldSystemFont(ofSize: Values.mediumFontSize) @@ -36,16 +59,30 @@ public class ConfirmationModal: Modal { return result }() + private lazy var textFieldContainer: UIView = { + let result: UIView = UIView() + result.themeBorderColor = .borderSeparator + result.layer.cornerRadius = 11 + result.layer.borderWidth = 1 + result.isHidden = true + result.set(.height, to: 40) + + return result + }() + + private lazy var textField: UITextField = { + let result: UITextField = UITextField() + result.font = .systemFont(ofSize: Values.smallFontSize) + result.themeTextColor = .textPrimary + result.delegate = self + + return result + }() + private lazy var imageViewContainer: UIView = { let result: UIView = UIView() result.isHidden = true - let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( - target: self, - action: #selector(imageViewTapped) - ) - result.addGestureRecognizer(gestureRecogniser) - return result }() @@ -70,7 +107,7 @@ public class ConfirmationModal: Modal { }() private lazy var contentStackView: UIStackView = { - let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, imageViewContainer ]) + let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel, textFieldContainer, imageViewContainer ]) result.axis = .vertical result.spacing = Values.smallSpacing result.isLayoutMarginsRelativeArrangement = true @@ -132,13 +169,22 @@ public class ConfirmationModal: Modal { } public override func populateContentView() { + let gestureRecogniser: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(contentViewTapped) + ) + contentView.addGestureRecognizer(gestureRecogniser) + contentView.addSubview(mainStackView) contentView.addSubview(closeButton) + textFieldContainer.addSubview(textField) + textField.pin(to: textFieldContainer, withInset: 12) + imageViewContainer.addSubview(profileView) profileView.center(.horizontal, in: imageViewContainer) - profileView.pin(.top, to: .top, of: imageViewContainer)//, withInset: 15) - profileView.pin(.bottom, to: .bottom, of: imageViewContainer)//, withInset: -15) + profileView.pin(.top, to: .top, of: imageViewContainer) + profileView.pin(.bottom, to: .bottom, of: imageViewContainer) mainStackView.pin(to: contentView) closeButton.pin(.top, to: .top, of: contentView, withInset: 8) @@ -149,6 +195,7 @@ public class ConfirmationModal: Modal { public func updateContent(with info: Info) { internalOnBodyTap = nil + internalOnTextChanged = nil internalOnConfirm = { modal in if info.dismissOnConfirm { modal.close() @@ -161,6 +208,8 @@ public class ConfirmationModal: Modal { info.onCancel?(modal) } + contentTapGestureRecognizer.isEnabled = true + imageViewTapGestureRecognizer.isEnabled = false // Set the content based on the provided info titleLabel.text = info.title @@ -179,6 +228,15 @@ public class ConfirmationModal: Modal { explanationLabel.attributedText = attributedText explanationLabel.isHidden = false + case .input(let explanation, let placeholder, let value, let clearButton, let onTextChanged): + explanationLabel.attributedText = explanation + explanationLabel.isHidden = (explanation == nil) + textField.placeholder = placeholder + textField.text = (value ?? "") + textField.clearButtonMode = (clearButton ? .always : .never) + textFieldContainer.isHidden = false + internalOnTextChanged = onTextChanged + case .image(let placeholder, let value, let icon, let style, let accessibility, let onClick): imageViewContainer.isAccessibilityElement = (accessibility != nil) imageViewContainer.accessibilityIdentifier = accessibility?.identifier @@ -193,6 +251,8 @@ public class ConfirmationModal: Modal { ) ) internalOnBodyTap = onClick + contentTapGestureRecognizer.isEnabled = false + imageViewTapGestureRecognizer.isEnabled = true } confirmButton.accessibilityLabel = info.confirmAccessibility?.label @@ -216,8 +276,38 @@ public class ConfirmationModal: Modal { contentView.accessibilityIdentifier = info.accessibility?.identifier } + // MARK: - UITextFieldDelegate + + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + public func textFieldShouldClear(_ textField: UITextField) -> Bool { + internalOnTextChanged?("") + return true + } + + public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let text: String = textField.text, let textRange: Range = Range(range, in: text) { + let updatedText = text.replacingCharacters(in: textRange, with: string) + + internalOnTextChanged?(updatedText) + } + + return true + } + // MARK: - Interaction + @objc private func contentViewTapped() { + if textField.isFirstResponder { + textField.resignFirstResponder() + } + + internalOnBodyTap?() + } + @objc private func imageViewTapped() { internalOnBodyTap?() } @@ -400,8 +490,14 @@ public extension ConfirmationModal.Info { case none case text(String) case attributedText(NSAttributedString) - // FIXME: Implement these - // case input(placeholder: String, value: String?) + case input( + explanation: NSAttributedString?, + placeholder: String, + initialValue: String?, + clearButton: Bool, + onChange: (String) -> () + ) + // FIXME: Implement this // case radio(explanation: NSAttributedString?, options: [(title: String, selected: Bool)]) case image( placeholderData: Data?, @@ -418,14 +514,15 @@ public extension ConfirmationModal.Info { case (.text(let lhsText), .text(let rhsText)): return (lhsText == rhsText) case (.attributedText(let lhsText), .attributedText(let rhsText)): return (lhsText == rhsText) - // FIXME: Implement these - //case (.input(let lhsPlaceholder, let lhsValue), .input(let rhsPlaceholder, let rhsValue)): - // return ( - // lhsPlaceholder == rhsPlaceholder && - // lhsValue == rhsValue && - // ) + case (.input(let lhsExplanation, let lhsPlaceholder, let lhsInitialValue, let lhsClearButton, _), .input(let rhsExplanation, let rhsPlaceholder, let rhsInitialValue, let rhsClearButton, _)): + return ( + lhsExplanation == rhsExplanation && + lhsPlaceholder == rhsPlaceholder && + lhsInitialValue == rhsInitialValue && + lhsClearButton == rhsClearButton + ) - // FIXME: Implement these + // FIXME: Implement this //case (.radio(let lhsExplanation, let lhsOptions), .radio(let rhsExplanation, let rhsOptions)): // return ( // lhsExplanation == rhsExplanation && @@ -450,6 +547,12 @@ public extension ConfirmationModal.Info { case .none: break case .text(let text): text.hash(into: &hasher) case .attributedText(let text): text.hash(into: &hasher) + + case .input(let explanation, let placeholder, let initialValue, let clearButton, _): + explanation.hash(into: &hasher) + placeholder.hash(into: &hasher) + initialValue.hash(into: &hasher) + clearButton.hash(into: &hasher) case .image(let placeholder, let value, let icon, let style, let accessibility, _): placeholder.hash(into: &hasher) diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 4993be3f7..1a28da053 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import CryptoKit import Combine import GRDB import SignalCoreKit @@ -492,3 +493,38 @@ public extension ValueObservation { .eraseToAnyPublisher() } } + +// MARK: - Debug Convenience + +#if DEBUG +public extension Storage { + func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) { + var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() + defer { keySpec.resetBytes(in: 0.. #import #import +#import diff --git a/SessionUtilitiesKit/Utilities/CExceptionHelper.h b/SessionUtilitiesKit/Utilities/CExceptionHelper.h new file mode 100644 index 000000000..6d5cd8adc --- /dev/null +++ b/SessionUtilitiesKit/Utilities/CExceptionHelper.h @@ -0,0 +1,16 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +#ifndef __CExceptionHelper_h__ +#define __CExceptionHelper_h__ + +#import + +#define noEscape __attribute__((noescape)) + +@interface CExceptionHelper: NSObject + ++ (BOOL)performSafely:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error; + +@end + +#endif diff --git a/SessionUtilitiesKit/Utilities/CExceptionHelper.mm b/SessionUtilitiesKit/Utilities/CExceptionHelper.mm new file mode 100644 index 000000000..fac2e007e --- /dev/null +++ b/SessionUtilitiesKit/Utilities/CExceptionHelper.mm @@ -0,0 +1,36 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// This logic is not foolproof and may result in memory-leaks, when possible we should look to remove this +// and use the native C++ <-> Swift interoperability coming with Swift 5.9 +// +// This solution was sourced from the following link, for more information please refer to this thread: +// https://forums.swift.org/t/pitch-a-swift-representation-for-thrown-and-caught-exceptions/54583 + +#import "CExceptionHelper.h" +#include + +@implementation CExceptionHelper + ++ (BOOL)performSafely:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error { + try { + tryBlock(); + return YES; + } + catch(NSException* e) { + *error = [[NSError alloc] initWithDomain:e.name code:-1 userInfo:e.userInfo]; + return NO; + } + catch (std::exception& e) { + NSString* what = [NSString stringWithUTF8String: e.what()]; + NSDictionary* userInfo = @{NSLocalizedDescriptionKey : what}; + *error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-2 userInfo:userInfo]; + return NO; + } + catch(...) { + NSDictionary* userInfo = @{NSLocalizedDescriptionKey:@"Other C++ exception"}; + *error = [[NSError alloc] initWithDomain:@"cpp_exception" code:-3 userInfo:userInfo]; + return NO; + } +} + +@end