From 4d44e26d5e8ea622da26e93a2c4ad1d9c44b19ad Mon Sep 17 00:00:00 2001 From: kvmgithub Date: Fri, 8 May 2026 19:59:55 +0200 Subject: [PATCH] fix: harden Android sync and download flows Use shared app database paths across JS, Kotlin, and workers so sync, account, and download state stay in one SQLite store. Move SAF deletion into the Android bridge so downloaded books, optional Smart Audiobook Player covers, and empty per-book folders are cleaned up without deleting unrelated files. --- .gitignore | 5 +- app.config.js | 3 +- modules/expo-rust-bridge/android/build.gradle | 1 + .../android/src/main/AndroidManifest.xml | 21 +- .../java/expo/modules/rustbridge/AppPaths.kt | 12 + .../rustbridge/DownloadActionReceiver.kt | 3 +- .../rustbridge/DownloadOrchestrator.kt | 13 +- .../modules/rustbridge/DownloadService.kt | 4 +- .../rustbridge/ExpoRustBridgeModule.kt | 498 ++- .../rustbridge/tasks/BackgroundTaskManager.kt | 4 +- .../rustbridge/tasks/DownloadWorker.kt | 21 + .../rustbridge/workers/LibrarySyncWorker.kt | 3 +- .../rustbridge/workers/TokenRefreshWorker.kt | 13 +- modules/expo-rust-bridge/index.ts | 58 +- native/rust-core/Cargo.lock | 3472 +++++++++++++++++ native/rust-core/examples/oauth_test.rs | 43 +- native/rust-core/src/api/auth.rs | 100 +- native/rust-core/src/api/client.rs | 107 +- native/rust-core/src/api/content.rs | 72 +- native/rust-core/src/api/license.rs | 189 +- native/rust-core/src/crypto/aax.rs | 38 +- .../src/download/persistent_manager.rs | 46 + native/rust-core/src/jni_bridge.rs | 392 +- native/rust-core/src/storage/accounts.rs | 61 +- .../test_fixtures/registration_response.json | 84 + .../tests/download_manager_integration.rs | 185 +- package.json | 1 + plugins/withDownloadService.js | 132 +- scripts/build-rust-android.sh | 48 +- src/screens/LibraryScreen.tsx | 76 +- src/screens/LoginScreen.tsx | 18 +- src/screens/SettingsScreen.tsx | 31 +- src/screens/SimpleAccountScreen.tsx | 87 +- src/screens/TaskDebugScreen.tsx | 45 +- src/utils/appPaths.ts | 19 + 35 files changed, 5202 insertions(+), 703 deletions(-) create mode 100644 modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/AppPaths.kt create mode 100644 native/rust-core/Cargo.lock create mode 100644 native/rust-core/test_fixtures/registration_response.json create mode 100644 src/utils/appPaths.ts diff --git a/.gitignore b/.gitignore index 821d68e..3811f4e 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,6 @@ modules/**/ios/build/ # Rust native/rust-core/target/ -native/rust-core/Cargo.lock *.so *.dylib *.dll @@ -86,7 +85,9 @@ tests/fixtures/test_account.json .env.test *.aax *.aaxc -/native/rust-core/test_fixtures/ +/native/rust-core/test_fixtures/* +!/native/rust-core/test_fixtures/ +!/native/rust-core/test_fixtures/registration_response.json # Development/testing tools test-workers.sh diff --git a/app.config.js b/app.config.js index 448258c..f89b9cf 100644 --- a/app.config.js +++ b/app.config.js @@ -35,7 +35,8 @@ export default { permissions: [ "POST_NOTIFICATIONS", "FOREGROUND_SERVICE", - "FOREGROUND_SERVICE_DATA_SYNC" + "FOREGROUND_SERVICE_DATA_SYNC", + "RECEIVE_BOOT_COMPLETED" ] }, web: { diff --git a/modules/expo-rust-bridge/android/build.gradle b/modules/expo-rust-bridge/android/build.gradle index ac643aa..02dcc33 100644 --- a/modules/expo-rust-bridge/android/build.gradle +++ b/modules/expo-rust-bridge/android/build.gradle @@ -19,6 +19,7 @@ repositories { } android { + namespace 'expo.modules.rustbridge' compileSdkVersion 34 defaultConfig { diff --git a/modules/expo-rust-bridge/android/src/main/AndroidManifest.xml b/modules/expo-rust-bridge/android/src/main/AndroidManifest.xml index 4d587a7..ca2d065 100644 --- a/modules/expo-rust-bridge/android/src/main/AndroidManifest.xml +++ b/modules/expo-rust-bridge/android/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + + @@ -8,6 +9,12 @@ android:exported="false" android:foregroundServiceType="dataSync" /> + + + + + + + + + + + diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/AppPaths.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/AppPaths.kt new file mode 100644 index 0000000..40025e5 --- /dev/null +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/AppPaths.kt @@ -0,0 +1,12 @@ +package expo.modules.rustbridge + +import android.content.Context +import java.io.File + +object AppPaths { + private const val DATABASE_FILE_NAME = "audible.db" + + fun databasePath(context: Context): String { + return File(context.filesDir, DATABASE_FILE_NAME).absolutePath + } +} diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadActionReceiver.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadActionReceiver.kt index f31b4cf..e83805e 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadActionReceiver.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadActionReceiver.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.util.Log import org.json.JSONArray import org.json.JSONObject -import java.io.File /** * BroadcastReceiver for handling notification action buttons @@ -32,7 +31,7 @@ class DownloadActionReceiver : BroadcastReceiver() { return } - val dbPath = File(context.cacheDir, "audible.db").absolutePath + val dbPath = AppPaths.databasePath(context) Log.d(TAG, "Received action: ${intent.action} for ASIN: $asin") diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadOrchestrator.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadOrchestrator.kt index 7fdab4d..52ec75f 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadOrchestrator.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadOrchestrator.kt @@ -486,7 +486,7 @@ class DownloadOrchestrator( progressCallback?.invoke(asin, "copying", 0.0, 0, 0) // Copy to final destination - copyToFinalDestination(asin, title, decryptedCachePath, outputDirectory, coverArtPath) + val finalPath = copyToFinalDestination(asin, title, decryptedCachePath, outputDirectory, coverArtPath) // Cleanup encrypted file File(encryptedPath).delete() @@ -494,8 +494,8 @@ class DownloadOrchestrator( // Cleanup cover art temp file coverArtPath?.let { File(it).delete() } - // Mark as completed in DB - resolvedTaskId?.let { updateTaskStatusInDb(it, "completed") } + // Mark as completed in DB with the final SAF/file path + resolvedTaskId?.let { updateTaskStatusInDb(it, "completed", finalPath) } } catch (e: Exception) { Log.e(TAG, "Conversion failed for $asin", e) @@ -514,7 +514,7 @@ class DownloadOrchestrator( decryptedCachePath: String, outputDirectory: String, coverArtPath: String? - ) = withContext(Dispatchers.IO) { + ): String = withContext(Dispatchers.IO) { val cachedFile = File(decryptedCachePath) var finalPath = decryptedCachePath @@ -594,6 +594,8 @@ class DownloadOrchestrator( clearManuallyPaused(asin) completionCallback?.invoke(asin, title, finalPath) + + finalPath } /** @@ -1239,12 +1241,13 @@ class DownloadOrchestrator( /** * Update task status in the database via JNI */ - private fun updateTaskStatusInDb(taskId: String, status: String) { + private fun updateTaskStatusInDb(taskId: String, status: String, outputPath: String? = null) { try { val params = JSONObject().apply { put("db_path", dbPath) put("task_id", taskId) put("status", status) + outputPath?.let { put("output_path", it) } } ExpoRustBridgeModule.nativeUpdateDownloadTaskStatus(params.toString()) Log.d(TAG, "Updated task $taskId status to $status in DB") diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadService.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadService.kt index b8dcc54..51a4ba2 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadService.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/DownloadService.kt @@ -8,7 +8,6 @@ import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import org.json.JSONObject -import java.io.File import kotlinx.coroutines.* /** @@ -145,8 +144,7 @@ class DownloadService : Service() { Log.d(TAG, "Service created") // Get database path from intent or use default - val cacheDir = applicationContext.cacheDir - dbPath = File(cacheDir, "audible.db").absolutePath + dbPath = AppPaths.databasePath(applicationContext) orchestrator = DownloadOrchestrator(applicationContext, dbPath) notificationManager = DownloadNotificationManager(applicationContext) diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/ExpoRustBridgeModule.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/ExpoRustBridgeModule.kt index b141f68..cca4d94 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/ExpoRustBridgeModule.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/ExpoRustBridgeModule.kt @@ -7,7 +7,7 @@ import org.json.JSONObject import org.json.JSONArray import android.content.Context import android.net.Uri -import androidx.documentfile.provider.DocumentFile +import android.provider.DocumentsContract import expo.modules.rustbridge.workers.WorkerScheduler import java.io.File @@ -509,8 +509,7 @@ class ExpoRustBridgeModule : Module() { AsyncFunction("enqueueDownloadNew") { asin: String, title: String, author: String?, accountJson: String, outputDirectory: String, quality: String -> try { val context = appContext.reactContext ?: throw Exception("Context not available") - val cacheDir = context.cacheDir - val dbPath = File(cacheDir, "audible.db").absolutePath + val dbPath = AppPaths.databasePath(context) // Use DownloadService for downloads DownloadService.enqueueBook( @@ -631,6 +630,25 @@ class ExpoRustBridgeModule : Module() { } } + /** + * Delete account from SQLite database. + * + * @param dbPath Database path + * @param accountId Account identifier + */ + AsyncFunction("deleteAccount") { dbPath: String, accountId: String -> + try { + val params = JSONObject().apply { + put("db_path", dbPath) + put("account_id", accountId) + } + val result = nativeDeleteAccount(params.toString()) + parseJsonResponse(result) + } catch (e: Exception) { + mapOf("success" to false, "error" to e.message) + } + } + /** * Enable automatic library sync. * @@ -873,13 +891,54 @@ class ExpoRustBridgeModule : Module() { */ AsyncFunction("clearBookDownloadState") { dbPath: String, asin: String, deleteFile: Boolean -> try { + val context = appContext.reactContext ?: throw Exception("Context not available") + var cleanupResult = DeleteCleanupResult(fileDeleted = false) + var deleteError: String? = null + var rustShouldDeleteFile = deleteFile + + if (deleteFile) { + val filePath = getBookFilePathForDeletion(dbPath, asin) + if (!filePath.isNullOrBlank()) { + rustShouldDeleteFile = false + try { + cleanupResult = deleteDownloadedFile(context, filePath) + if (!cleanupResult.fileDeleted) { + deleteError = "File deletion returned false" + } + } catch (e: Exception) { + deleteError = e.message ?: e.javaClass.simpleName + android.util.Log.w("ExpoRustBridge", "Failed to delete downloaded file $filePath", e) + } + } + } + val params = JSONObject().apply { put("db_path", dbPath) put("asin", asin) - put("delete_file", deleteFile) + put("delete_file", rustShouldDeleteFile) } val result = nativeClearBookDownloadState(params.toString()) - parseJsonResponse(result) + val parsed = parseJsonResponse(result) + + if (deleteFile && !rustShouldDeleteFile && parsed["success"] == true) { + val data = mutableMapOf() + (parsed["data"] as? Map<*, *>)?.forEach { (key, value) -> + if (key is String) { + data[key] = value + } + } + data["file_deleted"] = cleanupResult.fileDeleted + data["deleted_path"] = cleanupResult.deletedPath + data["cover_deleted"] = cleanupResult.coverDeleted + data["book_folder_deleted"] = cleanupResult.bookFolderDeleted + data["author_folder_deleted"] = cleanupResult.authorFolderDeleted + cleanupResult.cleanupError?.let { data["cleanup_error"] = it } + deleteError?.let { data["delete_error"] = it } + + mapOf("success" to true, "data" to data) + } else { + parsed + } } catch (e: Exception) { mapOf("success" to false, "error" to e.message) } @@ -922,20 +981,17 @@ class ExpoRustBridgeModule : Module() { * @param audioFilePath Path to the audio file (cover will be saved in same directory) */ AsyncFunction("createCoverArtFile") { asin: String, coverUrl: String, audioFilePath: String -> + var coverFile: File? = null + var originalBitmap: android.graphics.Bitmap? = null + var resizedBitmap: android.graphics.Bitmap? = null + try { val context = appContext.reactContext ?: throw Exception("Context not available") - // Get the directory containing the audio file - val audioUri = Uri.parse(if (audioFilePath.startsWith("content://")) audioFilePath else "file://$audioFilePath") - val audioFile = DocumentFile.fromSingleUri(context, audioUri) - ?: throw Exception("Could not access audio file") - - val targetDir = audioFile.parentFile - ?: throw Exception("Could not access parent directory") - // Download cover image to cache val cacheDir = context.cacheDir - val coverFile = File(cacheDir, "cover_$asin.jpg") + val cacheCoverFile = File(cacheDir, "cover_$asin.jpg") + coverFile = cacheCoverFile // Download the cover image val url = java.net.URL(coverUrl.replace(Regex("_SL\\d+_"), "_SL500_")) @@ -948,49 +1004,42 @@ class ExpoRustBridgeModule : Module() { throw Exception("Failed to download cover image: HTTP ${connection.responseCode}") } - coverFile.outputStream().use { output -> + cacheCoverFile.outputStream().use { output -> connection.inputStream.use { input -> input.copyTo(output) } } // Load and resize cover image - val originalBitmap = android.graphics.BitmapFactory.decodeFile(coverFile.absolutePath) + val original = android.graphics.BitmapFactory.decodeFile(cacheCoverFile.absolutePath) ?: throw Exception("Failed to decode cover image") + originalBitmap = original - val resizedBitmap = android.graphics.Bitmap.createScaledBitmap( - originalBitmap, + val resized = android.graphics.Bitmap.createScaledBitmap( + original, 500, 500, true ) + resizedBitmap = resized - // Delete existing EmbeddedCover.jpg if present - targetDir.findFile("EmbeddedCover.jpg")?.delete() - - // Create new file - val embeddedCover = targetDir.createFile("image/jpeg", "EmbeddedCover.jpg") - ?: throw Exception("Failed to create EmbeddedCover.jpg") - - // Write JPEG - context.contentResolver.openOutputStream(embeddedCover.uri)?.use { outputStream -> - resizedBitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 90, outputStream) - } ?: throw Exception("Failed to open output stream for EmbeddedCover.jpg") - - // Cleanup - originalBitmap.recycle() - resizedBitmap.recycle() - coverFile.delete() + val coverPath = writeEmbeddedCover(context, audioFilePath, resized) mapOf( "success" to true, "data" to mapOf( - "coverPath" to embeddedCover.uri.toString(), + "coverPath" to coverPath, "message" to "Cover art created successfully" ) ) } catch (e: Exception) { mapOf("success" to false, "error" to e.message) + } finally { + if (resizedBitmap != originalBitmap) { + resizedBitmap?.recycle() + } + originalBitmap?.recycle() + coverFile?.delete() } } @@ -1279,6 +1328,384 @@ class ExpoRustBridgeModule : Module() { } } + private data class DeleteCleanupResult( + val fileDeleted: Boolean, + val deletedPath: String? = null, + val coverDeleted: Boolean = false, + val bookFolderDeleted: Boolean = false, + val authorFolderDeleted: Boolean = false, + val cleanupError: String? = null + ) + + private data class TreeDocumentContext( + val treeUri: Uri, + val treeDocumentId: String, + val documentId: String, + val parentDocumentId: String? + ) + + private data class DocumentChild( + val documentId: String, + val displayName: String, + val mimeType: String? + ) + + private fun getBookFilePathForDeletion(dbPath: String, asin: String): String? { + val params = JSONObject().apply { + put("db_path", dbPath) + put("asin", asin) + } + val parsed = parseJsonResponse(nativeGetBookFilePath(params.toString())) + if (parsed["success"] != true) { + return null + } + + val data = parsed["data"] as? Map<*, *> ?: return null + return data["file_path"] as? String + } + + private fun deleteDownloadedFile(context: Context, filePath: String): DeleteCleanupResult { + return if (filePath.startsWith("content://")) { + deleteContentDocument(context, Uri.parse(filePath), filePath) + } else { + deleteFilePath(filePath) + } + } + + private fun deleteFilePath(filePath: String): DeleteCleanupResult { + val file = File(filePath.removePrefix("file://")) + val fileDeleted = file.exists() && file.delete() + + return DeleteCleanupResult( + fileDeleted = fileDeleted, + deletedPath = if (fileDeleted) filePath else null + ) + } + + private fun deleteContentDocument(context: Context, uri: Uri, originalPath: String): DeleteCleanupResult { + val resolver = context.contentResolver + val documentId = try { + DocumentsContract.getDocumentId(uri) + } catch (_: Exception) { + null + } + val treeContext = documentId?.let { findTreeDocumentContext(context, it) } + var fileDeleted = false + + try { + if (DocumentsContract.isDocumentUri(context, uri) && DocumentsContract.deleteDocument(resolver, uri)) { + fileDeleted = true + } + } catch (e: Exception) { + android.util.Log.d("ExpoRustBridge", "Direct document delete failed for $uri: ${e.message}") + } + + if (!fileDeleted && treeContext != null) { + try { + fileDeleted = deleteDocumentById(context, treeContext.treeUri, treeContext.documentId) + } catch (e: Exception) { + android.util.Log.d("ExpoRustBridge", "Tree document delete failed for $uri: ${e.message}") + } + } + + if (!fileDeleted) { + fileDeleted = try { + resolver.delete(uri, null, null) > 0 + } catch (e: Exception) { + android.util.Log.d("ExpoRustBridge", "Content resolver delete failed for $uri: ${e.message}") + false + } + } + + if (!fileDeleted) { + return DeleteCleanupResult(fileDeleted = false) + } + + var coverDeleted = false + var bookFolderDeleted = false + var authorFolderDeleted = false + var cleanupError: String? = null + + if (treeContext?.parentDocumentId != null) { + try { + val cleanup = cleanupDeletedContentDocument(context, treeContext) + coverDeleted = cleanup.coverDeleted + bookFolderDeleted = cleanup.bookFolderDeleted + authorFolderDeleted = cleanup.authorFolderDeleted + } catch (e: Exception) { + cleanupError = e.message ?: e.javaClass.simpleName + android.util.Log.w("ExpoRustBridge", "Downloaded-file cleanup failed for $uri", e) + } + } + + return DeleteCleanupResult( + fileDeleted = true, + deletedPath = originalPath, + coverDeleted = coverDeleted, + bookFolderDeleted = bookFolderDeleted, + authorFolderDeleted = authorFolderDeleted, + cleanupError = cleanupError + ) + } + + private fun cleanupDeletedContentDocument( + context: Context, + treeContext: TreeDocumentContext + ): DeleteCleanupResult { + val parentId = treeContext.parentDocumentId ?: return DeleteCleanupResult(fileDeleted = true) + var coverDeleted = false + var bookFolderDeleted = false + var authorFolderDeleted = false + + val siblingsAfterFileDelete = listDocumentChildren(context, treeContext.treeUri, parentId) + val hasOtherAudioFiles = siblingsAfterFileDelete.any { child -> + child.documentId != treeContext.documentId && isAudioFile(child.displayName, child.mimeType) + } + + if (!hasOtherAudioFiles) { + val cover = siblingsAfterFileDelete.firstOrNull { it.displayName == "EmbeddedCover.jpg" } + if (cover != null) { + coverDeleted = deleteDocumentById(context, treeContext.treeUri, cover.documentId) + } + } + + if (parentId != treeContext.treeDocumentId && + isDocumentDirectoryEmpty(context, treeContext.treeUri, parentId)) { + bookFolderDeleted = deleteDocumentById(context, treeContext.treeUri, parentId) + + if (bookFolderDeleted) { + val authorDocumentId = getParentDocumentId(parentId) + if (!authorDocumentId.isNullOrBlank() && + authorDocumentId != treeContext.treeDocumentId && + isDocumentDirectoryEmpty(context, treeContext.treeUri, authorDocumentId)) { + authorFolderDeleted = deleteDocumentById(context, treeContext.treeUri, authorDocumentId) + } + } + } + + return DeleteCleanupResult( + fileDeleted = true, + coverDeleted = coverDeleted, + bookFolderDeleted = bookFolderDeleted, + authorFolderDeleted = authorFolderDeleted + ) + } + + private fun findTreeDocumentContext(context: Context, documentId: String): TreeDocumentContext? { + return context.contentResolver.persistedUriPermissions + .filter { it.isWritePermission } + .mapNotNull { permission -> + val treeDocumentId = try { + DocumentsContract.getTreeDocumentId(permission.uri) + } catch (_: Exception) { + null + } + + if (!treeDocumentId.isNullOrBlank() && isDocumentWithinTree(documentId, treeDocumentId)) { + TreeDocumentContext( + treeUri = permission.uri, + treeDocumentId = treeDocumentId, + documentId = documentId, + parentDocumentId = getParentDocumentId(documentId) + ) + } else { + null + } + } + .maxByOrNull { it.treeDocumentId.length } + } + + private fun isDocumentWithinTree(documentId: String, treeDocumentId: String): Boolean { + return documentId == treeDocumentId || + documentId.startsWith("$treeDocumentId/") || + (treeDocumentId.endsWith(":") && documentId.startsWith(treeDocumentId)) + } + + private fun getParentDocumentId(documentId: String): String? { + val index = documentId.lastIndexOf('/') + return if (index > 0) documentId.substring(0, index) else null + } + + private fun listDocumentChildren( + context: Context, + treeUri: Uri, + parentDocumentId: String + ): List { + val resolver = context.contentResolver + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocumentId) + val projection = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE + ) + val children = mutableListOf() + + resolver.query(childrenUri, projection, null, null, null)?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val nameColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val mimeColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) + + while (cursor.moveToNext()) { + children.add( + DocumentChild( + documentId = cursor.getString(idColumn), + displayName = cursor.getString(nameColumn), + mimeType = cursor.getString(mimeColumn) + ) + ) + } + } + + return children + } + + private fun isDocumentDirectoryEmpty(context: Context, treeUri: Uri, documentId: String): Boolean { + return listDocumentChildren(context, treeUri, documentId).isEmpty() + } + + private fun deleteDocumentById(context: Context, treeUri: Uri, documentId: String): Boolean { + val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId) + return DocumentsContract.deleteDocument(context.contentResolver, documentUri) + } + + private fun isAudioFile(displayName: String, mimeType: String?): Boolean { + if (mimeType?.startsWith("audio/") == true) { + return true + } + + val lowerName = displayName.lowercase() + return lowerName.endsWith(".m4b") || + lowerName.endsWith(".m4a") || + lowerName.endsWith(".mp4") || + lowerName.endsWith(".mp3") || + lowerName.endsWith(".aac") || + lowerName.endsWith(".flac") || + lowerName.endsWith(".ogg") || + lowerName.endsWith(".opus") || + lowerName.endsWith(".wav") || + lowerName.endsWith(".aax") || + lowerName.endsWith(".aaxc") + } + + private fun writeEmbeddedCover( + context: Context, + audioFilePath: String, + bitmap: android.graphics.Bitmap + ): String { + return if (audioFilePath.startsWith("content://")) { + writeEmbeddedCoverToDocumentTree(context, Uri.parse(audioFilePath), bitmap) + } else { + val path = audioFilePath.removePrefix("file://") + val targetDir = File(path).parentFile + ?: throw Exception("Could not access parent directory") + + if (!targetDir.exists() || !targetDir.isDirectory) { + throw Exception("Could not access parent directory") + } + + if (!targetDir.canWrite()) { + throw Exception("No write permission for parent directory") + } + + val embeddedCover = File(targetDir, "EmbeddedCover.jpg") + if (embeddedCover.exists() && !embeddedCover.delete()) { + throw Exception("Failed to delete existing EmbeddedCover.jpg") + } + + embeddedCover.outputStream().use { outputStream -> + if (!bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 90, outputStream)) { + throw Exception("Failed to write EmbeddedCover.jpg") + } + } + + embeddedCover.absolutePath + } + } + + private fun writeEmbeddedCoverToDocumentTree( + context: Context, + audioUri: Uri, + bitmap: android.graphics.Bitmap + ): String { + val resolver = context.contentResolver + val documentId = try { + DocumentsContract.getDocumentId(audioUri) + } catch (_: Exception) { + null + } + + val treeDocumentId = try { + DocumentsContract.getTreeDocumentId(audioUri) + } catch (_: Exception) { + null + } + + if (documentId.isNullOrBlank() || treeDocumentId.isNullOrBlank()) { + throw Exception( + "Cannot create EmbeddedCover.jpg next to this file because Android only granted access to the file, not its folder." + ) + } + + val parentDocumentId = if (documentId.contains('/')) { + documentId.substringBeforeLast('/') + } else if (documentId != treeDocumentId) { + treeDocumentId + } else { + "" + } + if (parentDocumentId.isBlank()) { + throw Exception("Could not access parent directory") + } + + deleteDocumentInDirectory(context, audioUri, parentDocumentId, "EmbeddedCover.jpg") + + val parentUri = DocumentsContract.buildDocumentUriUsingTree(audioUri, parentDocumentId) + val embeddedCoverUri = DocumentsContract.createDocument( + resolver, + parentUri, + "image/jpeg", + "EmbeddedCover.jpg" + ) ?: throw Exception("Failed to create EmbeddedCover.jpg") + + resolver.openOutputStream(embeddedCoverUri)?.use { outputStream -> + if (!bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 90, outputStream)) { + throw Exception("Failed to write EmbeddedCover.jpg") + } + } ?: throw Exception("Failed to open output stream for EmbeddedCover.jpg") + + return embeddedCoverUri.toString() + } + + private fun deleteDocumentInDirectory( + context: Context, + treeUri: Uri, + parentDocumentId: String, + displayName: String + ) { + val resolver = context.contentResolver + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, parentDocumentId) + val projection = arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME + ) + + resolver.query(childrenUri, projection, null, null, null)?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val nameColumn = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + + while (cursor.moveToNext()) { + if (cursor.getString(nameColumn) == displayName) { + val childDocumentId = cursor.getString(idColumn) + val childUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId) + + if (!DocumentsContract.deleteDocument(resolver, childUri)) { + throw Exception("Failed to delete existing $displayName") + } + } + } + } + } + // ============================================================================ // COMPANION OBJECT // ============================================================================ @@ -1335,6 +1762,7 @@ class ExpoRustBridgeModule : Module() { // Account functions @JvmStatic external fun nativeSaveAccount(paramsJson: String): String @JvmStatic external fun nativeGetPrimaryAccount(paramsJson: String): String + @JvmStatic external fun nativeDeleteAccount(paramsJson: String): String // Testing functions @JvmStatic external fun nativeClearDownloadState(paramsJson: String): String diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/BackgroundTaskManager.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/BackgroundTaskManager.kt index 03ae608..df98a03 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/BackgroundTaskManager.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/BackgroundTaskManager.kt @@ -2,6 +2,7 @@ package expo.modules.rustbridge.tasks import android.content.Context import android.util.Log +import expo.modules.rustbridge.AppPaths import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import java.util.PriorityQueue @@ -352,8 +353,7 @@ class BackgroundTaskManager private constructor( * Get database path */ fun getDbPath(): String { - val cacheDir = context.cacheDir - return "${cacheDir.absolutePath}/audible.db" + return AppPaths.databasePath(context) } /** diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/DownloadWorker.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/DownloadWorker.kt index 32460f8..4c008fe 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/DownloadWorker.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/tasks/DownloadWorker.kt @@ -519,6 +519,9 @@ class DownloadWorker( coverArtPath?.let { File(it).delete() } // Mark as completed + task.getMetadataString(DownloadTaskMetadata.RUST_TASK_ID)?.let { rustTaskId -> + updateRustTaskStatusInDb(rustTaskId, "completed", finalPath) + } task.status = TaskStatus.COMPLETED task.completedAt = java.util.Date() manager.emitEvent(TaskEvent.DownloadComplete( @@ -841,6 +844,24 @@ class DownloadWorker( return prefs.getStringSet(PREF_MANUALLY_PAUSED, emptySet()) ?: emptySet() } + /** + * Keep the persistent Rust download task in sync with the final converted file path. + */ + private fun updateRustTaskStatusInDb(taskId: String, status: String, outputPath: String? = null) { + try { + val params = JSONObject().apply { + put("db_path", manager.getDbPath()) + put("task_id", taskId) + put("status", status) + outputPath?.let { put("output_path", it) } + } + ExpoRustBridgeModule.nativeUpdateDownloadTaskStatus(params.toString()) + Log.d(TAG, "Updated Rust task $taskId status to $status") + } catch (e: Exception) { + Log.e(TAG, "Failed to update Rust task status: ${e.message}") + } + } + // ======================================================================== // Helper Methods // ======================================================================== diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/LibrarySyncWorker.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/LibrarySyncWorker.kt index 7bcbe9d..a86c7d9 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/LibrarySyncWorker.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/LibrarySyncWorker.kt @@ -7,6 +7,7 @@ import android.os.Build import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import expo.modules.rustbridge.AppPaths import expo.modules.rustbridge.ExpoRustBridgeModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -68,7 +69,7 @@ class LibrarySyncWorker( Log.d(TAG, "Library sync worker started") // Get database path - val dbPath = applicationContext.cacheDir.absolutePath + "/audible.db" + val dbPath = AppPaths.databasePath(applicationContext) // Load account from SQLite database val getAccountParams = JSONObject().apply { diff --git a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/TokenRefreshWorker.kt b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/TokenRefreshWorker.kt index 2e637fe..329381a 100644 --- a/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/TokenRefreshWorker.kt +++ b/modules/expo-rust-bridge/android/src/main/java/expo/modules/rustbridge/workers/TokenRefreshWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import expo.modules.rustbridge.AppPaths import expo.modules.rustbridge.ExpoRustBridgeModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -66,7 +67,7 @@ class TokenRefreshWorker( Log.d(TAG, "Token refresh worker started") // Get database path - val dbPath = applicationContext.cacheDir.absolutePath + "/audible.db" + val dbPath = AppPaths.databasePath(applicationContext) // Load account from SQLite database val getAccountParams = JSONObject().apply { @@ -96,10 +97,6 @@ class TokenRefreshWorker( // Debug: Log identity structure Log.d(TAG, "Identity keys: ${identity.keys().asSequence().toList()}") - // Log current refresh token from database (for debugging) - val currentRefreshToken = identity.optString("refresh_token", "") - Log.d(TAG, "Loaded refresh_token from DB: ${if (currentRefreshToken.isEmpty()) "EMPTY/NULL" else currentRefreshToken.substring(0, minOf(20, currentRefreshToken.length)) + "..."}") - // Get token expiry val accessTokenObj = identity.opt("access_token") val expiresAtStr = when (accessTokenObj) { @@ -144,10 +141,6 @@ class TokenRefreshWorker( val refreshToken = identity.getString("refresh_token") val deviceSerial = identity.getString("device_serial_number") - // Log refresh token for debugging (first 20 chars only) - val tokenPreview = if (refreshToken.length > 20) refreshToken.substring(0, 20) + "..." else refreshToken - Log.d(TAG, "Using refresh_token: $tokenPreview") - // Call Rust to refresh token Log.d(TAG, "Calling Rust to refresh token") val refreshParams = JSONObject().apply { @@ -174,7 +167,7 @@ class TokenRefreshWorker( val newRefreshToken = if (dataObj.has("refresh_token") && !dataObj.isNull("refresh_token")) { val token = dataObj.getString("refresh_token") if (token.isNotEmpty()) { - Log.d(TAG, "Amazon returned new refresh_token: ${token.substring(0, minOf(20, token.length))}...") + Log.d(TAG, "Amazon returned new refresh_token") token } else { Log.d(TAG, "Amazon returned empty refresh_token, keeping old one") diff --git a/modules/expo-rust-bridge/index.ts b/modules/expo-rust-bridge/index.ts index a0a3a51..c4e017b 100644 --- a/modules/expo-rust-bridge/index.ts +++ b/modules/expo-rust-bridge/index.ts @@ -707,6 +707,11 @@ export interface ExpoRustBridgeModule { */ getPrimaryAccount(dbPath: string): Promise>; + /** + * Delete account from SQLite database. + */ + deleteAccount(dbPath: string, accountId: string): Promise>; + /** * Clear download state for all books (for testing). * Resets download status but keeps book metadata. @@ -736,7 +741,16 @@ export interface ExpoRustBridgeModule { dbPath: string, asin: string, deleteFile: boolean - ): Promise>; + ): Promise>; /** * Set the file path for a book manually. @@ -873,6 +887,8 @@ if (!NativeModule) { throw new Error('ExpoRustBridge native module failed to load'); } +const ExpoRustBridge: ExpoRustBridgeModule = NativeModule; + // ============================================================================ // Error Handling // ============================================================================ @@ -964,15 +980,12 @@ function initiateOAuth( ): OAuthFlowData { console.log('[ExpoRustBridge] initiateOAuth called with locale:', localeCode); const serial = deviceSerial || generateDeviceSerial(); - console.log('[ExpoRustBridge] Using device serial:', serial); console.log('[ExpoRustBridge] Calling NativeModule.generateOAuthUrl...'); const response = NativeModule!.generateOAuthUrl(localeCode, serial); - console.log('[ExpoRustBridge] Response from native:', response); const data = unwrapResult(response); console.log('[ExpoRustBridge] OAuth data unwrapped successfully'); - console.log('[ExpoRustBridge] Authorization URL:', data.authorization_url); return { url: data.authorization_url, @@ -1010,15 +1023,13 @@ async function completeOAuthFlow( pkceVerifier: string ): Promise { console.log('[ExpoRustBridge] completeOAuthFlow called'); - console.log('[ExpoRustBridge] Callback URL:', callbackUrl); // Parse callback URL console.log('[ExpoRustBridge] Parsing callback URL...'); const parseResponse = NativeModule!.parseOAuthCallback(callbackUrl); - console.log('[ExpoRustBridge] Parse response:', parseResponse); const { authorization_code } = unwrapResult(parseResponse); - console.log('[ExpoRustBridge] Authorization code extracted:', authorization_code); + console.log('[ExpoRustBridge] Authorization code parsed'); // Exchange authorization code for tokens const tokenResponse = await NativeModule!.exchangeAuthCode( @@ -1029,11 +1040,7 @@ async function completeOAuthFlow( ); const tokens = unwrapResult(tokenResponse); - - // Log complete token response for debugging - console.log('[ExpoRustBridge] ========== COMPLETE TOKEN RESPONSE =========='); - console.log(JSON.stringify(tokens, null, 2)); - console.log('[ExpoRustBridge] ==============================================='); + console.log('[ExpoRustBridge] Token exchange completed'); return tokens; } @@ -1653,6 +1660,17 @@ async function getPrimaryAccount(dbPath: string): Promise { return JSON.parse(data.account); } +/** + * Delete account from SQLite database. + * + * @param dbPath - Database path + * @param accountId - Account identifier + */ +async function deleteAccount(dbPath: string, accountId: string): Promise { + const response = await NativeModule!.deleteAccount(dbPath, accountId); + unwrapResult(response); +} + /** * Clear download state for all books (for testing). * @@ -1693,7 +1711,16 @@ async function clearBookDownloadState( dbPath: string, asin: string, deleteFile: boolean = false -): Promise<{ cleared: boolean; file_deleted: boolean; deleted_path: string | null }> { +): Promise<{ + cleared: boolean; + file_deleted: boolean; + deleted_path: string | null; + cover_deleted?: boolean; + book_folder_deleted?: boolean; + author_folder_deleted?: boolean; + delete_error?: string | null; + cleanup_error?: string | null; +}> { const response = await NativeModule!.clearBookDownloadState(dbPath, asin, deleteFile); return unwrapResult(response); } @@ -1902,12 +1929,12 @@ async function requestNotificationPermission(): Promise { // Exports // ============================================================================ -export default NativeModule; +export default ExpoRustBridge; export type { OAuthFlowData }; export { - NativeModule as ExpoRustBridge, + ExpoRustBridge, initiateOAuth, completeOAuthFlow, refreshToken, @@ -1950,6 +1977,7 @@ export { // Account Storage (SQLite) saveAccount, getPrimaryAccount, + deleteAccount, // Testing clearDownloadState, getBookFilePath, diff --git a/native/rust-core/Cargo.lock b/native/rust-core/Cargo.lock new file mode 100644 index 0000000..b666bc8 --- /dev/null +++ b/native/rust-core/Cargo.lock @@ -0,0 +1,3472 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna 1.1.0", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-core" +version = "0.0.1" +dependencies = [ + "aes", + "anyhow", + "base64", + "cbc", + "chrono", + "clap", + "futures-util", + "hex", + "jni", + "lazy_static", + "pkcs8", + "rand", + "regex", + "reqwest", + "rsa", + "serde", + "serde_json", + "sha2", + "sqlx", + "tempfile", + "thiserror", + "tokio", + "uniffi", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64", + "bitflags 2.11.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64", + "bitflags 2.11.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "uniffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb08c58c7ed7033150132febe696bef553f891b1ede57424b40d87a89e3c170" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_build", + "uniffi_core", + "uniffi_macros", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cade167af943e189a55020eda2c314681e223f1e42aca7c4e52614c2b627698f" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "once_cell", + "paste", + "serde", + "textwrap", + "toml", + "uniffi_meta", + "uniffi_udl", +] + +[[package]] +name = "uniffi_build" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7cf32576e08104b7dc2a6a5d815f37616e66c6866c2a639fe16e6d2286b75b" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802d2051a700e3ec894c79f80d2705b69d85844dafbbe5d1a92776f8f48b563a" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uniffi_core" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" +dependencies = [ + "anyhow", + "bytes", + "log", + "once_cell", + "paste", + "static_assertions", +] + +[[package]] +name = "uniffi_macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c65a5b12ec544ef136693af8759fb9d11aefce740fb76916721e876639033b" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a74ed96c26882dac1ca9b93ca23c827e284bacbd7ec23c6f0b0372f747d59e4" +dependencies = [ + "anyhow", + "bytes", + "siphasher", + "uniffi_checksum_derive", +] + +[[package]] +name = "uniffi_testing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6f984f0781f892cc864a62c3a5c60361b1ccbd68e538e6c9fbced5d82268ac" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", +] + +[[package]] +name = "uniffi_udl" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037820a4cfc4422db1eaa82f291a3863c92c7d1789dc513489c36223f9b4cdfc" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "uniffi_testing", + "weedle2", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna 1.1.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/native/rust-core/examples/oauth_test.rs b/native/rust-core/examples/oauth_test.rs index 15e6f3a..27c7b16 100644 --- a/native/rust-core/examples/oauth_test.rs +++ b/native/rust-core/examples/oauth_test.rs @@ -11,8 +11,8 @@ //! ``` use rust_core::api::auth::{ - generate_authorization_url, parse_authorization_callback, exchange_authorization_code, - Locale, PkceChallenge, OAuthState, + exchange_authorization_code, generate_authorization_url, parse_authorization_callback, Locale, + OAuthState, PkceChallenge, }; use std::io::{self, Write}; @@ -26,7 +26,7 @@ async fn main() -> Result<(), Box> { .replace("-", "") .to_uppercase(); - println!("📱 Generated Device Serial: {}\n", device_serial); + println!("📱 Generated device serial\n"); // Step 2: Select locale println!("📍 Select your Audible region:"); @@ -48,14 +48,17 @@ async fn main() -> Result<(), Box> { _ => Locale::us(), }; - println!("✓ Using locale: {} ({})\n", locale.name, locale.country_code); + println!( + "✓ Using locale: {} ({})\n", + locale.name, locale.country_code + ); // Step 3: Generate PKCE and OAuth URL let pkce = PkceChallenge::generate()?; let state = OAuthState::generate(); - println!("🔑 PKCE Verifier: {}", pkce.verifier); - println!("🎲 OAuth State: {}\n", state.value); + println!("🔑 PKCE verifier generated"); + println!("🎲 OAuth state generated\n"); let auth_url = generate_authorization_url(&locale, &device_serial, &pkce, &state)?; @@ -85,12 +88,11 @@ async fn main() -> Result<(), Box> { } println!("\n🔍 Parsing callback URL..."); - println!("URL: {}\n", callback_url); // Step 5: Parse authorization code match parse_authorization_callback(callback_url) { Ok(auth_code) => { - println!("✅ Authorization Code: {}\n", auth_code); + println!("✅ Authorization code parsed\n"); // Step 6: Exchange code for tokens println!("🔄 Exchanging authorization code for tokens...\n"); @@ -99,10 +101,23 @@ async fn main() -> Result<(), Box> { Ok(tokens) => { println!("\n🎉 SUCCESS! Authentication Complete!\n"); println!("{}", "=".repeat(80)); - println!("Access Token: {}...", &tokens.access_token[..30]); - println!("Refresh Token: {}...", &tokens.refresh_token[..30]); - println!("Token Type: {}", tokens.token_type); - println!("Expires In: {} seconds", tokens.expires_in); + println!( + "Access Token: {}", + if tokens.bearer.access_token.is_empty() { + "missing" + } else { + "received" + } + ); + println!( + "Refresh Token: {}", + if tokens.bearer.refresh_token.is_empty() { + "missing" + } else { + "received" + } + ); + println!("Expires In: {} seconds", tokens.bearer.expires_in); println!("{}", "=".repeat(80)); println!("\n✨ You can now use these tokens to access your Audible library!"); } @@ -117,7 +132,9 @@ async fn main() -> Result<(), Box> { println!("\n❌ Failed to Parse Callback URL!"); println!("Error: {:?}\n", e); println!("Expected format:"); - println!(" https://www.amazon.com/ap/maplanding?...&openid.oa2.authorization_code=XXXXX"); + println!( + " https://www.amazon.com/ap/maplanding?...&openid.oa2.authorization_code=XXXXX" + ); } } diff --git a/native/rust-core/src/api/auth.rs b/native/rust-core/src/api/auth.rs index 5ed34b0..91fd76c 100644 --- a/native/rust-core/src/api/auth.rs +++ b/native/rust-core/src/api/auth.rs @@ -117,8 +117,7 @@ //! # use rust_core::error::Result; //! # async fn example() -> Result<()> { //! // Create a new account -//! let mut account = Account::new("user@example.com".to_string()); -//! account.set_locale(Locale::us()); +//! let mut account = Account::new("user@example.com".to_string())?; //! //! // Authenticate (in real app, this would involve external browser) //! // let identity = authenticate_with_browser(&account.locale()).await?; @@ -1093,9 +1092,9 @@ pub fn generate_authorization_url( &format!("https://www.{}/ap/maplanding", amazon_domain), ); query.append_pair( - "openid.assoc_handle", - &format!("amzn_audible_ios_{}", locale.country_code), - ); + "openid.assoc_handle", + &format!("amzn_audible_ios_{}", locale.country_code), + ); query.append_pair( "openid.identity", "http://specs.openid.net/auth/2.0/identifier_select", @@ -1421,32 +1420,24 @@ pub async fn exchange_authorization_code( if !response.status().is_success() { let status = response.status(); let error_body = response.text().await.unwrap_or_default(); + let error_preview = if error_body.chars().count() > 512 { + format!("{}...", error_body.chars().take(512).collect::()) + } else { + error_body + }; eprintln!("=== Registration Failed ==="); eprintln!("Status: {}", status); - eprintln!("Response: {}", error_body); + eprintln!("Response: {}", error_preview); eprintln!("==========================="); return Err(LibationError::AuthenticationFailed { - message: format!("Token exchange failed (status {}): {}", status, error_body), + message: format!("Token exchange failed (status {})", status), account_id: None, }); } let response_text = response.text().await.unwrap_or_default(); - // Try to save to app's cache directory (writable) - let cache_paths = [ - "/data/data/com.librisync.app/cache/registration.json", - "/sdcard/Download/audible_registration.json", - ]; - - for path in &cache_paths { - if let Ok(_) = std::fs::write(path, &response_text) { - eprintln!("✅ Saved registration to: {}", path); - break; - } - } - - // Parse the response we just logged + // Parse the response without persisting tokens or customer data to disk. let register_response: serde_json::Value = serde_json::from_str(&response_text).map_err(|e| LibationError::InvalidApiResponse { message: format!("Failed to parse registration response: {}", e), @@ -2209,7 +2200,10 @@ mod tests { // Germany should use amazon.de assert!(url.starts_with("https://www.amazon.de/ap/signin")); // DE marketplace ID should be AN7V1F1VY261K (not AN7EY7DTAW63G which is AU) - assert!(url.contains("AN7V1F1VY261K"), "DE should use marketplace ID AN7V1F1VY261K"); + assert!( + url.contains("AN7V1F1VY261K"), + "DE should use marketplace ID AN7V1F1VY261K" + ); } #[test] @@ -2217,7 +2211,10 @@ mod tests { let locale = Locale::de(); assert_eq!(locale.country_code, "de"); assert_eq!(locale.domain, "audible.de"); - assert!(!locale.with_username, "DE locale should not use username-based auth"); + assert!( + !locale.with_username, + "DE locale should not use username-based auth" + ); } #[test] @@ -2232,7 +2229,10 @@ mod tests { #[test] fn test_locale_br_in_all() { let all = Locale::all(); - assert!(all.iter().any(|l| l.country_code == "br"), "BR locale should be in Locale::all()"); + assert!( + all.iter().any(|l| l.country_code == "br"), + "BR locale should be in Locale::all()" + ); } #[test] @@ -2251,7 +2251,10 @@ mod tests { let url = generate_authorization_url(&locale, device_serial, &pkce, &state).unwrap(); assert!(url.starts_with("https://www.amazon.com.br/ap/signin")); - assert!(url.contains("A10J1VAYUDTYRN"), "BR should use marketplace ID A10J1VAYUDTYRN"); + assert!( + url.contains("A10J1VAYUDTYRN"), + "BR should use marketplace ID A10J1VAYUDTYRN" + ); } #[test] @@ -2262,12 +2265,22 @@ mod tests { let state = OAuthState::generate(); let url = generate_authorization_url(&locale, device_serial, &pkce, &state).unwrap(); - assert!(url.starts_with("https://www.amazon"), - "Locale {} should generate a valid Amazon auth URL, got: {}", locale.country_code, &url[..50]); - assert!(url.contains("marketPlaceId="), - "Locale {} auth URL should contain a marketplace ID", locale.country_code); - assert!(url.contains("openid.mode=checkid_setup"), - "Locale {} auth URL should contain openid mode", locale.country_code); + assert!( + url.starts_with("https://www.amazon"), + "Locale {} should generate a valid Amazon auth URL, got: {}", + locale.country_code, + &url[..50] + ); + assert!( + url.contains("marketPlaceId="), + "Locale {} auth URL should contain a marketplace ID", + locale.country_code + ); + assert!( + url.contains("openid.mode=checkid_setup"), + "Locale {} auth URL should contain openid mode", + locale.country_code + ); } } @@ -2438,9 +2451,9 @@ mod tests { let pkce = PkceChallenge::generate().unwrap(); let state = OAuthState::generate(); - println!("📱 Device Serial: {}", device_serial); - println!("🔐 PKCE Verifier: {}", &pkce.verifier[..20]); - println!("🎲 State: {}\n", &state.value[..20]); + println!("📱 Device serial generated"); + println!("🔐 PKCE verifier generated"); + println!("🎲 OAuth state generated\n"); let auth_url = generate_authorization_url(&locale, &device_serial, &pkce, &state).unwrap(); @@ -2467,10 +2480,7 @@ mod tests { // Step 4: Parse callback match parse_authorization_callback(callback_url) { Ok(authorization_code) => { - println!( - "✅ Authorization code received: {}...", - &authorization_code[..20] - ); + println!("✅ Authorization code received"); // Step 5: Exchange code for tokens println!("🔄 Exchanging code for tokens...\n"); @@ -2485,14 +2495,8 @@ mod tests { { Ok(token_response) => { println!("✅ Token Exchange Successful!"); - println!( - " Access Token: {}...", - &token_response.bearer.access_token[..30] - ); - println!( - " Refresh Token: {}...", - &token_response.bearer.refresh_token[..30] - ); + println!(" Access Token: received"); + println!(" Refresh Token: received"); println!( " Expires In: {} seconds", token_response.bearer.expires_in @@ -2504,9 +2508,9 @@ mod tests { match get_activation_bytes(&locale, &token_response.bearer.access_token) .await { - Ok(activation_bytes) => { + Ok(_) => { println!("✅ Activation Bytes Retrieved!"); - println!(" Activation Bytes: {}\n", activation_bytes); + println!(" Activation Bytes: received\n"); println!("🎉 OAuth Flow Complete!"); println!("\n📊 Summary:"); diff --git a/native/rust-core/src/api/client.rs b/native/rust-core/src/api/client.rs index a714219..b2b7c60 100644 --- a/native/rust-core/src/api/client.rs +++ b/native/rust-core/src/api/client.rs @@ -17,7 +17,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! HTTP client for Audible API //! //! This module provides a robust HTTP client wrapper for the Audible API with features including: @@ -93,10 +92,10 @@ //! "ca", "fr", "de", "in", "it", "co.jp", "es"]; //! ``` -use crate::error::{LibationError, Result}; use crate::api::auth::{Account, Identity, Locale}; -use reqwest::{Client, Method, Request, Response, StatusCode}; +use crate::error::{LibationError, Result}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}; +use reqwest::{Client, Method, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -303,7 +302,7 @@ impl ClientConfigBuilder { /// use rust_core::api::auth::Account; /// /// # async fn example() -> rust_core::error::Result<()> { -/// let account = Account::new("user@example.com".to_string()); +/// let account = Account::new("user@example.com".to_string())?; /// let client = AudibleClient::new(account)?; /// /// // GET request @@ -360,7 +359,9 @@ impl AudibleClient { // Validate account fields // Reference: ApiExtended.cs:31-33 (ArgumentValidator checks) if account.account_id.is_empty() { - return Err(LibationError::MissingRequiredField("account_id".to_string())); + return Err(LibationError::MissingRequiredField( + "account_id".to_string(), + )); } // Build HTTP client with configuration @@ -371,10 +372,7 @@ impl AudibleClient { HeaderValue::from_str(&config.user_agent) .map_err(|e| LibationError::InvalidInput(format!("Invalid user agent: {}", e)))?, ); - headers.insert( - ACCEPT, - HeaderValue::from_static("application/json"), - ); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); let mut client_builder = Client::builder() .timeout(config.timeout) @@ -450,13 +448,8 @@ impl AudibleClient { Q: Serialize, { let url = format!("{}{}", self.base_url, endpoint); - self.request_with_retry(|client, headers| { - client - .get(&url) - .query(query) - .headers(headers) - }) - .await + self.request_with_retry(|client, headers| client.get(&url).query(query).headers(headers)) + .await } /// Perform a POST request with JSON body @@ -491,13 +484,8 @@ impl AudibleClient { T: serde::de::DeserializeOwned, { let url = format!("{}{}", self.base_url, endpoint); - self.request_with_retry(|client, headers| { - client - .post(&url) - .headers(headers) - .form(form) - }) - .await + self.request_with_retry(|client, headers| client.post(&url).headers(headers).form(form)) + .await } /// Generic HTTP request with automatic token refresh and retry logic @@ -626,7 +614,9 @@ impl AudibleClient { )); // Exponential backoff: 1s, 2s, 4s... - let delay = Duration::from_secs(INITIAL_RETRY_DELAY_SECS * 2_u64.pow(attempts - 1)); + let delay = Duration::from_secs( + INITIAL_RETRY_DELAY_SECS * 2_u64.pow(attempts - 1), + ); sleep(delay).await; continue; } @@ -639,13 +629,17 @@ impl AudibleClient { } // Network error - retry with backoff - Err(e) if attempts < self.config.max_retries && self.is_retryable_network_error(&e) => { + Err(e) + if attempts < self.config.max_retries + && self.is_retryable_network_error(&e) => + { last_error = Some(LibationError::network_error( format!("Network request failed: {}", e), true, )); - let delay = Duration::from_secs(INITIAL_RETRY_DELAY_SECS * 2_u64.pow(attempts - 1)); + let delay = + Duration::from_secs(INITIAL_RETRY_DELAY_SECS * 2_u64.pow(attempts - 1)); sleep(delay).await; continue; } @@ -661,13 +655,13 @@ impl AudibleClient { } // All retries exhausted - Err(last_error.unwrap_or_else(|| { - LibationError::ApiRequestFailed { + Err( + last_error.unwrap_or_else(|| LibationError::ApiRequestFailed { message: format!("Request failed after {} attempts", attempts), status_code: None, endpoint: None, - } - })) + }), + ) } /// Build authentication headers from account tokens @@ -682,8 +676,9 @@ impl AudibleClient { let auth_value = format!("Bearer {}", identity.access_token.token); headers.insert( AUTHORIZATION, - HeaderValue::from_str(&auth_value) - .map_err(|e| LibationError::InvalidInput(format!("Invalid auth token: {}", e)))?, + HeaderValue::from_str(&auth_value).map_err(|e| { + LibationError::InvalidInput(format!("Invalid auth token: {}", e)) + })?, ); } @@ -699,11 +694,14 @@ impl AudibleClient { let url = response.url().clone(); // Get response text first so we can log it on parse error - let response_text = response.text().await.map_err(|e| LibationError::ApiRequestFailed { - message: format!("Failed to read response body: {}", e), - status_code: Some(status.as_u16()), - endpoint: Some(url.path().to_string()), - })?; + let response_text = response + .text() + .await + .map_err(|e| LibationError::ApiRequestFailed { + message: format!("Failed to read response body: {}", e), + status_code: Some(status.as_u16()), + endpoint: Some(url.path().to_string()), + })?; match serde_json::from_str::(&response_text) { Ok(data) => Ok(data), @@ -757,9 +755,7 @@ impl AudibleClient { /// Extract endpoint path from full URL fn extract_endpoint_from_url(&self, url: &str) -> String { - url.strip_prefix(&self.base_url) - .unwrap_or(url) - .to_string() + url.strip_prefix(&self.base_url).unwrap_or(url).to_string() } /// Refresh authentication tokens @@ -769,7 +765,9 @@ impl AudibleClient { async fn refresh_tokens(&self) -> Result<()> { // TODO: Port token refresh logic from C# Identity class // For now, return NotImplemented error - Err(LibationError::not_implemented("Token refresh not yet implemented")) + Err(LibationError::not_implemented( + "Token refresh not yet implemented", + )) } /// Download file with progress callback @@ -795,12 +793,7 @@ impl AudibleClient { })?; let headers = self.build_auth_headers().await?; - let response = self - .client - .get(url) - .headers(headers) - .send() - .await?; + let response = self.client.get(url).headers(headers).send().await?; if !response.status().is_success() { return Err(LibationError::DownloadFailed(format!( @@ -843,11 +836,20 @@ mod tests { #[test] fn test_audible_domain_from_str() { - assert_eq!(AudibleDomain::from_str("audible.com"), Some(AudibleDomain::Us)); + assert_eq!( + AudibleDomain::from_str("audible.com"), + Some(AudibleDomain::Us) + ); assert_eq!(AudibleDomain::from_str("com"), Some(AudibleDomain::Us)); - assert_eq!(AudibleDomain::from_str("audible.co.uk"), Some(AudibleDomain::Uk)); + assert_eq!( + AudibleDomain::from_str("audible.co.uk"), + Some(AudibleDomain::Uk) + ); assert_eq!(AudibleDomain::from_str("co.uk"), Some(AudibleDomain::Uk)); - assert_eq!(AudibleDomain::from_str("audible.de"), Some(AudibleDomain::De)); + assert_eq!( + AudibleDomain::from_str("audible.de"), + Some(AudibleDomain::De) + ); assert_eq!(AudibleDomain::from_str("invalid"), None); } @@ -887,6 +889,9 @@ mod tests { let result = AudibleClient::new(account); assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), LibationError::MissingRequiredField(_))); + assert!(matches!( + result.unwrap_err(), + LibationError::MissingRequiredField(_) + )); } } diff --git a/native/rust-core/src/api/content.rs b/native/rust-core/src/api/content.rs index 99da995..4970d0e 100644 --- a/native/rust-core/src/api/content.rs +++ b/native/rust-core/src/api/content.rs @@ -17,7 +17,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! Content metadata and catalog queries //! //! # Reference C# Sources @@ -75,11 +74,11 @@ //! //! Reference: DownloadOptions.Factory.cs:33 - api.GetContentMetadataAsync() -use crate::error::{LibationError, Result}; use crate::api::client::AudibleClient; +use crate::error::{LibationError, Result}; +use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use chrono::{DateTime, Utc, NaiveDate}; // ============================================================================ // CORE TYPES - DRM and Codecs @@ -546,7 +545,8 @@ impl AudibleClient { /// # use rust_core::api::client::AudibleClient; /// # use rust_core::api::auth::Account; /// # async fn example() -> rust_core::error::Result<()> { - /// let client = AudibleClient::new(Account::default())?; + /// let account = Account::new("user@example.com".to_string())?; + /// let client = AudibleClient::new(account)?; /// let product = client.get_catalog_product("B002V5D7B0").await?; /// println!("Title: {}", product.title); /// # Ok(()) @@ -568,7 +568,8 @@ impl AudibleClient { "series", "category_ladders", "product_extended_attrs", - ].join(","); + ] + .join(","); let params = vec![ ("response_groups", response_groups), @@ -589,18 +590,20 @@ impl AudibleClient { // Parse response // The API wraps the product in a "product" field - let product_json = response - .get("product") - .ok_or_else(|| LibationError::InvalidApiResponse { - message: "Missing 'product' field in response".to_string(), - response_body: Some(response.to_string()), - })?; - - serde_json::from_value(product_json.clone()) - .map_err(|e| LibationError::InvalidApiResponse { + let product_json = + response + .get("product") + .ok_or_else(|| LibationError::InvalidApiResponse { + message: "Missing 'product' field in response".to_string(), + response_body: Some(response.to_string()), + })?; + + serde_json::from_value(product_json.clone()).map_err(|e| { + LibationError::InvalidApiResponse { message: format!("Failed to parse catalog product: {}", e), response_body: Some(product_json.to_string()), - }) + } + }) } /// Get multiple products in a single batch request @@ -628,20 +631,26 @@ impl AudibleClient { /// # use rust_core::api::client::AudibleClient; /// # use rust_core::api::auth::Account; /// # async fn example() -> rust_core::error::Result<()> { - /// let client = AudibleClient::new(Account::default())?; + /// let account = Account::new("user@example.com".to_string())?; + /// let client = AudibleClient::new(account)?; /// let asins = vec!["B002V5D7B0".to_string(), "B002V1O97Y".to_string()]; /// let products = client.get_catalog_products_batch(asins).await?; /// println!("Retrieved {} products", products.len()); /// # Ok(()) /// # } /// ``` - pub async fn get_catalog_products_batch(&self, asins: Vec) -> Result> { + pub async fn get_catalog_products_batch( + &self, + asins: Vec, + ) -> Result> { // Validate batch size (max 50 per request) // Reference: ApiExtended.cs:24 - BatchSize = 50 if asins.len() > crate::api::client::BATCH_SIZE { - return Err(LibationError::invalid_input( - format!("Too many ASINs in batch: {} (max {})", asins.len(), crate::api::client::BATCH_SIZE) - )); + return Err(LibationError::invalid_input(format!( + "Too many ASINs in batch: {} (max {})", + asins.len(), + crate::api::client::BATCH_SIZE + ))); } if asins.is_empty() { @@ -663,7 +672,8 @@ impl AudibleClient { "series", "category_ladders", "product_extended_attrs", - ].join(","); + ] + .join(","); // Build query with comma-separated ASINs let asin_param = asins.join(","); @@ -736,10 +746,13 @@ impl AudibleClient { /// # use rust_core::api::client::AudibleClient; /// # use rust_core::api::auth::Account; /// # async fn example() -> rust_core::error::Result<()> { - /// let client = AudibleClient::new(Account::default())?; + /// let account = Account::new("user@example.com".to_string())?; + /// let client = AudibleClient::new(account)?; /// let metadata = client.get_content_metadata("B002V5D7B0").await?; - /// println!("Runtime: {} minutes", metadata.chapter_info.runtime_length_ms / 60000); - /// println!("Chapters: {}", metadata.chapter_info.chapters.len()); + /// if let Some(chapter_info) = metadata.chapter_info { + /// println!("Runtime: {} minutes", chapter_info.runtime_length_ms / 60000); + /// println!("Chapters: {}", chapter_info.chapters.len()); + /// } /// # Ok(()) /// # } /// ``` @@ -750,15 +763,14 @@ impl AudibleClient { // Parse metadata // The API may wrap in a "content_metadata" field or return directly - let metadata_json = response - .get("content_metadata") - .unwrap_or(&response); + let metadata_json = response.get("content_metadata").unwrap_or(&response); - serde_json::from_value(metadata_json.clone()) - .map_err(|e| LibationError::InvalidApiResponse { + serde_json::from_value(metadata_json.clone()).map_err(|e| { + LibationError::InvalidApiResponse { message: format!("Failed to parse content metadata: {}", e), response_body: Some(metadata_json.to_string()), - }) + } + }) } } diff --git a/native/rust-core/src/api/license.rs b/native/rust-core/src/api/license.rs index 7d9c0c3..cb6ebd1 100644 --- a/native/rust-core/src/api/license.rs +++ b/native/rust-core/src/api/license.rs @@ -17,7 +17,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! License and download voucher management //! //! # Reference C# Sources @@ -86,13 +85,11 @@ //! //! Reference: DownloadOptions.Factory.cs:100 - api.WidevineDrmLicense() -use crate::error::{LibationError, Result}; use crate::api::client::AudibleClient; -use crate::api::content::{ - DrmType, Codec, DownloadQuality, ChapterTitlesType, ContentMetadata -}; -use serde::{Deserialize, Serialize}; +use crate::api::content::{ChapterTitlesType, Codec, ContentMetadata, DownloadQuality, DrmType}; +use crate::error::{LibationError, Result}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; // ============================================================================ // LICENSE REQUEST STRUCTURES @@ -132,7 +129,10 @@ pub struct LicenseRequest { /// Chapter titles type (Flat or Tree) /// Reference: DownloadOptions.Factory.cs:80 - ChapterTitlesType.Tree - #[serde(rename = "chapter_titles_type", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "chapter_titles_type", + skip_serializing_if = "Option::is_none" + )] pub chapter_titles_type: Option, /// Request spatial audio if available @@ -292,8 +292,10 @@ impl KeyData { .map_err(|e| LibationError::InvalidInput(format!("Invalid hex key: {}", e)))?; let iv_bytes = if let Some(iv_str) = iv_hex { - Some(hex::decode(iv_str) - .map_err(|e| LibationError::InvalidInput(format!("Invalid hex IV: {}", e)))?) + Some( + hex::decode(iv_str) + .map_err(|e| LibationError::InvalidInput(format!("Invalid hex IV: {}", e)))?, + ) } else { None }; @@ -313,19 +315,20 @@ impl KeyData { /// => voucher is null ? null : [new KeyData(voucher.Key, voucher.Iv)]; /// ``` pub fn from_base64(key: &str, iv: Option<&str>) -> Result { - use base64::{Engine as _, engine::general_purpose}; + use base64::{engine::general_purpose, Engine as _}; let key_bytes = general_purpose::STANDARD .decode(key) .map_err(|e| LibationError::InvalidInput(format!("Invalid base64 key: {}", e)))?; - let iv_bytes = if let Some(iv_str) = iv { - Some(general_purpose::STANDARD - .decode(iv_str) - .map_err(|e| LibationError::InvalidInput(format!("Invalid base64 IV: {}", e)))?) - } else { - None - }; + let iv_bytes = + if let Some(iv_str) = iv { + Some(general_purpose::STANDARD.decode(iv_str).map_err(|e| { + LibationError::InvalidInput(format!("Invalid base64 IV: {}", e)) + })?) + } else { + None + }; Ok(Self { key_part_1: key_bytes, @@ -364,18 +367,21 @@ impl KeyData { account_id: &str, asin: &str, ) -> Result { - use base64::{Engine as _, engine::general_purpose}; - use sha2::{Sha256, Digest}; use aes::Aes128; - use cbc::{Decryptor, cipher::{BlockDecryptMut, KeyIvInit}}; + use base64::{engine::general_purpose, Engine as _}; + use cbc::{ + cipher::{BlockDecryptMut, KeyIvInit}, + Decryptor, + }; + use sha2::{Digest, Sha256}; // Decode base64 ciphertext // Reference: ContentLicenseDtoV10.cs:38 let ciphertext = general_purpose::STANDARD .decode(license_response_b64) - .map_err(|e| LibationError::InvalidInput( - format!("Invalid base64 license_response: {}", e) - ))?; + .map_err(|e| { + LibationError::InvalidInput(format!("Invalid base64 license_response: {}", e)) + })?; // Derive key and IV from SHA256 hash // Reference: ContentLicenseDtoV10.cs:24-36 @@ -391,44 +397,44 @@ impl KeyData { // Reference: ContentLicenseDtoV10.cs:40-43 - uses Aes.Create() with 16-byte key type Aes128CbcDec = Decryptor; - let cipher = Aes128CbcDec::new_from_slices(&key, &iv) - .map_err(|e| LibationError::InvalidInput( - format!("Failed to create cipher: {:?}", e) - ))?; + let cipher = Aes128CbcDec::new_from_slices(&key, &iv).map_err(|e| { + LibationError::InvalidInput(format!("Failed to create cipher: {:?}", e)) + })?; // Decrypt in place let mut buffer = ciphertext.clone(); let plaintext = cipher .decrypt_padded_mut::(&mut buffer) - .map_err(|e| LibationError::InvalidInput( - format!("Failed to decrypt license_response: {:?}", e) - ))?; + .map_err(|e| { + LibationError::InvalidInput(format!("Failed to decrypt license_response: {:?}", e)) + })?; // Remove null bytes and parse as ASCII // Reference: ContentLicenseDtoV10.cs:44 - let plaintext_no_nulls: Vec = plaintext.iter() - .copied() - .take_while(|&b| b != 0) - .collect(); + let plaintext_no_nulls: Vec = + plaintext.iter().copied().take_while(|&b| b != 0).collect(); - let json_str = String::from_utf8(plaintext_no_nulls) - .map_err(|e| LibationError::InvalidInput( - format!("Decrypted license is not valid UTF-8: {}", e) - ))?; + let json_str = String::from_utf8(plaintext_no_nulls).map_err(|e| { + LibationError::InvalidInput(format!("Decrypted license is not valid UTF-8: {}", e)) + })?; // Debug: print decrypted JSON eprintln!("🔍 DEBUG: Decrypted voucher JSON:\n{}\n", json_str); // Parse JSON to get Voucher // Reference: ContentLicenseDtoV10.cs:46 - VoucherDtoV10.FromJson(plainText) - let voucher: Voucher = serde_json::from_str(&json_str) - .map_err(|e| LibationError::InvalidInput( - format!("Failed to parse decrypted voucher JSON: {}\nJSON was: {}", e, json_str) - ))?; - - eprintln!("🔍 DEBUG: Voucher key length: {}, iv length: {:?}", + let voucher: Voucher = serde_json::from_str(&json_str).map_err(|e| { + LibationError::InvalidInput(format!( + "Failed to parse decrypted voucher JSON: {}\nJSON was: {}", + e, json_str + )) + })?; + + eprintln!( + "🔍 DEBUG: Voucher key length: {}, iv length: {:?}", voucher.key.len(), - voucher.iv.as_ref().map(|s| s.len())); + voucher.iv.as_ref().map(|s| s.len()) + ); // Convert voucher to KeyData // Check if key is hex (32 chars) or base64 (24 chars) @@ -526,7 +532,8 @@ impl AudibleClient { /// # use rust_core::api::auth::Account; /// # use rust_core::api::license::LicenseRequest; /// # async fn example() -> rust_core::error::Result<()> { - /// let client = AudibleClient::new(Account::default())?; + /// let account = Account::new("user@example.com".to_string())?; + /// let client = AudibleClient::new(account)?; /// let request = LicenseRequest::default(); /// let license = client.get_download_license("B002V5D7B0", &request).await?; /// println!("DRM type: {:?}", license.drm_type); @@ -544,15 +551,14 @@ impl AudibleClient { // Parse license response // The API may wrap in "content_license" or return directly - let license_json = response - .get("content_license") - .unwrap_or(&response); + let license_json = response.get("content_license").unwrap_or(&response); - serde_json::from_value(license_json.clone()) - .map_err(|e| LibationError::InvalidApiResponse { + serde_json::from_value(license_json.clone()).map_err(|e| { + LibationError::InvalidApiResponse { message: format!("Failed to parse content license: {}", e), response_body: Some(license_json.to_string()), - }) + } + }) } /// Build download license with decryption keys @@ -599,7 +605,7 @@ impl AudibleClient { drm_type: Some(if prefer_widevine { DrmType::Widevine } else { - DrmType::Adrm // Default to Audible DRM (AAX/AAXC) + DrmType::Adrm // Default to Audible DRM (AAX/AAXC) }), }; @@ -619,10 +625,7 @@ impl AudibleClient { // Reference: DownloadOptions.Factory.cs:46-54 - DecryptionKeys = ToKeys(license.Voucher) let decryption_keys = if let Some(ref voucher) = license.voucher { // Structured voucher with key/iv fields (already decrypted) - let key_data = KeyData::from_base64( - &voucher.key, - voucher.iv.as_deref(), - )?; + let key_data = KeyData::from_base64(&voucher.key, voucher.iv.as_deref())?; Some(vec![key_data]) } else if let Some(ref license_response) = license.license_response { // For AAXC files, the license_response is AES-encrypted @@ -630,10 +633,11 @@ impl AudibleClient { // Reference: ContentLicenseDtoV10.cs:13-14, 19-47 let account_lock = self.account(); let account = account_lock.lock().await; - let identity = account.identity.as_ref() - .ok_or_else(|| LibationError::InvalidState( - "No identity in account - cannot decrypt license_response".to_string() - ))?; + let identity = account.identity.as_ref().ok_or_else(|| { + LibationError::InvalidState( + "No identity in account - cannot decrypt license_response".to_string(), + ) + })?; let key_data = KeyData::from_license_response( license_response, @@ -732,7 +736,10 @@ impl AudibleClient { /// /// # Returns /// Output format (M4b or Mp3) - pub fn determine_output_format(license: &DownloadLicense, convert_to_mp3: bool) -> OutputFormat { + pub fn determine_output_format( + license: &DownloadLicense, + convert_to_mp3: bool, + ) -> OutputFormat { // Unencrypted content is always MP3 if !license.drm_type.is_encrypted() { return OutputFormat::Mp3; @@ -824,7 +831,7 @@ impl AudibleClient { _challenge: &[u8], ) -> Result> { Err(LibationError::not_implemented( - "Widevine license exchange requires CDM integration (see license.rs TODO)" + "Widevine license exchange requires CDM integration (see license.rs TODO)", )) } } @@ -850,7 +857,7 @@ mod tests { #[test] fn test_key_data_file_type_aaxc() { let key_data = KeyData { - key_part_1: vec![0; 16], // 16 bytes + key_part_1: vec![0; 16], // 16 bytes key_part_2: Some(vec![0; 16]), // 16 bytes }; @@ -869,7 +876,7 @@ mod tests { #[test] fn test_key_data_from_base64() { - use base64::{Engine as _, engine::general_purpose}; + use base64::{engine::general_purpose, Engine as _}; let key = general_purpose::STANDARD.encode(b"testkey1234567890"); let iv = general_purpose::STANDARD.encode(b"testiv1234567890"); @@ -915,9 +922,9 @@ mod tests { #[tokio::test] #[ignore] // Only run with --ignored flag since it requires real API credentials async fn test_download_book_b07t2f8vjm() { - use crate::api::registration::RegistrationResponse; - use crate::api::client::AudibleClient; use crate::api::auth::Account; + use crate::api::client::AudibleClient; + use crate::api::registration::RegistrationResponse; println!("\n=== Download Book B07T2F8VJM Integration Test ===\n"); @@ -929,12 +936,13 @@ mod tests { .expect("Failed to parse registration response fixture"); let locale = crate::api::auth::Locale::us(); - let identity = reg_response.to_identity(locale) + let identity = reg_response + .to_identity(locale) .expect("Failed to convert registration to identity"); // Create account with identity - let mut account = Account::new(identity.customer_info.user_id.clone()) - .expect("Failed to create account"); + let mut account = + Account::new(identity.customer_info.user_id.clone()).expect("Failed to create account"); account.set_account_name(identity.customer_info.name.clone()); account.set_identity(identity); @@ -942,8 +950,7 @@ mod tests { // Step 2: Create API client println!("\n🔧 Creating Audible API client..."); - let client = AudibleClient::new(account) - .expect("Failed to create API client"); + let client = AudibleClient::new(account).expect("Failed to create API client"); println!("✅ Client created"); // Step 3: Request download license for B07T2F8VJM @@ -951,11 +958,13 @@ mod tests { println!("\n📥 Requesting download license for ASIN: {}", TEST_ASIN); println!(" Quality: High"); - let license_result = client.build_download_license( - TEST_ASIN, - DownloadQuality::High, - false // Don't prefer Widevine (use AAX/AAXC) - ).await; + let license_result = client + .build_download_license( + TEST_ASIN, + DownloadQuality::High, + false, // Don't prefer Widevine (use AAX/AAXC) + ) + .await; let license = match license_result { Ok(lic) => { @@ -971,7 +980,8 @@ mod tests { // Step 4: Display license information println!("\n📋 License Information:"); println!(" DRM Type: {:?}", license.drm_type); - println!(" Content URL: {}", + println!( + " Content URL: {}", if license.download_url.len() > 100 { format!("{}...", &license.download_url[..100]) } else { @@ -990,13 +1000,17 @@ mod tests { // Display activation bytes/keys as hex if keys[0].key_part_1.len() == 4 { // AAX: 4-byte activation bytes - let hex_bytes = keys[0].key_part_1.iter() + let hex_bytes = keys[0] + .key_part_1 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); println!(" Activation Bytes (AAX): {}", hex_bytes); } else if keys[0].key_part_1.len() == 16 { // AAXC: 16-byte key - let hex_key = keys[0].key_part_1.iter() + let hex_key = keys[0] + .key_part_1 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); println!(" Key (AAXC): {}", hex_key); @@ -1005,7 +1019,8 @@ mod tests { if let Some(ref key2) = keys[0].key_part_2 { println!(" Key 2 Length: {} bytes", key2.len()); if key2.len() == 16 { - let hex_iv = key2.iter() + let hex_iv = key2 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); println!(" IV (AAXC): {}", hex_iv); @@ -1038,7 +1053,8 @@ mod tests { if !chapter_info.chapters.is_empty() { println!("\n First Chapters:"); for (i, chapter) in chapter_info.chapters.iter().take(3).enumerate() { - println!(" {}. {} ({}ms - {}ms)", + println!( + " {}. {} ({}ms - {}ms)", i + 1, chapter.title, chapter.start_offset_ms, @@ -1046,7 +1062,10 @@ mod tests { ); } if chapter_info.chapters.len() > 3 { - println!(" ... and {} more chapters", chapter_info.chapters.len() - 3); + println!( + " ... and {} more chapters", + chapter_info.chapters.len() - 3 + ); } } } else { @@ -1127,7 +1146,9 @@ mod tests { // Save activation bytes hex if available if let Some(ref keys) = license.decryption_keys { if !keys.is_empty() && keys[0].key_part_1.len() == 4 { - let hex = keys[0].key_part_1.iter() + let hex = keys[0] + .key_part_1 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); license_json["activation_bytes_hex"] = serde_json::json!(hex); diff --git a/native/rust-core/src/crypto/aax.rs b/native/rust-core/src/crypto/aax.rs index 149b6da..b452a38 100644 --- a/native/rust-core/src/crypto/aax.rs +++ b/native/rust-core/src/crypto/aax.rs @@ -17,7 +17,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! AAX file decryption (legacy Audible format) //! //! # Reference C# Sources @@ -37,7 +36,7 @@ //! 1. Use activation bytes as key //! 2. Call FFmpeg with `-activation_bytes` parameter //! 3. FFmpeg command (from Libation): -//! ``` +//! ```text //! ffmpeg -activation_bytes -i input.aax -vn -c:a copy output.m4b //! ``` //! 4. `-vn`: No video (strip cover art, will re-add later) @@ -53,7 +52,7 @@ //! 5. Write decrypted MP4 //! - FFmpeg approach is simpler and battle-tested -use crate::crypto::activation::{ActivationBytes, format_activation_bytes}; +use crate::crypto::activation::{format_activation_bytes, ActivationBytes}; use crate::error::{LibationError, Result}; use std::path::{Path, PathBuf}; use std::process::Stdio; @@ -195,9 +194,7 @@ async fn check_ffmpeg_available() -> Result<()> { { Ok(status) if status.success() => Ok(()), Ok(_) => Err(LibationError::FfmpegNotFound), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - Err(LibationError::FfmpegNotFound) - } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(LibationError::FfmpegNotFound), Err(e) => Err(LibationError::FfmpegError(format!( "Failed to check FFmpeg availability: {}", e @@ -217,11 +214,7 @@ async fn check_ffmpeg_available() -> Result<()> { /// /// # Returns /// Configured Command ready to execute -fn build_ffmpeg_command( - input: &Path, - output: &Path, - activation_bytes: &str, -) -> Result { +fn build_ffmpeg_command(input: &Path, output: &Path, activation_bytes: &str) -> Result { let mut cmd = Command::new("ffmpeg"); cmd @@ -274,18 +267,21 @@ where })?; // Capture stderr for progress parsing - let stderr = child.stderr.take().ok_or_else(|| { - LibationError::FfmpegError("Failed to capture FFmpeg stderr".to_string()) - })?; + let stderr = child + .stderr + .take() + .ok_or_else(|| LibationError::FfmpegError("Failed to capture FFmpeg stderr".to_string()))?; let mut reader = BufReader::new(stderr).lines(); let mut error_output = String::new(); let mut duration_seconds: Option = None; // Read FFmpeg output line by line - while let Some(line) = reader.next_line().await.map_err(|e| { - LibationError::FfmpegError(format!("Failed to read FFmpeg output: {}", e)) - })? { + while let Some(line) = reader + .next_line() + .await + .map_err(|e| LibationError::FfmpegError(format!("Failed to read FFmpeg output: {}", e)))? + { // Accumulate error output for debugging error_output.push_str(&line); error_output.push('\n'); @@ -421,7 +417,10 @@ fn parse_timestamp(timestamp: &str) -> Option { /// 2. Running FFmpeg with -t 5 to decrypt only first 5 seconds /// 3. Checking if output is valid /// 4. Cleaning up temporary file -pub async fn verify_activation_bytes(file: &Path, activation_bytes: &ActivationBytes) -> Result { +pub async fn verify_activation_bytes( + file: &Path, + activation_bytes: &ActivationBytes, +) -> Result { // Create temporary directory for test output let temp_dir = std::env::temp_dir(); let temp_output = temp_dir.join(format!("aax_verify_{}.m4b", std::process::id())); @@ -543,7 +542,8 @@ mod tests { #[test] fn test_parse_duration_from_line_no_duration() { - let line = "frame= 123 fps= 45 q=-1.0 size= 12345kB time=00:12:34.56 bitrate= 123.4kbits/s"; + let line = + "frame= 123 fps= 45 q=-1.0 size= 12345kB time=00:12:34.56 bitrate= 123.4kbits/s"; assert_eq!(parse_duration_from_line(line), None); } diff --git a/native/rust-core/src/download/persistent_manager.rs b/native/rust-core/src/download/persistent_manager.rs index 221b1d7..871031f 100644 --- a/native/rust-core/src/download/persistent_manager.rs +++ b/native/rust-core/src/download/persistent_manager.rs @@ -664,6 +664,52 @@ impl PersistentDownloadManager { Ok(()) } + /// Update task status and optionally set error/output path. + pub async fn update_task_status_with_details( + &self, + task_id: &str, + status: TaskStatus, + error: Option<&str>, + output_path: Option<&str>, + ) -> Result<()> { + match (error, output_path) { + (Some(err_msg), Some(path)) => { + sqlx::query("UPDATE DownloadTasks SET status = ?, error = ?, output_path = ? WHERE task_id = ?") + .bind(status.as_str()) + .bind(err_msg) + .bind(path) + .bind(task_id) + .execute(&*self.pool) + .await?; + } + (Some(err_msg), None) => { + sqlx::query("UPDATE DownloadTasks SET status = ?, error = ? WHERE task_id = ?") + .bind(status.as_str()) + .bind(err_msg) + .bind(task_id) + .execute(&*self.pool) + .await?; + } + (None, Some(path)) => { + sqlx::query("UPDATE DownloadTasks SET status = ?, error = NULL, output_path = ? WHERE task_id = ?") + .bind(status.as_str()) + .bind(path) + .bind(task_id) + .execute(&*self.pool) + .await?; + } + (None, None) => { + sqlx::query("UPDATE DownloadTasks SET status = ?, error = NULL WHERE task_id = ?") + .bind(status.as_str()) + .bind(task_id) + .execute(&*self.pool) + .await?; + } + } + + Ok(()) + } + /// Store conversion keys and output directory for a task (enables retry without re-download) pub async fn store_conversion_keys(&self, task_id: &str, aaxc_key: &str, aaxc_iv: &str, output_directory: &str) -> Result<()> { sqlx::query("UPDATE DownloadTasks SET aaxc_key = ?, aaxc_iv = ?, output_directory = ? WHERE task_id = ?") diff --git a/native/rust-core/src/jni_bridge.rs b/native/rust-core/src/jni_bridge.rs index 4ba1dd0..24b5169 100644 --- a/native/rust-core/src/jni_bridge.rs +++ b/native/rust-core/src/jni_bridge.rs @@ -17,7 +17,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . - //! JNI bridge for Android - Exposes Rust core functionality to React Native //! //! This module provides JNI wrapper functions that expose the Rust core @@ -55,8 +54,8 @@ use jni::JNIEnv; use serde::{Deserialize, Serialize}; use std::panic::{self, AssertUnwindSafe}; -use std::sync::Mutex; use std::collections::HashMap; +use std::sync::Mutex; // Lazy static tokio runtime for async operations lazy_static::lazy_static! { @@ -69,7 +68,9 @@ lazy_static::lazy_static! { } /// Get or create a download manager for the given database path -async fn get_or_create_manager(db_path: &str) -> crate::Result> { +async fn get_or_create_manager( + db_path: &str, +) -> crate::Result> { let mut managers = DOWNLOAD_MANAGERS.lock().unwrap(); if let Some(manager) = managers.get(db_path) { @@ -81,7 +82,8 @@ async fn get_or_create_manager(db_path: &str) -> crate::Result crate::Result crate::Result { - env.get_string(&jstr) - .map(|s| s.into()) - .map_err(|e| crate::LibationError::InvalidInput(format!("JNI string conversion failed: {}", e))) + env.get_string(&jstr).map(|s| s.into()).map_err(|e| { + crate::LibationError::InvalidInput(format!("JNI string conversion failed: {}", e)) + }) } /// Convert Rust result to JSON response string @@ -109,11 +111,13 @@ fn result_to_json(result: crate::Result) -> String { Ok(data) => serde_json::json!({ "success": true, "data": data - }).to_string(), + }) + .to_string(), Err(e) => serde_json::json!({ "success": false, "error": e.to_string() - }).to_string(), + }) + .to_string(), } } @@ -122,7 +126,8 @@ fn success_response(data: T) -> String { serde_json::json!({ "success": true, "data": data - }).to_string() + }) + .to_string() } /// Create error response JSON @@ -130,7 +135,8 @@ fn error_response(error: &str) -> String { serde_json::json!({ "success": false, "error": error - }).to_string() + }) + .to_string() } /// Wrap a function call with panic catching @@ -212,7 +218,8 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGenera let params_str = match jstring_to_string(&mut env, params_json) { Ok(s) => s, Err(e) => { - return env.new_string(error_response(&e.to_string())) + return env + .new_string(error_response(&e.to_string())) .expect("Failed to create Java string") .into_raw(); } @@ -231,7 +238,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGenera // Get locale let locale = crate::api::auth::Locale::from_country_code(¶ms.locale_code) - .ok_or_else(|| crate::LibationError::InvalidInput(format!("Invalid locale: {}", params.locale_code)))?; + .ok_or_else(|| { + crate::LibationError::InvalidInput(format!( + "Invalid locale: {}", + params.locale_code + )) + })?; // Generate PKCE and state let pkce = crate::api::auth::PkceChallenge::generate()?; @@ -374,7 +386,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeExchan .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; let locale = crate::api::auth::Locale::from_country_code(¶ms.locale_code) - .ok_or_else(|| crate::LibationError::InvalidInput(format!("Invalid locale: {}", params.locale_code)))?; + .ok_or_else(|| { + crate::LibationError::InvalidInput(format!( + "Invalid locale: {}", + params.locale_code + )) + })?; let pkce = crate::api::auth::PkceChallenge { verifier: params.pkce_verifier, @@ -388,7 +405,8 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeExchan ¶ms.authorization_code, ¶ms.device_serial, &pkce, - ).await + ) + .await })?; Ok(success_response(result)) @@ -448,14 +466,20 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeRefres .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; let locale = crate::api::auth::Locale::from_country_code(¶ms.locale_code) - .ok_or_else(|| crate::LibationError::InvalidInput(format!("Invalid locale: {}", params.locale_code)))?; + .ok_or_else(|| { + crate::LibationError::InvalidInput(format!( + "Invalid locale: {}", + params.locale_code + )) + })?; let result = RUNTIME.block_on(async { crate::api::auth::refresh_access_token( &locale, ¶ms.refresh_token, ¶ms.device_serial, - ).await + ) + .await })?; Ok(success_response(result)) @@ -525,9 +549,13 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeEnsure let db = crate::storage::Database::new(¶ms.db_path).await?; // Parse original account to get expiry before refresh - let original_account: crate::api::auth::Account = serde_json::from_str(¶ms.account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; - let original_expiry = original_account.identity.as_ref() + let original_account: crate::api::auth::Account = + serde_json::from_str(¶ms.account_json).map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; + let original_expiry = original_account + .identity + .as_ref() .map(|i| i.access_token.expires_at); // Ensure token is valid @@ -535,12 +563,17 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeEnsure db.pool(), ¶ms.account_json, params.refresh_threshold_minutes, - ).await?; + ) + .await?; // Parse updated account to get new expiry - let updated_account: crate::api::auth::Account = serde_json::from_str(&account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; - let new_expiry = updated_account.identity.as_ref() + let updated_account: crate::api::auth::Account = + serde_json::from_str(&account_json).map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; + let new_expiry = updated_account + .identity + .as_ref() .map(|i| i.access_token.expires_at); let was_refreshed = original_expiry != new_expiry; @@ -605,7 +638,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetAct .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; let locale = crate::api::auth::Locale::from_country_code(¶ms.locale_code) - .ok_or_else(|| crate::LibationError::InvalidInput(format!("Invalid locale: {}", params.locale_code)))?; + .ok_or_else(|| { + crate::LibationError::InvalidInput(format!( + "Invalid locale: {}", + params.locale_code + )) + })?; let result = RUNTIME.block_on(async { crate::api::auth::get_activation_bytes(&locale, ¶ms.access_token).await @@ -677,7 +715,9 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeSyncLi .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; let account: crate::api::auth::Account = serde_json::from_str(¶ms.account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; + .map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; let result = RUNTIME.block_on(async { let db = crate::storage::Database::new(¶ms.db_path).await?; @@ -756,10 +796,13 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeSyncLi db.pool(), ¶ms.account_json, 30, // Refresh if expiring within 30 minutes - ).await?; + ) + .await?; let account: crate::api::auth::Account = serde_json::from_str(&account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; + .map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; let mut client = crate::api::client::AudibleClient::new(account.clone())?; @@ -822,7 +865,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetBoo let result = RUNTIME.block_on(async { let db = crate::storage::Database::new(¶ms.db_path).await?; - let books = crate::storage::queries::list_books_with_relations(db.pool(), params.limit, params.offset).await?; + let books = crate::storage::queries::list_books_with_relations( + db.pool(), + params.limit, + params.offset, + ) + .await?; let total_count = crate::storage::queries::count_books(db.pool()).await?; // Convert BookWithRelations to JSON with arrays for authors/narrators @@ -922,7 +970,11 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetBoo let result = RUNTIME.block_on(async { let db = crate::storage::Database::new(¶ms.db_path).await?; - let book = crate::storage::queries::find_book_with_relations_by_asin(db.pool(), ¶ms.asin).await?; + let book = crate::storage::queries::find_book_with_relations_by_asin( + db.pool(), + ¶ms.asin, + ) + .await?; if let Some(book) = book { let book_json = serde_json::json!({ @@ -960,7 +1012,10 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetBoo }); Ok::<_, crate::LibationError>(book_json) } else { - Err(crate::LibationError::not_found(format!("Book not found: {}", params.asin))) + Err(crate::LibationError::not_found(format!( + "Book not found: {}", + params.asin + ))) } })?; @@ -1023,7 +1078,8 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeSearch db.pool(), ¶ms.query, params.limit, - ).await?; + ) + .await?; let response = serde_json::json!({ "books": books, @@ -1130,8 +1186,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetBoo }; } - let books = crate::storage::queries::list_books_with_filters(db.pool(), &query_params).await?; - let total_count = crate::storage::queries::count_books_with_filters(db.pool(), &query_params).await?; + let books = + crate::storage::queries::list_books_with_filters(db.pool(), &query_params) + .await?; + let total_count = + crate::storage::queries::count_books_with_filters(db.pool(), &query_params) + .await?; // Convert BookWithRelations to JSON with arrays for authors/narrators let books_json: Vec = books.iter().map(|book| { @@ -1384,7 +1444,8 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeDecryp let params: Params = serde_json::from_str(¶ms_str) .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; - let activation_bytes = crate::crypto::activation::ActivationBytes::from_hex(¶ms.activation_bytes)?; + let activation_bytes = + crate::crypto::activation::ActivationBytes::from_hex(¶ms.activation_bytes)?; let result = RUNTIME.block_on(async { let decrypter = crate::crypto::aax::AaxDecrypter::new(activation_bytes); @@ -1523,7 +1584,9 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeValida let params: Params = serde_json::from_str(¶ms_str) .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; - let valid = crate::crypto::activation::ActivationBytes::from_hex(¶ms.activation_bytes).is_ok(); + let valid = + crate::crypto::activation::ActivationBytes::from_hex(¶ms.activation_bytes) + .is_ok(); let response = serde_json::json!({ "valid": valid, @@ -1625,15 +1688,22 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeBuildF let result = RUNTIME.block_on(async { // Get book metadata let db = crate::storage::Database::new(¶ms.db_path).await?; - let book = crate::storage::queries::find_book_with_relations_by_asin(db.pool(), ¶ms.asin).await? - .ok_or_else(|| crate::LibationError::not_found(format!("Book not found: {}", params.asin)))?; + let book = crate::storage::queries::find_book_with_relations_by_asin( + db.pool(), + ¶ms.asin, + ) + .await? + .ok_or_else(|| { + crate::LibationError::not_found(format!("Book not found: {}", params.asin)) + })?; // Convert to AudioMetadata let metadata = book.to_audio_metadata(); // Parse naming pattern - let pattern = crate::file::paths::NamingPattern::from_string(¶ms.naming_pattern) - .unwrap_or(crate::file::paths::NamingPattern::AuthorSeriesBook); + let pattern = + crate::file::paths::NamingPattern::from_string(¶ms.naming_pattern) + .unwrap_or(crate::file::paths::NamingPattern::AuthorSeriesBook); // Build path let file_path = crate::file::paths::build_file_path(&metadata, pattern, "m4b")?; @@ -1709,7 +1779,12 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetCus "es" => crate::api::auth::Locale::es(), "in" => crate::api::auth::Locale::in_(), "jp" => crate::api::auth::Locale::jp(), - _ => return Err(crate::LibationError::InvalidInput(format!("Unknown locale: {}", params.locale_code))), + _ => { + return Err(crate::LibationError::InvalidInput(format!( + "Unknown locale: {}", + params.locale_code + ))) + } }; // Create identity with access token @@ -1810,7 +1885,9 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeDownlo // Parse account let account: crate::api::auth::Account = serde_json::from_str(¶ms.account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; + .map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; // Parse quality let quality = match params.quality.as_str() { @@ -1825,27 +1902,38 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeDownlo let client = crate::api::client::AudibleClient::new(account)?; // Get download license - let license = client.build_download_license(¶ms.asin, quality, false).await?; + let license = client + .build_download_license(¶ms.asin, quality, false) + .await?; // Extract AAXC keys let (key_hex, iv_hex) = if let Some(ref keys) = license.decryption_keys { if !keys.is_empty() && keys[0].key_part_1.len() == 16 { - let key = keys[0].key_part_1.iter() + let key = keys[0] + .key_part_1 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); let iv = if let Some(ref iv_bytes) = keys[0].key_part_2 { - iv_bytes.iter() + iv_bytes + .iter() .map(|b| format!("{:02x}", b)) .collect::() } else { - return Err(crate::LibationError::InvalidInput("No IV in AAXC keys".to_string())); + return Err(crate::LibationError::InvalidInput( + "No IV in AAXC keys".to_string(), + )); }; (key, iv) } else { - return Err(crate::LibationError::InvalidInput("Unsupported key format (only AAXC supported)".to_string())); + return Err(crate::LibationError::InvalidInput( + "Unsupported key format (only AAXC supported)".to_string(), + )); } } else { - return Err(crate::LibationError::InvalidInput("No decryption keys in license".to_string())); + return Err(crate::LibationError::InvalidInput( + "No decryption keys in license".to_string(), + )); }; // Download encrypted file to cache directory @@ -1884,31 +1972,46 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeDownlo use futures_util::StreamExt; use tokio::io::AsyncWriteExt; - let mut file = tokio::fs::File::create(&encrypted_path).await - .map_err(|e| crate::LibationError::internal(format!("Failed to create file {}: {}", encrypted_path, e)))?; + let mut file = tokio::fs::File::create(&encrypted_path) + .await + .map_err(|e| { + crate::LibationError::internal(format!( + "Failed to create file {}: {}", + encrypted_path, e + )) + })?; let mut stream = response.bytes_stream(); while let Some(chunk) = stream.next().await { - let chunk = chunk - .map_err(|e| crate::LibationError::NetworkError { - message: format!("Stream error: {}", e), - is_transient: true, - })?; - file.write_all(&chunk).await - .map_err(|e| crate::LibationError::internal(format!("Write failed: {}", e)))?; + let chunk = chunk.map_err(|e| crate::LibationError::NetworkError { + message: format!("Stream error: {}", e), + is_transient: true, + })?; + file.write_all(&chunk).await.map_err(|e| { + crate::LibationError::internal(format!("Write failed: {}", e)) + })?; } - file.flush().await + file.flush() + .await .map_err(|e| crate::LibationError::internal(format!("Flush failed: {}", e)))?; // Return encrypted file path and decryption keys // The TypeScript/Kotlin layer will use FFmpeg-Kit to decrypt - let file_metadata = tokio::fs::metadata(&encrypted_path).await - .map_err(|e| crate::LibationError::not_found(format!("Downloaded file not found: {}", encrypted_path)))?; + let file_metadata = tokio::fs::metadata(&encrypted_path).await.map_err(|e| { + crate::LibationError::not_found(format!( + "Downloaded file not found: {}", + encrypted_path + )) + })?; // Fetch book metadata from database if db_path provided let book_metadata = if let Some(ref db_path) = params.db_path { let db = crate::storage::Database::new(db_path).await?; - crate::storage::queries::find_book_with_relations_by_asin(db.pool(), ¶ms.asin).await? + crate::storage::queries::find_book_with_relations_by_asin( + db.pool(), + ¶ms.asin, + ) + .await? } else { None }; @@ -2053,7 +2156,9 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetDow // For now, WorkManager backup handles token refresh let account: crate::api::auth::Account = serde_json::from_str(¶ms.account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; + .map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; let quality = match params.quality.as_str() { "Low" => crate::api::content::DownloadQuality::Low, @@ -2064,32 +2169,46 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetDow }; let client = crate::api::client::AudibleClient::new(account)?; - let license = client.build_download_license(¶ms.asin, quality, false).await?; + let license = client + .build_download_license(¶ms.asin, quality, false) + .await?; // Extract AAXC keys let (key_hex, iv_hex) = if let Some(ref keys) = license.decryption_keys { if !keys.is_empty() && keys[0].key_part_1.len() == 16 { - let key = keys[0].key_part_1.iter() + let key = keys[0] + .key_part_1 + .iter() .map(|b| format!("{:02x}", b)) .collect::(); let iv = if let Some(ref iv_bytes) = keys[0].key_part_2 { - iv_bytes.iter() + iv_bytes + .iter() .map(|b| format!("{:02x}", b)) .collect::() } else { - return Err(crate::LibationError::InvalidInput("No IV in AAXC keys".to_string())); + return Err(crate::LibationError::InvalidInput( + "No IV in AAXC keys".to_string(), + )); }; (key, iv) } else { - return Err(crate::LibationError::InvalidInput("Unsupported key format (only AAXC supported)".to_string())); + return Err(crate::LibationError::InvalidInput( + "Unsupported key format (only AAXC supported)".to_string(), + )); } } else { - return Err(crate::LibationError::InvalidInput("No decryption keys in license".to_string())); + return Err(crate::LibationError::InvalidInput( + "No decryption keys in license".to_string(), + )); }; // Build request headers let mut request_headers = std::collections::HashMap::new(); - request_headers.insert("User-Agent".to_string(), "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0".to_string()); + request_headers.insert( + "User-Agent".to_string(), + "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0".to_string(), + ); // Get file size from HTTP HEAD request let http_client = reqwest::Client::new(); @@ -2198,15 +2317,17 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeEnqueu let task_id = RUNTIME.block_on(async { let manager = get_or_create_manager(¶ms.db_path).await?; - manager.enqueue_download( - params.asin, - params.title, - params.download_url, - params.total_bytes, - params.download_path, - params.output_path, - params.request_headers, - ).await + manager + .enqueue_download( + params.asin, + params.title, + params.download_url, + params.total_bytes, + params.download_path, + params.output_path, + params.request_headers, + ) + .await })?; let response = serde_json::json!({ @@ -2504,7 +2625,8 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeCancel /// "db_path": "/data/data/.../audible.db", /// "task_id": "uuid-string", /// "status": "decrypting", -/// "error": null +/// "error": null, +/// "output_path": "content://..." /// } /// ``` #[no_mangle] @@ -2522,6 +2644,7 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeUpdate task_id: String, status: String, error: Option, + output_path: Option, } match (move || -> crate::Result { @@ -2533,11 +2656,14 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeUpdate RUNTIME.block_on(async { let manager = get_or_create_manager(¶ms.db_path).await?; - manager.update_task_status_with_error( - ¶ms.task_id, - status, - params.error.as_deref(), - ).await + manager + .update_task_status_with_details( + ¶ms.task_id, + status, + params.error.as_deref(), + params.output_path.as_deref(), + ) + .await })?; Ok(success_response(serde_json::json!({"success": true}))) @@ -2589,12 +2715,14 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeStoreC RUNTIME.block_on(async { let manager = get_or_create_manager(¶ms.db_path).await?; - manager.store_conversion_keys( - ¶ms.task_id, - ¶ms.aaxc_key, - ¶ms.aaxc_iv, - ¶ms.output_directory, - ).await + manager + .store_conversion_keys( + ¶ms.task_id, + ¶ms.aaxc_key, + ¶ms.aaxc_iv, + ¶ms.output_directory, + ) + .await })?; Ok(success_response(serde_json::json!({"success": true}))) @@ -2655,13 +2783,16 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeSaveAc // Extract account_id from JSON let account: serde_json::Value = serde_json::from_str(¶ms.account_json) - .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; + .map_err(|e| { + crate::LibationError::InvalidInput(format!("Invalid account JSON: {}", e)) + })?; - let account_id = account["account_id"] - .as_str() - .ok_or_else(|| crate::LibationError::InvalidInput("Missing account_id".to_string()))?; + let account_id = account["account_id"].as_str().ok_or_else(|| { + crate::LibationError::InvalidInput("Missing account_id".to_string()) + })?; - crate::storage::accounts::save_account(db.pool(), account_id, ¶ms.account_json).await?; + crate::storage::accounts::save_account(db.pool(), account_id, ¶ms.account_json) + .await?; Ok(success_response(serde_json::json!({"saved": true}))) }) @@ -2734,6 +2865,60 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetPri .into_raw() } +/// Delete account from database +/// +/// # Arguments (JSON string) +/// ```json +/// { +/// "db_path": "/data/data/.../audible.db", +/// "account_id": "account-id" +/// } +/// ``` +/// +/// # Returns (JSON) +/// ```json +/// { +/// "success": true, +/// "data": { "deleted": true } +/// } +/// ``` +#[no_mangle] +pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeDeleteAccount( + mut env: JNIEnv, + _class: JClass, + params_json: JString, +) -> jstring { + let params_str_result = jstring_to_string(&mut env, params_json); + + let response = catch_panic(move || { + #[derive(Deserialize)] + struct Params { + db_path: String, + account_id: String, + } + + match (move || -> crate::Result { + let params_str = params_str_result?; + let params: Params = serde_json::from_str(¶ms_str) + .map_err(|e| crate::LibationError::InvalidInput(format!("Invalid JSON: {}", e)))?; + + RUNTIME.block_on(async { + let db = crate::storage::Database::new(¶ms.db_path).await?; + crate::storage::accounts::delete_account(db.pool(), ¶ms.account_id).await?; + + Ok(success_response(serde_json::json!({"deleted": true}))) + }) + })() { + Ok(result) => result, + Err(e) => error_response(&e.to_string()), + } + }); + + env.new_string(response) + .expect("Failed to create Java string") + .into_raw() +} + /// Clear download state for all books /// /// Resets download status but keeps all book metadata. @@ -2773,8 +2958,11 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeClearD RUNTIME.block_on(async { let db = crate::storage::Database::new(¶ms.db_path).await?; - let books_updated = crate::storage::queries::clear_download_state(db.pool()).await?; - Ok(success_response(serde_json::json!({"books_updated": books_updated}))) + let books_updated = + crate::storage::queries::clear_download_state(db.pool()).await?; + Ok(success_response( + serde_json::json!({"books_updated": books_updated}), + )) }) })() { Ok(result) => result, @@ -2828,8 +3016,11 @@ pub extern "C" fn Java_expo_modules_rustbridge_ExpoRustBridgeModule_nativeGetBoo RUNTIME.block_on(async { let db = crate::storage::Database::new(¶ms.db_path).await?; - let file_path = crate::storage::queries::get_book_file_path(db.pool(), ¶ms.asin).await?; - Ok(success_response(serde_json::json!({"file_path": file_path}))) + let file_path = + crate::storage::queries::get_book_file_path(db.pool(), ¶ms.asin).await?; + Ok(success_response( + serde_json::json!({"file_path": file_path}), + )) }) })() { Ok(result) => result, @@ -3063,7 +3254,8 @@ mod tests { #[test] fn test_result_to_json_error() { - let result: crate::Result = Err(crate::LibationError::InvalidInput("test error".to_string())); + let result: crate::Result = + Err(crate::LibationError::InvalidInput("test error".to_string())); let json = result_to_json(result); assert!(json.contains("\"success\":false")); assert!(json.contains("test error")); diff --git a/native/rust-core/src/storage/accounts.rs b/native/rust-core/src/storage/accounts.rs index 7f4cefb..04068b2 100644 --- a/native/rust-core/src/storage/accounts.rs +++ b/native/rust-core/src/storage/accounts.rs @@ -23,30 +23,22 @@ use sqlx::SqlitePool; /// /// # Returns /// Success status -pub async fn save_account( - pool: &SqlitePool, - account_id: &str, - account_json: &str, -) -> Result<()> { +pub async fn save_account(pool: &SqlitePool, account_id: &str, account_json: &str) -> Result<()> { // Parse JSON to extract key fields let account: serde_json::Value = serde_json::from_str(account_json) .map_err(|e| LibationError::InvalidInput(format!("Invalid account JSON: {}", e)))?; - let account_name = account["account_name"] - .as_str() - .unwrap_or(account_id); + let account_name = account["account_name"].as_str().unwrap_or(account_id); let locale_code = account["locale"]["country_code"] .as_str() .ok_or_else(|| LibationError::InvalidInput("Missing locale country_code".to_string()))?; // Extract identity JSON - let identity_json = account["identity"] - .to_string(); + let identity_json = account["identity"].to_string(); // Extract token expiry if available - let token_expires_at = account["identity"]["access_token"]["expires_at"] - .as_str(); + let token_expires_at = account["identity"]["access_token"]["expires_at"].as_str(); let decrypt_key = account["decrypt_key"].as_str(); @@ -90,10 +82,7 @@ pub async fn save_account( /// /// # Returns /// Complete account JSON or None if not found -pub async fn get_account( - pool: &SqlitePool, - account_id: &str, -) -> Result> { +pub async fn get_account(pool: &SqlitePool, account_id: &str) -> Result> { let row: Option<(String, String, String, String, Option)> = sqlx::query_as( r#" SELECT @@ -112,16 +101,21 @@ pub async fn get_account( if let Some((acc_id, acc_name, locale_code, identity_json, decrypt_key)) = row { // Parse identity JSON from database - let identity: serde_json::Value = serde_json::from_str(&identity_json) - .map_err(|e| LibationError::InvalidState(format!("Corrupt identity JSON in database: {}", e)))?; + let identity: serde_json::Value = serde_json::from_str(&identity_json).map_err(|e| { + LibationError::InvalidState(format!("Corrupt identity JSON in database: {}", e)) + })?; + + let locale = identity.get("locale").cloned().unwrap_or_else(|| { + serde_json::json!({ + "country_code": locale_code + }) + }); // Reconstruct account using serde_json (proper serialization) let mut account = serde_json::json!({ "account_id": acc_id, "account_name": acc_name, - "locale": { - "country_code": locale_code - }, + "locale": locale, "identity": identity, "library_scan": true }); @@ -196,10 +190,7 @@ pub async fn update_token_expiry( /// # Arguments /// * `pool` - Database connection pool /// * `account_id` - Account identifier -pub async fn update_last_sync( - pool: &SqlitePool, - account_id: &str, -) -> Result<()> { +pub async fn update_last_sync(pool: &SqlitePool, account_id: &str) -> Result<()> { sqlx::query( r#" UPDATE Accounts @@ -219,10 +210,7 @@ pub async fn update_last_sync( /// # Arguments /// * `pool` - Database connection pool /// * `account_id` - Account identifier -pub async fn delete_account( - pool: &SqlitePool, - account_id: &str, -) -> Result<()> { +pub async fn delete_account(pool: &SqlitePool, account_id: &str) -> Result<()> { sqlx::query("DELETE FROM Accounts WHERE account_id = ?") .bind(account_id) .execute(pool) @@ -243,11 +231,12 @@ mod tests { let account_json = r#"{ "account_id": "test@example.com", "account_name": "Test Account", - "locale": {"country_code": "us"}, + "locale": {"country_code": "us", "name": "United States", "domain": "audible.com", "with_username": true}, "identity": { "access_token": {"token": "abc123", "expires_at": "2025-01-01T00:00:00Z"}, "refresh_token": "xyz789", - "device_serial_number": "ABC123" + "device_serial_number": "ABC123", + "locale": {"country_code": "us", "name": "United States", "domain": "audible.com", "with_username": true} }, "decrypt_key": "12345678" }"#; @@ -267,6 +256,8 @@ mod tests { let retrieved_json: serde_json::Value = serde_json::from_str(&retrieved).unwrap(); assert_eq!(retrieved_json["account_id"], "test@example.com"); assert_eq!(retrieved_json["locale"]["country_code"], "us"); + assert_eq!(retrieved_json["locale"]["domain"], "audible.com"); + assert_eq!(retrieved_json["locale"]["with_username"], true); } #[tokio::test] @@ -276,8 +267,12 @@ mod tests { let account1 = r#"{"account_id": "first@example.com", "account_name": "First", "locale": {"country_code": "us"}, "identity": {"access_token": {"token": "a"},"refresh_token": "b","device_serial_number": "c"}}"#; let account2 = r#"{"account_id": "second@example.com", "account_name": "Second", "locale": {"country_code": "uk"}, "identity": {"access_token": {"token": "d"},"refresh_token": "e","device_serial_number": "f"}}"#; - save_account(db.pool(), "first@example.com", account1).await.unwrap(); - save_account(db.pool(), "second@example.com", account2).await.unwrap(); + save_account(db.pool(), "first@example.com", account1) + .await + .unwrap(); + save_account(db.pool(), "second@example.com", account2) + .await + .unwrap(); // Primary should be first one created let primary = get_primary_account(db.pool()).await.unwrap().unwrap(); diff --git a/native/rust-core/test_fixtures/registration_response.json b/native/rust-core/test_fixtures/registration_response.json new file mode 100644 index 0000000..a007fae --- /dev/null +++ b/native/rust-core/test_fixtures/registration_response.json @@ -0,0 +1,84 @@ +{ + "request_id": "8e570a18-8232-4df0-a212-c0d21860fcd5", + "response": { + "success": { + "customer_id": "amzn1.account.AGMGLSGIFYVALF2MEO4F3JJQRLSA", + "tokens": { + "bearer": { + "access_token": "Atna|synthetic-access-token-not-valid-for-api-use-000000000000000000000000000000000000000000000000000000000000", + "refresh_token": "Atnr|synthetic-refresh-token-not-valid-for-api-use-000000000000000000000000000000000000000000000000000000000000", + "expires_in": "3600" + }, + "mac_dms": { + "device_private_key": "MIIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "adp_token": "{enc:synthetic-encrypted-payload}{key:synthetic-key}{iv:synthetic-iv}{name:Henning's 7th Android}{serial:B45EF975C33A7B7E8DAF4D96E39B8040}" + }, + "website_cookies": [ + { + "Name": "session-id", + "Value": "139-0923163-3019105", + "Domain": ".amazon.com", + "Path": "/", + "Expires": "2 Oct 2045 16:26:51 GMT", + "Secure": "true", + "HttpOnly": "false" + }, + { + "Name": "ubid-main", + "Value": "synthetic-ubid-main", + "Domain": ".amazon.com", + "Path": "/", + "Expires": "2 Oct 2045 16:26:51 GMT", + "Secure": "true", + "HttpOnly": "false" + }, + { + "Name": "x-main", + "Value": "synthetic-x-main", + "Domain": ".amazon.com", + "Path": "/", + "Expires": "7 Oct 2026 16:26:51 GMT", + "Secure": "true", + "HttpOnly": "true" + }, + { + "Name": "at-main", + "Value": "\"Atza|synthetic-cookie-not-valid-for-api-use-000000000000000000000000000000000000000000\"", + "Domain": ".amazon.com", + "Path": "/", + "Expires": "7 Oct 2026 16:26:51 GMT", + "Secure": "true", + "HttpOnly": "true" + }, + { + "Name": "sess-at-main", + "Value": "\"synthetic-session-cookie-not-valid-for-api-use\"", + "Domain": ".amazon.com", + "Path": "/", + "Expires": "7 Oct 2026 16:26:51 GMT", + "Secure": "true", + "HttpOnly": "true" + } + ], + "store_authentication_cookie": { + "cookie": "synthetic-store-auth-cookie-not-valid-for-api-use-000000000000000000000000000000000000000000" + }, + "website_cookies_ttl": 31536000 + }, + "extensions": { + "device_info": { + "device_name": "Henning's 7th Android", + "device_serial_number": "B45EF975C33A7B7E8DAF4D96E39B8040", + "device_type": "A10KISP2GWF0E4" + }, + "customer_info": { + "account_pool": "Amazon", + "user_id": "amzn1.account.AGMGLSGIFYVALF2MEO4F3JJQRLSA", + "home_region": "NA", + "name": "Henning Berge", + "given_name": "Henning" + } + } + } + } +} diff --git a/native/rust-core/tests/download_manager_integration.rs b/native/rust-core/tests/download_manager_integration.rs index 1680b82..8318076 100644 --- a/native/rust-core/tests/download_manager_integration.rs +++ b/native/rust-core/tests/download_manager_integration.rs @@ -16,10 +16,13 @@ const TEST_ASIN: &str = "B07NP9L44Y"; /// Load account from test fixture fn load_test_account() -> Result> { - use rust_core::api::auth::{Identity, Locale, AccessToken}; + use rust_core::api::auth::{AccessToken, Identity, Locale}; use rust_core::api::registration::RegistrationResponse; - let fixture_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test_fixtures/registration_response.json"); + let fixture_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/test_fixtures/registration_response.json" + ); let fixture_data = std::fs::read_to_string(fixture_path)?; let reg_response: RegistrationResponse = serde_json::from_str(&fixture_data)?; @@ -30,7 +33,8 @@ fn load_test_account() -> Result> { let access_token = AccessToken { token: bearer.access_token.clone(), - expires_at: chrono::Utc::now() + chrono::Duration::seconds(bearer.expires_in.parse::().unwrap_or(3600)), + expires_at: chrono::Utc::now() + + chrono::Duration::seconds(bearer.expires_in.parse::().unwrap_or(3600)), }; let identity = Identity::new( @@ -69,11 +73,9 @@ async fn test_download_book_with_manager() -> Result<(), Box Result<(), Box Result<(), Box().ok()) .unwrap_or(0); - println!(" ✓ File size: {} bytes ({:.2} MB)", total_bytes, total_bytes as f64 / 1024.0 / 1024.0); + println!( + " ✓ File size: {} bytes ({:.2} MB)", + total_bytes, + total_bytes as f64 / 1024.0 / 1024.0 + ); // Enqueue download println!("\n5. Setting up download manager..."); - let task_id = manager.enqueue_download( - TEST_ASIN.to_string(), - "A Mind of Her Own".to_string(), - license.download_url.clone(), - total_bytes, - encrypted_path.to_str().unwrap().to_string(), - output_path.to_str().unwrap().to_string(), - request_headers, - ).await?; + let task_id = manager + .enqueue_download( + TEST_ASIN.to_string(), + "A Mind of Her Own".to_string(), + license.download_url.clone(), + total_bytes, + encrypted_path.to_str().unwrap().to_string(), + output_path.to_str().unwrap().to_string(), + request_headers, + ) + .await?; println!(" ✓ Download enqueued: {}", task_id); // Monitor progress @@ -161,7 +177,8 @@ async fn test_download_book_with_manager() -> Result<(), Box= Duration::from_secs(2) || task.status != TaskStatus::Downloading { + if last_print.elapsed() >= Duration::from_secs(2) || task.status != TaskStatus::Downloading + { let percentage = task.progress_percentage(); let speed = if task.bytes_downloaded > last_progress { let bytes_diff = task.bytes_downloaded - last_progress; @@ -177,11 +194,7 @@ async fn test_download_book_with_manager() -> Result<(), Box Result<(), Box 0 { @@ -222,9 +239,15 @@ async fn test_download_book_with_manager() -> Result<(), Box Result<(), Box> // Get license println!("\n2. Getting download license..."); let client = AudibleClient::new(account.clone())?; - let license = client.build_download_license(TEST_ASIN, DownloadQuality::High, false).await?; + let license = client + .build_download_license(TEST_ASIN, DownloadQuality::High, false) + .await?; println!(" ✓ License obtained"); // Setup manager @@ -260,7 +285,10 @@ async fn test_pause_resume_download() -> Result<(), Box> let output_path = download_dir.join(format!("{}.m4b", TEST_ASIN)); let mut request_headers = std::collections::HashMap::new(); - request_headers.insert("User-Agent".to_string(), "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0".to_string()); + request_headers.insert( + "User-Agent".to_string(), + "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0".to_string(), + ); // Get file size from HTTP HEAD let http_client = reqwest::Client::new(); @@ -279,15 +307,17 @@ async fn test_pause_resume_download() -> Result<(), Box> // Enqueue download println!("\n3. Starting download..."); - let task_id = manager.enqueue_download( - TEST_ASIN.to_string(), - "Test Book".to_string(), - license.download_url.clone(), - total_bytes, - encrypted_path.to_str().unwrap().to_string(), - output_path.to_str().unwrap().to_string(), - request_headers, - ).await?; + let task_id = manager + .enqueue_download( + TEST_ASIN.to_string(), + "Test Book".to_string(), + license.download_url.clone(), + total_bytes, + encrypted_path.to_str().unwrap().to_string(), + output_path.to_str().unwrap().to_string(), + request_headers, + ) + .await?; println!(" ✓ Download started: {}", task_id); // Wait for some progress (5 seconds) @@ -295,7 +325,8 @@ async fn test_pause_resume_download() -> Result<(), Box> tokio::time::sleep(Duration::from_secs(5)).await; let task_before_pause = manager.get_task(&task_id).await?; - println!(" ✓ Progress: {:.1}% ({} bytes)", + println!( + " ✓ Progress: {:.1}% ({} bytes)", task_before_pause.progress_percentage(), task_before_pause.bytes_downloaded ); @@ -307,7 +338,10 @@ async fn test_pause_resume_download() -> Result<(), Box> let task_paused = manager.get_task(&task_id).await?; assert_eq!(task_paused.status, TaskStatus::Paused); - println!(" ✓ Download paused at {} bytes", task_paused.bytes_downloaded); + println!( + " ✓ Download paused at {} bytes", + task_paused.bytes_downloaded + ); // Resume download println!("\n6. Resuming download..."); @@ -318,7 +352,8 @@ async fn test_pause_resume_download() -> Result<(), Box> let task_resumed = manager.get_task(&task_id).await?; println!(" ✓ Download resumed: {:?}", task_resumed.status); - println!(" ✓ Current progress: {:.1}% ({} bytes)", + println!( + " ✓ Current progress: {:.1}% ({} bytes)", task_resumed.progress_percentage(), task_resumed.bytes_downloaded ); @@ -337,6 +372,7 @@ async fn test_pause_resume_download() -> Result<(), Box> } #[tokio::test] +#[ignore] // Requires public internet access; run explicitly with --ignored. async fn test_download_manager_with_public_url() -> Result<(), Box> { println!("\n=== Testing PersistentDownloadManager with public file ===\n"); @@ -376,19 +412,25 @@ async fn test_download_manager_with_public_url() -> Result<(), Box().ok()) .unwrap_or(0); - println!(" ✓ File size: {} bytes ({:.2} MB)", total_bytes, total_bytes as f64 / 1024.0 / 1024.0); + println!( + " ✓ File size: {} bytes ({:.2} MB)", + total_bytes, + total_bytes as f64 / 1024.0 / 1024.0 + ); // Enqueue download println!("\n3. Enqueueing download..."); - let task_id = manager.enqueue_download( - "TEST001".to_string(), - "Test File".to_string(), - test_url.to_string(), - total_bytes, - download_path.to_str().unwrap().to_string(), - output_path.to_str().unwrap().to_string(), - headers, - ).await?; + let task_id = manager + .enqueue_download( + "TEST001".to_string(), + "Test File".to_string(), + test_url.to_string(), + total_bytes, + download_path.to_str().unwrap().to_string(), + output_path.to_str().unwrap().to_string(), + headers, + ) + .await?; println!(" ✓ Download enqueued: {}", task_id); // Monitor progress @@ -403,7 +445,8 @@ async fn test_download_manager_with_public_url() -> Result<(), Box= Duration::from_secs(1) || task.status != TaskStatus::Downloading { + if last_print.elapsed() >= Duration::from_secs(1) || task.status != TaskStatus::Downloading + { let percentage = task.progress_percentage(); let speed = if task.bytes_downloaded > last_progress { let bytes_diff = task.bytes_downloaded - last_progress; @@ -419,11 +462,7 @@ async fn test_download_manager_with_public_url() -> Result<(), Box Result<(), Box 0 { @@ -485,15 +528,17 @@ async fn test_list_downloads() -> Result<(), Box> { headers.insert("User-Agent".to_string(), "Test".to_string()); for i in 1..=5 { - let task_id = manager.enqueue_download( - format!("B00{}", i), - format!("Test Book {}", i), - format!("https://example.com/book{}.aax", i), - 1000000 * i as u64, - format!("/tmp/book{}.aax", i), - format!("/tmp/book{}.m4b", i), - headers.clone(), - ).await?; + let task_id = manager + .enqueue_download( + format!("B00{}", i), + format!("Test Book {}", i), + format!("https://example.com/book{}.aax", i), + 1000000 * i as u64, + format!("/tmp/book{}.aax", i), + format!("/tmp/book{}.m4b", i), + headers.clone(), + ) + .await?; task_ids.push(task_id); } println!(" ✓ Enqueued 5 downloads"); diff --git a/package.json b/package.json index 7ea05a7..5cbe283 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "ios": "npm run build:rust:ios && expo run:ios", "ios:dev": "expo start --ios", "web": "expo start --web", + "typecheck": "tsc --noEmit", "build:rust": "./scripts/build-rust.sh", "build:rust:android": "./scripts/build-rust-android.sh", "build:rust:ios": "./scripts/build-rust-ios.sh", diff --git a/plugins/withDownloadService.js b/plugins/withDownloadService.js index cb2f27a..4c91ac6 100644 --- a/plugins/withDownloadService.js +++ b/plugins/withDownloadService.js @@ -1,7 +1,7 @@ const { withAndroidManifest } = require('@expo/config-plugins'); /** - * Expo config plugin to add DownloadService and DownloadActionReceiver to AndroidManifest.xml + * Expo config plugin to add background/download services and receivers to AndroidManifest.xml * * This ensures that when running `npx expo prebuild`, the service and receiver are properly * declared in the generated AndroidManifest.xml. @@ -9,53 +9,117 @@ const { withAndroidManifest } = require('@expo/config-plugins'); const withDownloadService = (config) => { return withAndroidManifest(config, async (config) => { const androidManifest = config.modResults; + const manifest = androidManifest.manifest; const application = androidManifest.manifest.application[0]; - // Add DownloadService - const serviceExists = application.service?.some( - (service) => service.$['android:name'] === 'expo.modules.rustbridge.DownloadService' - ); + const ensurePermission = (name) => { + if (!manifest['uses-permission']) { + manifest['uses-permission'] = []; + } + + const exists = manifest['uses-permission'].some( + (permission) => permission.$['android:name'] === name + ); + + if (!exists) { + manifest['uses-permission'].push({ + $: { + 'android:name': name, + }, + }); + } + }; - if (!serviceExists) { + const ensureService = (name, attrs) => { if (!application.service) { application.service = []; } - application.service.push({ - $: { - 'android:name': 'expo.modules.rustbridge.DownloadService', - 'android:exported': 'false', - 'android:foregroundServiceType': 'dataSync', - }, - }); - } + const existing = application.service.find( + (service) => service.$['android:name'] === name + ); - // Add DownloadActionReceiver - const receiverExists = application.receiver?.some( - (receiver) => receiver.$['android:name'] === 'expo.modules.rustbridge.DownloadActionReceiver' - ); + if (existing) { + existing.$ = { + ...existing.$, + ...attrs, + }; + } else { + application.service.push({ + $: { + 'android:name': name, + ...attrs, + }, + }); + } + }; - if (!receiverExists) { + const ensureReceiver = (name, attrs, actions) => { if (!application.receiver) { application.receiver = []; } - application.receiver.push({ - $: { - 'android:name': 'expo.modules.rustbridge.DownloadActionReceiver', - 'android:exported': 'false', - }, - 'intent-filter': [ - { - action: [ - { $: { 'android:name': 'expo.modules.rustbridge.ACTION_PAUSE' } }, - { $: { 'android:name': 'expo.modules.rustbridge.ACTION_RESUME' } }, - { $: { 'android:name': 'expo.modules.rustbridge.ACTION_CANCEL' } }, - ], + const existing = application.receiver.find( + (receiver) => receiver.$['android:name'] === name + ); + + const intentFilter = { + action: actions.map((action) => ({ + $: { 'android:name': action }, + })), + }; + + if (existing) { + existing.$ = { + ...existing.$, + ...attrs, + }; + existing['intent-filter'] = [intentFilter]; + } else { + application.receiver.push({ + $: { + 'android:name': name, + ...attrs, }, - ], - }); - } + 'intent-filter': [intentFilter], + }); + } + }; + + ensurePermission('android.permission.RECEIVE_BOOT_COMPLETED'); + + // Add DownloadService + ensureService('expo.modules.rustbridge.DownloadService', { + 'android:exported': 'false', + 'android:foregroundServiceType': 'dataSync', + }); + + // Add BackgroundTaskService + ensureService('expo.modules.rustbridge.tasks.BackgroundTaskService', { + 'android:exported': 'false', + 'android:foregroundServiceType': 'dataSync', + }); + + // Add DownloadActionReceiver + ensureReceiver( + 'expo.modules.rustbridge.DownloadActionReceiver', + { 'android:exported': 'false' }, + [ + 'expo.modules.rustbridge.PAUSE_DOWNLOAD', + 'expo.modules.rustbridge.RESUME_DOWNLOAD', + 'expo.modules.rustbridge.CANCEL_DOWNLOAD', + ] + ); + + // Add BootReceiver + ensureReceiver( + 'expo.modules.rustbridge.tasks.BootReceiver', + { 'android:exported': 'false' }, + [ + 'android.intent.action.BOOT_COMPLETED', + 'android.intent.action.MY_PACKAGE_REPLACED', + ] + ); return config; }); diff --git a/scripts/build-rust-android.sh b/scripts/build-rust-android.sh index bfef533..2c7dfda 100755 --- a/scripts/build-rust-android.sh +++ b/scripts/build-rust-android.sh @@ -30,18 +30,46 @@ fi NDK_PATH="${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}" echo -e "${GREEN}Using NDK at: $NDK_PATH${NC}" -# Add NDK toolchain to PATH (works on both Intel and Apple Silicon Macs) -export PATH="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin:$PATH" +# Detect the host toolchain directory used by the Android NDK. +HOST_TAG="linux-x86_64" +case "$(uname -s)" in + Darwin) + HOST_TAG="darwin-x86_64" + ;; + Linux) + HOST_TAG="linux-x86_64" + ;; + *) + echo -e "${RED}Unsupported host OS: $(uname -s)${NC}" + exit 1 + ;; +esac + +TOOLCHAIN_BIN="$NDK_PATH/toolchains/llvm/prebuilt/$HOST_TAG/bin" +if [ ! -d "$TOOLCHAIN_BIN" ]; then + echo -e "${RED}NDK toolchain not found: $TOOLCHAIN_BIN${NC}" + exit 1 +fi + +echo -e "${GREEN}Using NDK host toolchain: $HOST_TAG${NC}" +export PATH="$TOOLCHAIN_BIN:$PATH" # Set up cross-compilation environment for all architectures -export CC_aarch64_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android30-clang" -export AR_aarch64_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" -export CC_armv7_linux_androideabi="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi30-clang" -export AR_armv7_linux_androideabi="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" -export CC_i686_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/i686-linux-android30-clang" -export AR_i686_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" -export CC_x86_64_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android30-clang" -export AR_x86_64_linux_android="$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" +export CC_aarch64_linux_android="$TOOLCHAIN_BIN/aarch64-linux-android30-clang" +export AR_aarch64_linux_android="$TOOLCHAIN_BIN/llvm-ar" +export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TOOLCHAIN_BIN/aarch64-linux-android30-clang" + +export CC_armv7_linux_androideabi="$TOOLCHAIN_BIN/armv7a-linux-androideabi30-clang" +export AR_armv7_linux_androideabi="$TOOLCHAIN_BIN/llvm-ar" +export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$TOOLCHAIN_BIN/armv7a-linux-androideabi30-clang" + +export CC_i686_linux_android="$TOOLCHAIN_BIN/i686-linux-android30-clang" +export AR_i686_linux_android="$TOOLCHAIN_BIN/llvm-ar" +export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$TOOLCHAIN_BIN/i686-linux-android30-clang" + +export CC_x86_64_linux_android="$TOOLCHAIN_BIN/x86_64-linux-android30-clang" +export AR_x86_64_linux_android="$TOOLCHAIN_BIN/llvm-ar" +export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TOOLCHAIN_BIN/x86_64-linux-android30-clang" # Install Rust targets if not already installed echo -e "${YELLOW}Checking Rust targets...${NC}" diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx index 0afe7df..499e2ac 100644 --- a/src/screens/LibraryScreen.tsx +++ b/src/screens/LibraryScreen.tsx @@ -24,11 +24,13 @@ import { setBookFilePath, createCoverArtFile, requestNotificationPermission, + getPrimaryAccount, + saveAccount, } from '../../modules/expo-rust-bridge'; import type {Book, Account, DownloadTask} from '../../modules/expo-rust-bridge'; -import {Paths} from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; import * as DocumentPicker from 'expo-document-picker'; +import {getDatabasePath} from '../utils/appPaths'; const DOWNLOAD_PATH_KEY = 'download_path'; const LIBRARY_PREFS_KEY = 'library_preferences'; @@ -116,9 +118,7 @@ export default function LibraryScreen() { useEffect(() => { const pollProgress = () => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const tasks = listDownloadTasks(dbPath); const taskMap = new Map(); @@ -170,9 +170,7 @@ export default function LibraryScreen() { const loadFilterOptions = async () => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); try { initializeDatabase(dbPath); @@ -191,9 +189,7 @@ export default function LibraryScreen() { const loadBooks = async (reset: boolean = false) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); console.log('[LibraryScreen] Loading books from:', dbPath); @@ -366,14 +362,15 @@ export default function LibraryScreen() { return; } - const accountData = await SecureStore.getItemAsync('audible_account'); - if (!accountData) { + const dbPath = getDatabasePath(); + initializeDatabase(dbPath); + + let account = await getPrimaryAccount(dbPath); + if (!account) { Alert.alert('Error', 'Please log in first'); return; } - let account: Account = JSON.parse(accountData); - if (account.identity?.access_token) { const expiresAt = new Date(account.identity.access_token.expires_at); const now = new Date(); @@ -390,7 +387,7 @@ export default function LibraryScreen() { const newExpiresAt = new Date(Date.now() + parseInt(newTokens.expires_in.toString()) * 1000).toISOString(); account.identity.access_token.expires_at = newExpiresAt; - await SecureStore.setItemAsync('audible_account', JSON.stringify(account)); + await saveAccount(dbPath, account); console.log('[LibraryScreen] Token refreshed successfully'); } catch (refreshError) { console.error('[LibraryScreen] Token refresh failed:', refreshError); @@ -439,9 +436,7 @@ export default function LibraryScreen() { const handlePauseDownload = (book: Book) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const task = downloadTasks.get(book.audible_product_id); if (task) { @@ -455,9 +450,7 @@ export default function LibraryScreen() { const handleResumeDownload = (book: Book) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const task = downloadTasks.get(book.audible_product_id); if (task) { @@ -471,9 +464,7 @@ export default function LibraryScreen() { const handleCancelDownload = (book: Book) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const task = downloadTasks.get(book.audible_product_id); if (task) { @@ -500,9 +491,7 @@ export default function LibraryScreen() { const handleRetryConversion = async (book: Book) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); await retryConversion(dbPath, book.audible_product_id); console.log('[LibraryScreen] Conversion retry started:', book.title); @@ -514,9 +503,7 @@ export default function LibraryScreen() { const handleMarkAsNotDownloaded = async (book: Book) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); // Check if file exists const filePath = await getBookFilePath(dbPath, book.audible_product_id); @@ -548,11 +535,26 @@ export default function LibraryScreen() { onPress: async () => { try { const result = await clearBookDownloadState(dbPath, book.audible_product_id, true); - console.log('[LibraryScreen] Deleted file and cleared status:', book.title); if (result.file_deleted) { - Alert.alert('Success', `File deleted and download status cleared for "${book.title}".`); + console.log('[LibraryScreen] Deleted file and cleared status:', { + title: book.title, + coverDeleted: result.cover_deleted, + bookFolderDeleted: result.book_folder_deleted, + authorFolderDeleted: result.author_folder_deleted, + cleanupError: result.cleanup_error, + }); + if (result.cleanup_error) { + Alert.alert('Partial Cleanup', `File deleted and download status cleared for "${book.title}".\n\nCleanup error: ${result.cleanup_error}`); + } else { + Alert.alert('Success', `File deleted and download status cleared for "${book.title}".`); + } } else { - Alert.alert('Partial Success', `Download status cleared, but file could not be deleted.\n\nYou may need to delete it manually.`); + console.warn('[LibraryScreen] Cleared status but file delete failed:', { + title: book.title, + error: result.delete_error, + }); + const deleteError = result.delete_error ? `\n\nDelete error: ${result.delete_error}` : ''; + Alert.alert('Partial Success', `Download status cleared, but file could not be deleted.${deleteError}\n\nYou may need to delete it manually.`); } loadBooks(true); } catch (error: any) { @@ -607,9 +609,7 @@ export default function LibraryScreen() { } const file = result.assets[0]; - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); console.log('[LibraryScreen] Selected file:', file.uri); @@ -635,9 +635,7 @@ export default function LibraryScreen() { } // Get the book's file path - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const filePath = await getBookFilePath(dbPath, book.audible_product_id); diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index eef2ae0..f53f50b 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -8,7 +8,6 @@ import { RustBridgeError, } from '../../modules/expo-rust-bridge'; import type { Account, Locale } from '../../modules/expo-rust-bridge'; -import * as SecureStore from 'expo-secure-store'; import { useStyles } from '../hooks/useStyles'; import { useTheme } from '../styles/theme'; import type { Theme } from '../hooks/useStyles'; @@ -61,8 +60,7 @@ export default function LoginScreen({ onLoginSuccess }: LoginScreenProps) { console.log('[LoginScreen] Starting OAuth flow for region:', region.name, '(' + region.code + ')'); const flowData = initiateOAuth(region.code); - console.log('[LoginScreen] OAuth URL generated:', flowData.url); - console.log('[LoginScreen] Device serial:', flowData.deviceSerial); + console.log('[LoginScreen] OAuth URL generated'); oauthDataRef.current = { pkceVerifier: flowData.pkceVerifier, @@ -94,7 +92,7 @@ export default function LoginScreen({ onLoginSuccess }: LoginScreenProps) { // Handle WebView navigation state changes const handleNavigationStateChange = async (navState: any) => { const { url } = navState; - console.log('[LoginScreen] WebView navigated to:', url); + console.log('[LoginScreen] WebView navigation event'); // Log all maplanding URLs to debug OAuth flow if (url.includes('/ap/maplanding')) { @@ -178,17 +176,7 @@ export default function LoginScreen({ onLoginSuccess }: LoginScreenProps) { }, }; - console.log('[LoginScreen] Skipping activation bytes during login'); - - // Store account in secure storage - console.log('[LoginScreen] Storing account in secure storage...'); - await SecureStore.setItemAsync('audible_account', JSON.stringify(account)); - - // Store token expiry if available - if (account.identity?.access_token?.expires_at) { - console.log('[LoginScreen] Storing token expiry:', account.identity.access_token.expires_at); - await SecureStore.setItemAsync('token_expires_at', account.identity.access_token.expires_at); - } + console.log('[LoginScreen] Skipping activation bytes during login'); setStatus('Login successful!'); console.log('[LoginScreen] Login complete! Calling onLoginSuccess'); diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index ccf335b..94b7d11 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -6,7 +6,7 @@ import Constants from 'expo-constants'; import { useStyles } from '../hooks/useStyles'; import { useTheme } from '../styles/theme'; import type { Theme } from '../hooks/useStyles'; -import { Directory, File, Paths } from 'expo-file-system'; +import { Directory, Paths } from 'expo-file-system'; import * as SecureStore from 'expo-secure-store'; import { scheduleTokenRefresh, @@ -15,6 +15,7 @@ import { cancelLibrarySync, ExpoRustBridge, } from '../../modules/expo-rust-bridge'; +import { getDatabaseFiles } from '../utils/appPaths'; const DOWNLOAD_PATH_KEY = 'download_path'; const NAMING_PATTERN_KEY = 'naming_pattern'; @@ -346,28 +347,12 @@ export default function SettingsScreen() { try { console.log('[Settings] Deleting database files...'); - // Delete main database file - const dbFile = new File(Paths.cache, 'audible.db'); - console.log('[Settings] Database exists:', dbFile.exists); - if (dbFile.exists) { - await dbFile.delete(); - console.log('[Settings] Deleted audible.db'); - } - - // Delete WAL file - const walFile = new File(Paths.cache, 'audible.db-wal'); - console.log('[Settings] WAL exists:', walFile.exists); - if (walFile.exists) { - await walFile.delete(); - console.log('[Settings] Deleted audible.db-wal'); - } - - // Delete SHM file - const shmFile = new File(Paths.cache, 'audible.db-shm'); - console.log('[Settings] SHM exists:', shmFile.exists); - if (shmFile.exists) { - await shmFile.delete(); - console.log('[Settings] Deleted audible.db-shm'); + for (const dbFile of getDatabaseFiles()) { + console.log('[Settings] Database file exists:', dbFile.name, dbFile.exists); + if (dbFile.exists) { + await dbFile.delete(); + console.log('[Settings] Deleted database file:', dbFile.name); + } } Alert.alert( diff --git a/src/screens/SimpleAccountScreen.tsx b/src/screens/SimpleAccountScreen.tsx index 44d339a..709bd1d 100644 --- a/src/screens/SimpleAccountScreen.tsx +++ b/src/screens/SimpleAccountScreen.tsx @@ -13,16 +13,17 @@ import { getCustomerInformation, saveAccount, getPrimaryAccount, + deleteAccount, SyncStats, cancelAllBackgroundTasks, scheduleLibrarySync, scheduleTokenRefresh, } from '../../modules/expo-rust-bridge'; import type { Account } from '../../modules/expo-rust-bridge'; -import { Paths } from 'expo-file-system'; import { useStyles } from '../hooks/useStyles'; import { useTheme } from '../styles/theme'; import type { Theme } from '../hooks/useStyles'; +import { getDatabasePath } from '../utils/appPaths'; export default function SimpleAccountScreen() { const styles = useStyles(createStyles); @@ -61,10 +62,7 @@ export default function SimpleAccountScreen() { const loadAccount = async () => { try { - // Get database path - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); console.log('[SimpleAccountScreen] Loading account from SQLite database'); initializeDatabase(dbPath); @@ -77,12 +75,19 @@ export default function SimpleAccountScreen() { setAccount(loadedAccount); // Load token expiry - await loadTokenInfo(); + await loadTokenInfo(loadedAccount); // Load previously synced book count await loadSyncedBooks(loadedAccount); } else { console.log('[SimpleAccountScreen] No account found in SQLite database'); + setAccount(null); + setSyncStats(null); + setLastSyncDate(null); + setTokenExpiry(null); + setTimeRemaining(null); + setAccountName(null); + setConnectionStatus('checking'); } } catch (error) { console.error('[SimpleAccountScreen] Failed to load account:', error); @@ -93,9 +98,7 @@ export default function SimpleAccountScreen() { const loadSyncedBooks = async (acc: Account) => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); console.log('[SimpleAccountScreen] Checking for synced books at:', dbPath); @@ -142,15 +145,15 @@ export default function SimpleAccountScreen() { } }; - const loadTokenInfo = async () => { + const loadTokenInfo = async (sourceAccount: Account | null = account) => { try { let expiryStr = await SecureStore.getItemAsync('token_expires_at'); // If not found, try to extract from account identity - if (!expiryStr && account?.identity) { + if (!expiryStr && sourceAccount?.identity) { console.log('[SimpleAccountScreen] Token expiry not in SecureStore, extracting from account'); // Access token is an object with token and expires_at properties - const accessToken = account.identity.access_token; + const accessToken = sourceAccount.identity.access_token; if (typeof accessToken === 'object' && accessToken.expires_at) { expiryStr = accessToken.expires_at; } @@ -232,10 +235,7 @@ export default function SimpleAccountScreen() { console.log('[SimpleAccountScreen] Login successful, saving to SQLite'); try { - // Get database path - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); // Initialize database initializeDatabase(dbPath); @@ -244,8 +244,13 @@ export default function SimpleAccountScreen() { await saveAccount(dbPath, newAccount); console.log('[SimpleAccountScreen] Account saved to SQLite database'); - // Also keep in SecureStore for backward compatibility (can remove later) - await SecureStore.setItemAsync('audible_account', JSON.stringify(newAccount)); + const expiresAt = newAccount.identity?.access_token?.expires_at; + if (expiresAt) { + await SecureStore.setItemAsync('token_expires_at', expiresAt); + const expiry = new Date(expiresAt); + setTokenExpiry(expiry); + updateTimeRemaining(expiry); + } // Update state setAccount(newAccount); @@ -304,9 +309,37 @@ export default function SimpleAccountScreen() { console.error('[SimpleAccountScreen] Failed to cancel background tasks:', error); } - await SecureStore.deleteItemAsync('audible_account'); - setAccount(null); - setSyncStats(null); + try { + // Delete from SQLite first; otherwise a restart/focus reload would restore the login. + if (account?.account_id) { + const dbPath = getDatabasePath(); + initializeDatabase(dbPath); + await deleteAccount(dbPath, account.account_id); + console.log('[SimpleAccountScreen] Account deleted from SQLite database'); + } + + await Promise.all([ + SecureStore.deleteItemAsync('audible_account'), + SecureStore.deleteItemAsync('token_expires_at'), + SecureStore.deleteItemAsync('last_sync_date'), + SecureStore.deleteItemAsync('audible_access_token'), + SecureStore.deleteItemAsync('audible_refresh_token'), + SecureStore.deleteItemAsync('audible_token_expires_at'), + SecureStore.deleteItemAsync('audible_device_serial'), + SecureStore.deleteItemAsync('audible_locale_code'), + ]); + + setAccount(null); + setSyncStats(null); + setLastSyncDate(null); + setTokenExpiry(null); + setTimeRemaining(null); + setAccountName(null); + setConnectionStatus('checking'); + } catch (error) { + console.error('[SimpleAccountScreen] Failed to log out:', error); + Alert.alert('Logout Failed', 'Could not remove the local account data.'); + } }, }, ] @@ -355,17 +388,12 @@ export default function SimpleAccountScreen() { }, }; - // Get database path - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); // Save updated account to SQLite (single source of truth) await saveAccount(dbPath, updatedAccount); console.log('[SimpleAccountScreen] Updated account saved to SQLite'); - // Also save to SecureStore for backward compatibility - await SecureStore.setItemAsync('audible_account', JSON.stringify(updatedAccount)); await SecureStore.setItemAsync('token_expires_at', newExpiry.toISOString()); // Update state @@ -392,9 +420,7 @@ export default function SimpleAccountScreen() { setIsSyncing(true); // Initialize database - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); console.log('[SimpleAccountScreen] Database path:', dbPath); initializeDatabase(dbPath); @@ -424,7 +450,6 @@ export default function SimpleAccountScreen() { // Persist refreshed account await saveAccount(dbPath, syncAccount); - await SecureStore.setItemAsync('audible_account', JSON.stringify(syncAccount)); await SecureStore.setItemAsync('token_expires_at', newExpiry.toISOString()); setAccount(syncAccount); setTokenExpiry(newExpiry); diff --git a/src/screens/TaskDebugScreen.tsx b/src/screens/TaskDebugScreen.tsx index c1ea9e6..44a7a1b 100644 --- a/src/screens/TaskDebugScreen.tsx +++ b/src/screens/TaskDebugScreen.tsx @@ -24,8 +24,9 @@ import { isBackgroundServiceRunning, type BackgroundTask, } from '../../modules/expo-rust-bridge'; -import { Directory, File, Paths } from 'expo-file-system'; +import { Directory, Paths } from 'expo-file-system'; import Button from '../components/Button'; +import { getDatabasePath } from '../utils/appPaths'; /** * Task Debug Screen @@ -68,9 +69,7 @@ export default function TaskDebugScreen() { const checkAccount = async () => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); initializeDatabase(dbPath); const account = await getPrimaryAccount(dbPath); @@ -170,9 +169,7 @@ export default function TaskDebugScreen() { style: 'destructive', onPress: async () => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); const booksUpdated = await clearDownloadState(dbPath); Alert.alert('Success', `Download state cleared for ${booksUpdated} books.`); @@ -196,9 +193,7 @@ export default function TaskDebugScreen() { style: 'destructive', onPress: async () => { try { - const cacheUri = Paths.cache.uri; - const cachePath = cacheUri.replace('file://', ''); - const dbPath = `${cachePath.replace(/\/$/, '')}/audible.db`; + const dbPath = getDatabasePath(); // Clear download state first (before deleting books) const booksUpdated = await clearDownloadState(dbPath); @@ -334,16 +329,16 @@ export default function TaskDebugScreen() { ); }; - const handleListCacheFiles = async () => { + const handleListAppFiles = async () => { try { - console.log('[TaskDebug] Listing cache directory recursively...'); - console.log('[TaskDebug] Cache path:', Paths.cache.uri); + console.log('[TaskDebug] Listing app files directory recursively...'); + console.log('[TaskDebug] App files path:', Paths.document.uri); - const cacheDir = new Directory(Paths.cache); + const appFilesDir = new Directory(Paths.document); - if (!cacheDir.exists) { - console.log('[TaskDebug] Cache directory does not exist'); - Alert.alert('Cache Directory', 'Cache directory does not exist'); + if (!appFilesDir.exists) { + console.log('[TaskDebug] App files directory does not exist'); + Alert.alert('App Files', 'App files directory does not exist'); return; } @@ -360,7 +355,7 @@ export default function TaskDebugScreen() { const name = item.uri.split('/').filter(Boolean).pop() || 'unknown'; const decodedName = decodeURIComponent(name); - // Skip system/cache folders + // Skip noisy system/cache folders if they appear in this listing. const skipFolders = ['WebView', 'http-cache', 'Crash Reports', 'image_cache']; if (skipFolders.includes(decodedName) && prefix === '') { continue; @@ -401,7 +396,7 @@ export default function TaskDebugScreen() { } }; - await listRecursive(cacheDir); + await listRecursive(appFilesDir); console.log('[TaskDebug] ================'); console.log('[TaskDebug] Total:', fileCount, 'files,', dirCount, 'directories'); @@ -412,12 +407,12 @@ export default function TaskDebugScreen() { const more = fileDetails.length > 15 ? `\n... and ${fileDetails.length - 15} more` : ''; const totalSizeMB = (totalSize / 1024 / 1024).toFixed(2); Alert.alert( - 'Cache Directory', + 'App Files', `${fileCount} files, ${dirCount} dirs\nTotal: ${totalSizeMB} MB\n\n${summary}${more}\n\nCheck console for full listing.` ); } catch (error: any) { - console.error('[TaskDebug] Failed to list cache files:', error); - Alert.alert('Error', error.message || 'Failed to list cache directory'); + console.error('[TaskDebug] Failed to list app files:', error); + Alert.alert('Error', error.message || 'Failed to list app files directory'); } }; @@ -674,8 +669,8 @@ export default function TaskDebugScreen() { />