Coverage Summary for Class: RestoredDatabaseVerifier (com.georgeci.moneysurfer.data.backup)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| RestoredDatabaseVerifier |
100%
(1/1)
|
100%
(5/5)
|
62.5%
(5/8)
|
93.8%
(15/16)
|
90%
(117/130)
|
package com.georgeci.moneysurfer.data.backup
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.sqlite.driver.bundled.SQLITE_OPEN_READONLY
import com.georgeci.moneysurfer.domain.backup.BackupError
import okio.FileSystem
import okio.Path
/**
* Rejects a restored SQLite file before it ever replaces the live one:
* header magic first (cheap, avoids handing arbitrary bytes to the SQLite
* parser), then `PRAGMA integrity_check` through the bundled (current)
* SQLite build on a read-only connection.
*/
internal object RestoredDatabaseVerifier {
fun verify(fs: FileSystem, path: Path) {
if (!hasSqliteMagic(fs, path)) {
throw BackupError.CorruptDatabase("not an SQLite database file")
}
val verdict = runIntegrityCheck(path)
if (verdict != INTEGRITY_OK) {
throw BackupError.CorruptDatabase(verdict.take(MAX_REASON_LENGTH))
}
}
private fun hasSqliteMagic(fs: FileSystem, path: Path): Boolean = runCatching {
fs.read(path) { readByteArray(SQLITE_MAGIC.size.toLong()) }.contentEquals(SQLITE_MAGIC)
}.getOrDefault(false)
/**
* Returns the first `PRAGMA integrity_check` result row ("ok" for a
* healthy file); any SQLite-level failure to open or query the file is
* itself a corruption verdict.
*/
private fun runIntegrityCheck(path: Path): String = try {
val connection = BundledSQLiteDriver().open(path.toString(), SQLITE_OPEN_READONLY)
try {
firstIntegrityRow(connection)
} finally {
connection.close()
}
} catch (
@Suppress("TooGenericExceptionCaught") t: Throwable,
) {
throw BackupError.CorruptDatabase("integrity_check failed: ${t.message}", t)
}
private fun firstIntegrityRow(connection: SQLiteConnection): String {
val statement = connection.prepare(PRAGMA_INTEGRITY_CHECK)
return try {
if (statement.step()) statement.getText(0) else "integrity_check returned no result"
} finally {
statement.close()
}
}
private const val PRAGMA_INTEGRITY_CHECK = "PRAGMA integrity_check"
private const val INTEGRITY_OK = "ok"
private const val MAX_REASON_LENGTH = 200
/** First 16 bytes of every SQLite 3 database file. */
private val SQLITE_MAGIC: ByteArray = "SQLite format 3\u0000".encodeToByteArray()
}