Use truncate mode to replace the contents of existing files

`ContentResolver.openOutputStream(Uri)` does not truncate existing files. If the amount of data written is smaller than the file size, you end up with new data at the beginning of the file followed by old data at the end of the file.
This commit is contained in:
cketti 2022-03-29 16:12:12 +02:00
parent 539d198f8f
commit b9b5cab772
7 changed files with 13 additions and 12 deletions

1
changelog.d/5663.bugfix Normal file
View File

@ -0,0 +1 @@
Fixed key export when overwriting existing files

View File

@ -34,7 +34,7 @@ class KeysExporter @Inject constructor(
suspend fun export(password: String, uri: Uri) { suspend fun export(password: String, uri: Uri) {
withContext(dispatchers.io) { withContext(dispatchers.io) {
val data = session.cryptoService().exportRoomKeys(password) val data = session.cryptoService().exportRoomKeys(password)
context.contentResolver.openOutputStream(uri) context.contentResolver.openOutputStream(uri, "wt")
?.use { it.write(data) } ?.use { it.write(data) }
?: throw IllegalStateException("Unable to open file for writing") ?: throw IllegalStateException("Unable to open file for writing")
verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong()) verifyExportedKeysOutputFileSize(uri, expectedSize = data.size.toLong())

View File

@ -165,7 +165,7 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment<Fr
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
Try { Try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
requireContext().contentResolver.openOutputStream(uri) requireContext().contentResolver.openOutputStream(uri, "wt")
?.use { os -> ?.use { os ->
os.write(data.toByteArray()) os.write(data.toByteArray())
os.flush() os.flush()

View File

@ -81,7 +81,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
val uri = activityResult.data?.data ?: return@registerStartForActivityResult val uri = activityResult.data?.data ?: return@registerStartForActivityResult
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri)!!)) sharedViewModel.handle(BootstrapActions.SaveKeyToUri(requireContext().contentResolver!!.openOutputStream(uri, "wt")!!))
} catch (failure: Throwable) { } catch (failure: Throwable) {
sharedViewModel.handle(BootstrapActions.SaveReqFailed) sharedViewModel.handle(BootstrapActions.SaveReqFailed)
} }

View File

@ -106,7 +106,7 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment<FragmentDev
when (it) { when (it) {
is KeyRequestEvents.SaveAudit -> { is KeyRequestEvents.SaveAudit -> {
tryOrNull { tryOrNull {
requireContext().contentResolver?.openOutputStream(it.uri) requireContext().contentResolver?.openOutputStream(it.uri, "wt")
?.use { os -> os.write(it.raw.toByteArray()) } ?.use { os -> os.write(it.raw.toByteArray()) }
} }
} }

View File

@ -53,7 +53,7 @@ class KeysExporterTest {
@Test @Test
fun `when exporting then writes exported keys to context output stream`() { fun `when exporting then writes exported keys to context output stream`() {
givenFileDescriptorWithSize(size = A_ROOM_KEYS_EXPORT.size.toLong()) givenFileDescriptorWithSize(size = A_ROOM_KEYS_EXPORT.size.toLong())
val outputStream = context.givenOutputStreamFor(A_URI) val outputStream = context.givenOutputStreamFor(A_URI, mode = "wt")
runTest { keysExporter.export(A_PASSWORD, A_URI) } runTest { keysExporter.export(A_PASSWORD, A_URI) }
@ -63,7 +63,7 @@ class KeysExporterTest {
@Test @Test
fun `given different file size returned for export when exporting then throws UnexpectedExportKeysFileSizeException`() { fun `given different file size returned for export when exporting then throws UnexpectedExportKeysFileSizeException`() {
givenFileDescriptorWithSize(size = 110) givenFileDescriptorWithSize(size = 110)
context.givenOutputStreamFor(A_URI) context.givenOutputStreamFor(A_URI, mode = "wt")
assertFailsWith<UnexpectedExportKeysFileSizeException> { assertFailsWith<UnexpectedExportKeysFileSizeException> {
runTest { keysExporter.export(A_PASSWORD, A_URI) } runTest { keysExporter.export(A_PASSWORD, A_URI) }
@ -72,7 +72,7 @@ class KeysExporterTest {
@Test @Test
fun `given output stream is unavailable for exporting to when exporting then throws IllegalStateException`() { fun `given output stream is unavailable for exporting to when exporting then throws IllegalStateException`() {
context.givenMissingOutputStreamFor(A_URI) context.givenMissingOutputStreamFor(A_URI, mode = "wt")
assertFailsWith<IllegalStateException>(message = "Unable to open file for writing") { assertFailsWith<IllegalStateException>(message = "Unable to open file for writing") {
runTest { keysExporter.export(A_PASSWORD, A_URI) } runTest { keysExporter.export(A_PASSWORD, A_URI) }
@ -82,7 +82,7 @@ class KeysExporterTest {
@Test @Test
fun `given exported file is missing after export when exporting then throws IllegalStateException`() { fun `given exported file is missing after export when exporting then throws IllegalStateException`() {
context.givenFileDescriptor(A_URI, mode = "r") { null } context.givenFileDescriptor(A_URI, mode = "r") { null }
context.givenOutputStreamFor(A_URI) context.givenOutputStreamFor(A_URI, mode = "wt")
assertFailsWith<IllegalStateException>(message = "Exported file not found") { assertFailsWith<IllegalStateException>(message = "Exported file not found") {
runTest { keysExporter.export(A_PASSWORD, A_URI) } runTest { keysExporter.export(A_PASSWORD, A_URI) }

View File

@ -39,13 +39,13 @@ class FakeContext(
every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor every { contentResolver.openFileDescriptor(uri, mode, null) } returns fileDescriptor
} }
fun givenOutputStreamFor(uri: Uri): OutputStream { fun givenOutputStreamFor(uri: Uri, mode: String): OutputStream {
val outputStream = mockk<OutputStream>(relaxed = true) val outputStream = mockk<OutputStream>(relaxed = true)
every { contentResolver.openOutputStream(uri) } returns outputStream every { contentResolver.openOutputStream(uri, mode) } returns outputStream
return outputStream return outputStream
} }
fun givenMissingOutputStreamFor(uri: Uri) { fun givenMissingOutputStreamFor(uri: Uri, mode: String) {
every { contentResolver.openOutputStream(uri) } returns null every { contentResolver.openOutputStream(uri, mode) } returns null
} }
} }