diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index 0221d37d..59511853 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -13,8 +13,10 @@ import android.os.ParcelFileDescriptor import android.util.Log import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.metadata.BackupMetadata +import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.DecryptionFailedException import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataReader @@ -40,6 +42,7 @@ private val TAG = RestoreCoordinator::class.java.simpleName @Suppress("BlockingMethodInNonBlockingContext") internal class RestoreCoordinator( private val context: Context, + private val crypto: Crypto, private val settingsManager: SettingsManager, private val metadataManager: MetadataManager, private val notificationManager: BackupNotificationManager, @@ -193,22 +196,59 @@ internal class RestoreCoordinator( val state = this.state ?: throw IllegalStateException("no state") if (!state.packages.hasNext()) return NO_MORE_PACKAGES - val version = state.backupMetadata.version val packageInfo = state.packages.next() - val packageName = packageInfo.packageName + val version = state.backupMetadata.version + if (version == 0.toByte()) return nextRestorePackageV0(state, packageInfo) + val packageName = packageInfo.packageName + val type = try { + when (state.backupMetadata.packageMetadataMap[packageName]?.backupType) { + BackupType.KV -> { + val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) + if (plugin.hasData(state.token, name)) { + Log.i(TAG, "Found K/V data for $packageName.") + kv.initializeState(version, state.token, packageInfo, state.pmPackageInfo) + state.currentPackage = packageName + TYPE_KEY_VALUE + } else throw IOException("No data found for $packageName. Skipping.") + } + BackupType.FULL -> { + val name = crypto.getNameForPackage(state.backupMetadata.salt, packageName) + if (plugin.hasData(state.token, name)) { + Log.i(TAG, "Found full backup data for $packageName.") + full.initializeState(version, state.token, packageInfo) + state.currentPackage = packageName + TYPE_FULL_STREAM + } else throw IOException("No data found for $packageName. Skipping.") + } + null -> throw IOException("No backup type found for $packageName. Skipping.") + } + } catch (e: IOException) { + Log.e(TAG, "Error finding restore data for $packageName.", e) + failedPackages.add(packageName) + // don't return null and cause abort here, but try next package + return nextRestorePackage() + } + return RestoreDescription(packageName, type) + } + + private suspend fun nextRestorePackageV0( + state: RestoreCoordinatorState, + packageInfo: PackageInfo + ): RestoreDescription? { + val packageName = packageInfo.packageName val type = try { when { // check key/value data first and if available, don't even check for full data kv.hasDataForPackage(state.token, packageInfo) -> { Log.i(TAG, "Found K/V data for $packageName.") - kv.initializeState(version, state.token, packageInfo, state.pmPackageInfo) + kv.initializeState(0x00, state.token, packageInfo, state.pmPackageInfo) state.currentPackage = packageName TYPE_KEY_VALUE } full.hasDataForPackage(state.token, packageInfo) -> { Log.i(TAG, "Found full backup data for $packageName.") - full.initializeState(version, state.token, packageInfo) + full.initializeState(0x00, state.token, packageInfo) state.currentPackage = packageName TYPE_FULL_STREAM } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt index f66623eb..833b47da 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreModule.kt @@ -7,5 +7,7 @@ val restoreModule = module { single { OutputFactory() } single { KVRestore(get().kvRestorePlugin, get(), get(), get()) } single { FullRestore(get().fullRestorePlugin, get(), get(), get()) } - single { RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get()) } + single { + RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) + } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 52f20e47..f5647c23 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -100,6 +100,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) private val restore = RestoreCoordinator( context, + crypto, settingsManager, metadataManager, notificationManager, @@ -186,7 +187,8 @@ internal class CoordinatorIntegrationTest : TransportTest() { assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo))) // find data for K/V backup - coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true + every { crypto.getNameForPackage(metadata.salt, packageInfo.packageName) } returns name + coEvery { backupPlugin.hasData(token, name) } returns true val restoreDescription = restore.nextRestorePackage() ?: fail() assertEquals(packageInfo.packageName, restoreDescription.packageName) @@ -262,7 +264,8 @@ internal class CoordinatorIntegrationTest : TransportTest() { assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo))) // find data for K/V backup - coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns true + every { crypto.getNameForPackage(metadata.salt, packageInfo.packageName) } returns name + coEvery { backupPlugin.hasData(token, name) } returns true val restoreDescription = restore.nextRestorePackage() ?: fail() assertEquals(packageInfo.packageName, restoreDescription.packageName) @@ -288,6 +291,11 @@ internal class CoordinatorIntegrationTest : TransportTest() { @Test fun `test full backup and restore with two chunks`() = runBlocking { + // package is of type FULL + val packageMetadata = metadata.packageMetadataMap[packageInfo.packageName]!! + metadata.packageMetadataMap[packageInfo.packageName] = + packageMetadata.copy(backupType = BackupType.FULL) + // return streams from plugin and app data val bOutputStream = ByteArrayOutputStream() val bInputStream = ByteArrayInputStream(appData) @@ -327,9 +335,9 @@ internal class CoordinatorIntegrationTest : TransportTest() { restore.beforeStartRestore(metadata) assertEquals(TRANSPORT_OK, restore.startRestore(token, arrayOf(packageInfo))) - // find data only for full backup - coEvery { kvRestorePlugin.hasDataForPackage(token, packageInfo) } returns false - coEvery { fullRestorePlugin.hasDataForPackage(token, packageInfo) } returns true + // finds data for full backup + every { crypto.getNameForPackage(metadata.salt, packageInfo.packageName) } returns name + coEvery { backupPlugin.hasData(token, name) } returns true val restoreDescription = restore.nextRestorePackage() ?: fail() assertEquals(packageInfo.packageName, restoreDescription.packageName) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt index 74795d5d..d3060f94 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/TransportTest.kt @@ -13,8 +13,11 @@ import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.BackupMetadata +import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.METADATA_SALT_SIZE import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.metadata.PackageMetadata +import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.settings.SettingsManager import io.mockk.every import io.mockk.mockk @@ -50,8 +53,13 @@ internal abstract class TransportTest { salt = getRandomBase64(METADATA_SALT_SIZE), androidVersion = Random.nextInt(), androidIncremental = getRandomString(), - deviceName = getRandomString() + deviceName = getRandomString(), + packageMetadataMap = PackageMetadataMap().apply { + put(packageInfo.packageName, PackageMetadata(backupType = BackupType.KV)) + } ) + protected val name = getRandomString(12) + protected val name2 = getRandomString(23) init { mockkStatic(Log::class) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index ab5c0619..1bd51fea 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -11,6 +11,7 @@ import android.os.ParcelFileDescriptor import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.header.VERSION +import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.MetadataReader import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.settings.Storage @@ -45,6 +46,7 @@ internal class RestoreCoordinatorTest : TransportTest() { private val restore = RestoreCoordinator( context, + crypto, settingsManager, metadataManager, notificationManager, @@ -66,6 +68,11 @@ internal class RestoreCoordinatorTest : TransportTest() { private val packageName = packageInfo.packageName private val storageName = getRandomString() + init { + metadata.packageMetadataMap[packageInfo2.packageName] = + PackageMetadata(backupType = BackupType.FULL) + } + @Test fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking { val encryptedMetadata = EncryptedMetadata(token) { inputStream } @@ -210,37 +217,69 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test - fun `nextRestorePackage() returns KV description and takes precedence`() = runBlocking { + fun `nextRestorePackage() returns KV description`() = runBlocking { restore.beforeStartRestore(metadata) restore.startRestore(token, packageInfoArray) - coEvery { kv.hasDataForPackage(token, packageInfo) } returns true + every { crypto.getNameForPackage(metadata.salt, packageName) } returns name + coEvery { plugin.hasData(token, name) } returns true every { kv.initializeState(VERSION, token, packageInfo) } just Runs + val expected = RestoreDescription(packageName, TYPE_KEY_VALUE) + assertEquals(expected, restore.nextRestorePackage()) + } + + @Test + fun `v0 nextRestorePackage() returns KV description and takes precedence`() = runBlocking { + restore.beforeStartRestore(metadata.copy(version = 0x00)) + restore.startRestore(token, packageInfoArray) + + coEvery { kv.hasDataForPackage(token, packageInfo) } returns true + every { kv.initializeState(0x00, token, packageInfo) } just Runs + val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE) assertEquals(expected, restore.nextRestorePackage()) } @Test - fun `nextRestorePackage() returns full description if no KV data found`() = runBlocking { - restore.beforeStartRestore(metadata) + fun `v0 nextRestorePackage() returns full description if no KV data found`() = runBlocking { + restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray) coEvery { kv.hasDataForPackage(token, packageInfo) } returns false coEvery { full.hasDataForPackage(token, packageInfo) } returns true - every { full.initializeState(VERSION, token, packageInfo) } just Runs + every { full.initializeState(0x00, token, packageInfo) } just Runs val expected = RestoreDescription(packageInfo.packageName, TYPE_FULL_STREAM) assertEquals(expected, restore.nextRestorePackage()) } @Test - fun `nextRestorePackage() returns NO_MORE_PACKAGES if data found`() = runBlocking { + fun `nextRestorePackage() returns NO_MORE_PACKAGES if data not found`() = runBlocking { restore.beforeStartRestore(metadata) - restore.startRestore(token, packageInfoArray) + restore.startRestore(token, packageInfoArray2) - coEvery { kv.hasDataForPackage(token, packageInfo) } returns false - coEvery { full.hasDataForPackage(token, packageInfo) } returns false + every { crypto.getNameForPackage(metadata.salt, packageName) } returns name + coEvery { plugin.hasData(token, name) } returns false + every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 + coEvery { plugin.hasData(token, name2) } returns false + + assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage()) + } + + @Test + fun `nextRestorePackage() tries next package if one has no backup type()`() = runBlocking { + metadata.packageMetadataMap[packageName] = + metadata.packageMetadataMap[packageName]!!.copy(backupType = null) + restore.beforeStartRestore(metadata) + restore.startRestore(token, packageInfoArray2) + + every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 + coEvery { plugin.hasData(token, name2) } returns true + every { full.initializeState(VERSION, token, packageInfo2) } just Runs + + val expected = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) + assertEquals(expected, restore.nextRestorePackage()) assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage()) } @@ -250,15 +289,38 @@ internal class RestoreCoordinatorTest : TransportTest() { restore.beforeStartRestore(metadata) restore.startRestore(token, packageInfoArray2) - coEvery { kv.hasDataForPackage(token, packageInfo) } returns true + every { crypto.getNameForPackage(metadata.salt, packageName) } returns name + coEvery { plugin.hasData(token, name) } returns true every { kv.initializeState(VERSION, token, packageInfo) } just Runs val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE) assertEquals(expected, restore.nextRestorePackage()) + every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 + coEvery { plugin.hasData(token, name2) } returns true + every { full.initializeState(VERSION, token, packageInfo2) } just Runs + + val expected2 = + RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) + assertEquals(expected2, restore.nextRestorePackage()) + + assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage()) + } + + @Test + fun `v0 nextRestorePackage() returns all packages from startRestore()`() = runBlocking { + restore.beforeStartRestore(metadata.copy(version = 0x00)) + restore.startRestore(token, packageInfoArray2) + + coEvery { kv.hasDataForPackage(token, packageInfo) } returns true + every { kv.initializeState(0.toByte(), token, packageInfo) } just Runs + + val expected = RestoreDescription(packageInfo.packageName, TYPE_KEY_VALUE) + assertEquals(expected, restore.nextRestorePackage()) + coEvery { kv.hasDataForPackage(token, packageInfo2) } returns false coEvery { full.hasDataForPackage(token, packageInfo2) } returns true - every { full.initializeState(VERSION, token, packageInfo2) } just Runs + every { full.initializeState(0.toByte(), token, packageInfo2) } just Runs val expected2 = RestoreDescription(packageInfo2.packageName, TYPE_FULL_STREAM) assertEquals(expected2, restore.nextRestorePackage()) @@ -267,8 +329,8 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test - fun `when kv#hasDataForPackage() throws, it tries next package`() = runBlocking { - restore.beforeStartRestore(metadata) + fun `v0 when kv#hasDataForPackage() throws, it tries next package`() = runBlocking { + restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray) coEvery { kv.hasDataForPackage(token, packageInfo) } throws IOException() @@ -277,8 +339,21 @@ internal class RestoreCoordinatorTest : TransportTest() { } @Test - fun `when full#hasDataForPackage() throws, it tries next package`() = runBlocking { + fun `when plugin#hasData() throws, it tries next package`() = runBlocking { restore.beforeStartRestore(metadata) + restore.startRestore(token, packageInfoArray2) + + every { crypto.getNameForPackage(metadata.salt, packageName) } returns name + coEvery { plugin.hasData(token, name) } returns false + every { crypto.getNameForPackage(metadata.salt, packageInfo2.packageName) } returns name2 + coEvery { plugin.hasData(token, name2) } throws IOException() + + assertEquals(NO_MORE_PACKAGES, restore.nextRestorePackage()) + } + + @Test + fun `v0 when full#hasDataForPackage() throws, it tries next package`() = runBlocking { + restore.beforeStartRestore(metadata.copy(version = 0x00)) restore.startRestore(token, packageInfoArray) coEvery { kv.hasDataForPackage(token, packageInfo) } returns false diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt index 38a9b2d0..fd1fe72b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt @@ -55,6 +55,7 @@ internal class RestoreV0IntegrationTest : TransportTest() { FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) private val restore = RestoreCoordinator( context, + crypto, settingsManager, metadataManager, notificationManager,