From 733694d464451eeb1b4483c17ec92c7aae2de223 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 21 May 2024 17:43:01 +1000 Subject: [PATCH] Defensive coding for C API conversation, threading & logging tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Reworked some of the C API conversions to try to prevent invalid cases • Tweaked the threading around libSession networking callbacks to minimise Swift code blocking libSession threads • More logging tweaks --- Scripts/LintLocalizableStrings.swift | 3 - .../Settings/ThreadSettingsViewModel.swift | 28 ++-- Session/Meta/MainAppContext.swift | 5 +- .../PushRegistrationManager.swift | 2 +- .../UserNotificationsAdaptee.swift | 6 +- .../Jobs/Types/MessageSendJob.swift | 3 +- .../Config Handling/LibSession+Contacts.swift | 10 +- .../LibSession+ConvoInfoVolatile.swift | 51 +++---- .../Config Handling/LibSession+Shared.swift | 10 +- .../LibSession+UserGroups.swift | 38 ++--- .../LibSession+UserProfile.swift | 2 +- .../LibSession+SessionMessagingKit.swift | 51 ++++--- .../Errors/MessageSenderError.swift | 2 +- .../Pollers/OpenGroupPoller.swift | 2 +- .../Utilities/ProfileManager.swift | 14 +- .../NotificationServiceExtension.swift | 1 - .../SAEScreenLockViewController.swift | 18 +-- .../ShareAppExtensionContext.swift | 2 +- .../LibSession/LibSession+Networking.swift | 132 ++++++++++++------ .../General/Collection+Utilities.swift | 37 +++-- .../LibSession/LibSessionError.swift | 2 + .../Networking/BatchRequest.swift | 2 +- 22 files changed, 257 insertions(+), 164 deletions(-) diff --git a/Scripts/LintLocalizableStrings.swift b/Scripts/LintLocalizableStrings.swift index 851fb78e7..b957d354a 100755 --- a/Scripts/LintLocalizableStrings.swift +++ b/Scripts/LintLocalizableStrings.swift @@ -36,7 +36,6 @@ extension ProjectState { .contains("precondition(", caseSensitive: false), .contains("preconditionFailure(", caseSensitive: false), .contains("print(", caseSensitive: false), - .contains("NSLog(", caseSensitive: false), .contains("SNLog(", caseSensitive: false), .contains("Log.setup(", caseSensitive: false), .contains("Log.trace(", caseSensitive: false), @@ -80,8 +79,6 @@ extension ProjectState { ), .contains("SQL(", caseSensitive: false), .regex(".*static var databaseTableName: String"), - .regex("Logger\\..*\\("), - .regex("OWSLogger\\..*\\("), .regex("case .* = "), .regex("Error.*\\(") ] diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 276575511..2414dc9ac 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -357,16 +357,14 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi case .community: guard - let server: String = threadViewModel.openGroupServer, - let roomToken: String = threadViewModel.openGroupRoomToken, - let publicKey: String = threadViewModel.openGroupPublicKey + let urlString: String = LibSession.communityUrlFor( + server: threadViewModel.openGroupServer, + roomToken: threadViewModel.openGroupRoomToken, + publicKey: threadViewModel.openGroupPublicKey + ) else { return } - UIPasteboard.general.string = LibSession.communityUrlFor( - server: server, - roomToken: roomToken, - publicKey: publicKey - ) + UIPasteboard.general.string = urlString } self?.showToast( @@ -765,17 +763,13 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi private func addUsersToOpenGoup(threadViewModel: SessionThreadViewModel, selectedUsers: Set) { guard let name: String = threadViewModel.openGroupName, - let server: String = threadViewModel.openGroupServer, - let roomToken: String = threadViewModel.openGroupRoomToken, - let publicKey: String = threadViewModel.openGroupPublicKey + let communityUrl: String = LibSession.communityUrlFor( + server: threadViewModel.openGroupServer, + roomToken: threadViewModel.openGroupRoomToken, + publicKey: threadViewModel.openGroupPublicKey + ) else { return } - let communityUrl: String = LibSession.communityUrlFor( - server: server, - roomToken: roomToken, - publicKey: publicKey - ) - dependencies.storage.writeAsync { [dependencies] db in let currentUserSessionId: String = getUserHexEncodedPublicKey(db, using: dependencies) try selectedUsers.forEach { userId in diff --git a/Session/Meta/MainAppContext.swift b/Session/Meta/MainAppContext.swift index 40bb5d42c..e5849ac49 100644 --- a/Session/Meta/MainAppContext.swift +++ b/Session/Meta/MainAppContext.swift @@ -74,7 +74,6 @@ final class MainAppContext: AppContext { AssertIsOnMainThread() self.reportedApplicationState = .inactive - OWSLogger.info("") NotificationCenter.default.post( name: .sessionWillEnterForeground, @@ -149,10 +148,10 @@ final class MainAppContext: AppContext { if blockingObjects.count > 1 { logString = "\(logString) (and \(blockingObjects.count - 1) others)" } - OWSLogger.info(logString) + Log.info(logString) } else { - OWSLogger.info("Unblocking Sleep.") + Log.info("Unblocking Sleep.") } } UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index f8c98f90a..52c817e60 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -188,7 +188,7 @@ public enum PushRegistrationError: Error { .map { tokenData -> String in if self.isSusceptibleToFailedPushRegistration { // Sentinal in case this bug is fixed - OWSLogger.debug("Device was unexpectedly able to complete push registration even though it was susceptible to failure.") + Log.debug("Device was unexpectedly able to complete push registration even though it was susceptible to failure.") } return tokenData.toHexString() diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index e33173dc8..c871e8def 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -265,7 +265,7 @@ public class UserNotificationActionHandler: NSObject { case .finished: break case .failure(let error): completionHandler() - OWSLogger.error("Failed to handle notification response: \(error)") + Log.error("Failed to handle notification response: \(error)") } }, receiveValue: { _ in completionHandler() } @@ -281,14 +281,14 @@ public class UserNotificationActionHandler: NSObject { switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier: - OWSLogger.debug("Notification response: default action") + Log.debug("Notification response: default action") return actionHandler.showThread(userInfo: userInfo) .setFailureType(to: Error.self) .eraseToAnyPublisher() case UNNotificationDismissActionIdentifier: // TODO - mark as read? - OWSLogger.debug("Notification response: dismissed notification") + Log.debug("Notification response: dismissed notification") return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index 7d6b527ca..c7e507928 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -162,6 +162,7 @@ public enum MessageSendJob: JobExecutor { // Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error let originalSentTimestamp: UInt64? = details.message.sentTimestamp + let startTime: CFTimeInterval = CACurrentMediaTime() /// Perform the actual message sending - this will timeout if the entire process takes longer than `HTTP.defaultTimeout * 2` /// which can occur if it needs to build a new onion path (which doesn't actually have any limits so can take forever in rare cases) @@ -193,7 +194,7 @@ public enum MessageSendJob: JobExecutor { case .failure(let error): switch error { case MessageSenderError.sendJobTimeout: - SNLog("[MessageSendJob] Couldn't send message due to error: \(error) (paths: \(LibSession.pathsDescription)).") + SNLog("[MessageSendJob] Failed after \(CACurrentMediaTime() - startTime)s: \(error).") // In this case the `MessageSender` process gets cancelled so we need to // call `handleFailedMessageSend` to update the statuses correctly diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index e1569b9db..70a1441c3 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -283,9 +283,11 @@ internal extension LibSession { // Update the name try targetContacts .forEach { info in - var sessionId: [CChar] = info.id.cArray.nullTerminated() var contact: contacts_contact = contacts_contact() - guard contacts_get_or_construct(conf, &contact, &sessionId) else { + guard + var sessionId: [CChar] = info.id.cString(using: .utf8), + contacts_get_or_construct(conf, &contact, &sessionId) + else { /// It looks like there are some situations where this object might not get created correctly (and /// will throw due to the implicit unwrapping) as a result we put it in a guard and throw instead SNLog("Unable to upsert contact to LibSession: \(LibSession.lastError(conf))") @@ -382,10 +384,10 @@ internal extension LibSession { // contacts are new/invalid, and if so, fetch any profile data we have for them let newContactIds: [String] = targetContacts .compactMap { contactData -> String? in - var cContactId: [CChar] = contactData.id.cArray.nullTerminated() var contact: contacts_contact = contacts_contact() guard + var cContactId: [CChar] = contactData.id.cString(using: .utf8), contacts_get(conf, &contact, &cContactId), String(libSessionVal: contact.name, nullIfEmpty: true) != nil else { return contactData.id } @@ -557,7 +559,7 @@ public extension LibSession { publicKey: getUserHexEncodedPublicKey(db) ) { conf in contactIds.forEach { sessionId in - var cSessionId: [CChar] = sessionId.cArray.nullTerminated() + guard var cSessionId: [CChar] = sessionId.cString(using: .utf8) else { return } // Don't care if the contact doesn't exist contacts_erase(conf, &cSessionId) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift index 9feecb7ec..6573afaf5 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift @@ -132,7 +132,10 @@ internal extension LibSession { } try validChanges.forEach { threadInfo in - var cThreadId: [CChar] = threadInfo.threadId.cArray.nullTerminated() + guard var cThreadId: [CChar] = threadInfo.threadId.cString(using: .utf8) else { + SNLog("Unable to upsert contact volatile info to LibSession: \(LibSessionError.invalidCConversion)") + throw LibSessionError.invalidCConversion + } switch threadInfo.variant { case .contact: @@ -179,9 +182,9 @@ internal extension LibSession { case .community: guard - var cBaseUrl: [CChar] = threadInfo.openGroupUrlInfo?.server.cArray.nullTerminated(), - var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cArray.nullTerminated(), - var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo?.publicKey.bytes + var cBaseUrl: [CChar] = threadInfo.openGroupUrlInfo?.server.cString(using: .utf8), + var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cString(using: .utf8), + var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo.map({ Array(Data(hex: $0.publicKey)) }) else { SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info") return @@ -248,8 +251,8 @@ internal extension LibSession { for: .convoInfoVolatile, publicKey: getUserHexEncodedPublicKey(db) ) { conf in - volatileContactIds.forEach { contactId in - var cSessionId: [CChar] = contactId.cArray.nullTerminated() + try volatileContactIds.forEach { contactId in + var cSessionId: [CChar] = try contactId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() // Don't care if the data doesn't exist convo_info_volatile_erase_1to1(conf, &cSessionId) @@ -263,8 +266,8 @@ internal extension LibSession { for: .convoInfoVolatile, publicKey: getUserHexEncodedPublicKey(db) ) { conf in - volatileLegacyGroupIds.forEach { legacyGroupId in - var cLegacyGroupId: [CChar] = legacyGroupId.cArray.nullTerminated() + try volatileLegacyGroupIds.forEach { legacyGroupId in + var cLegacyGroupId: [CChar] = try legacyGroupId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() // Don't care if the data doesn't exist convo_info_volatile_erase_legacy_group(conf, &cLegacyGroupId) @@ -278,9 +281,9 @@ internal extension LibSession { for: .convoInfoVolatile, publicKey: getUserHexEncodedPublicKey(db) ) { conf in - volatileCommunityInfo.forEach { urlInfo in - var cBaseUrl: [CChar] = urlInfo.server.cArray.nullTerminated() - var cRoom: [CChar] = urlInfo.roomToken.cArray.nullTerminated() + try volatileCommunityInfo.forEach { urlInfo in + var cBaseUrl: [CChar] = try urlInfo.server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + var cRoom: [CChar] = try urlInfo.roomToken.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() // Don't care if the data doesn't exist convo_info_volatile_erase_community(conf, &cBaseUrl, &cRoom) @@ -332,34 +335,34 @@ public extension LibSession { .map { conf in switch threadVariant { case .contact: - var cThreadId: [CChar] = threadId.cArray.nullTerminated() var oneToOne: convo_info_volatile_1to1 = convo_info_volatile_1to1() - guard convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) else { - return false - } + guard + var cThreadId: [CChar] = threadId.cString(using: .utf8), + convo_info_volatile_get_1to1(conf, &oneToOne, &cThreadId) + else { return false } return (oneToOne.last_read >= timestampMs) case .legacyGroup: - var cThreadId: [CChar] = threadId.cArray.nullTerminated() var legacyGroup: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group() - guard convo_info_volatile_get_legacy_group(conf, &legacyGroup, &cThreadId) else { - return false - } + guard + var cThreadId: [CChar] = threadId.cString(using: .utf8), + convo_info_volatile_get_legacy_group(conf, &legacyGroup, &cThreadId) + else { return false } return (legacyGroup.last_read >= timestampMs) case .community: guard let openGroup: OpenGroup = openGroup else { return false } - var cBaseUrl: [CChar] = openGroup.server.cArray.nullTerminated() - var cRoomToken: [CChar] = openGroup.roomToken.cArray.nullTerminated() var convoCommunity: convo_info_volatile_community = convo_info_volatile_community() - guard convo_info_volatile_get_community(conf, &convoCommunity, &cBaseUrl, &cRoomToken) else { - return false - } + guard + var cBaseUrl: [CChar] = openGroup.server.cString(using: .utf8), + var cRoomToken: [CChar] = openGroup.roomToken.cString(using: .utf8), + convo_info_volatile_get_community(conf, &convoCommunity, &cBaseUrl, &cRoomToken) + else { return false } return (convoCommunity.last_read >= timestampMs) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift index d6a1a0c66..af1158cdb 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Shared.swift @@ -396,7 +396,7 @@ public extension LibSession { .config(for: configVariant, publicKey: userPublicKey) .wrappedValue .map { conf in - var cThreadId: [CChar] = threadId.cArray.nullTerminated() + guard var cThreadId: [CChar] = threadId.cString(using: .utf8) else { return false } switch threadVariant { case .contact: @@ -422,10 +422,12 @@ public extension LibSession { .read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }? .first - guard let urlInfo: OpenGroupUrlInfo = maybeUrlInfo else { return false } + guard + let urlInfo: OpenGroupUrlInfo = maybeUrlInfo, + var cBaseUrl: [CChar] = urlInfo.server.cString(using: .utf8), + var cRoom: [CChar] = urlInfo.roomToken.cString(using: .utf8) + else { return false } - var cBaseUrl: [CChar] = urlInfo.server.cArray.nullTerminated() - var cRoom: [CChar] = urlInfo.roomToken.cArray.nullTerminated() var community: ugroups_community_info = ugroups_community_info() /// Not handling the `hidden` behaviour for communities so just indicate the existence diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 75ca4223e..c65d3be8e 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -405,7 +405,7 @@ internal extension LibSession { try legacyGroups .forEach { legacyGroup in - var cGroupId: [CChar] = legacyGroup.id.cArray.nullTerminated() + var cGroupId: [CChar] = try legacyGroup.id.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() guard let userGroup: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cGroupId) else { /// It looks like there are some situations where this object might not get created correctly (and /// will throw due to the implicit unwrapping) as a result we put it in a guard and throw instead @@ -460,13 +460,13 @@ internal extension LibSession { let membersIdsToAdd: Set = memberIds.subtracting(existingMemberIds) let membersIdsToRemove: Set = existingMemberIds.subtracting(memberIds) - membersIdsToAdd.forEach { memberId in - var cProfileId: [CChar] = memberId.cArray.nullTerminated() + try membersIdsToAdd.forEach { memberId in + var cProfileId: [CChar] = try memberId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() ugroups_legacy_member_add(userGroup, &cProfileId, false) } - membersIdsToRemove.forEach { memberId in - var cProfileId: [CChar] = memberId.cArray.nullTerminated() + try membersIdsToRemove.forEach { memberId in + var cProfileId: [CChar] = try memberId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() ugroups_legacy_member_remove(userGroup, &cProfileId) } } @@ -480,13 +480,13 @@ internal extension LibSession { let adminIdsToAdd: Set = adminIds.subtracting(existingAdminIds) let adminIdsToRemove: Set = existingAdminIds.subtracting(adminIds) - adminIdsToAdd.forEach { adminId in - var cProfileId: [CChar] = adminId.cArray.nullTerminated() + try adminIdsToAdd.forEach { adminId in + var cProfileId: [CChar] = try adminId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() ugroups_legacy_member_add(userGroup, &cProfileId, true) } - adminIdsToRemove.forEach { adminId in - var cProfileId: [CChar] = adminId.cArray.nullTerminated() + try adminIdsToRemove.forEach { adminId in + var cProfileId: [CChar] = try adminId.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() ugroups_legacy_member_remove(userGroup, &cProfileId) } } @@ -512,9 +512,15 @@ internal extension LibSession { try communities .forEach { community in - var cBaseUrl: [CChar] = community.urlInfo.server.cArray.nullTerminated() - var cRoom: [CChar] = community.urlInfo.roomToken.cArray.nullTerminated() - var cPubkey: [UInt8] = Data(hex: community.urlInfo.publicKey).cArray + guard + var cBaseUrl: [CChar] = community.urlInfo.server.cString(using: .utf8), + var cRoom: [CChar] = community.urlInfo.roomToken.cString(using: .utf8) + else { + SNLog("Unable to upsert community conversation to LibSession: \(LibSessionError.invalidCConversion)") + throw LibSessionError.invalidCConversion + } + + var cPubkey: [UInt8] = Array(Data(hex: community.urlInfo.publicKey)) var userCommunity: ugroups_community_info = ugroups_community_info() guard user_groups_get_or_construct_community(conf, &userCommunity, &cBaseUrl, &cRoom, &cPubkey) else { @@ -569,8 +575,8 @@ public extension LibSession { for: .userGroups, publicKey: getUserHexEncodedPublicKey(db) ) { conf in - var cBaseUrl: [CChar] = server.cArray.nullTerminated() - var cRoom: [CChar] = roomToken.cArray.nullTerminated() + var cBaseUrl: [CChar] = try server.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + var cRoom: [CChar] = try roomToken.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() // Don't care if the community doesn't exist user_groups_erase_community(conf, &cBaseUrl, &cRoom) @@ -610,7 +616,7 @@ public extension LibSession { ) { conf in guard conf != nil else { throw LibSessionError.nilConfigObject } - var cGroupId: [CChar] = groupPublicKey.cArray.nullTerminated() + var cGroupId: [CChar] = try groupPublicKey.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() let userGroup: UnsafeMutablePointer? = user_groups_get_legacy_group(conf, &cGroupId) // Need to make sure the group doesn't already exist (otherwise we will end up overriding the @@ -734,7 +740,7 @@ public extension LibSession { publicKey: getUserHexEncodedPublicKey(db) ) { conf in legacyGroupIds.forEach { threadId in - var cGroupId: [CChar] = threadId.cArray.nullTerminated() + guard var cGroupId: [CChar] = threadId.cString(using: .utf8) else { return } // Don't care if the group doesn't exist user_groups_erase_legacy_group(conf, &cGroupId) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 9ada948b8..469b6340e 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -178,7 +178,7 @@ internal extension LibSession { guard conf != nil else { throw LibSessionError.nilConfigObject } // Update the name - var updatedName: [CChar] = profile.name.cArray.nullTerminated() + var updatedName: [CChar] = try profile.name.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() user_profile_set_name(conf, &updatedName) // Either assign the updated profile pic, or sent a blank profile pic (to remove the current one) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 3aedf69f1..153db836d 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -336,10 +336,12 @@ public extension LibSession { return LibSession .config(for: variant, publicKey: publicKey) .mutate { conf in - guard conf != nil else { return nil } + guard + conf != nil, + var cHash: [CChar] = serverHash.cString(using: .utf8) + else { return nil } // Mark the config as pushed - var cHash: [CChar] = serverHash.cArray.nullTerminated() config_confirm_pushed(conf, seqNo, &cHash) // Update the result to indicate whether the config needs to be dumped @@ -407,13 +409,28 @@ public extension LibSession { .config(for: key, publicKey: publicKey) .mutate { conf in // Merge the messages - var mergeHashes: [UnsafePointer?] = value - .map { message in message.serverHash.cArray.nullTerminated() } - .unsafeCopy() - var mergeData: [UnsafePointer?] = value - .map { message -> [UInt8] in message.data.bytes } - .unsafeCopy() - var mergeSize: [Int] = value.map { $0.data.count } + var mergeHashes: [UnsafePointer?] = (try? (value + .compactMap { message in message.serverHash.cString(using: .utf8) } + .unsafeCopyCStringArray())) + .defaulting(to: []) + var mergeData: [UnsafePointer?] = (try? (value + .map { message -> [UInt8] in Array(message.data) } + .unsafeCopyUInt8Array())) + .defaulting(to: []) + defer { + mergeHashes.forEach { $0?.deallocate() } + mergeData.forEach { $0?.deallocate() } + } + + guard + conf != nil, + mergeHashes.count == value.count, + mergeData.count == value.count, + mergeHashes.allSatisfy({ $0 != nil }), + mergeData.allSatisfy({ $0 != nil }) + else { return SNLog("[LibSession] Failed to correctly allocate merge data") } + + var mergeSize: [size_t] = value.map { size_t($0.data.count) } var mergedHashesPtr: UnsafeMutablePointer? try CExceptionHelper.performSafely { mergedHashesPtr = config_merge( @@ -424,8 +441,6 @@ public extension LibSession { value.count ) } - mergeHashes.forEach { $0?.deallocate() } - mergeData.forEach { $0?.deallocate() } // Get the list of hashes from the config (to determine which were successful) let mergedHashes: [String] = mergedHashesPtr @@ -537,12 +552,12 @@ fileprivate extension LibSession { public extension LibSession { static func parseCommunity(url: String) -> (room: String, server: String, publicKey: String)? { - var cFullUrl: [CChar] = url.cArray.nullTerminated() var cBaseUrl: [CChar] = [CChar](repeating: 0, count: COMMUNITY_BASE_URL_MAX_LENGTH) var cRoom: [CChar] = [CChar](repeating: 0, count: COMMUNITY_ROOM_MAX_LENGTH) var cPubkey: [UInt8] = [UInt8](repeating: 0, count: OpenGroup.pubkeyByteLength) guard + var cFullUrl: [CChar] = url.cString(using: .utf8), community_parse_full_url(&cFullUrl, &cBaseUrl, &cRoom, &cPubkey) && !String(cString: cRoom).isEmpty && !String(cString: cBaseUrl).isEmpty && @@ -559,10 +574,14 @@ public extension LibSession { return (room, baseUrl, pubkeyHex) } - static func communityUrlFor(server: String, roomToken: String, publicKey: String) -> String { - var cBaseUrl: [CChar] = server.cArray.nullTerminated() - var cRoom: [CChar] = roomToken.cArray.nullTerminated() - var cPubkey: [UInt8] = Data(hex: publicKey).cArray + static func communityUrlFor(server: String?, roomToken: String?, publicKey: String?) -> String? { + guard + var cBaseUrl: [CChar] = server?.cString(using: .utf8), + var cRoom: [CChar] = roomToken?.cString(using: .utf8), + let publicKey: String = publicKey + else { return nil } + + var cPubkey: [UInt8] = Array(Data(hex: publicKey)) var cFullUrl: [CChar] = [CChar](repeating: 0, count: COMMUNITY_FULL_URL_MAX_LENGTH) community_make_full_url(&cBaseUrl, &cRoom, &cPubkey, &cFullUrl) diff --git a/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift b/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift index b609456a3..2c68ad097 100644 --- a/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift +++ b/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift @@ -44,7 +44,7 @@ public enum MessageSenderError: Error, CustomStringConvertible, Equatable { case .noUsername: return "Missing username (MessageSenderError.noUsername)." case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded (MessageSenderError.attachmentsNotUploaded)." case .blindingFailed: return "Couldn't blind the sender (MessageSenderError.blindingFailed)." - case .sendJobTimeout: return "Send job timeout (likely due to path building taking too long - MessageSenderError.sendJobTimeout)." + case .sendJobTimeout: return "Send job timeout (MessageSenderError.sendJobTimeout)." // Closed groups case .noThread: return "Couldn't find a thread associated with the given group public key (MessageSenderError.noThread)." diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index defda46d8..238b68aca 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -275,7 +275,7 @@ extension OpenGroupAPI { } } - SNLog("Open group polling to \(server) failed due to error: \(error). Setting failure count to \(pollFailureCount).") + SNLog("Open group polling to \(server) failed due to error: \(error). Setting failure count to \(pollFailureCount + 1).") // Add a note to the logs that this happened if !prunedIds.isEmpty { diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index bd5044e63..2a6fc348f 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -213,7 +213,7 @@ public struct ProfileManager { let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName) var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: funcName) - OWSLogger.verbose("downloading profile avatar: \(profile.id)") + Log.trace("downloading profile avatar: \(profile.id)") currentAvatarDownloads.mutate { $0.insert(profile.id) } let useOldServer: Bool = (profileUrlStringAtStart.contains(FileServerAPI.oldServer)) @@ -240,12 +240,12 @@ public struct ProfileManager { !latestProfileKey.isEmpty, latestProfileKey == profileKeyAtStart else { - OWSLogger.warn("Ignoring avatar download for obsolete user profile.") + Log.warn("Ignoring avatar download for obsolete user profile.") return } guard profileUrlStringAtStart == latestProfile.profilePictureUrl else { - OWSLogger.warn("Avatar url has changed during download.") + Log.warn("Avatar url has changed during download.") if latestProfile.profilePictureUrl?.isEmpty == false { self.downloadAvatar(for: latestProfile) @@ -254,14 +254,14 @@ public struct ProfileManager { } guard let decryptedData: Data = decryptData(data: data, key: profileKeyAtStart) else { - OWSLogger.warn("Avatar data for \(profile.id) could not be decrypted.") + Log.warn("Avatar data for \(profile.id) could not be decrypted.") return } try? decryptedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic]) guard UIImage(contentsOfFile: filePath) != nil else { - OWSLogger.warn("Avatar image for \(profile.id) could not be loaded.") + Log.warn("Avatar image for \(profile.id) could not be loaded.") return } @@ -317,7 +317,7 @@ public struct ProfileManager { profileAvatarCache.mutate { $0[fileName] = nil } } - OWSLogger.verbose(existingProfileUrl != nil ? + Log.debug(existingProfileUrl != nil ? "Updating local profile on service with cleared avatar." : "Updating local profile on service with no avatar." ) @@ -441,7 +441,7 @@ public struct ProfileManager { // * Encrypt it // * Upload it to asset service // * Send asset service info to Signal Service - OWSLogger.verbose("Updating local profile on service with new avatar.") + Log.debug("Updating local profile on service with new avatar.") let fileName: String = UUID().uuidString.appendingFileExtension(fileExtension) let filePath: String = ProfileManager.profileAvatarFilepath(filename: fileName) diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 0555165f1..6d313e507 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -258,7 +258,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension }, migrationsCompletion: { [weak self] result, needsConfigSync in switch result { - // Only 'NSLog' works in the extension - viewable via Console.app case .failure(let error): Log.error("Failed to complete migrations: \(error).") self?.completeSilenty() diff --git a/SessionShareExtension/SAEScreenLockViewController.swift b/SessionShareExtension/SAEScreenLockViewController.swift index 79bfc5a03..7474bd714 100644 --- a/SessionShareExtension/SAEScreenLockViewController.swift +++ b/SessionShareExtension/SAEScreenLockViewController.swift @@ -26,10 +26,6 @@ final class SAEScreenLockViewController: ScreenLockViewController { fatalError("init(coder:) has not been implemented") } - deinit { - OWSLogger.verbose("Dealloc: \(type(of: self))") - } - // MARK: - UI private lazy var titleLabel: UILabel = { @@ -107,21 +103,21 @@ final class SAEScreenLockViewController: ScreenLockViewController { // If we're already showing the auth UI; abort. if self.isShowingAuthUI { return } - OWSLogger.info("try to unlock screen lock") + Log.info("try to unlock screen lock") isShowingAuthUI = true ScreenLock.shared.tryToUnlockScreenLock( success: { [weak self] in AssertIsOnMainThread() - OWSLogger.info("unlock screen lock succeeded.") + Log.info("unlock screen lock succeeded.") self?.isShowingAuthUI = false self?.shareViewDelegate?.shareViewWasUnlocked() }, failure: { [weak self] error in AssertIsOnMainThread() - OWSLogger.info("unlock screen lock failed.") + Log.info("unlock screen lock failed.") self?.isShowingAuthUI = false self?.ensureUI() @@ -129,7 +125,7 @@ final class SAEScreenLockViewController: ScreenLockViewController { }, unexpectedFailure: { [weak self] error in AssertIsOnMainThread() - OWSLogger.info("unlock screen lock unexpectedly failed.") + Log.info("unlock screen lock unexpectedly failed.") self?.isShowingAuthUI = false @@ -142,7 +138,7 @@ final class SAEScreenLockViewController: ScreenLockViewController { }, cancel: { [weak self] in AssertIsOnMainThread() - OWSLogger.info("unlock screen lock cancelled.") + Log.info("unlock screen lock cancelled.") self?.isShowingAuthUI = false self?.ensureUI() @@ -174,7 +170,7 @@ final class SAEScreenLockViewController: ScreenLockViewController { func unlockButtonWasTapped() { AssertIsOnMainThread() - OWSLogger.info("unlockButtonWasTapped") + Log.info("unlockButtonWasTapped") self.tryToPresentAuthUIToUnlockScreenLock() } @@ -182,7 +178,7 @@ final class SAEScreenLockViewController: ScreenLockViewController { // MARK: - Transitions @objc private func dismissPressed() { - OWSLogger.debug("unlock screen lock cancelled.") + Log.debug("unlock screen lock cancelled.") self.cancelShareExperience() } diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift index ddd3e546d..06d4b91fe 100644 --- a/SessionShareExtension/ShareAppExtensionContext.swift +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -123,7 +123,7 @@ final class ShareAppExtensionContext: AppContext { } func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { - OWSLogger.info("Ignoring request to show/hide status bar since we're in an app extension") + Log.info("Ignoring request to show/hide status bar since we're in an app extension") } func setNetworkActivityIndicatorVisible(_ value: Bool) { diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index fdae14419..6a4c47ca0 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -107,11 +107,21 @@ public extension LibSession { guard swarmSize > 0, let cSwarm: UnsafeMutablePointer = swarmPtr - else { return resolver(Result.failure(SnodeAPIError.unableToRetrieveSwarm)) } + else { + // Dispatch async so we don't hold up the libSession thread (which can block other requests) + DispatchQueue.global(qos: .default).async { + resolver(Result.failure(SnodeAPIError.unableToRetrieveSwarm)) + } + return + } var nodes: Set = [] (0..= count, let cSwarm: UnsafeMutablePointer = nodesPtr - else { return resolver(Result.failure(SnodeAPIError.unableToRetrieveSwarm)) } + else { + // Dispatch async so we don't hold up the libSession thread (which can block other requests) + DispatchQueue.global(qos: .default).async { + resolver(Result.failure(SnodeAPIError.unableToRetrieveSwarm)) + } + return + } var nodes: Set = [] (0.. { resolver in let callbackWrapper: CWrapper = CWrapper { success, timeout, statusCode, data in - switch processError(success, timeout, statusCode, data, using: dependencies) { - case .some(let error): resolver(Result.failure(error)) - case .none: resolver(Result.success((Network.ResponseInfo(code: Int(statusCode), headers: [:]), data))) + let maybeError: Error? = processError(success, timeout, statusCode, data, using: dependencies) + + // Dispatch async so we don't hold up the libSession thread (which can block other requests) + DispatchQueue.global(qos: .default).async { + switch maybeError { + case .some(let error): resolver(Result.failure(error)) + case .none: + resolver(Result.success((Network.ResponseInfo(code: Int(statusCode), headers: [:]), data))) + } } } let cWrapperPtr: UnsafeMutableRawPointer = Unmanaged.passRetained(callbackWrapper).toOpaque() @@ -191,7 +217,7 @@ public extension LibSession { case .snode(let snode): let cSwarmPublicKey: UnsafePointer? = swarmPublicKey.map { // Quick way to drop '05' prefix if present - $0.suffix(64).cArray.nullTerminated().unsafeCopy() + $0.suffix(64).cString(using: .utf8)?.unsafeCopy() } callbackWrapper.addUnsafePointerToCleanup(cSwarmPublicKey) @@ -211,30 +237,46 @@ public extension LibSession { ) case .server(let method, let scheme, let host, let endpoint, let port, let headers, let x25519PublicKey): + let headerInfo: [(key: String, value: String)]? = headers?.map { ($0.key, $0.value) } + + // Handle the more complicated type conversions first + let cHeaderKeysContent: [UnsafePointer?] = (try? ((headerInfo ?? []) + .map { $0.key.cString(using: .utf8) } + .unsafeCopyCStringArray())) + .defaulting(to: []) + let cHeaderValuesContent: [UnsafePointer?] = (try? ((headerInfo ?? []) + .map { $0.value.cString(using: .utf8) } + .unsafeCopyCStringArray())) + .defaulting(to: []) + + guard + cHeaderKeysContent.count == cHeaderValuesContent.count, + cHeaderKeysContent.allSatisfy({ $0 != nil }), + cHeaderValuesContent.allSatisfy({ $0 != nil }) + else { + cHeaderKeysContent.forEach { $0?.deallocate() } + cHeaderValuesContent.forEach { $0?.deallocate() } + cWrapperPtr.deallocate() + return resolver(Result.failure(LibSessionError.invalidCConversion)) + } + + // Convert the other types let targetScheme: String = (scheme ?? "https") - let cMethod: UnsafePointer? = (method ?? "GET").cArray - .nullTerminated() + let cMethod: UnsafePointer? = (method ?? "GET") + .cString(using: .utf8)? .unsafeCopy() - let cTargetScheme: UnsafePointer? = targetScheme.cArray - .nullTerminated() + let cTargetScheme: UnsafePointer? = targetScheme + .cString(using: .utf8)? .unsafeCopy() - let cHost: UnsafePointer? = host.cArray - .nullTerminated() + let cHost: UnsafePointer? = host + .cString(using: .utf8)? .unsafeCopy() - let cEndpoint: UnsafePointer? = endpoint.cArray - .nullTerminated() + let cEndpoint: UnsafePointer? = endpoint + .cString(using: .utf8)? .unsafeCopy() let cX25519Pubkey: UnsafePointer? = x25519PublicKey .suffix(64) // Quick way to drop '05' prefix if present - .cArray - .nullTerminated() - .unsafeCopy() - let headerInfo: [(key: String, value: String)]? = headers?.map { ($0.key, $0.value) } - let cHeaderKeysContent: [UnsafePointer?] = (headerInfo ?? []) - .map { $0.key.cArray.nullTerminated() } - .unsafeCopy() - let cHeaderValuesContent: [UnsafePointer?] = (headerInfo ?? []) - .map { $0.value.cArray.nullTerminated() } + .cString(using: .utf8)? .unsafeCopy() let cHeaderKeys: UnsafeMutablePointer?>? = cHeaderKeysContent .unsafeCopy() @@ -304,10 +346,14 @@ public extension LibSession { // Otherwise create a new network var error: [CChar] = [CChar](repeating: 0, count: 256) var network: UnsafeMutablePointer? - let cCachePath: [CChar] = snodeCachePath.cArray.nullTerminated() + + guard let cCachePath: [CChar] = snodeCachePath.cString(using: .utf8) else { + Log.error("[LibQuic] Unable to create network object: \(LibSessionError.invalidCConversion)") + return nil + } guard network_init(&network, cCachePath, Features.useTestnet, true, &error) else { - SNLog("[LibQuic Error] Unable to create network object: \(String(cString: error))") + Log.error("[LibQuic] Unable to create network object: \(String(cString: error))") return nil } @@ -336,12 +382,15 @@ public extension LibSession { private static func updateNetworkStatus(cStatus: CONNECTION_STATUS) { let status: NetworkStatus = NetworkStatus(status: cStatus) - SNLog("Network status changed to: \(status)") - lastNetworkStatus.mutate { lastNetworkStatus in - lastNetworkStatus = status - - networkStatusCallbacks.wrappedValue.forEach { _, callback in - callback(status) + // Dispatch async so we don't hold up the libSession thread that triggered the update + DispatchQueue.global(qos: .default).async { + Log.info("Network status changed to: \(status)") + lastNetworkStatus.mutate { lastNetworkStatus in + lastNetworkStatus = status + + networkStatusCallbacks.wrappedValue.forEach { _, callback in + callback(status) + } } } } @@ -374,11 +423,14 @@ public extension LibSession { // Need to free the cPathsPtr as we are the owner cPathsPtr?.deallocate() - lastPaths.mutate { lastPaths in - lastPaths = paths - - pathsChangedCallbacks.wrappedValue.forEach { id, callback in - callback(paths, id) + // Dispatch async so we don't hold up the libSession thread that triggered the update + DispatchQueue.global(qos: .default).async { + lastPaths.mutate { lastPaths in + lastPaths = paths + + pathsChangedCallbacks.wrappedValue.forEach { id, callback in + callback(paths, id) + } } } } @@ -401,14 +453,14 @@ public extension LibSession { case (400, .some(let responseString)): return NetworkError.badRequest(error: responseString, rawData: data) case (401, _): - SNLog("Unauthorised (Failed to verify the signature).") + Log.warn("Unauthorised (Failed to verify the signature).") return NetworkError.unauthorised case (404, _): return NetworkError.notFound /// A snode will return a `406` but onion requests v4 seems to return `425` so handle both case (406, _), (425, _): - SNLog("The user's clock is out of sync with the service node network.") + Log.warn("The user's clock is out of sync with the service node network.") return SnodeAPIError.clockOutOfSync case (421, _): return SnodeAPIError.unassociatedPubkey diff --git a/SessionUtilitiesKit/General/Collection+Utilities.swift b/SessionUtilitiesKit/General/Collection+Utilities.swift index e541cc40c..e34b16406 100644 --- a/SessionUtilitiesKit/General/Collection+Utilities.swift +++ b/SessionUtilitiesKit/General/Collection+Utilities.swift @@ -34,14 +34,33 @@ public extension Collection { } } +public extension Collection where Element == [CChar]? { + /// This creates an array of UnsafePointer types to access data of the C strings in memory. This array provides no automated + /// memory management of it's children so after use you are responsible for handling the life cycle of the child elements and + /// need to call `deallocate()` on each child. + func unsafeCopyCStringArray() throws -> [UnsafePointer?] { + return try self.map { value in + guard let value: [CChar] = value else { return nil } + + let copy = UnsafeMutableBufferPointer.allocate(capacity: value.underestimatedCount) + var remaining: (unwritten: Array.Iterator, index: Int) = copy.initialize(from: value) + guard remaining.unwritten.next() == nil else { throw LibSessionError.invalidCConversion } + + return UnsafePointer(copy.baseAddress) + } + } +} + public extension Collection where Element == [CChar] { /// This creates an array of UnsafePointer types to access data of the C strings in memory. This array provides no automated /// memory management of it's children so after use you are responsible for handling the life cycle of the child elements and /// need to call `deallocate()` on each child. - func unsafeCopy() -> [UnsafePointer?] { - return self.map { value in - let copy = UnsafeMutableBufferPointer.allocate(capacity: value.count) - _ = copy.initialize(from: value) + func unsafeCopyCStringArray() throws -> [UnsafePointer?] { + return try self.map { value in + let copy = UnsafeMutableBufferPointer.allocate(capacity: value.underestimatedCount) + var remaining: (unwritten: Array.Iterator, index: Int) = copy.initialize(from: value) + guard remaining.unwritten.next() == nil else { throw LibSessionError.invalidCConversion } + return UnsafePointer(copy.baseAddress) } } @@ -51,10 +70,12 @@ public extension Collection where Element == [UInt8] { /// This creates an array of UnsafePointer types to access data of the C strings in memory. This array provides no automated /// memory management of it's children so after use you are responsible for handling the life cycle of the child elements and /// need to call `deallocate()` on each child. - func unsafeCopy() -> [UnsafePointer?] { - return self.map { value in - let copy = UnsafeMutableBufferPointer.allocate(capacity: value.count) - _ = copy.initialize(from: value) + func unsafeCopyUInt8Array() throws -> [UnsafePointer?] { + return try self.map { value in + let copy = UnsafeMutableBufferPointer.allocate(capacity: value.underestimatedCount) + var remaining: (unwritten: Array.Iterator, index: Int) = copy.initialize(from: value) + guard remaining.unwritten.next() == nil else { throw LibSessionError.invalidCConversion } + return UnsafePointer(copy.baseAddress) } } diff --git a/SessionUtilitiesKit/LibSession/LibSessionError.swift b/SessionUtilitiesKit/LibSession/LibSessionError.swift index c2bb5cdf2..44fb7e771 100644 --- a/SessionUtilitiesKit/LibSession/LibSessionError.swift +++ b/SessionUtilitiesKit/LibSession/LibSessionError.swift @@ -11,6 +11,7 @@ public enum LibSessionError: LocalizedError { case userDoesNotExist case getOrConstructFailedUnexpectedly case processingLoopLimitReached + case invalidCConversion case libSessionError(String) case unknown @@ -32,6 +33,7 @@ public enum LibSessionError: LocalizedError { case .userDoesNotExist: return "User does not exist." case .getOrConstructFailedUnexpectedly: return "'getOrConstruct' failed unexpectedly." case .processingLoopLimitReached: return "Processing loop limit reached." + case .invalidCConversion: return "Invalid conversation to C type." case .libSessionError(let error): return "\(error)\(error.hasSuffix(".") ? "" : ".")" case .unknown: return "An unknown error occurred." diff --git a/SessionUtilitiesKit/Networking/BatchRequest.swift b/SessionUtilitiesKit/Networking/BatchRequest.swift index f5b3fbe18..b1ce71c77 100644 --- a/SessionUtilitiesKit/Networking/BatchRequest.swift +++ b/SessionUtilitiesKit/Networking/BatchRequest.swift @@ -25,7 +25,7 @@ public extension Network { self.requests = requests.map { Child(request: $0) } if requests.count > BatchRequest.childRequestLimit { - SNLog("[BatchRequest] Constructed request with \(requests.count) subrequests when the limit is \(BatchRequest.childRequestLimit)") + Log.warn("[BatchRequest] Constructed request with \(requests.count) subrequests when the limit is \(BatchRequest.childRequestLimit)") } }