mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 12:05:20 +00:00
Invalidate media deletion callbacks during panic
This commit is contained in:
@@ -179,10 +179,21 @@ struct BLEIncomingFileStore: @unchecked Sendable {
|
||||
func panicWipe(
|
||||
hasDurablePendingMarker: Bool = false
|
||||
) throws {
|
||||
// The receipt index caches tombstones as well as accepted payloads.
|
||||
// Always invalidate it on return, including partial-failure paths, so
|
||||
// no pre-panic receiver decision survives after identity reset.
|
||||
defer { privateMediaReceipts.resetForPanic() }
|
||||
// The receipt index caches tombstones as well as accepted payloads,
|
||||
// while payload coordination retains save/delete reservations. Always
|
||||
// invalidate both on return, including partial-failure paths, so no
|
||||
// pre-panic receiver decision survives after identity reset.
|
||||
defer {
|
||||
privateMediaReceipts.resetForPanic()
|
||||
payloadCoordination.lock.lock()
|
||||
payloadCoordination.pendingDeliveryPaths.removeAll(
|
||||
keepingCapacity: false
|
||||
)
|
||||
payloadCoordination.deletionReservations.removeAll(
|
||||
keepingCapacity: false
|
||||
)
|
||||
payloadCoordination.lock.unlock()
|
||||
}
|
||||
|
||||
let markerError: Error?
|
||||
do {
|
||||
|
||||
@@ -3492,6 +3492,18 @@ extension BLEService {
|
||||
}
|
||||
}
|
||||
|
||||
func _test_emitTransportEvent(
|
||||
_ event: TransportEvent,
|
||||
completion: @escaping () -> Void,
|
||||
finalization: @escaping (TransportEventDeliveryOutcome) -> Void
|
||||
) {
|
||||
emitTransportEvent(
|
||||
event,
|
||||
completion: completion,
|
||||
finalization: finalization
|
||||
)
|
||||
}
|
||||
|
||||
func _test_handlePacket(_ packet: BitchatPacket, fromPeerID: PeerID, preseedPeer: Bool = true, signingPublicKey: Data? = nil) {
|
||||
if preseedPeer {
|
||||
// Ensure the synthetic peer is known and marked verified for public-message tests
|
||||
@@ -4504,11 +4516,24 @@ extension BLEService {
|
||||
completion: (() -> Void)? = nil,
|
||||
finalization: ((TransportEventDeliveryOutcome) -> Void)? = nil
|
||||
) {
|
||||
notifyUI { [weak self] in
|
||||
guard let generation = capturePanicLifecycleGeneration() else {
|
||||
Task { @MainActor in
|
||||
finalization?(.rejected)
|
||||
}
|
||||
return
|
||||
}
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self,
|
||||
self.isCurrentPanicLifecycleGeneration(generation) else {
|
||||
finalization?(.rejected)
|
||||
return
|
||||
}
|
||||
TransportEventDeliveryGate.attempt(
|
||||
shouldDeliver: { shouldDeliver?() ?? true },
|
||||
shouldDeliver: {
|
||||
self.isCurrentPanicLifecycleGeneration(generation)
|
||||
&& (shouldDeliver?() ?? true)
|
||||
},
|
||||
deliver: {
|
||||
guard let self else { return .rejected }
|
||||
return self.deliverTransportEvent(event)
|
||||
},
|
||||
completion: { completion?() },
|
||||
|
||||
@@ -415,6 +415,7 @@ final class ChatMediaTransferCoordinator {
|
||||
|
||||
private(set) var transferIdToMessageIDs: [String: [String]] = [:]
|
||||
private(set) var messageIDToTransferId: [String: String] = [:]
|
||||
private var deletionGeneration: UInt64 = 0
|
||||
private var reconnectRetryRecords: [
|
||||
String: PrivateMediaReconnectRetryRecord
|
||||
] = [:]
|
||||
@@ -1099,10 +1100,14 @@ final class ChatMediaTransferCoordinator {
|
||||
return
|
||||
}
|
||||
|
||||
let generation = deletionGeneration
|
||||
context.persistDeletedPrivateMedia(
|
||||
messageIDs: [messageID]
|
||||
) { [weak self] persisted in
|
||||
guard let self else { return }
|
||||
guard let self,
|
||||
self.deletionGeneration == generation else {
|
||||
return
|
||||
}
|
||||
guard persisted else {
|
||||
SecureLogger.error(
|
||||
"Refusing to delete private media without a durable tombstone id=\(messageID.prefix(12))…",
|
||||
@@ -1191,6 +1196,7 @@ final class ChatMediaTransferCoordinator {
|
||||
/// is the last filesystem mutation before the transaction can complete.
|
||||
func resetForPanic() {
|
||||
imagePreparationBarrier.invalidateAndWait()
|
||||
deletionGeneration &+= 1
|
||||
peersResolvingReconnectRetry.removeAll(keepingCapacity: false)
|
||||
for task in reconnectRetryExpiryTasks.values {
|
||||
task.cancel()
|
||||
|
||||
@@ -390,6 +390,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
PendingPrivateChatClear
|
||||
] = []
|
||||
@MainActor private var privateChatClearInFlight = false
|
||||
@MainActor private var privateChatClearGeneration: UInt64 = 0
|
||||
private var pendingLegacyPrivateMediaConsents: [PendingLegacyPrivateMediaConsent] = []
|
||||
|
||||
private func performDeliveryUpdate(_ update: @escaping @MainActor (ChatDeliveryCoordinator) -> Void) {
|
||||
@@ -718,8 +719,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
}
|
||||
privateChatClearInFlight = true
|
||||
let request = queuedPrivateChatClears.removeFirst()
|
||||
performPrivateChatClear(request) { [weak self] in
|
||||
guard let self else { return }
|
||||
let generation = privateChatClearGeneration
|
||||
performPrivateChatClear(
|
||||
request,
|
||||
generation: generation
|
||||
) { [weak self] in
|
||||
guard let self,
|
||||
self.privateChatClearGeneration == generation else {
|
||||
return
|
||||
}
|
||||
self.privateChatClearInFlight = false
|
||||
self.startNextPrivateChatClearIfNeeded()
|
||||
}
|
||||
@@ -728,8 +736,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
@MainActor
|
||||
private func performPrivateChatClear(
|
||||
_ request: PendingPrivateChatClear,
|
||||
generation: UInt64,
|
||||
completion: @escaping @MainActor () -> Void
|
||||
) {
|
||||
guard privateChatClearGeneration == generation else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
let peerID = request.peerID
|
||||
let selectedConversationID = request.sourceConversationID
|
||||
let messagesToClear = request.messages
|
||||
@@ -816,6 +829,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
persisted: Bool,
|
||||
durableStableIDs: Set<String>
|
||||
) {
|
||||
guard privateChatClearGeneration == generation else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
guard persisted else {
|
||||
SecureLogger.error(
|
||||
"Refusing to clear private chat without durable media tombstones peer=\(peerID.id.prefix(8))…",
|
||||
@@ -1537,6 +1554,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
// handles before clearing state or removing the media directory.
|
||||
mediaTransferCoordinator.resetForPanic()
|
||||
liveVoiceCoordinator.resetForPanic()
|
||||
privateChatClearGeneration &+= 1
|
||||
queuedPrivateChatClears.removeAll(keepingCapacity: false)
|
||||
privateChatClearInFlight = false
|
||||
|
||||
// Deny and release any clear-media confirmations before identities,
|
||||
// message state, and local files are wiped.
|
||||
|
||||
Reference in New Issue
Block a user