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
This commit is contained in:
parent
65bf7d7d82
commit
d2c82cb915
|
@ -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")
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
FD2B4B032949887A00AB4848 /* QueryInterfaceRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QueryInterfaceRequest+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD3003652A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigMessageReceiveJob.swift; sourceTree = "<group>"; };
|
||||
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CExceptionHelper.h; sourceTree = "<group>"; };
|
||||
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CExceptionHelper.mm; sourceTree = "<group>"; };
|
||||
FD368A6729DE8F9B000DBF1E /* _012_AddFTSIfNeeded.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _012_AddFTSIfNeeded.swift; sourceTree = "<group>"; };
|
||||
FD368A6929DE9E30000DBF1E /* UIContextualAction+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Utilities.swift"; sourceTree = "<group>"; };
|
||||
FD37E9C228A1C6F3003AE748 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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<NoNav, HelpViewModel.Section, HelpViewModel.Section> {
|
||||
#if DEBUG
|
||||
private var databaseKeyEncryptionPassword: String = ""
|
||||
#endif
|
||||
|
||||
// MARK: - Section
|
||||
|
||||
public enum Section: SessionTableSection {
|
||||
|
@ -17,6 +22,9 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
case feedback
|
||||
case faq
|
||||
case support
|
||||
#if DEBUG
|
||||
case exportDatabase
|
||||
#endif
|
||||
|
||||
var style: SessionTableSectionStyle { .padding }
|
||||
}
|
||||
|
@ -136,6 +144,28 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
]
|
||||
)
|
||||
]
|
||||
#if DEBUG
|
||||
.appending(
|
||||
SectionModel(
|
||||
model: .exportDatabase,
|
||||
elements: [
|
||||
SessionCell.Info(
|
||||
id: .support,
|
||||
title: "Export Database",
|
||||
rightAccessory: .icon(
|
||||
UIImage(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
size: .small
|
||||
),
|
||||
styling: SessionCell.StyleInfo(
|
||||
tintColor: .danger
|
||||
),
|
||||
onTapView: { [weak self] view in self?.exportDatabase(view) }
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
#endif
|
||||
}
|
||||
.removeDuplicates()
|
||||
.publisher(in: Storage.shared)
|
||||
|
@ -197,4 +227,132 @@ class HelpViewModel: SessionTableViewModel<NoNav, HelpViewModel.Section, HelpVie
|
|||
showShareSheet()
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private func exportDatabase(_ targetView: UIView?) {
|
||||
let generatedPassword: String = UUID().uuidString
|
||||
self.databaseKeyEncryptionPassword = generatedPassword
|
||||
|
||||
self.transitionToScreen(
|
||||
ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "Export Database",
|
||||
body: .input(
|
||||
explanation: NSAttributedString(
|
||||
string: """
|
||||
Sharing the database and key together is dangerous!
|
||||
|
||||
We've generated a secure password for you but feel free to provide your own (we will show the generated password again after exporting)
|
||||
|
||||
This password will be used to encrypt the database decryption key and will be exported alongside the database
|
||||
"""
|
||||
),
|
||||
placeholder: "Enter a password",
|
||||
initialValue: generatedPassword,
|
||||
clearButton: true,
|
||||
onChange: { [weak self] value in self?.databaseKeyEncryptionPassword = value }
|
||||
),
|
||||
confirmTitle: "Export",
|
||||
dismissOnConfirm: false,
|
||||
onConfirm: { [weak self] modal in
|
||||
modal.dismiss(animated: true) {
|
||||
guard let password: String = self?.databaseKeyEncryptionPassword, password.count >= 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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> = 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<config_object>?
|
||||
) 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<config_object>?,
|
||||
latestConfigSentTimestampMs: Int64
|
||||
) -> [String: ContactData] {
|
||||
var result: [String: ContactData] = [:]
|
||||
var contact: contacts_contact = contacts_contact()
|
||||
let contactIterator: UnsafeMutablePointer<contacts_iterator> = 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<config_object>?
|
||||
) -> [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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<contacts_contact>? = 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_data> = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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_data> = 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> = 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<CChar>?
|
||||
var conf: UnsafeMutablePointer<config_object>?
|
||||
|
||||
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<UInt8>? = 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_data> = config_push(conf)
|
||||
expect(pushData6.pointee.seqno).to(equal(3))
|
||||
|
||||
let pushData7: UnsafeMutablePointer<config_push_data> = 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<CChar>?] = [cFakeHash3b].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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<CChar>?] = [cFakeHash3a].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [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_data> = config_push(conf)
|
||||
expect(pushData8.pointee.seqno).to(equal(4))
|
||||
|
||||
let pushData9: UnsafeMutablePointer<config_push_data> = 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> = 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<CChar>?
|
||||
var conf: UnsafeMutablePointer<config_object>?
|
||||
|
||||
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<CChar>?
|
||||
var conf: UnsafeMutablePointer<config_object>?
|
||||
|
||||
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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<contacts_contact>? = 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_data> = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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_data> = 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> = 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_data> = config_push(conf)
|
||||
expect(pushData6.pointee.seqno).to(equal(3))
|
||||
|
||||
let pushData7: UnsafeMutablePointer<config_push_data> = 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<CChar>?] = [cFakeHash3b].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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<CChar>?] = [cFakeHash3a].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [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_data> = config_push(conf)
|
||||
expect(pushData8.pointee.seqno).to(equal(4))
|
||||
|
||||
let pushData9: UnsafeMutablePointer<config_push_data> = 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> = 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<config_object>?,
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
|
||||
expect(legacyGroup1?.pointee).to(beNil())
|
||||
expect(user_groups_size(conf)).to(equal(0))
|
||||
|
||||
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = 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<CChar>? = 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_data> = 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<CChar>? = 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<ugroups_legacy_group_info>? = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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_string_list>? = 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_data> = 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_string_list>? = 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<ugroups_legacy_group_info>? = 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<CChar>? = 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_data> = 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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
|
||||
expect(legacyGroup1?.pointee).to(beNil())
|
||||
expect(user_groups_size(conf)).to(equal(0))
|
||||
|
||||
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = 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<CChar>? = 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_data> = 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<CChar>? = 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_data> = 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_data> = 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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
|
||||
.to(equal([fakeHash2]))
|
||||
currentHashes3?.deallocate()
|
||||
|
||||
var dump2: UnsafeMutablePointer<UInt8>? = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData1: [UnsafePointer<UInt8>?] = [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<ugroups_legacy_group_info>? = 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_data> = 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_data> = 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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
|
||||
.to(equal([fakeHash3]))
|
||||
currentHashes4?.deallocate()
|
||||
|
||||
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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<ugroups_legacy_group_info>? = 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_data> = 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<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [
|
||||
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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
|
||||
.to(equal([fakeHash4]))
|
||||
currentHashes5?.deallocate()
|
||||
|
||||
let pushData12: UnsafeMutablePointer<config_push_data> = 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_data> = 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<UInt8>? = nil
|
||||
var dump1Len: Int = 0
|
||||
config_dump(conf, &dump1, &dump1Len)
|
||||
|
||||
let error2: UnsafeMutablePointer<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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_data> = 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_string_list>? = 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_data> = 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_string_list>? = 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<ugroups_legacy_group_info>? = 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<CChar>? = 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_data> = 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_data> = 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_data> = 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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
|
||||
.to(equal([fakeHash2]))
|
||||
currentHashes3?.deallocate()
|
||||
|
||||
var dump2: UnsafeMutablePointer<UInt8>? = 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_data> = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData1: [UnsafePointer<UInt8>?] = [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<ugroups_legacy_group_info>? = 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_data> = 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_data> = 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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
|
||||
.to(equal([fakeHash3]))
|
||||
currentHashes4?.deallocate()
|
||||
|
||||
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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_data> = 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<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [
|
||||
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_string_list>? = config_current_hashes(conf2)
|
||||
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
|
||||
.to(equal([fakeHash4]))
|
||||
currentHashes5?.deallocate()
|
||||
|
||||
let pushData12: UnsafeMutablePointer<config_push_data> = 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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<CChar>? = 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_data> = 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<UInt8>? = 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:<le1:=dee"))
|
||||
pushData1.deallocate()
|
||||
toPushDecrypted?.deallocate()
|
||||
|
||||
// This should also be unset:
|
||||
let pic: user_profile_pic = user_profile_get_pic(conf)
|
||||
expect(String(libSessionVal: pic.url)).to(beEmpty())
|
||||
|
||||
// Now let's go set a profile name and picture:
|
||||
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
|
||||
let p: user_profile_pic = user_profile_pic(
|
||||
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
|
||||
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
|
||||
)
|
||||
expect(user_profile_set_pic(conf, p)).to(equal(0))
|
||||
user_profile_set_nts_priority(conf, 9)
|
||||
|
||||
// Retrieve them just to make sure they set properly:
|
||||
let namePtr2: UnsafePointer<CChar>? = 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_data> = 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<UInt8>? = 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<UInt8>? = 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<CChar>? = nil
|
||||
var conf: UnsafeMutablePointer<config_object>? = 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<CChar>? = 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_data> = 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<UInt8>? = 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<UInt8>? = 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:<le1:=dee"))
|
||||
pushData1.deallocate()
|
||||
toPushDecrypted?.deallocate()
|
||||
|
||||
// This should also be unset:
|
||||
let pic: user_profile_pic = user_profile_get_pic(conf)
|
||||
expect(String(libSessionVal: pic.url)).to(beEmpty())
|
||||
|
||||
// Now let's go set a profile name and picture:
|
||||
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
|
||||
let p: user_profile_pic = user_profile_pic(
|
||||
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
|
||||
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
|
||||
)
|
||||
expect(user_profile_set_pic(conf, p)).to(equal(0))
|
||||
user_profile_set_nts_priority(conf, 9)
|
||||
|
||||
// Retrieve them just to make sure they set properly:
|
||||
let namePtr2: UnsafePointer<CChar>? = 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_data> = 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<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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<CChar>?] = [cFakeHash1].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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<UInt8>? = 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_data> = 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_data> = 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<UInt8>? = nil
|
||||
var dump4Len: Int = 0
|
||||
config_dump(conf, &dump4, &dump4Len);
|
||||
var dump5: UnsafeMutablePointer<UInt8>? = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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<CChar>?] = [cFakeHash3].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [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_data> = config_push(conf)
|
||||
let pushData6: UnsafeMutablePointer<config_push_data> = 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<UInt8>? = nil
|
||||
var dump6Len: Int = 0
|
||||
config_dump(conf, &dump6, &dump6Len);
|
||||
var dump7: UnsafeMutablePointer<UInt8>? = 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<UInt8>? = 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<UInt8>? = 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<UInt8>? = 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<CChar>? = nil
|
||||
var conf2: UnsafeMutablePointer<config_object>? = 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<CChar>?] = [cFakeHash1].unsafeCopy()
|
||||
var mergeData: [UnsafePointer<UInt8>?] = [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<UInt8>? = 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_data> = 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_data> = 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<UInt8>? = nil
|
||||
var dump4Len: Int = 0
|
||||
config_dump(conf, &dump4, &dump4Len);
|
||||
var dump5: UnsafeMutablePointer<UInt8>? = 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<CChar>?] = [cFakeHash2].unsafeCopy()
|
||||
var mergeData2: [UnsafePointer<UInt8>?] = [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<CChar>?] = [cFakeHash3].unsafeCopy()
|
||||
var mergeData3: [UnsafePointer<UInt8>?] = [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_data> = config_push(conf)
|
||||
let pushData6: UnsafeMutablePointer<config_push_data> = 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<UInt8>? = nil
|
||||
var dump6Len: Int = 0
|
||||
config_dump(conf, &dump6, &dump6Len);
|
||||
var dump7: UnsafeMutablePointer<UInt8>? = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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..<keySpec.count) } // Reset content immediately after use
|
||||
|
||||
guard var passwordData: Data = password.data(using: .utf8) else { throw StorageError.generic }
|
||||
defer { passwordData.resetBytes(in: 0..<passwordData.count) } // Reset content immediately after use
|
||||
|
||||
/// Encrypt the `keySpec` value using a SHA256 of the password provided and a nonce then base64-encode the encrypted
|
||||
/// data and save it to a temporary file to share alongside the database
|
||||
///
|
||||
/// Decrypt the key via the termincal on macOS by running the command in the project root directory
|
||||
/// `swift ./Scropts/DecryptExportedKey.swift {BASE64_CIPHERTEXT} {PASSWORD}`
|
||||
///
|
||||
/// Where `BASE64_CIPHERTEXT` is the content of the `key.enc` file and `PASSWORD` is the password provided via the
|
||||
/// prompt during export
|
||||
let nonce: ChaChaPoly.Nonce = ChaChaPoly.Nonce()
|
||||
let hash: SHA256.Digest = SHA256.hash(data: passwordData)
|
||||
let key: SymmetricKey = SymmetricKey(data: Data(hash.makeIterator()))
|
||||
let sealedBox: ChaChaPoly.SealedBox = try ChaChaPoly.seal(keySpec, using: key, nonce: nonce, authenticating: Data())
|
||||
let keyInfoPath: String = "\(NSTemporaryDirectory())key.enc"
|
||||
let encryptedKeyBase64: String = sealedBox.combined.base64EncodedString()
|
||||
try encryptedKeyBase64.write(toFile: keyInfoPath, atomically: true, encoding: .utf8)
|
||||
|
||||
return (
|
||||
Storage.databasePath,
|
||||
keyInfoPath
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -16,4 +16,5 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
|
|||
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/OWSBackgroundTask.h>
|
||||
#import <SessionUtilitiesKit/CExceptionHelper.h>
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
#ifndef __CExceptionHelper_h__
|
||||
#define __CExceptionHelper_h__
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#define noEscape __attribute__((noescape))
|
||||
|
||||
@interface CExceptionHelper: NSObject
|
||||
|
||||
+ (BOOL)performSafely:(noEscape void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
|
@ -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 <exception>
|
||||
|
||||
@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
|
Loading…
Reference in New Issue