Skip to content

Commit 1db75ca

Browse files
committed
Improve backup error handling & warnings
1 parent 7de5a19 commit 1db75ca

File tree

14 files changed

+202
-111
lines changed

14 files changed

+202
-111
lines changed

android/app/src/main/java/org/bitcoinppl/cove/AppManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class AppManager private constructor() : FfiReconcile {
180180

181181
fun findTapSignerWallet(ts: TapSigner): WalletMetadata? = rust.findTapSignerWallet(ts)
182182

183+
@Throws(KeychainException::class)
183184
fun getTapSignerBackup(ts: TapSigner): ByteArray? = rust.getTapSignerBackup(ts)
184185

185186
fun saveTapSignerBackup(ts: TapSigner, backup: ByteArray): Boolean =

android/app/src/main/java/org/bitcoinppl/cove/flows/SelectedWalletFlow/WalletMoreOptionsSheet.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import org.bitcoinppl.cove.AppSheetState
3434
import org.bitcoinppl.cove.TaggedItem
3535
import org.bitcoinppl.cove.WalletManager
3636
import org.bitcoinppl.cove.flows.TapSignerFlow.rememberBackupExportLauncher
37+
import org.bitcoinppl.cove_core.AppAlertState
3738
import org.bitcoinppl.cove_core.AfterPinAction
3839
import org.bitcoinppl.cove_core.CoinControlRoute
3940
import org.bitcoinppl.cove_core.HardwareWalletMetadata
@@ -168,7 +169,7 @@ fun WalletMoreOptionsSheet(
168169
// launcher for creating backup file
169170
val createBackupLauncher =
170171
rememberBackupExportLauncher(app) {
171-
app.getTapSignerBackup(tapSigner)
172+
app.getTapSignerBackup(tapSigner) // throws KeychainException
172173
?: throw IllegalStateException("Backup not available")
173174
}
174175

@@ -195,19 +196,26 @@ fun WalletMoreOptionsSheet(
195196
label = "Download Backup",
196197
onClick = {
197198
onDismiss()
198-
// check if backup already exists in cache
199-
val backup = app.getTapSignerBackup(tapSigner)
200-
if (backup != null) {
201-
val fileName = "${tapSigner.identFileNamePrefix()}_backup.txt"
202-
createBackupLauncher.launch(fileName)
203-
} else {
204-
// open TapSigner flow with Backup action
205-
val route =
206-
TapSignerRoute.EnterPin(
207-
tapSigner = tapSigner,
208-
action = AfterPinAction.Backup,
199+
try {
200+
val backup = app.getTapSignerBackup(tapSigner)
201+
if (backup != null) {
202+
val fileName = "${tapSigner.identFileNamePrefix()}_backup.txt"
203+
createBackupLauncher.launch(fileName)
204+
} else {
205+
val route =
206+
TapSignerRoute.EnterPin(
207+
tapSigner = tapSigner,
208+
action = AfterPinAction.Backup,
209+
)
210+
app.sheetState = TaggedItem(AppSheetState.TapSigner(route))
211+
}
212+
} catch (e: Exception) {
213+
app.alertState = TaggedItem(
214+
AppAlertState.General(
215+
title = "Error",
216+
message = "Failed to retrieve backup: ${e.message ?: "Unknown error"}",
209217
)
210-
app.sheetState = TaggedItem(AppSheetState.TapSigner(route))
218+
)
211219
}
212220
},
213221
)

android/app/src/main/java/org/bitcoinppl/cove/flows/SettingsFlow/BackupExportScreen.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ fun BackupExportScreen(
8686
var isExporting by remember { mutableStateOf(false) }
8787
var showConfirmDialog by remember { mutableStateOf(false) }
8888
var errorMessage by remember { mutableStateOf<String?>(null) }
89+
var warningMessage by remember { mutableStateOf<String?>(null) }
8990
var pendingResult by remember { mutableStateOf<BackupResult?>(null) }
9091

9192
DisposableEffect(Unit) {
@@ -268,6 +269,9 @@ fun BackupExportScreen(
268269
val result = withContext(Dispatchers.IO) {
269270
backupManager.export(password)
270271
}
272+
if (result.warnings.isNotEmpty()) {
273+
warningMessage = "Some data could not be exported:\n" + result.warnings.joinToString("\n")
274+
}
271275
pendingResult = result
272276
exportLauncher.launch(result.filename)
273277
} catch (e: CancellationException) {
@@ -302,6 +306,19 @@ fun BackupExportScreen(
302306
},
303307
)
304308
}
309+
310+
warningMessage?.let { msg ->
311+
AlertDialog(
312+
onDismissRequest = { warningMessage = null },
313+
title = { Text("Export Warnings") },
314+
text = { Text(msg) },
315+
confirmButton = {
316+
TextButton(onClick = { warningMessage = null }) {
317+
Text("OK")
318+
}
319+
},
320+
)
321+
}
305322
}
306323

307324
private fun backupExportErrorMessage(e: Exception): String = when (e) {

android/app/src/main/java/org/bitcoinppl/cove/flows/SettingsFlow/BackupImportScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ fun BackupImportScreen(
106106
scope.launch {
107107
try {
108108
val (bytes, name) = withContext(Dispatchers.IO) {
109+
val docFile = DocumentFile.fromSingleUri(context, uri)
110+
val fileSize = docFile?.length() ?: 0
111+
if (fileSize > 50_000_000) {
112+
throw BackupException.InvalidFormat()
113+
}
114+
109115
val bytes = context.contentResolver.openInputStream(uri)?.readBytes()
110116
?: throw java.io.IOException("Failed to read file")
111117

android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25307,6 +25307,8 @@ data class BackupResult (
2530725307
var `data`: kotlin.ByteArray
2530825308
,
2530925309
var `filename`: kotlin.String
25310+
,
25311+
var `warnings`: List<kotlin.String>
2531025312

2531125313
){
2531225314

@@ -25325,17 +25327,20 @@ public object FfiConverterTypeBackupResult: FfiConverterRustBuffer<BackupResult>
2532525327
return BackupResult(
2532625328
FfiConverterByteArray.read(buf),
2532725329
FfiConverterString.read(buf),
25330+
FfiConverterSequenceString.read(buf),
2532825331
)
2532925332
}
2533025333

2533125334
override fun allocationSize(value: BackupResult) = (
2533225335
FfiConverterByteArray.allocationSize(value.`data`) +
25333-
FfiConverterString.allocationSize(value.`filename`)
25336+
FfiConverterString.allocationSize(value.`filename`) +
25337+
FfiConverterSequenceString.allocationSize(value.`warnings`)
2533425338
)
2533525339

2533625340
override fun write(value: BackupResult, buf: ByteBuffer) {
2533725341
FfiConverterByteArray.write(value.`data`, buf)
2533825342
FfiConverterString.write(value.`filename`, buf)
25343+
FfiConverterSequenceString.write(value.`warnings`, buf)
2533925344
}
2534025345
}
2534125346

@@ -29483,6 +29488,14 @@ sealed class BackupException: kotlin.Exception() {
2948329488
get() = "v1=${ v1 }"
2948429489
}
2948529490

29491+
class UnsupportedPayloadVersion(
29492+
29493+
val v1: kotlin.UInt
29494+
) : BackupException() {
29495+
override val message
29496+
get() = "v1=${ v1 }"
29497+
}
29498+
2948629499
class Truncated(
2948729500
) : BackupException() {
2948829501
override val message
@@ -29587,29 +29600,32 @@ public object FfiConverterTypeBackupError : FfiConverterRustBuffer<BackupExcepti
2958729600
4 -> BackupException.UnsupportedVersion(
2958829601
FfiConverterUInt.read(buf),
2958929602
)
29590-
5 -> BackupException.Truncated()
29591-
6 -> BackupException.Encryption(
29603+
5 -> BackupException.UnsupportedPayloadVersion(
29604+
FfiConverterUInt.read(buf),
29605+
)
29606+
6 -> BackupException.Truncated()
29607+
7 -> BackupException.Encryption(
2959229608
FfiConverterString.read(buf),
2959329609
)
29594-
7 -> BackupException.Serialization(
29610+
8 -> BackupException.Serialization(
2959529611
FfiConverterString.read(buf),
2959629612
)
29597-
8 -> BackupException.Deserialization(
29613+
9 -> BackupException.Deserialization(
2959829614
FfiConverterString.read(buf),
2959929615
)
29600-
9 -> BackupException.Gather(
29616+
10 -> BackupException.Gather(
2960129617
FfiConverterString.read(buf),
2960229618
)
29603-
10 -> BackupException.Restore(
29619+
11 -> BackupException.Restore(
2960429620
FfiConverterString.read(buf),
2960529621
)
29606-
11 -> BackupException.Keychain(
29622+
12 -> BackupException.Keychain(
2960729623
FfiConverterString.read(buf),
2960829624
)
29609-
12 -> BackupException.Database(
29625+
13 -> BackupException.Database(
2961029626
FfiConverterString.read(buf),
2961129627
)
29612-
13 -> BackupException.Decompression(
29628+
14 -> BackupException.Decompression(
2961329629
FfiConverterString.read(buf),
2961429630
)
2961529631
else -> throw RuntimeException("invalid error enum value, something is very wrong!!")
@@ -29635,6 +29651,11 @@ public object FfiConverterTypeBackupError : FfiConverterRustBuffer<BackupExcepti
2963529651
4UL
2963629652
+ FfiConverterUInt.allocationSize(value.v1)
2963729653
)
29654+
is BackupException.UnsupportedPayloadVersion -> (
29655+
// Add the size for the Int that specifies the variant plus the size needed for all fields
29656+
4UL
29657+
+ FfiConverterUInt.allocationSize(value.v1)
29658+
)
2963829659
is BackupException.Truncated -> (
2963929660
// Add the size for the Int that specifies the variant plus the size needed for all fields
2964029661
4UL
@@ -29701,47 +29722,52 @@ public object FfiConverterTypeBackupError : FfiConverterRustBuffer<BackupExcepti
2970129722
FfiConverterUInt.write(value.v1, buf)
2970229723
Unit
2970329724
}
29704-
is BackupException.Truncated -> {
29725+
is BackupException.UnsupportedPayloadVersion -> {
2970529726
buf.putInt(5)
29727+
FfiConverterUInt.write(value.v1, buf)
2970629728
Unit
2970729729
}
29708-
is BackupException.Encryption -> {
29730+
is BackupException.Truncated -> {
2970929731
buf.putInt(6)
29732+
Unit
29733+
}
29734+
is BackupException.Encryption -> {
29735+
buf.putInt(7)
2971029736
FfiConverterString.write(value.v1, buf)
2971129737
Unit
2971229738
}
2971329739
is BackupException.Serialization -> {
29714-
buf.putInt(7)
29740+
buf.putInt(8)
2971529741
FfiConverterString.write(value.v1, buf)
2971629742
Unit
2971729743
}
2971829744
is BackupException.Deserialization -> {
29719-
buf.putInt(8)
29745+
buf.putInt(9)
2972029746
FfiConverterString.write(value.v1, buf)
2972129747
Unit
2972229748
}
2972329749
is BackupException.Gather -> {
29724-
buf.putInt(9)
29750+
buf.putInt(10)
2972529751
FfiConverterString.write(value.v1, buf)
2972629752
Unit
2972729753
}
2972829754
is BackupException.Restore -> {
29729-
buf.putInt(10)
29755+
buf.putInt(11)
2973029756
FfiConverterString.write(value.v1, buf)
2973129757
Unit
2973229758
}
2973329759
is BackupException.Keychain -> {
29734-
buf.putInt(11)
29760+
buf.putInt(12)
2973529761
FfiConverterString.write(value.v1, buf)
2973629762
Unit
2973729763
}
2973829764
is BackupException.Database -> {
29739-
buf.putInt(12)
29765+
buf.putInt(13)
2974029766
FfiConverterString.write(value.v1, buf)
2974129767
Unit
2974229768
}
2974329769
is BackupException.Decompression -> {
29744-
buf.putInt(13)
29770+
buf.putInt(14)
2974529771
FfiConverterString.write(value.v1, buf)
2974629772
Unit
2974729773
}

ios/Cove/AppManager.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ private let walletModeChangeDelayMs = 250
123123
rust.findTapSignerWallet(tapSigner: ts)
124124
}
125125

126-
public func getTapSignerBackup(_ ts: TapSigner) -> Data? {
127-
rust.getTapSignerBackup(tapSigner: ts)
126+
public func getTapSignerBackup(_ ts: TapSigner) throws -> Data? {
127+
try rust.getTapSignerBackup(tapSigner: ts)
128128
}
129129

130130
public func saveTapSignerBackup(_ ts: TapSigner, _ backup: Data) -> Bool {

ios/Cove/Flows/SelectedWalletFlow/MoreInfoPopover.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ struct MoreInfoPopover: View {
7171

7272
@ViewBuilder
7373
func DownloadBackupButton(_ t: TapSigner) -> some View {
74-
if let backup = app.getTapSignerBackup(t) {
74+
if let backup = try? app.getTapSignerBackup(t) {
7575
let content = hexEncode(bytes: backup)
7676
let prefix = t.identFileNamePrefix()
7777
let filename = "\(prefix)_backup.txt"

ios/Cove/Flows/SettingsFlow/BackupExportView.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ struct BackupExportView: View {
88
@State private var isExporting = false
99
@State private var showConfirmation = false
1010
@State private var errorMessage: String? = nil
11+
@State private var warningMessage: String? = nil
1112
@State private var exportTask: Task<Void, Never>? = nil
12-
private let tempExportURL = FileManager.default.temporaryDirectory.appendingPathComponent("cove-backup-export.covb")
1313
@State private var tempFileURL: URL? = nil
1414
@State private var shareSheetPresented = false
1515

@@ -89,12 +89,14 @@ struct BackupExportView: View {
8989
.onDisappear {
9090
exportTask?.cancel()
9191
password = ""
92-
do {
93-
try FileManager.default.removeItem(at: tempExportURL)
94-
} catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {
95-
// file doesn't exist, nothing to clean up
96-
} catch {
97-
Log.error("Failed to remove temporary backup file: \(error)")
92+
if let url = tempFileURL {
93+
do {
94+
try FileManager.default.removeItem(at: url)
95+
} catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {
96+
// file doesn't exist, nothing to clean up
97+
} catch {
98+
Log.error("Failed to remove temporary backup file: \(error)")
99+
}
98100
}
99101
tempFileURL = nil
100102
}
@@ -112,6 +114,14 @@ struct BackupExportView: View {
112114
} message: {
113115
Text(errorMessage ?? "Unknown error")
114116
}
117+
.alert("Export Warnings", isPresented: .init(
118+
get: { warningMessage != nil },
119+
set: { if !$0 { warningMessage = nil } }
120+
)) {
121+
Button("OK") { warningMessage = nil }
122+
} message: {
123+
Text(warningMessage ?? "")
124+
}
115125
}
116126

117127
private func exportBackup() {
@@ -120,12 +130,19 @@ struct BackupExportView: View {
120130
do {
121131
let result = try await backupManager.export(password: password)
122132

123-
let fileURL = tempExportURL
133+
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(result.filename)
124134
try result.data.write(to: fileURL, options: [.atomic, .completeFileProtection])
125135

136+
let warnings = result.warnings
137+
126138
await MainActor.run {
127139
tempFileURL = fileURL
128140
isExporting = false
141+
142+
if !warnings.isEmpty {
143+
warningMessage = "Some data could not be exported:\n" + warnings.joined(separator: "\n")
144+
}
145+
129146
shareSheetPresented = true
130147
ShareSheet.present(for: fileURL) { completed in
131148
shareSheetPresented = false

ios/Cove/Flows/SettingsFlow/BackupImportView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ struct BackupImportView: View {
147147
defer { url.stopAccessingSecurityScopedResource() }
148148

149149
do {
150+
let attrs = try url.resourceValues(forKeys: [.fileSizeKey])
151+
if let size = attrs.fileSize, size > 50_000_000 {
152+
throw BackupError.InvalidFormat
153+
}
154+
150155
let data = try Data(contentsOf: url)
151156
try backupManager.validateFormat(data: data)
152157

0 commit comments

Comments
 (0)