diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt index ed464b03..c70a0545 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -7,6 +7,8 @@ import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderBackupPlugin import com.stevesoltys.seedvault.plugins.saf.DocumentsProviderRestorePlugin import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage +import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH +import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD import com.stevesoltys.seedvault.plugins.saf.deleteContents import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.BackupPlugin @@ -25,9 +27,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.koin.core.KoinComponent import org.koin.core.inject +import java.io.IOException import kotlin.random.Random - @RunWith(AndroidJUnit4::class) @Suppress("BlockingMethodInNonBlockingContext") class PluginTest : KoinComponent { @@ -48,7 +50,7 @@ class PluginTest : KoinComponent { fun setup() { every { mockedSettingsManager.getStorage() } returns settingsManager.getStorage() storage.rootBackupDir?.deleteContents() - ?: error("Select a storage location in the app first!") + ?: error("Select a storage location in the app first!") } @After @@ -162,7 +164,7 @@ class PluginTest : KoinComponent { // define key/value pair records val record1 = Pair(getRandomBase64(23), getRandomByteArray(1337)) val record2 = Pair(getRandomBase64(42), getRandomByteArray(42 * 1024)) - val record3 = Pair(getRandomBase64(255), getRandomByteArray(5 * 1024 * 1024)) + val record3 = Pair(getRandomBase64(128), getRandomByteArray(5 * 1024 * 1024)) // write first record kvBackup.ensureRecordStorageForPackage(packageInfo) @@ -177,7 +179,10 @@ class PluginTest : KoinComponent { var records = kvRestore.listRecords(token, packageInfo) assertEquals(1, records.size) assertEquals(record1.first, records[0]) - assertReadEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first)) + assertReadEquals( + record1.second, + kvRestore.getInputStreamForRecord(token, packageInfo, record1.first) + ) // write second and third record kvBackup.ensureRecordStorageForPackage(packageInfo) @@ -187,9 +192,18 @@ class PluginTest : KoinComponent { // all records for package are found and returned properly records = kvRestore.listRecords(token, packageInfo) assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted()) - assertReadEquals(record1.second, kvRestore.getInputStreamForRecord(token, packageInfo, record1.first)) - assertReadEquals(record2.second, kvRestore.getInputStreamForRecord(token, packageInfo, record2.first)) - assertReadEquals(record3.second, kvRestore.getInputStreamForRecord(token, packageInfo, record3.first)) + assertReadEquals( + record1.second, + kvRestore.getInputStreamForRecord(token, packageInfo, record1.first) + ) + assertReadEquals( + record2.second, + kvRestore.getInputStreamForRecord(token, packageInfo, record2.first) + ) + assertReadEquals( + record3.second, + kvRestore.getInputStreamForRecord(token, packageInfo, record3.first) + ) // delete record3 and ensure that the other two are still found kvBackup.deleteRecord(packageInfo, record3.first) @@ -211,13 +225,17 @@ class PluginTest : KoinComponent { // initialize storage with given token initStorage(token) + // FIXME get Nextcloud to have the same limit + val max = if (isNextcloud()) MAX_KEY_LENGTH_NEXTCLOUD else MAX_KEY_LENGTH + // define record with maximum key length and one above the maximum - val recordMax = Pair(getRandomBase64(255), getRandomByteArray(1024)) - val recordOver = Pair(getRandomBase64(256), getRandomByteArray(1024)) + val recordMax = Pair(getRandomBase64(max), getRandomByteArray(1024)) + val recordOver = Pair(getRandomBase64(max + 1), getRandomByteArray(1024)) // write max record kvBackup.ensureRecordStorageForPackage(packageInfo) - kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first).writeAndClose(recordMax.second) + kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first) + .writeAndClose(recordMax.second) // max record is found correctly assertTrue(kvRestore.hasDataForPackage(token, packageInfo)) @@ -226,8 +244,17 @@ class PluginTest : KoinComponent { // write exceeding key length record kvBackup.ensureRecordStorageForPackage(packageInfo) - coAssertThrows(IllegalStateException::class.java) { - kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first).writeAndClose(recordOver.second) + if (isNextcloud()) { + // Nextcloud simply refuses to write long filenames + coAssertThrows(IOException::class.java) { + kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first) + .writeAndClose(recordOver.second) + } + } else { + coAssertThrows(IllegalStateException::class.java) { + kvBackup.getOutputStreamForRecord(packageInfo, recordOver.first) + .writeAndClose(recordOver.second) + } } } @@ -278,4 +305,8 @@ class PluginTest : KoinComponent { assertTrue(backupPlugin.initializeDevice(newToken = token)) } + private fun isNextcloud(): Boolean { + return backupPlugin.providerPackageName == "com.nextcloud.client" + } + } diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt index 8246820f..59b3eea3 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorageTest.kt @@ -54,7 +54,7 @@ class DocumentsStorageTest : KoinComponent { fun setup() = runBlocking { assertNotNull("Select a storage location in the app first!", storage.rootBackupDir) file = storage.rootBackupDir?.createOrGetFile(context, filename) - ?: throw RuntimeException("Could not create test file") + ?: error("Could not create test file") } @After diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt index bc0192da..a8e5b921 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderKVBackup.kt @@ -2,12 +2,16 @@ package com.stevesoltys.seedvault.plugins.saf import android.content.Context import android.content.pm.PackageInfo +import android.util.Log import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.transport.backup.DEFAULT_QUOTA_KEY_VALUE_BACKUP import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin import java.io.IOException import java.io.OutputStream +const val MAX_KEY_LENGTH = 255 +const val MAX_KEY_LENGTH_NEXTCLOUD = 228 + @Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderKVBackup( private val storage: DocumentsStorage, @@ -37,7 +41,8 @@ internal class DocumentsProviderKVBackup( override suspend fun removeDataOfPackage(packageInfo: PackageInfo) { // we cannot use the cached this.packageFile here, // because this can be called before [ensureRecordStorageForPackage] - val packageFile = storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return + val packageFile = + storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return packageFile.delete() } @@ -54,6 +59,15 @@ internal class DocumentsProviderKVBackup( packageInfo: PackageInfo, key: String ): OutputStream { + check(key.length < MAX_KEY_LENGTH) { + "Key $key for ${packageInfo.packageName} is too long: ${key.length} chars." + } + if (key.length > MAX_KEY_LENGTH_NEXTCLOUD) { + Log.e( + DocumentsProviderKVBackup::class.simpleName, + "Key $key for ${packageInfo.packageName} is too long: ${key.length} chars." + ) + } val packageFile = this.packageFile ?: throw AssertionError() packageFile.assertRightFile(packageInfo) val keyFile = packageFile.createOrGetFile(context, key)