Path: blob/main/SignalServiceKit/Backups/Archiving/Archivers/BackupArchive+Errors.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SwiftProtobuf
extension BackupArchive {
public typealias RawError = Swift.Error
// MARK: - Archiving
/// Error while archiving a single frame.
public struct ArchiveFrameError<AppIdType: BackupArchive.LoggableId>: BackupArchive.LoggableError {
public enum ErrorType {
/// Message types for which edit history is unexpected.
public enum UnexpectedRevisionsMessageType {
case remoteDeletedMessage
case contactMessage
case stickerMessage
case updateMessage
case paymentNotification
case giftBadge
case viewOnceMessage
case poll
case adminDeletedMessage
}
/// 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 ``BackupArchive.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 ``BackupArchive.ChatId``.
case referencedThreadIdMissing(ThreadUniqueId)
/// The object we are archiving references a custom chat color that should already have an id assigned
/// from having been archived, but does not.
/// e.g. we try to archive the chat style of a thread, but there is no ``BackupArchive.CustomChatColorId``.
case referencedCustomChatColorMissing(CustomChatColor.Key)
/// We were unable to fetch the OWSRecipientIdentity for a recipient.
case unableToFetchRecipientIdentity(RawError)
/// 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
/// There was a message in a contact thread with a recipient that was not self
/// or the contact in the thread. We can recover from this and know
/// historical bugs made it possible, but we log it nonetheless.
case messageFromOtherRecipientInContactThread
/// Custom chat colors should never have light/dark theme. The UI
/// disallows it and the proto cannot represent it.
case themedCustomChatColor
/// A `TSInteraction` database row was invalid, and we couldn't
/// instantiate a `TSInteraction` from it.
case invalidInteractionDatabaseRow(RawError)
/// An incoming message has an invalid or missing author address information,
/// causing the message to be skipped.
case invalidIncomingMessageAuthor
/// An incoming message came from the self recipient.
case incomingMessageFromSelf
/// An message not from self in Note to Self
case nonSelfAuthorInNoteToSelf
/// 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 quote of type `NORMAL` was missing both text and attachments.
case quoteTypeNormalMissingTextAndAttachments
/// A link preview is missing its url
case linkPreviewMissingUrl
/// A link preview's URL isn't in the message body
case linkPreviewUrlNotInBody
/// A sticker message had no associated attachment for the sticker's image contents.
case stickerMessageMissingStickerAttachment
/// A story reply did not have an aci author.
case storyReplyAuthorMissingAci
/// A story reply had empty context (text or reaction).
case storyReplyEmptyContents
/// We only support backing up 1:1 story replies.
case storyReplyInGroupThread
/// A reaction has an invalid or missing author address information, causing the
/// reaction to be skipped.
case invalidReactionAddress
/// A reaction has an invalid (too large) timestamp.
case invalidReactionTimestamp
/// 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 call status.
case groupCallRecordHadInvalidCallStatus
/// A distribution list had no distributionId; the distribution id assigned in the error should be ignored.
case distributionListMissingDistributionId
/// The recipients for a distribution list couldn't be fetched.
case unableToFetchDistributionListRecipients
/// A distribution list had ``TSThreadStoryViewMode/default``.
case distributionListHasDefaultViewMode
/// A custom (non-MyStory) distribution list had a ``TSThreadStoryViewMode/blocklist``.
case customDistributionListBlocklistViewMode
/// The story distribution list was marked as deleted but missing a deletion timestamp
case distributionListMissingDeletionTimestamp
/// The story distribution list was marked as deleted but had an invalid deletion timestamp.
case distributionListInvalidTimestamp
/// An interaction used to create a verification-state update was
/// missing info as to its author.
case verificationStateUpdateInteractionMissingAuthor
/// An interaction used to create a phone number change was missing
/// info as to its author.
case phoneNumberChangeInteractionMissingAuthor
/// An interaction used to create a payment activation request was
/// missing info as to its author.
case paymentActivationRequestInteractionMissingAuthor
/// An interaction used to create a payments-activated request was
/// missing info as to its author.
case paymentsActivatedInteractionMissingAuthor
/// An interaction used to create an identity-key change was missing
/// info as to its author.
case identityKeyChangeInteractionMissingAuthor
/// An interaction used to create a decryption error update was
/// missing info as to its author.
case decryptionErrorInteractionMissingAuthor
/// We found a non-simple chat update type when expecting a simple
/// chat update.
case foundComplexChatUpdateTypeWhenExpectingSimple
/// A "verification state change" info message was not of the
/// expected SDS record type, ``OWSVerificationStateChangeMessage``.
case verificationStateChangeNotExpectedSDSRecordType
/// An "unknown protocol version" info message was not of the
/// expected SDS record type, ``OWSUnknownProtocolVersionMessage``.
case unknownProtocolVersionNotExpectedSDSRecordType
/// A simple chat update message that was expected to be in a 1:1
/// thread was not, in fact, in a 1:1 thread.
case simpleChatUpdateMessageNotInContactThread
/// We failed to fetch payment information for a payment message.
case paymentInfoFetchFailed(RawError)
/// The payment message was missing required additional payment information.
case missingPaymentInformation
/// A "disappearing message config update" info message was not of
/// the expected SDS record type, ``OWSDisappearingConfigurationUpdateInfoMessage``.
case disappearingMessageConfigUpdateNotExpectedSDSRecordType
/// An ``OWSDisappearingConfigurationUpdateInfoMessage`` info
/// message was unexpectedly missing author info.
case disappearingMessageConfigUpdateMissingAuthor
/// A "profile change update" info message was missing author info.
case profileChangeUpdateMissingAuthor
/// A "profile change update" info message was missing the before or
/// after profile name.
case profileChangeUpdateMissingNames
/// A "thread merge update" info message was missing info as to the
/// contact whose threads were merged.
case threadMergeUpdateMissingAuthor
/// A "session switchover update" info message was missing info as
/// to the switched-over-from session.
case sessionSwitchoverUpdateMissingAuthor
/// A "learned profile update" info message was missing the display
/// name from before we learned the profile.
case learnedProfileUpdateMissingPreviousName
/// A "learned profile update" info message contained an invalid
/// E164 as its "previous name".
case learnedProfileUpdateInvalidE164
/// A "learned profile update" info message was missing info as to
/// the profile that was learned.
case learnedProfileUpdateMissingAuthor
/// We failed to fetch the edit history for a message.
case editHistoryFailedToFetch
/// We failed to read the ``StoryContextAssociatedData``. Note that it can
/// be nil (missing); this is a SQL error when we tried to read.
case unableToReadStoryContextAssociatedData(Error)
/// An unviewed view-once message had an unexpected attachment count.
case unviewedViewOnceMessageUnexpectedAttachmentCount(Int)
/// An ad hoc call's ``CallRecord/conversationId`` is not a
/// call link, which is illegal.
case adHocCallDoesNotHaveCallLinkAsConversationId
/// An ad hoc call has an invalid start timestamp.
case invalidAdHocCallTimestamp
/// A message of an unexpected type had edit history.
case revisionsPresentOnUnexpectedMessage(UnexpectedRevisionsMessageType)
/// A message's edit history contained an unexpected type.
case revisionWasUnexpectedMessage(UnexpectedRevisionsMessageType)
/// A poll terminate message was missing a question
case pollEndMissingQuestion
/// A poll terminate message was missing all persistable data
case pollEndMissingPersistableData
/// An interaction that claims to be a poll does not have associated poll data
case pollMissing
/// Poll option should have a rowId but it does not
case pollOptionIdMissing
/// Poll db row doesn't fit into PollRecord Swift type
case invalidPollRecordDatabaseRow
/// Poll option db row doesn't fit into PollOptionRecord Swift type
case invalidPollOptionRecordDatabaseRow
/// Poll vote db row doesn't fit into PollVoteRecord Swift type
case invalidPollVoteRecordDatabaseRow
/// An interaction that claims to be a poll does not have a poll question
case pollMessageMissingQuestionBody
/// A poll vote recipient id was not found
case pollVoteAuthorSignalRecipientIdMissing
/// Author Aci for end poll message was invalid
case endPollUpdateInvalidAuthorAci
/// A pin message chat update was missing all persistable data
case pinMessageChatUpdateMissingPersistableData
}
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: BackupArchive.LoggableError
public var typeLogString: String {
return "ArchiveFrameError: \(String(describing: type))"
}
public var idLogString: String {
switch type {
case .distributionListMissingDistributionId:
return "\(id.typeLogString).{ID missing}"
default:
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,
.referencedCustomChatColorMissing,
.contactThreadMissingAddress:
// Collapse these by the id they refer to, which is in the "type".
return idLogString
case .incomingMessageFromSelf, .nonSelfAuthorInNoteToSelf, .messageFromOtherRecipientInContactThread:
// Collapse these all together.
return id.typeLogString
case
.fileIOError,
.groupMasterKeyError,
.themedCustomChatColor,
.unableToFetchRecipientIdentity,
.distributionListMissingDistributionId,
.unableToFetchDistributionListRecipients,
.distributionListHasDefaultViewMode,
.customDistributionListBlocklistViewMode,
.distributionListMissingDeletionTimestamp,
.distributionListInvalidTimestamp,
.invalidInteractionDatabaseRow,
.invalidIncomingMessageAuthor,
.invalidOutgoingMessageRecipient,
.invalidQuoteAuthor,
.quoteTypeNormalMissingTextAndAttachments,
.linkPreviewMissingUrl,
.linkPreviewUrlNotInBody,
.stickerMessageMissingStickerAttachment,
.storyReplyAuthorMissingAci,
.storyReplyEmptyContents,
.storyReplyInGroupThread,
.invalidReactionAddress,
.invalidReactionTimestamp,
.emptyGroupUpdate,
.missingLocalProfile,
.missingLocalProfileKey,
.missingRequiredGroupMemberParams,
.groupCallRecordHadInvalidCallStatus,
.verificationStateUpdateInteractionMissingAuthor,
.phoneNumberChangeInteractionMissingAuthor,
.identityKeyChangeInteractionMissingAuthor,
.decryptionErrorInteractionMissingAuthor,
.paymentActivationRequestInteractionMissingAuthor,
.paymentsActivatedInteractionMissingAuthor,
.foundComplexChatUpdateTypeWhenExpectingSimple,
.verificationStateChangeNotExpectedSDSRecordType,
.unknownProtocolVersionNotExpectedSDSRecordType,
.simpleChatUpdateMessageNotInContactThread,
.paymentInfoFetchFailed,
.missingPaymentInformation,
.disappearingMessageConfigUpdateNotExpectedSDSRecordType,
.disappearingMessageConfigUpdateMissingAuthor,
.profileChangeUpdateMissingAuthor,
.profileChangeUpdateMissingNames,
.threadMergeUpdateMissingAuthor,
.sessionSwitchoverUpdateMissingAuthor,
.learnedProfileUpdateMissingPreviousName,
.learnedProfileUpdateInvalidE164,
.learnedProfileUpdateMissingAuthor,
.editHistoryFailedToFetch,
.unableToReadStoryContextAssociatedData,
.unviewedViewOnceMessageUnexpectedAttachmentCount,
.adHocCallDoesNotHaveCallLinkAsConversationId,
.invalidAdHocCallTimestamp,
.revisionsPresentOnUnexpectedMessage,
.revisionWasUnexpectedMessage,
.pollMissing,
.pollOptionIdMissing,
.invalidPollRecordDatabaseRow,
.invalidPollOptionRecordDatabaseRow,
.invalidPollVoteRecordDatabaseRow,
.pollMessageMissingQuestionBody,
.pollVoteAuthorSignalRecipientIdMissing,
.endPollUpdateInvalidAuthorAci,
.pollEndMissingQuestion,
.pollEndMissingPersistableData,
.pinMessageChatUpdateMissingPersistableData:
// Log any others as we see them.
return nil
}
}
public var logLevel: BackupArchive.LogLevel {
switch type {
case
.protoSerializationError,
.referencedRecipientIdMissing,
.referencedThreadIdMissing,
.referencedCustomChatColorMissing,
.unableToFetchRecipientIdentity,
.fileIOError,
.groupMasterKeyError,
.themedCustomChatColor,
.distributionListMissingDistributionId,
.unableToFetchDistributionListRecipients,
.distributionListHasDefaultViewMode,
.customDistributionListBlocklistViewMode,
.distributionListMissingDeletionTimestamp,
.distributionListInvalidTimestamp,
.invalidIncomingMessageAuthor,
.invalidOutgoingMessageRecipient,
.invalidQuoteAuthor,
.linkPreviewMissingUrl,
.storyReplyAuthorMissingAci,
.storyReplyEmptyContents,
.storyReplyInGroupThread,
.invalidReactionAddress,
.invalidReactionTimestamp,
.emptyGroupUpdate,
.missingLocalProfile,
.missingLocalProfileKey,
.missingRequiredGroupMemberParams,
.groupCallRecordHadInvalidCallStatus,
.verificationStateUpdateInteractionMissingAuthor,
.phoneNumberChangeInteractionMissingAuthor,
.identityKeyChangeInteractionMissingAuthor,
.decryptionErrorInteractionMissingAuthor,
.paymentActivationRequestInteractionMissingAuthor,
.paymentsActivatedInteractionMissingAuthor,
.foundComplexChatUpdateTypeWhenExpectingSimple,
.verificationStateChangeNotExpectedSDSRecordType,
.unknownProtocolVersionNotExpectedSDSRecordType,
.simpleChatUpdateMessageNotInContactThread,
.paymentInfoFetchFailed,
.missingPaymentInformation,
.disappearingMessageConfigUpdateNotExpectedSDSRecordType,
.disappearingMessageConfigUpdateMissingAuthor,
.profileChangeUpdateMissingAuthor,
.threadMergeUpdateMissingAuthor,
.sessionSwitchoverUpdateMissingAuthor,
.learnedProfileUpdateMissingPreviousName,
.learnedProfileUpdateInvalidE164,
.learnedProfileUpdateMissingAuthor,
.editHistoryFailedToFetch,
.unableToReadStoryContextAssociatedData,
.unviewedViewOnceMessageUnexpectedAttachmentCount,
.adHocCallDoesNotHaveCallLinkAsConversationId,
.invalidAdHocCallTimestamp,
.revisionsPresentOnUnexpectedMessage,
.revisionWasUnexpectedMessage,
.pollMissing,
.pollOptionIdMissing,
.invalidPollRecordDatabaseRow,
.invalidPollOptionRecordDatabaseRow,
.invalidPollVoteRecordDatabaseRow,
.pollMessageMissingQuestionBody,
.pollVoteAuthorSignalRecipientIdMissing,
.endPollUpdateInvalidAuthorAci,
.pollEndMissingQuestion,
.pollEndMissingPersistableData,
.pinMessageChatUpdateMissingPersistableData:
return .error
case .invalidInteractionDatabaseRow:
// We've seen real world databases with interaction rows that
// failed to deserialize into TSInteraciton instances. We'll
// drop them from the Backup.
return .warning
case .contactThreadMissingAddress:
// We've seen real-world databases with TSContactThreads that
// have no contact identifiers (aci/pni/e64).
// These cause us to drop the TSThread from the backup, but
// we can mark these as warnings. If these threads have
// any messages in them, those will fail at log level error.
return .warning
case .profileChangeUpdateMissingNames:
// We've seen real world databases with profileChange TSInfoMessages
// that don't have names on them. We filter these at render time
// (see `hasRenderableChanges`), so drop them from the backup
// with a warning but not an error.
return .warning
case .quoteTypeNormalMissingTextAndAttachments:
// We've seen real-world databases with "empty" quotes; we will
// drop the quote on export and issue a warning.
return .warning
case .linkPreviewUrlNotInBody:
// We've seen real world databases with invalid link previews; we
// just drop these on export and just issue a warning.
return .warning
case .incomingMessageFromSelf:
// We've seen real world databases with messages from self; we
// fudge these into outgoing messages on export and issue a warning.
return .warning
case .nonSelfAuthorInNoteToSelf:
// We've seen real world databases with messages from other authors
// in note to self; we fudge these into outgoing messages on export
// and issue a warning.
return .warning
case .stickerMessageMissingStickerAttachment:
// We lose a lot of stickers, apparently, in real world testing.
// Usually not the end of the world.
return .warning
case .messageFromOtherRecipientInContactThread:
// We've seen real world databases, particular with chats
// that predate the introduction of ACIs, that have
// mismatches due to missing or hallucinated ACIs on
// message rows not matching the TSContactThread row.
return .warning
}
}
}
/// Error archiving an entire category of frames; not attributable to a
/// single frame.
public struct FatalArchivingError: BackupArchive.LoggableError {
public enum ErrorType {
/// Error iterating over all SignalRecipients for backup purposes.
case recipientIteratorError(RawError)
/// Error iterating over all threads for backup purposes.
case threadIteratorError(RawError)
/// We fetched a thread (via the iterator) with no sqlite row id.
case fetchedThreadMissingRowId
/// Some unrecognized thread was found when iterating over all threads.
case unrecognizedThreadType
/// Error iterating over all interactions for backup purposes.
case interactionIteratorError(RawError)
/// We fetched an interaction (via the iterator) with no sqlite row id.
case fetchedInteractionMissingRowId
/// Error fetching reactions for a message.
case reactionIteratorError(RawError)
/// Error iterating over all sticker packs for backup purposes.
case stickerPackIteratorError(RawError)
/// Error iterating over all call link records for backup purposes.
case callLinkRecordIteratorError(RawError)
/// Error iterating over all ad hoc calls for backup purposes.
case adHocCallIteratorError(RawError)
case oversizedTextCacheFetchError(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: BackupArchive.LoggableError
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
}
public var logLevel: BackupArchive.LogLevel {
// All of these are hard errors.
return .error
}
}
/// Error restoring a frame.
public struct RestoreFrameError<ProtoIdType: BackupArchive.LoggableId>: BackupArchive.LoggableError {
public enum ErrorType {
public enum InvalidProtoDataError {
/// No ``BackupProto_BackupInfo`` header found.
case missingBackupInfoHeader
/// The ``BackupProto_BackupInfo`` has an unsupported version.
case unsupportedBackupInfoVersion
/// The ``BackupProto_BackupInfo`` had a missing or invalid MediaRootBackupKey.
case invalidMediaRootBackupKey
/// The AccountData frame was missing or not present before other frames.
case accountDataNotFound
/// 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 ``Aes256Key`` profile key. Includes the class
/// of the offending proto.
case invalidProfileKey(protoClass: Any.Type)
/// Could not parse an ``IdentityKey`` from a contact.
case invalidContactIdentityKey
/// An invalid member (group, distribution list, etc) was specified as a distribution list member. Includes the offending proto
case invalidDistributionListMember(protoClass: Any.Type)
/// An invalid member label was associated with a group member.
case invalidMemberLabel
/// The backup tier in account settings was set but not able to be parsed by libsignal.
case invalidBackupTier
/// A ``BackupProto/Contact`` with no aci, pni, or e164.
case contactWithoutIdentifiers
/// A ``BackupProto/Contact`` for the local user. This shouldn't exist.
case otherContactWithLocalIdentifiers
/// Some custom chat color identifier being referenced was not present earlier in the backup file.
case customChatColorNotFound(CustomChatColorId)
/// A ``BackupProto_ChatStyle/Gradient`` had less than two colors.
case chatStyleGradientSingleOrNoColors
/// A directionless chat item was not an update message.
case directionlessChatItemNotUpdateMessage
/// A ``BackupProto/ChatItem`` has a missing or invalid dateSent.
case chatItemInvalidDateSent
/// 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
/// `BackupProto_Reaction` must come from either an Aci or an E164.
/// One in the backup did not.
case reactionNotFromAciOrE164
/// A ``BackupProto_StandardMessage`` had neither body text nor any attachments.
case emptyStandardMessage
/// A ``BackupProto_DirectStoryReplyMessage`` had an empty text body.
case directStoryReplyMessageEmpty
/// A ``BackupProto_DirectStoryReplyMessage`` had an empty text body, but a long-text attachment was present.
case directStoryReplyMessageEmptyWithLongText
/// A ``BackupProto_DirectStoryReplyMessage`` author didn't have an aci.
case directStoryReplyFromNonAci
/// A ``BackupProto_DirectStoryReplyMessage`` was in a group thread.
case directStoryReplyInGroupThread
/// A ``BackupProto_StandardMessage/text`` had inlined oversize text that
/// was too long (even for oversized text there is a limit).
case standardMessageWayTooOversizedBody
/// A ``BackupProto_StandardMessage/longText`` was present despite an inlined
/// oversize message body (longer than standard message body length). Long text
/// pointers should only be included if the attachment is undownloaded and unavailable for inlining.
case longTextStandardMessageWithOversizeBody
/// A ``BackupProto_StandardMessage/longText`` was present despite an empty
/// message body (the body text must always be a prefix of the long text)
case longTextStandardMessageMissingBody
/// A quoted message had no body, attachment, gift badge, or other
/// content in its representation of the original being quoted.
case quotedMessageEmptyContent
/// The text body in a quoted message was too long (as enforced by standard message sending).
/// Oversized text in quotes is unsupported on all platforms.
case quotedMessageOversizeText
/// A link preview with an empty string for the url
case linkPreviewEmptyUrl
/// Link preview urls must be present in the message body;
/// this error is for when they are not.
case linkPreviewUrlNotInBody
/// A ``BackupProto_ContactMessage/contact`` is missing.
case contactMessageMissingContactAttachment
/// A ``BackupProto_ContactAttachment/Phone/value`` was missing or empty.
case contactAttachmentPhoneNumberMissingValue
/// A ``BackupProto_ContactAttachment/Email/value`` was missing or empty.
case contactAttachmentEmailMissingValue
/// A ``BackupProto_ContactAttachment/PostalAddress`` with all empty fields;
/// at least some field has to be nonempty to be a valid address.
case contactAttachmentEmptyAddress
/// A `BackupProto_Group's` gv2 master key could not be parsed by libsignal.
case invalidGV2MasterKey
/// A `BackupProto_Group` was missing its group snapshot.
case missingGV2GroupSnapshot
/// 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
/// 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_GroupCall` chat item update was associated with
/// a thread that was not a group thread.
case groupCallNotInGroupThread
/// 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
/// A custom (non-MyStory) distribution list had ``BackupProto_DistributionList/PrivacyMode/all``
/// or ``BackupProto_DistributionList/PrivacyMode/allExcept``, which are only allowed
/// for My Story.
case customDistributionListPrivacyModeAllOrAllExcept
/// `BackupProto_DistributionListItem.deletionTimestamp` was invalid
case invalidDistributionListDeletionTimestamp
/// ``BackupProto_DistributionListItem`` was used as a recipient for
/// a ``BackupProto_Chat``; this isn't allowed.
case distributionListUsedAsChatRecipient
/// ``BackupProto_CallLink`` was used as a recipient for something
/// other than a ``BackupProto_AdHocCall``; this isn't allowed.
case callLinkUsedAsChatRecipient
/// A "verification state change" simple chat update was
/// associated with a non-contact recipient.
case verificationStateChangeNotFromContact
/// A "phone number chnaged" simple chat update was associated
/// with a non-contact recipient.
case phoneNumberChangeNotFromContact
/// An "end session" simple chat update was associated with a
/// non-contact recipient.
case endSessionNotFromContact
/// A "decryption error" simple chat update was associated with
/// a non-contact recipient.
case decryptionErrorNotFromContact
/// A "payments activation request" simple chat update was
/// associated with a recipient with no ACI.
case paymentsActivationRequestNotFromAci
/// A "payments activated" simple chat update was associated
/// with a recipient with no ACI.
case paymentsActivatedNotFromAci
/// An "unsupported protocol version" simple chat update was
/// associated with a non-contact recipient.
case unsupportedProtocolVersionNotFromContact
/// An ``BackupProto_PaymentNotification`` was sent
/// in a group chat (not a 1:1 chat).
case paymentNotificationInGroup
/// An "expiration timer update" was in a non-contact thread.
/// - Note
/// Expiration timer updates for group threads are handled via
/// a separate "group expiration timer update" proto.
case expirationTimerUpdateNotInContactThread
/// An "expiration timer" field contained a value that
/// overflowed the local type for expiration timers.
case expirationTimerOverflowedLocalType
/// A "profile change update" was not authored by a contact.
case profileChangeUpdateNotFromContact
/// A "thread merge update" was not authored by a contact.
case threadMergeUpdateNotFromContact
/// A "session switchover update" was not authored by a contact.
case sessionSwitchoverUpdateNotFromContact
/// A "learned profile update" was not authored by a contact.
case learnedProfileUpdateNotFromContact
/// An incoming message, or a revision for an incoming message,
/// were missing incoming details. (Revisions must have the same
/// directionality as their parent.)
case revisionOfIncomingMessageMissingIncomingDetails
/// An outgoing message, or a revision for an outgoing message,
/// were missing outgoing details. (Revisions must have the same
/// directionality as their parent.)
case revisionOfOutgoingMessageMissingOutgoingDetails
/// A ``BackupProto_MessageAttachment/clientUuid`` contained an invalid UUID.
case invalidAttachmentClientUUID
/// A ``BackupProto_CallLink/rootKey`` was invalid.
case callLinkInvalidRootKey
/// The recipient on an ad hoc call was not a call link. No other
/// recipient types are valid for an ad hoc call.
case recipientOfAdHocCallWasNotCallLink
/// The poll terminate message author had an invalid non-contact Address
case pollTerminateAuthorNotContact
/// Poll question was empty
case pollQuestionEmpty
/// The poll vote message author had an invalid non-contact Address
case pollVoteAuthorNotContact
/// We only expect one vote count per author, but there were multiple
case pollVoteCountRepeated
/// We expect all authors to have an associated latest vote count, but there wasn't
case noPollVoteCountForAuthor
/// The pin message author had an invalid non-contact Address
case pinMessageAuthorNotContact
/// There were more pinned messages than allowed
case invalidNumberOfPinnedMessages
/// A timestamp to help identify a target message overflowed a local type
case sentTimestampOverflowedLocalType
/// The admin delete author had an invalid non-contact address
case adminDeleteAuthorNotContact
}
/// 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)
/// The object being inserted depended on a CustomChatColor that should have been created earlier but was not.
case referencedCustomChatColorNotFound(CustomChatColor.Key)
case databaseModelMissingRowId(modelClass: AnyClass)
case databaseInsertionFailed(RawError)
/// We failed to properly create the attachment in the DB after restoring
case failedToCreateAttachment
/// 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)
/// Poll failed to insert in SQL
case pollCreateFailedToInsertInDatabase
/// Poll vote failed to insert in SQL
case pollVoteFailedToInsertInDatabase
/// Poll terminate failed to insert in SQL
case pollTerminateFailedToInsertInDatabase
}
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
.missingBackupInfoHeader,
.unsupportedBackupInfoVersion,
.invalidMediaRootBackupKey,
.accountDataNotFound,
.recipientIdNotFound,
.chatIdNotFound,
.invalidBackupTier:
// Collapse these by the id they refer to, which is in the "type".
return typeLogString
case .customChatColorNotFound(let id):
return id.idLogString
case
.invalidAci,
.invalidPni,
.invalidServiceId,
.invalidE164,
.invalidProfileKey,
.invalidContactIdentityKey,
.invalidDistributionListMember,
.invalidMemberLabel,
.contactWithoutIdentifiers,
.otherContactWithLocalIdentifiers,
.chatItemInvalidDateSent,
.chatStyleGradientSingleOrNoColors,
.directionlessChatItemNotUpdateMessage,
.incomingMessageNotFromAciOrE164,
.outgoingNonContactMessageRecipient,
.reactionNotFromAciOrE164,
.emptyStandardMessage,
.directStoryReplyMessageEmpty,
.directStoryReplyMessageEmptyWithLongText,
.directStoryReplyFromNonAci,
.directStoryReplyInGroupThread,
.standardMessageWayTooOversizedBody,
.longTextStandardMessageWithOversizeBody,
.longTextStandardMessageMissingBody,
.quotedMessageEmptyContent,
.quotedMessageOversizeText,
.linkPreviewEmptyUrl,
.linkPreviewUrlNotInBody,
.contactMessageMissingContactAttachment,
.contactAttachmentPhoneNumberMissingValue,
.contactAttachmentEmailMissingValue,
.contactAttachmentEmptyAddress,
.invalidGV2MasterKey,
.missingGV2GroupSnapshot,
.invitedGV2MemberMissingMemberDetails,
.failedToBuildGV2GroupModel,
.groupUpdateMessageInNonGroupChat,
.emptyGroupUpdates,
.sequenceOfRequestsAndCancelsWithLocalAci,
.invalidLocalProfileKey,
.invalidLocalUsernameLink,
.individualCallNotInContactThread,
.groupCallNotInGroupThread,
.groupCallRecipientIdNotAnAci,
.invalidDistributionListId,
.customDistributionListPrivacyModeAllOrAllExcept,
.invalidDistributionListDeletionTimestamp,
.distributionListUsedAsChatRecipient,
.verificationStateChangeNotFromContact,
.phoneNumberChangeNotFromContact,
.endSessionNotFromContact,
.decryptionErrorNotFromContact,
.paymentsActivationRequestNotFromAci,
.paymentsActivatedNotFromAci,
.paymentNotificationInGroup,
.unsupportedProtocolVersionNotFromContact,
.expirationTimerUpdateNotInContactThread,
.expirationTimerOverflowedLocalType,
.profileChangeUpdateNotFromContact,
.threadMergeUpdateNotFromContact,
.sessionSwitchoverUpdateNotFromContact,
.learnedProfileUpdateNotFromContact,
.revisionOfIncomingMessageMissingIncomingDetails,
.revisionOfOutgoingMessageMissingOutgoingDetails,
.invalidAttachmentClientUUID,
.callLinkInvalidRootKey,
.callLinkUsedAsChatRecipient,
.recipientOfAdHocCallWasNotCallLink,
.pollTerminateAuthorNotContact,
.pollQuestionEmpty,
.pollVoteAuthorNotContact,
.pollVoteCountRepeated,
.noPollVoteCountForAuthor,
.pinMessageAuthorNotContact,
.invalidNumberOfPinnedMessages,
.sentTimestampOverflowedLocalType,
.adminDeleteAuthorNotContact:
// Collapse all others by the id of the containing frame.
return idLogString
}
case .referencedChatThreadNotFound, .referencedGroupThreadNotFound, .failedToCreateAttachment:
// Collapse these by the id they refer to, which is in the "type".
return typeLogString
case .referencedCustomChatColorNotFound(let key):
// Collapse these by the key that isn't found.
return key.rawValue
case .databaseModelMissingRowId(let modelClass):
// Collapse these by the relevant class.
return "\(modelClass)"
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 .pollCreateFailedToInsertInDatabase,
.pollVoteFailedToInsertInDatabase,
.pollTerminateFailedToInsertInDatabase:
return typeLogString
}
}
public var logLevel: BackupArchive.LogLevel {
switch type {
case .invalidProtoData(let invalidProtoDataError):
switch invalidProtoDataError {
case
.missingBackupInfoHeader,
.unsupportedBackupInfoVersion,
.invalidMediaRootBackupKey,
.accountDataNotFound,
.recipientIdNotFound,
.chatIdNotFound,
.invalidAci,
.invalidPni,
.invalidServiceId,
.invalidE164,
.invalidProfileKey,
.invalidContactIdentityKey,
.invalidDistributionListMember,
.invalidMemberLabel,
.invalidBackupTier,
.contactWithoutIdentifiers,
.otherContactWithLocalIdentifiers,
.chatItemInvalidDateSent,
.chatStyleGradientSingleOrNoColors,
.customChatColorNotFound,
.directionlessChatItemNotUpdateMessage,
.incomingMessageNotFromAciOrE164,
.outgoingNonContactMessageRecipient,
.reactionNotFromAciOrE164,
.emptyStandardMessage,
.directStoryReplyMessageEmpty,
.directStoryReplyMessageEmptyWithLongText,
.directStoryReplyFromNonAci,
.directStoryReplyInGroupThread,
.standardMessageWayTooOversizedBody,
.longTextStandardMessageWithOversizeBody,
.longTextStandardMessageMissingBody,
.quotedMessageOversizeText,
.linkPreviewEmptyUrl,
.contactMessageMissingContactAttachment,
.contactAttachmentPhoneNumberMissingValue,
.contactAttachmentEmailMissingValue,
.contactAttachmentEmptyAddress,
.invalidGV2MasterKey,
.missingGV2GroupSnapshot,
.invitedGV2MemberMissingMemberDetails,
.failedToBuildGV2GroupModel,
.groupUpdateMessageInNonGroupChat,
.emptyGroupUpdates,
.sequenceOfRequestsAndCancelsWithLocalAci,
.invalidLocalProfileKey,
.invalidLocalUsernameLink,
.individualCallNotInContactThread,
.groupCallNotInGroupThread,
.groupCallRecipientIdNotAnAci,
.invalidDistributionListId,
.customDistributionListPrivacyModeAllOrAllExcept,
.invalidDistributionListDeletionTimestamp,
.distributionListUsedAsChatRecipient,
.verificationStateChangeNotFromContact,
.phoneNumberChangeNotFromContact,
.endSessionNotFromContact,
.decryptionErrorNotFromContact,
.paymentsActivationRequestNotFromAci,
.paymentsActivatedNotFromAci,
.paymentNotificationInGroup,
.unsupportedProtocolVersionNotFromContact,
.expirationTimerUpdateNotInContactThread,
.expirationTimerOverflowedLocalType,
.profileChangeUpdateNotFromContact,
.threadMergeUpdateNotFromContact,
.sessionSwitchoverUpdateNotFromContact,
.learnedProfileUpdateNotFromContact,
.revisionOfIncomingMessageMissingIncomingDetails,
.revisionOfOutgoingMessageMissingOutgoingDetails,
.invalidAttachmentClientUUID,
.callLinkInvalidRootKey,
.callLinkUsedAsChatRecipient,
.recipientOfAdHocCallWasNotCallLink,
.pollTerminateAuthorNotContact,
.pollQuestionEmpty,
.pollVoteAuthorNotContact,
.pollVoteCountRepeated,
.noPollVoteCountForAuthor,
.pinMessageAuthorNotContact,
.invalidNumberOfPinnedMessages,
.sentTimestampOverflowedLocalType,
.adminDeleteAuthorNotContact:
return .error
case .quotedMessageEmptyContent:
// It was historically possible to end up with a quote that
// had no contents (no body, no OWSAttachmentInfo, not view-once
// or a gift badge). The way this renders is as a quote of an
// attachment with no preview, just the text "Attachment".
return .warning
case .linkPreviewUrlNotInBody:
// Other client platforms had different validation rules
// that make it possible to restore what we consider an
// invalid link preview. We drop these link previews
// without dropping the containing message.
return .warning
}
case
.referencedChatThreadNotFound,
.referencedGroupThreadNotFound,
.failedToCreateAttachment,
.referencedCustomChatColorNotFound,
.databaseModelMissingRowId,
.databaseInsertionFailed,
.developerError,
.pollCreateFailedToInsertInDatabase,
.pollVoteFailedToInsertInDatabase,
.pollTerminateFailedToInsertInDatabase:
return .error
}
}
}
}
// MARK: - Log Collapsing
extension BackupArchive {
public enum LogLevel: Int {
/// Log these, but don't pull up the internal
/// dialog if all errors are warnings.
case warning
/// Log these and show the internal dialog if these happen.
case error
}
}
extension BackupArchive {
protocol LoggableError {
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 }
var logLevel: BackupArchive.LogLevel { get }
}
struct LoggableErrorAndProto {
let error: any BackupArchive.LoggableError
let wasFrameDropped: Bool
/// Nil for archiving, if we fail to even parse the proto on restore,
/// or if the feature flag is disabled such that this would be unused.
let protoJson: String?
init(
error: any BackupArchive.LoggableError,
wasFrameDropped: Bool,
protoFrame: SwiftProtobuf.Message? = nil,
) {
self.error = error
self.wasFrameDropped = wasFrameDropped
// Don't serialize proto frames if we aren't displaying errors.
if let protoFrame, BuildFlags.Backups.archiveErrorDisplay {
do {
self.protoJson = try String(
data: JSONSerialization.data(
withJSONObject: JSONSerialization.jsonObject(
with: protoFrame.jsonUTF8Data(),
options: .mutableContainers,
),
options: .prettyPrinted,
),
encoding: .utf8,
)
} catch let jsonError {
self.protoJson = "Unable to json encode proto: \(jsonError)"
}
} else {
self.protoJson = nil
}
}
}
static func collapse(_ errors: [LoggableErrorAndProto]) -> [CollapsedErrorLog] {
var collapsedLogs = OrderedDictionary<String, CollapsedErrorLog>()
for error in errors {
let collapseKey = error.error.collapseKey ?? UUID().uuidString
if var existingLog = collapsedLogs[collapseKey] {
existingLog.collapse(error)
collapsedLogs.replace(key: collapseKey, value: existingLog)
} else {
let newLog = CollapsedErrorLog(error)
collapsedLogs.append(key: collapseKey, value: newLog)
}
}
return Array(collapsedLogs.orderedValues)
}
fileprivate static let maxCollapsedIdLogCount = 10
public struct CollapsedErrorLog {
private let logger: PrefixedLogger
public private(set) var typeLogString: String
public private(set) var exampleCallsiteString: String
public private(set) var exampleProtoFrameJson: String?
public private(set) var errorCount: UInt = 0
public private(set) var idLogStrings: [String] = []
public private(set) var wasFrameDropped: Bool
public private(set) var logLevel: BackupArchive.LogLevel
init(_ error: LoggableErrorAndProto) {
self.logger = PrefixedLogger(prefix: "[Backups]")
self.typeLogString = error.error.typeLogString
self.exampleCallsiteString = error.error.callsiteLogString
self.exampleProtoFrameJson = error.protoJson
self.wasFrameDropped = error.wasFrameDropped
self.logLevel = error.error.logLevel
self.collapse(error)
}
mutating func collapse(_ error: LoggableErrorAndProto) {
self.errorCount += 1
self.wasFrameDropped = wasFrameDropped || error.wasFrameDropped
self.logLevel = LogLevel(rawValue: max(self.logLevel.rawValue, error.error.logLevel.rawValue))!
if exampleProtoFrameJson == nil, let protoJson = error.protoJson {
self.exampleProtoFrameJson = protoJson
}
if idLogStrings.count < BackupArchive.maxCollapsedIdLogCount {
idLogStrings.append(error.error.idLogString)
}
}
func log() {
let logString =
typeLogString + " "
+ "Dropped frame(s)? \(wasFrameDropped). "
+ "Repeated \(errorCount) times. "
+ "from: \(idLogStrings) "
+ "example callsite: \(exampleCallsiteString)"
switch logLevel {
case .warning:
logger.warn(logString)
case .error:
logger.error(logString)
}
}
}
}