0xDEAD10CC mitigation

This commit is contained in:
Justin Mazzocchi 2021-03-12 15:25:16 -08:00
parent ac2d1fb805
commit 58333b558b
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
5 changed files with 99 additions and 124 deletions

View file

@ -51,9 +51,7 @@ public extension ContentDatabase {
}
func insert(status: Status) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: status.save)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: status.save)
}
// swiftlint:disable:next function_body_length
@ -61,7 +59,7 @@ public extension ContentDatabase {
statuses: [Status],
timeline: Timeline,
loadMoreAndDirection: (LoadMore, LoadMore.Direction)? = nil) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
let timelineRecord = TimelineRecord(timeline: timeline)
try timelineRecord.save($0)
@ -122,12 +120,10 @@ public extension ContentDatabase {
}
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(context: Context, parentId: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for (index, status) in context.ancestors.enumerated() {
try status.save($0)
try StatusAncestorJoin(parentId: parentId, statusId: status.id, order: index).save($0)
@ -148,12 +144,10 @@ public extension ContentDatabase {
&& !context.descendants.map(\.id).contains(StatusDescendantJoin.Columns.statusId))
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(pinnedStatuses: [Status], accountId: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for (index, status) in pinnedStatuses.enumerated() {
try status.save($0)
try AccountPinnedStatusJoin(accountId: accountId, statusId: status.id, order: index).save($0)
@ -164,12 +158,10 @@ public extension ContentDatabase {
&& !pinnedStatuses.map(\.id).contains(AccountPinnedStatusJoin.Columns.statusId))
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func toggleShowContent(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
if let toggle = try StatusShowContentToggle
.filter(StatusShowContentToggle.Columns.statusId == id)
.fetchOne($0) {
@ -178,12 +170,10 @@ public extension ContentDatabase {
try StatusShowContentToggle(statusId: id).save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func toggleShowAttachments(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
if let toggle = try StatusShowAttachmentsToggle
.filter(StatusShowAttachmentsToggle.Columns.statusId == id)
.fetchOne($0) {
@ -192,23 +182,19 @@ public extension ContentDatabase {
try StatusShowAttachmentsToggle(statusId: id).save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func expand(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for id in ids {
try StatusShowContentToggle(statusId: id).save($0)
try StatusShowAttachmentsToggle(statusId: id).save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func collapse(ids: Set<Status.Id>) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
try StatusShowContentToggle
.filter(ids.contains(StatusShowContentToggle.Columns.statusId))
.deleteAll($0)
@ -216,29 +202,23 @@ public extension ContentDatabase {
.filter(ids.contains(StatusShowContentToggle.Columns.statusId))
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func update(id: Status.Id, poll: Poll) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
let data = try StatusRecord.databaseJSONEncoder(for: StatusRecord.Columns.poll.name).encode(poll)
try StatusRecord.filter(StatusRecord.Columns.id == id)
.updateAll($0, StatusRecord.Columns.poll.set(to: data))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func delete(id: Status.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: StatusRecord.filter(StatusRecord.Columns.id == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: StatusRecord.filter(StatusRecord.Columns.id == id).deleteAll)
}
func unfollow(id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
let statusIds = try Status.Id.fetchAll(
$0,
StatusRecord.filter(StatusRecord.Columns.accountId == id).select(StatusRecord.Columns.id))
@ -248,27 +228,21 @@ public extension ContentDatabase {
&& statusIds.contains(TimelineStatusJoin.Columns.statusId))
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func mute(id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
try StatusRecord.filter(StatusRecord.Columns.accountId == id).deleteAll($0)
try NotificationRecord.filter(NotificationRecord.Columns.accountId == id).deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func block(id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: AccountRecord.filter(AccountRecord.Columns.id == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: AccountRecord.filter(AccountRecord.Columns.id == id).deleteAll)
}
func insert(accounts: [Account], listId: AccountList.Id? = nil) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
var order: Int?
if let listId = listId {
@ -290,22 +264,18 @@ public extension ContentDatabase {
}
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func remove(id: Account.Id, from listId: AccountList.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(
databaseWriter.mutatingPublisher(
updates: AccountListJoin.filter(
AccountListJoin.Columns.accountId == id
&& AccountListJoin.Columns.accountListId == listId)
.deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(identityProofs: [IdentityProof], id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for identityProof in identityProofs {
try IdentityProofRecord(
accountId: id,
@ -317,12 +287,10 @@ public extension ContentDatabase {
.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(featuredTags: [FeaturedTag], id: Account.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for featuredTag in featuredTags {
try FeaturedTagRecord(
id: featuredTag.id,
@ -334,22 +302,18 @@ public extension ContentDatabase {
.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(relationships: [Relationship]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for relationship in relationships {
try relationship.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func setLists(_ lists: [List]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for list in lists {
try TimelineRecord(timeline: Timeline.list(list)).save($0)
}
@ -359,86 +323,66 @@ public extension ContentDatabase {
&& TimelineRecord.Columns.listTitle != nil)
.deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func createList(_ list: List) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: TimelineRecord(timeline: Timeline.list(list)).save)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: TimelineRecord(timeline: Timeline.list(list)).save)
}
func deleteList(id: List.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
}
func setFilters(_ filters: [Filter]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for filter in filters {
try filter.save($0)
}
try Filter.filter(!filters.map(\.id).contains(Filter.Columns.id)).deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: filter.save)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: filter.save)
}
func deleteFilter(id: Filter.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: Filter.filter(Filter.Columns.id == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: Filter.filter(Filter.Columns.id == id).deleteAll)
}
func setLastReadId(_ id: String, timelineId: Timeline.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: LastReadIdRecord(timelineId: timelineId, id: id).save)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: LastReadIdRecord(timelineId: timelineId, id: id).save)
}
func insert(notifications: [MastodonNotification]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for notification in notifications {
try notification.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(conversations: [Conversation]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for conversation in conversations {
try conversation.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func update(emojis: [Emoji]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for emoji in emojis {
try emoji.save($0)
}
try Emoji.filter(!emojis.map(\.shortcode).contains(Emoji.Columns.shortcode)).deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updateUse(emoji: String, system: Bool) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
let count = try Int.fetchOne(
$0,
EmojiUse.filter(EmojiUse.Columns.system == system && EmojiUse.Columns.emoji == emoji)
@ -446,24 +390,20 @@ public extension ContentDatabase {
try EmojiUse(emoji: emoji, system: system, lastUse: Date(), count: (count ?? 0) + 1).save($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func update(announcements: [Announcement]) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for announcement in announcements {
try announcement.save($0)
}
try Announcement.filter(!announcements.map(\.id).contains(Announcement.Columns.id)).deleteAll($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(results: Results) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
for account in results.accounts {
try account.save($0)
}
@ -472,14 +412,10 @@ public extension ContentDatabase {
try status.save($0)
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func insert(instance: Instance) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: instance.save)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: instance.save)
}
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[CollectionSection], Error> {
@ -697,7 +633,6 @@ public extension ContentDatabase {
private extension ContentDatabase {
static let cleanAfterLastReadIdCount = 40
static let ephemeralTimelines = NSCountedSet()
static func fileURL(id: Identity.Id, appGroup: String) throws -> URL {

View file

@ -20,6 +20,7 @@ extension DatabasePool {
configuration.busyMode = .timeout(5)
configuration.defaultTransactionKind = .immediate
configuration.observesSuspensionNotifications = true
configuration.prepareDatabase { db in
try db.usePassphrase(passphrase())
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")

View file

@ -0,0 +1,29 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Combine
import Foundation
import GRDB
// swiftlint:disable:next line_length
// https://github.com/groue/GRDB.swift/blob/master/Documentation/SharingADatabase.md#how-to-limit-the-0xdead10cc-exception
extension DatabaseWriter {
func mutatingPublisher<Output>(updates: @escaping (Database) throws -> Output) -> AnyPublisher<Never, Error> {
let publisher = writePublisher(updates: updates)
return publisher
.tryCatch { error -> AnyPublisher<Output, Error> in
if let databaseError = error as? DatabaseError, databaseError.isInterruptionError {
return NotificationCenter.default.publisher(for: Database.resumeNotification)
.timeout(.seconds(1), scheduler: DispatchQueue.global())
.flatMap { _ in publisher }
.eraseToAnyPublisher()
} else {
throw error
}
}
.retry(1)
.ignoreOutput()
.eraseToAnyPublisher()
}
}

View file

@ -32,7 +32,7 @@ public struct IdentityDatabase {
public extension IdentityDatabase {
func createIdentity(id: Identity.Id, url: URL, authenticated: Bool, pending: Bool) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(
databaseWriter.mutatingPublisher(
updates: IdentityRecord(
id: id,
url: url,
@ -44,28 +44,22 @@ public extension IdentityDatabase {
lastRegisteredDeviceToken: nil,
pushSubscriptionAlerts: .initial)
.save)
.ignoreOutput()
.eraseToAnyPublisher()
}
func deleteIdentity(id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: IdentityRecord.filter(IdentityRecord.Columns.id == id).deleteAll)
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: IdentityRecord.filter(IdentityRecord.Columns.id == id).deleteAll)
}
func updateLastUsedAt(id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
try IdentityRecord
.filter(IdentityRecord.Columns.id == id)
.updateAll($0, IdentityRecord.Columns.lastUsedAt.set(to: Date()))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updateInstance(_ instance: Instance, id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
try Identity.Instance(
uri: instance.uri,
streamingAPI: instance.urls.streamingApi,
@ -78,12 +72,10 @@ public extension IdentityDatabase {
.filter(IdentityRecord.Columns.id == id)
.updateAll($0, IdentityRecord.Columns.instanceURI.set(to: instance.uri))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updateAccount(_ account: Account, id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(
databaseWriter.mutatingPublisher(
updates: Identity.Account(
id: account.id,
identityId: id,
@ -97,22 +89,18 @@ public extension IdentityDatabase {
emojis: account.emojis,
followRequestCount: account.source?.followRequestsCount ?? 0)
.save)
.ignoreOutput()
.eraseToAnyPublisher()
}
func confirmIdentity(id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
try IdentityRecord
.filter(IdentityRecord.Columns.id == id)
.updateAll($0, IdentityRecord.Columns.pending.set(to: false))
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updatePreferences(_ preferences: Mastodon.Preferences, id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
guard let storedPreferences = try IdentityRecord.filter(IdentityRecord.Columns.id == id)
.fetchOne($0)?
.preferences else {
@ -121,20 +109,16 @@ public extension IdentityDatabase {
try Self.writePreferences(storedPreferences.updated(from: preferences), id: id)($0)
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func updatePreferences(_ preferences: Identity.Preferences, id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher(updates: Self.writePreferences(preferences, id: id))
.ignoreOutput()
.eraseToAnyPublisher()
databaseWriter.mutatingPublisher(updates: Self.writePreferences(preferences, id: id))
}
func updatePushSubscription(alerts: PushSubscription.Alerts,
deviceToken: Data? = nil,
id: Identity.Id) -> AnyPublisher<Never, Error> {
databaseWriter.writePublisher {
databaseWriter.mutatingPublisher {
let data = try IdentityRecord.databaseJSONEncoder(
for: IdentityRecord.Columns.pushSubscriptionAlerts.name)
.encode(alerts)
@ -149,8 +133,6 @@ public extension IdentityDatabase {
.updateAll($0, IdentityRecord.Columns.lastRegisteredDeviceToken.set(to: deviceToken))
}
}
.ignoreOutput()
.eraseToAnyPublisher()
}
func identityPublisher(id: Identity.Id, immediate: Bool) -> AnyPublisher<Identity, Error> {

View file

@ -1,6 +1,8 @@
// Copyright © 2020 Metabolist. All rights reserved.
import AVKit
import Combine
import GRDB
import ServiceLayer
import SwiftUI
import ViewModels
@ -8,10 +10,36 @@ import ViewModels
@main
struct MetatextApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
private var cancellables = Set<AnyCancellable>()
init() {
try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
try? ImageCacheConfiguration(environment: Self.environment).configure()
// swiftlint:disable:next line_length
// https://github.com/groue/GRDB.swift/blob/master/Documentation/SharingADatabase.md#how-to-limit-the-0xdead10cc-exception
// This would ideally be accomplished with `@Environment(\.scenePhase) private var scenePhase`
// and `.onChange(of: scenePhase)` on the `WindowGroup`, but that does not give an accurate
// aggregate scene activation state for iPad multitasking as of iOS 14.4.1
Publishers.MergeMany([UIScene.willConnectNotification,
UIScene.didDisconnectNotification,
UIScene.didActivateNotification,
UIScene.willDeactivateNotification,
UIScene.willEnterForegroundNotification,
UIScene.didEnterBackgroundNotification]
.map { NotificationCenter.default.publisher(for: $0) })
.map { _ in
UIApplication.shared.openSessions
.compactMap(\.scene)
.allSatisfy { $0.activationState == .background }
}
.removeDuplicates()
.sink {
NotificationCenter.default.post(
name: $0 ? Database.suspendNotification : Database.resumeNotification,
object: nil)
}
.store(in: &cancellables)
}
var body: some Scene {