diff --git a/app/build.gradle b/app/build.gradle index 685e0dd3..1d3a42ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -126,7 +126,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc03' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - lintChecks 'com.github.thirdegg:lint-rules:0.0.4-alpha' + lintChecks 'com.github.thirdegg:lint-rules:0.0.5-alpha' def junit_version = "5.5.2" // careful, upgrading this can change a Cipher's IV size in tests!? def mockk_version = "1.10.0" diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt index c70a0545..19fdc66c 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/PluginTest.kt @@ -141,11 +141,18 @@ class PluginTest : KoinComponent { initStorage(token) // write random bytes as APK - val apk = getRandomByteArray(1337) - backupPlugin.getApkOutputStream(packageInfo).writeAndClose(apk) + val apk1 = getRandomByteArray(1337 * 1024) + backupPlugin.getApkOutputStream(packageInfo).writeAndClose(apk1) // assert that read APK bytes match what was written - assertReadEquals(apk, restorePlugin.getApkInputStream(token, packageInfo.packageName)) + assertReadEquals(apk1, restorePlugin.getApkInputStream(token, packageInfo.packageName)) + + // write random bytes as another APK + val apk2 = getRandomByteArray(23 * 1024 * 1024) + backupPlugin.getApkOutputStream(packageInfo2).writeAndClose(apk2) + + // assert that read APK bytes match what was written + assertReadEquals(apk2, restorePlugin.getApkInputStream(token, packageInfo2.packageName)) } @Test @@ -226,11 +233,14 @@ class PluginTest : KoinComponent { initStorage(token) // FIXME get Nextcloud to have the same limit + // Since Nextcloud is using WebDAV and that seems to have undefined lower file name limits + // we might have to lower our maximum to accommodate for that. val max = if (isNextcloud()) MAX_KEY_LENGTH_NEXTCLOUD else MAX_KEY_LENGTH + val maxOver = if (isNextcloud()) max + 10 else max + 1 // define record with maximum key length and one above the maximum val recordMax = Pair(getRandomBase64(max), getRandomByteArray(1024)) - val recordOver = Pair(getRandomBase64(max + 1), getRandomByteArray(1024)) + val recordOver = Pair(getRandomBase64(maxOver), getRandomByteArray(1024)) // write max record kvBackup.ensureRecordStorageForPackage(packageInfo) @@ -306,7 +316,7 @@ class PluginTest : KoinComponent { } private fun isNextcloud(): Boolean { - return backupPlugin.providerPackageName == "com.nextcloud.client" + return backupPlugin.providerPackageName?.startsWith("com.nextcloud") ?: false } } 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 59b3eea3..5162ead7 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 @@ -118,6 +118,49 @@ class DocumentsStorageTest : KoinComponent { assertFalse(createdFile.exists()) } + @Test + fun testCreateTwoFiles() = runBlocking { + val mimeType = "application/octet-stream" + val dir = storage.rootBackupDir!! + + // create test file + val name1 = getRandomBase64(Random.nextInt(1, 10)) + val file1 = requireNotNull(dir.createFile(mimeType, name1)) + assertTrue(file1.exists()) + assertEquals(name1, file1.name) + assertEquals(0L, file1.length()) + + assertReadEquals(getRandomByteArray(0), context.contentResolver.openInputStream(file1.uri)) + + // write some data into it + val data1 = getRandomByteArray(5 * 1024 * 1024) + context.contentResolver.openOutputStream(file1.uri)!!.writeAndClose(data1) + assertEquals(data1.size.toLong(), file1.length()) + + // data should still be there + assertReadEquals(data1, context.contentResolver.openInputStream(file1.uri)) + + // create test file + val name2 = getRandomBase64(Random.nextInt(1, 10)) + val file2 = requireNotNull(dir.createFile(mimeType, name2)) + assertTrue(file2.exists()) + assertEquals(name2, file2.name) + + // write some data into it + val data2 = getRandomByteArray(12 * 1024 * 1024) + context.contentResolver.openOutputStream(file2.uri)!!.writeAndClose(data2) + assertEquals(data2.size.toLong(), file2.length()) + + // data should still be there + assertReadEquals(data2, context.contentResolver.openInputStream(file2.uri)) + + // delete files again + file1.delete() + file2.delete() + assertFalse(file1.exists()) + assertFalse(file2.exists()) + } + @Test fun testGetLoadedCursor() = runBlocking { // empty cursor extras are like not loading, returns same cursor right away 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 a8e5b921..e81bcbd5 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 @@ -10,7 +10,7 @@ import java.io.IOException import java.io.OutputStream const val MAX_KEY_LENGTH = 255 -const val MAX_KEY_LENGTH_NEXTCLOUD = 228 +const val MAX_KEY_LENGTH_NEXTCLOUD = 225 @Suppress("BlockingMethodInNonBlockingContext") internal class DocumentsProviderKVBackup( @@ -59,7 +59,7 @@ internal class DocumentsProviderKVBackup( packageInfo: PackageInfo, key: String ): OutputStream { - check(key.length < MAX_KEY_LENGTH) { + check(key.length <= MAX_KEY_LENGTH) { "Key $key for ${packageInfo.packageName} is too long: ${key.length} chars." } if (key.length > MAX_KEY_LENGTH_NEXTCLOUD) { diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt index cdc80a71..a76f76f8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt @@ -204,7 +204,9 @@ fun DocumentFile.deleteContents() { } fun DocumentFile.assertRightFile(packageInfo: PackageInfo) { - if (name != packageInfo.packageName) throw AssertionError() + if (name != packageInfo.packageName) { + throw AssertionError("Expected ${packageInfo.packageName}, but got $name") + } } /** diff --git a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt index dafb2c41..7d1f43e9 100644 --- a/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt +++ b/app/src/sharedTest/java/com/stevesoltys/seedvault/TestUtils.kt @@ -1,5 +1,6 @@ package com.stevesoltys.seedvault +import com.stevesoltys.seedvault.plugins.saf.MAX_KEY_LENGTH_NEXTCLOUD import kotlinx.coroutines.runBlocking import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals @@ -12,12 +13,14 @@ fun assertContains(stack: String?, needle: String) { if (stack?.contains(needle) != true) throw AssertionError() } +@Suppress("MagicNumber") fun getRandomByteArray(size: Int = Random.nextInt(1337)) = ByteArray(size).apply { Random.nextBytes(this) } private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '_' + '.' +@Suppress("MagicNumber") fun getRandomString(size: Int = Random.nextInt(1, 255)): String { return (1..size) .map { Random.nextInt(0, charPool.size) } @@ -26,9 +29,10 @@ fun getRandomString(size: Int = Random.nextInt(1, 255)): String { } // URL-save version (RFC 4648) -private val base64CharPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '+' + '_' + '=' +private val base64CharPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') // + '+' + '_' + '=' -fun getRandomBase64(size: Int = Random.nextInt(1, 255)): String { +@Suppress("MagicNumber") +fun getRandomBase64(size: Int = Random.nextInt(1, MAX_KEY_LENGTH_NEXTCLOUD)): String { return (1..size) .map { Random.nextInt(0, base64CharPool.size) } .map(base64CharPool::get) @@ -61,6 +65,7 @@ fun assertReadEquals(data: ByteArray, inputStream: InputStream?) = inputStream?. fun coAssertThrows(clazz: Class, block: suspend () -> Unit) { var thrown = false + @Suppress("TooGenericExceptionCaught") try { runBlocking { block()