516 lines
23 KiB
Swift
516 lines
23 KiB
Swift
//
|
|
// Copyright 2024 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalCoreKit
|
|
|
|
extension MessageBackup {
|
|
|
|
public typealias RawError = Swift.Error
|
|
|
|
// MARK: - Archiving
|
|
|
|
/// Error while archiving a single frame.
|
|
public struct ArchiveFrameError<AppIdType: MessageBackupLoggableId>: MessageBackupLoggableError {
|
|
public enum ErrorType {
|
|
/// An error occurred serializing the proto.
|
|
/// - Note
|
|
/// Logging the raw error is safe, as it'll just contain proto field
|
|
/// names.
|
|
case protoSerializationError(RawError)
|
|
/// An error occurred during file IO.
|
|
/// - Note
|
|
/// Logging the raw error is safe, as we generate the file we stream
|
|
/// without user input.
|
|
case fileIOError(RawError)
|
|
|
|
/// The object we are archiving references a recipient that should already have an id assigned
|
|
/// from having been archived, but does not.
|
|
/// e.g. we try to archive a message to a recipient aci, but that aci has no ``MessageBackup.RecipientId``.
|
|
case referencedRecipientIdMissing(RecipientArchivingContext.Address)
|
|
|
|
/// The object we are archiving references a chat that should already have an id assigned
|
|
/// from having been archived, but does not.
|
|
/// e.g. we try to archive a message to a thread, but that group has no ``MessageBackup.ChatId``.
|
|
case referencedThreadIdMissing(ThreadUniqueId)
|
|
|
|
/// An error generating the master key for a group, causing the group to be skipped.
|
|
case groupMasterKeyError(RawError)
|
|
|
|
/// A contact thread has an invalid or missing address information, causing the
|
|
/// thread to be skipped.
|
|
case contactThreadMissingAddress
|
|
|
|
/// An incoming message has an invalid or missing author address information,
|
|
/// causing the message to be skipped.
|
|
case invalidIncomingMessageAuthor
|
|
/// An outgoing message has an invalid or missing recipient address information,
|
|
/// causing the message to be skipped.
|
|
case invalidOutgoingMessageRecipient
|
|
/// An quote has an invalid or missing author address information,
|
|
/// causing the containing message to be skipped.
|
|
case invalidQuoteAuthor
|
|
|
|
/// A reaction has an invalid or missing author address information, causing the
|
|
/// reaction to be skipped.
|
|
case invalidReactionAddress
|
|
|
|
/// A group update message with no updates actually inside it, which is invalid.
|
|
case emptyGroupUpdate
|
|
|
|
/// The profile for the local user is missing.
|
|
case missingLocalProfile
|
|
/// The profile key for the local user is missing.
|
|
case missingLocalProfileKey
|
|
|
|
/// Parameters required to archive a GV2 group member are missing
|
|
case missingRequiredGroupMemberParams
|
|
|
|
/// A group call record had an invalid individual-call status.
|
|
case groupCallRecordHadIndividualCallStatus
|
|
|
|
/// A distributionListIdentifier memberRecipientId was invalid
|
|
case invalidDistributionListMemberAddress
|
|
|
|
/// The story distribution list contained memberRecipientIds for a privacy mode
|
|
/// that didn't expect any.
|
|
case distributionListUnexpectedRecipients
|
|
|
|
/// The story distribution list was marked as deleted but missing a deletion timestamp
|
|
case distributionListMissingDeletionTimestamp
|
|
|
|
/// The story distribution list was missing memberRecipiendIds for a privacy mode
|
|
/// where they should be present.
|
|
case distributionListMissingRecipients
|
|
}
|
|
|
|
private let type: ErrorType
|
|
private let id: AppIdType
|
|
private let file: StaticString
|
|
private let function: StaticString
|
|
private let line: UInt
|
|
|
|
/// Create a new error instance.
|
|
///
|
|
/// Exposed as a static method rather than an initializer to help
|
|
/// callsites have some context without needing to put the exhaustive
|
|
/// (namespaced) type name at each site.
|
|
public static func archiveFrameError(
|
|
_ type: ErrorType,
|
|
_ id: AppIdType,
|
|
_ file: StaticString = #file,
|
|
_ function: StaticString = #function,
|
|
_ line: UInt = #line
|
|
) -> ArchiveFrameError {
|
|
return ArchiveFrameError(type: type, id: id, file: file, function: function, line: line)
|
|
}
|
|
|
|
// MARK: MessageBackupLoggableError
|
|
|
|
public var typeLogString: String {
|
|
return "ArchiveFrameError: \(String(describing: type))"
|
|
}
|
|
|
|
public var idLogString: String {
|
|
return "\(id.typeLogString).\(id.idLogString)"
|
|
}
|
|
|
|
public var callsiteLogString: String {
|
|
return "\(file):\(function):\(line)"
|
|
}
|
|
|
|
public var collapseKey: String? {
|
|
switch type {
|
|
case .protoSerializationError(let rawError):
|
|
// We don't want to re-log every instance of this we see.
|
|
// Collapse them by the raw error itself.
|
|
return "\(rawError)"
|
|
case .referencedRecipientIdMissing, .referencedThreadIdMissing:
|
|
// Collapse these by the id they refer to, which is in the "type".
|
|
return typeLogString
|
|
case
|
|
.distributionListMissingDeletionTimestamp,
|
|
.distributionListMissingRecipients,
|
|
.distributionListUnexpectedRecipients,
|
|
.fileIOError,
|
|
.groupMasterKeyError,
|
|
.contactThreadMissingAddress,
|
|
.invalidDistributionListMemberAddress,
|
|
.invalidIncomingMessageAuthor,
|
|
.invalidOutgoingMessageRecipient,
|
|
.invalidQuoteAuthor,
|
|
.invalidReactionAddress,
|
|
.emptyGroupUpdate,
|
|
.missingLocalProfile,
|
|
.missingLocalProfileKey,
|
|
.missingRequiredGroupMemberParams,
|
|
.groupCallRecordHadIndividualCallStatus:
|
|
// Log any others as we see them.
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Error archiving an entire category of frames; not attributable to a
|
|
/// single frame.
|
|
public struct FatalArchivingError: MessageBackupLoggableError {
|
|
public enum ErrorType {
|
|
/// Error iterating over all threads for backup purposes.
|
|
case threadIteratorError(RawError)
|
|
|
|
/// Some unrecognized thread was found when iterating over all threads.
|
|
case unrecognizedThreadType
|
|
|
|
/// Error iterating over all interactions for backup purposes.
|
|
case interactionIteratorError(RawError)
|
|
|
|
/// These should never happen; it means some invariant in the backup code
|
|
/// we could not enforce with the type system was broken. Nothing was wrong with
|
|
/// the proto or local database; its the iOS backup code that has a bug somewhere.
|
|
case developerError(OWSAssertionError)
|
|
}
|
|
|
|
private let type: ErrorType
|
|
private let file: StaticString
|
|
private let function: StaticString
|
|
private let line: UInt
|
|
|
|
/// Create a new error instance.
|
|
///
|
|
/// Exposed as a static method rather than an initializer to help
|
|
/// callsites have some context without needing to put the exhaustive
|
|
/// (namespaced) type name at each site.
|
|
public static func fatalArchiveError(
|
|
_ type: ErrorType,
|
|
_ file: StaticString = #file,
|
|
_ function: StaticString = #function,
|
|
_ line: UInt = #line
|
|
) -> FatalArchivingError {
|
|
return FatalArchivingError(type: type, file: file, function: function, line: line)
|
|
}
|
|
|
|
// MARK: MessageBackupLoggableError
|
|
|
|
public var typeLogString: String {
|
|
return "FatalArchiveError: \(String(describing: type))"
|
|
}
|
|
|
|
public var idLogString: String {
|
|
return ""
|
|
}
|
|
|
|
public var callsiteLogString: String {
|
|
return "\(file):\(function):\(line)"
|
|
}
|
|
|
|
public var collapseKey: String? {
|
|
// Log each of these as we see them.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Error restoring a frame.
|
|
public struct RestoreFrameError<ProtoIdType: MessageBackupLoggableId>: MessageBackupLoggableError {
|
|
public enum ErrorType {
|
|
public enum InvalidProtoDataError {
|
|
/// Some recipient identifier being referenced was not present earlier in the backup file.
|
|
case recipientIdNotFound(RecipientId)
|
|
/// Some chat identifier being referenced was not present earlier in the backup file.
|
|
case chatIdNotFound(ChatId)
|
|
|
|
/// Could not parse an Aci. Includes the class of the offending proto.
|
|
case invalidAci(protoClass: Any.Type)
|
|
/// Could not parse an Pni. Includes the class of the offending proto.
|
|
case invalidPni(protoClass: Any.Type)
|
|
/// Could not parse an Aci. Includes the class of the offending proto.
|
|
case invalidServiceId(protoClass: Any.Type)
|
|
/// Could not parse an E164. Includes the class of the offending proto.
|
|
case invalidE164(protoClass: Any.Type)
|
|
/// Could not parse an ``OWSAES256Key`` profile key. Includes the class
|
|
/// of the offending proto.
|
|
case invalidProfileKey(protoClass: Any.Type)
|
|
/// An invalid member (group, distribution list, etc) was specified as a distribution list member. Includes the offending proto
|
|
case invalidDistributionListMember(protoClass: Any.Type)
|
|
|
|
/// A BackupProto.Contact with no aci, pni, or e164.
|
|
case contactWithoutIdentifiers
|
|
/// A BackupProto.Recipient with an unrecognized sub-type.
|
|
case unrecognizedRecipientType
|
|
/// A BackupProto.Contact for the local user. This shouldn't exist.
|
|
case otherContactWithLocalIdentifiers
|
|
|
|
/// A message must come from either an Aci or an E164.
|
|
/// One in the backup did not.
|
|
case incomingMessageNotFromAciOrE164
|
|
/// Outgoing message's BackupProto.SendStatus can only be for BackupProto.Contacts.
|
|
/// One in the backup was to a group, self recipient, or something else.
|
|
case outgoingNonContactMessageRecipient
|
|
/// A BackupProto.SendStatus had an unregonized BackupProto.SendStatusStatus.
|
|
case unrecognizedMessageSendStatus
|
|
/// A BackupProto.ChatItem with an unregonized item type.
|
|
case unrecognizedChatItemType
|
|
|
|
/// BackupProto.Reaction must come from either an Aci or an E164.
|
|
/// One in the backup did not.
|
|
case reactionNotFromAciOrE164
|
|
|
|
/// A BackupProto.BodyRange with a missing or unrecognized style.
|
|
case unrecognizedBodyRangeStyle
|
|
|
|
/// A BackupProto.Group's gv2 master key could not be parsed by libsignal.
|
|
case invalidGV2MasterKey
|
|
/// A BackupProtoGroup was missing its group snapshot.
|
|
case missingGV2GroupSnapshot
|
|
/// A ``BackupProtoGroup/BackupProtoFullGroupMember/role`` was
|
|
/// unrecognized. Includes the class of the offending proto.
|
|
case unrecognizedGV2MemberRole(protoClass: Any.Type)
|
|
/// A ``BackupProtoGroup/BackupProtoMemberPendingProfileKey`` was
|
|
/// missing its member details.
|
|
case invitedGV2MemberMissingMemberDetails
|
|
/// We failed to build a V2 group model while restoring a group.
|
|
case failedToBuildGV2GroupModel
|
|
|
|
/// A BackupProto.GroupChangeChatUpdate ChatItem with a non-group-chat chatId.
|
|
case groupUpdateMessageInNonGroupChat
|
|
/// A BackupProto.GroupChangeChatUpdate ChatItem without any updates!
|
|
case emptyGroupUpdates
|
|
/// A BackupProto.GroupSequenceOfRequestsAndCancelsUpdate where
|
|
/// the requester is the local user, which isn't allowed.
|
|
case sequenceOfRequestsAndCancelsWithLocalAci
|
|
/// An unrecognized BackupProto.GroupChangeChatUpdate.
|
|
case unrecognizedGroupUpdate
|
|
|
|
/// A frame was entirely missing its enclosed item.
|
|
case frameMissingItem
|
|
|
|
/// A profile key for the local user that could not be parsed into a valid aes256 key
|
|
case invalidLocalProfileKey
|
|
/// A profile key for the local user that could not be parsed into a valid aes256 key
|
|
case invalidLocalUsernameLink
|
|
|
|
/// A BackupProto.IndividualCall chat item update was associated
|
|
/// with a thread that was not a contact thread.
|
|
case individualCallNotInContactThread
|
|
/// A BackupProto.IndividualCall had an unrecognized type.
|
|
case individualCallUnrecognizedType
|
|
/// A BackupProto.IndividualCall had an unrecognized direction.
|
|
case individualCallUnrecognizedDirection
|
|
/// A BackupProto.IndividualCall had an unrecognized state.
|
|
case individualCallUnrecognizedState
|
|
|
|
/// A BackupProto.GroupCall chat item update was associated with
|
|
/// a thread that was not a group thread.
|
|
case groupCallNotInGroupThread
|
|
/// A BackupProto.GroupCall had an unrecognized state.
|
|
case groupCallUnrecognizedState
|
|
/// A BackupProto.GroupCall referenced a recipient that was not
|
|
/// a contact or otherwise did not contain an ACI.
|
|
case groupCallRecipientIdNotAnAci(RecipientId)
|
|
|
|
/// BackupProto.DistributionList.distributionId was not a valid UUID
|
|
case invalidDistributionListId
|
|
|
|
/// BackupProto.DistributionList.privacyMode was missing, or contained an unknown privacy mode
|
|
case invalidDistributionListPrivacyMode
|
|
|
|
/// The specified BackupProto.DistributionList.privacyMode was missing a list of associated member IDs
|
|
case invalidDistributionListPrivacyModeMissingRequiredMembers
|
|
|
|
/// BackupProto.DistributionListItem.deletionTimestamp was invalid
|
|
case invalidDistributionListDeletionTimestamp
|
|
}
|
|
|
|
/// The proto contained invalid or self-contradictory data, e.g an invalid ACI.
|
|
case invalidProtoData(InvalidProtoDataError)
|
|
|
|
/// The object being restored depended on a TSThread that should have been created earlier but was not.
|
|
/// This could be either a group or contact thread, we are restoring a frame that doesn't care (e.g. a ChatItem).
|
|
case referencedChatThreadNotFound(ThreadUniqueId)
|
|
/// The object being inserted depended on a TSGroupThread that should have been created earlier but was not.
|
|
/// The overlap with referencedChatThreadNotFound is confusing, but this is for restoring group-specific metadata.
|
|
case referencedGroupThreadNotFound(GroupId)
|
|
|
|
case databaseInsertionFailed(RawError)
|
|
|
|
/// These should never happen; it means some invariant we could not
|
|
/// enforce with the type system was broken. Nothing was wrong with
|
|
/// the proto; its the iOS code that has a bug somewhere.
|
|
case developerError(OWSAssertionError)
|
|
|
|
// [Backups] TODO: remove once all known types are handled.
|
|
case unimplemented
|
|
}
|
|
|
|
private let type: ErrorType
|
|
private let id: ProtoIdType
|
|
private let file: StaticString
|
|
private let function: StaticString
|
|
private let line: UInt
|
|
|
|
/// Create a new error instance.
|
|
///
|
|
/// Exposed as a static method rather than an initializer to help
|
|
/// callsites have some context without needing to put the exhaustive
|
|
/// (namespaced) type name at each site.
|
|
public static func restoreFrameError(
|
|
_ type: ErrorType,
|
|
_ id: ProtoIdType,
|
|
_ file: StaticString = #file,
|
|
_ function: StaticString = #function,
|
|
_ line: UInt = #line
|
|
) -> RestoreFrameError {
|
|
return RestoreFrameError(type: type, id: id, file: file, function: function, line: line)
|
|
}
|
|
|
|
public var typeLogString: String {
|
|
return "RestoreFrameError: \(String(describing: type))"
|
|
}
|
|
|
|
public var idLogString: String {
|
|
return "\(id.typeLogString).\(id.idLogString)"
|
|
}
|
|
|
|
public var callsiteLogString: String {
|
|
return "\(file):\(function) line \(line)"
|
|
}
|
|
|
|
public var collapseKey: String? {
|
|
switch type {
|
|
case .invalidProtoData(let invalidProtoDataError):
|
|
switch invalidProtoDataError {
|
|
case .recipientIdNotFound, .chatIdNotFound:
|
|
// Collapse these by the id they refer to, which is in the "type".
|
|
return typeLogString
|
|
case
|
|
.invalidAci,
|
|
.invalidPni,
|
|
.invalidServiceId,
|
|
.invalidE164,
|
|
.invalidProfileKey,
|
|
.contactWithoutIdentifiers,
|
|
.unrecognizedRecipientType,
|
|
.otherContactWithLocalIdentifiers,
|
|
.incomingMessageNotFromAciOrE164,
|
|
.outgoingNonContactMessageRecipient,
|
|
.unrecognizedMessageSendStatus,
|
|
.unrecognizedChatItemType,
|
|
.reactionNotFromAciOrE164,
|
|
.unrecognizedBodyRangeStyle,
|
|
.invalidGV2MasterKey,
|
|
.missingGV2GroupSnapshot,
|
|
.unrecognizedGV2MemberRole,
|
|
.invitedGV2MemberMissingMemberDetails,
|
|
.failedToBuildGV2GroupModel,
|
|
.groupUpdateMessageInNonGroupChat,
|
|
.emptyGroupUpdates,
|
|
.sequenceOfRequestsAndCancelsWithLocalAci,
|
|
.unrecognizedGroupUpdate,
|
|
.frameMissingItem,
|
|
.invalidDistributionListMember,
|
|
.invalidDistributionListDeletionTimestamp,
|
|
.invalidDistributionListId,
|
|
.invalidDistributionListPrivacyMode,
|
|
.invalidDistributionListPrivacyModeMissingRequiredMembers,
|
|
.invalidLocalProfileKey,
|
|
.invalidLocalUsernameLink,
|
|
.individualCallNotInContactThread,
|
|
.individualCallUnrecognizedType,
|
|
.individualCallUnrecognizedDirection,
|
|
.individualCallUnrecognizedState,
|
|
.groupCallNotInGroupThread,
|
|
.groupCallUnrecognizedState,
|
|
.groupCallRecipientIdNotAnAci:
|
|
// Collapse all others by the id of the containing frame.
|
|
return idLogString
|
|
}
|
|
case .referencedChatThreadNotFound, .referencedGroupThreadNotFound:
|
|
// Collapse these by the id they refer to, which is in the "type".
|
|
return typeLogString
|
|
case .databaseInsertionFailed(let rawError):
|
|
// We don't want to re-log every instance of this we see if they repeat.
|
|
// Collapse them by the raw error itself.
|
|
return "\(rawError)"
|
|
case .developerError:
|
|
// Log each of these as we see them.
|
|
return nil
|
|
case .unimplemented:
|
|
// Collapse these by the callsite.
|
|
return callsiteLogString
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Log Collapsing
|
|
|
|
internal protocol MessageBackupLoggableError {
|
|
var typeLogString: String { get }
|
|
var idLogString: String { get }
|
|
var callsiteLogString: String { get }
|
|
|
|
/// We want to collapse certain logs. Imagine a Chat is missing from a backup; we don't
|
|
/// want to print "Chat 1234 missing" for every message in that chat, that would be thousands
|
|
/// of log lines.
|
|
/// Instead we collapse these similar logs together, keep a count, and log that.
|
|
/// If this is non-nil, we do that collapsing, otherwise we log as-is.
|
|
var collapseKey: String? { get }
|
|
}
|
|
|
|
extension MessageBackup {
|
|
|
|
internal static func log<T: MessageBackupLoggableError>(_ errors: [T]) {
|
|
var logAsIs = [String]()
|
|
var collapsedLogs = OrderedDictionary<String, CollapsedErrorLog>()
|
|
for error in errors {
|
|
guard let collapseKey = error.collapseKey else {
|
|
logAsIs.append(
|
|
error.typeLogString + " "
|
|
+ error.idLogString + " "
|
|
+ error.callsiteLogString
|
|
)
|
|
continue
|
|
}
|
|
|
|
if var existingLog = collapsedLogs[collapseKey] {
|
|
existingLog.collapse(error)
|
|
collapsedLogs.replace(key: collapseKey, value: existingLog)
|
|
} else {
|
|
var newLog = CollapsedErrorLog()
|
|
newLog.collapse(error)
|
|
collapsedLogs.append(key: collapseKey, value: newLog)
|
|
}
|
|
}
|
|
|
|
logAsIs.forEach { Logger.error($0) }
|
|
collapsedLogs.orderedValues.forEach { $0.log() }
|
|
}
|
|
|
|
fileprivate static let maxCollapsedIdLogCount = 10
|
|
|
|
fileprivate struct CollapsedErrorLog {
|
|
var typeLogString: String?
|
|
var exampleCallsiteString: String?
|
|
var errorCount: UInt = 0
|
|
var idLogStrings: [String] = []
|
|
|
|
mutating func collapse(_ error: MessageBackupLoggableError) {
|
|
self.errorCount += 1
|
|
self.typeLogString = self.typeLogString ?? error.typeLogString
|
|
self.exampleCallsiteString = self.exampleCallsiteString ?? error.callsiteLogString
|
|
if idLogStrings.count < MessageBackup.maxCollapsedIdLogCount {
|
|
idLogStrings.append(error.idLogString)
|
|
}
|
|
}
|
|
|
|
func log() {
|
|
Logger.error(
|
|
(typeLogString ?? "") + " "
|
|
+ "Repeated \(errorCount) times. "
|
|
+ "from: \(idLogStrings) "
|
|
+ "example callsite: \(exampleCallsiteString ?? "none")"
|
|
)
|
|
}
|
|
}
|
|
}
|