diff --git a/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift b/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift index f41dd30e9..9775bbdad 100644 --- a/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift +++ b/Vault/Sources/CryptoEngine/Helpers/Data+Helpers.swift @@ -9,9 +9,12 @@ extension UInt64 { extension Data { /// An unsafe operation to cast the bytes of this data to the provided type. + /// + /// `Data` gives no alignment guarantee for its backing buffer — a slice can start at any byte + /// offset — so the load must tolerate misalignment. An aligned `load` traps at runtime instead. func asType(_: T.Type) -> T { withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in - ptr.load(as: T.self) + ptr.loadUnaligned(as: T.self) } } diff --git a/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift b/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift index 96f00de91..41248d46d 100644 --- a/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift +++ b/Vault/Sources/VaultiOS/Views/Detail/VaultDetailEncryptionEditView.swift @@ -27,7 +27,7 @@ struct VaultDetailEncryptionEditView: View { ) { self.title = title self.description = description - encryptionIsEnabled = encryptionInitiallyEnabled + _encryptionIsEnabled = State(initialValue: encryptionInitiallyEnabled) self.didSetNewEncryptionPassword = didSetNewEncryptionPassword self.didRemoveEncryption = didRemoveEncryption } diff --git a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift index d801400fb..f57fa4fe4 100644 --- a/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift +++ b/Vault/Sources/VaultiOS/Views/Feed/VaultItemFeedView.swift @@ -231,7 +231,9 @@ public struct VaultItemFeedView< dataModel.items.move(fromOffsets: [move.fromIndex], toOffset: move.toIndex) } Task { - try await dataModel.reorder(items: [dropItem], to: move.reorderingPosition) + // The list has already moved optimistically. A failed persist leaves the + // stored order untouched and the next reload restores the on-disk order. + try? await dataModel.reorder(items: [dropItem], to: move.reorderingPosition) } return true } diff --git a/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift b/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift index 9c3f64ce2..e3c1fde5d 100644 --- a/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift +++ b/Vault/Sources/VaultiOS/Views/General/AsyncButton.swift @@ -38,7 +38,10 @@ struct AsyncButton: View { isDisabled = false showProgressView = false } - try await action() + + // Errors belong to the action: call sites either catch them inline or surface them + // through their own view model state. The button only drives loading and disabled state. + try? await action() } }, label: { diff --git a/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift b/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift index 0dc09ac33..a993a69f1 100644 --- a/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift +++ b/Vault/Sources/VaultiOS/Views/Previews/VaultItemOnTapDecoratorViewGenerator.swift @@ -20,7 +20,11 @@ public struct VaultItemOnTapDecoratorViewGenerator< behaviour: VaultItemViewBehaviour, ) -> some View { Button { - Task { try await onTap(metadata.id) } + Task { + // Fail closed. A thrown error means the tap action did not complete — most often a + // failed authentication gating a copy — so the tap must do nothing at all. + try? await onTap(metadata.id) + } } label: { generator.makeVaultPreviewView(item: item, metadata: metadata, behaviour: behaviour) } diff --git a/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift b/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift index faed2a318..74edccc3e 100644 --- a/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift +++ b/Vault/Sources/VaultiOS/Views/Previews/VaultItemPreviewViewGenerator.swift @@ -20,11 +20,3 @@ public protocol VaultItemPreviewSceneResponder { func scenePhaseDidChange(to scene: ScenePhase) func didAppear() } - -// MARK: - Mock - -extension VaultItemPreviewViewGeneratorMock: VaultItemCopyActionHandler { - public func textToCopyForVaultItem(id _: Identifier) -> VaultTextCopyAction? { - nil - } -} diff --git a/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift b/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift index 62a50b853..cc6631e8a 100644 --- a/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift +++ b/Vault/Tests/FoundationExtensionsTests/PendingValueTests.swift @@ -166,7 +166,8 @@ struct PendingValueTests { let waitForTaskStart = Pending.signal() Task.detached(priority: .high) { await waitForTaskStart.fulfill() - _ = try await sut.wait() + // The wait is torn down by `sut.cancel()` at the end of the test, so a throw is expected. + _ = try? await sut.wait() } try await waitForTaskStart.wait() diff --git a/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift b/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift index 9a760a520..54a71c8ce 100644 --- a/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift +++ b/Vault/Tests/VaultCoreTests/Clock/IntervalTimerTests.swift @@ -21,10 +21,14 @@ enum IntervalTimerTests { let pendingStart = Pending.signal() let completed = Pending.signal() Task { - try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in - await pendingStart.fulfill() - try await sut.wait(for: 0.1) - await completed.fulfill() + do { + try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in + await pendingStart.fulfill() + try await sut.wait(for: 0.1) + await completed.fulfill() + } + } catch { + Issue.record(error) } } @@ -49,10 +53,14 @@ enum IntervalTimerTests { let pendingStart = Pending.signal() let completed = Pending.signal() Task { - try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in - await pendingStart.fulfill() - try await sut.wait(for: 0.1, tolerance: 0.1) - await completed.fulfill() + do { + try await confirmation(timeout: .seconds(1), expectedCount: 0) { _ in + await pendingStart.fulfill() + try await sut.wait(for: 0.1, tolerance: 0.1) + await completed.fulfill() + } + } catch { + Issue.record(error) } }