From f093b88305f4c0dbdebb5040a2f765563ccf8b72 Mon Sep 17 00:00:00 2001 From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:01:10 +1100 Subject: [PATCH] Fix more crashes (#1036) --- .../groups/EnterCommunityUrlFragment.kt | 2 +- .../libsignal/utilities/ByteArraySliceTest.kt | 44 +++++++++ .../messaging/file_server/FileServerApi.kt | 5 +- .../messaging/jobs/AttachmentDownloadJob.kt | 6 +- .../messaging/jobs/GroupAvatarDownloadJob.kt | 2 +- .../messaging/open_groups/OpenGroupApi.kt | 10 +- .../libsession/snode/OnionRequestAPI.kt | 22 ++--- .../libsession/utilities/DownloadUtilities.kt | 3 +- .../utilities/TextSecurePreferences.kt | 9 +- .../libsignal/utilities/ByteArraySlice.kt | 94 +++++++++++++++++++ .../session/libsignal/utilities/JsonUtil.java | 4 + 11 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 app/src/test/java/org/session/libsignal/utilities/ByteArraySliceTest.kt create mode 100644 libsignal/src/main/java/org/session/libsignal/utilities/ByteArraySlice.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt index a23e71c640..26c9fd1fdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EnterCommunityUrlFragment.kt @@ -66,7 +66,7 @@ class EnterCommunityUrlFragment : Fragment() { groups.iterator().forEach { defaultGroup -> val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsFlexboxLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> - val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + val bitmap = BitmapFactory.decodeByteArray(bytes.data, bytes.offset, bytes.len) RoundedBitmapDrawableFactory.create(resources, bitmap).apply { isCircular = true } diff --git a/app/src/test/java/org/session/libsignal/utilities/ByteArraySliceTest.kt b/app/src/test/java/org/session/libsignal/utilities/ByteArraySliceTest.kt new file mode 100644 index 0000000000..a9b2c64173 --- /dev/null +++ b/app/src/test/java/org/session/libsignal/utilities/ByteArraySliceTest.kt @@ -0,0 +1,44 @@ +package org.session.libsignal.utilities + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test +import org.session.libsignal.utilities.ByteArraySlice.Companion.view + +class ByteArraySliceTest { + @Test + fun `view works`() { + val sliced = byteArrayOf(1, 2, 3, 4, 5).view(1..3) + assertEquals(listOf(2, 3, 4), sliced.asList()) + } + + @Test + fun `re-view works`() { + val sliced = byteArrayOf(1, 2, 3, 4, 5).view(1..3) + val resliced = sliced.view(1..2) + assertEquals(listOf(3, 4), resliced.asList()) + } + + @Test + fun `decodeToString works`() { + assertEquals( + "hel", + "hello, world".toByteArray().view(0..2).decodeToString() + ) + } + + @Test + fun `inputStream works`() { + assertArrayEquals( + "hello, world".toByteArray(), + "hello, world".toByteArray().inputStream().readBytes() + ) + } + + @Test + fun `able to view empty array`() { + val sliced = byteArrayOf().view() + assertEquals(0, sliced.len) + assertEquals(0, sliced.offset) + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 18ad51692f..6f7d36804a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -15,6 +15,7 @@ import org.session.libsession.snode.utilities.await import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ByteArraySlice import org.session.libsignal.utilities.toHexString import kotlin.time.Duration.Companion.milliseconds @@ -51,7 +52,7 @@ object FileServerApi { return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } - private fun send(request: Request): Promise { + private fun send(request: Request): Promise { val url = server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL) val urlBuilder = HttpUrl.Builder() .scheme(url.scheme) @@ -106,7 +107,7 @@ object FileServerApi { } } - fun download(file: String): Promise { + fun download(file: String): Promise { val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file") return send(request) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 300c6c2dde..c07b527aa8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -18,8 +18,10 @@ import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.streams.AttachmentCipherInputStream import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ByteArraySlice.Companion.write import java.io.File import java.io.FileInputStream +import java.io.FileOutputStream import java.io.InputStream class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) : Job { @@ -138,8 +140,8 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) Log.d("AttachmentDownloadJob", "downloading open group attachment") val url = attachment.url.toHttpUrlOrNull()!! val fileID = url.pathSegments.last() - OpenGroupApi.download(fileID, openGroup.room, openGroup.server).await().let { - tempFile.writeBytes(it) + OpenGroupApi.download(fileID, openGroup.room, openGroup.server).await().let { data -> + FileOutputStream(tempFile).use { output -> output.write(data) } } } Log.d("AttachmentDownloadJob", "getting input stream") diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt index b2831e9029..98114c8b51 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -44,7 +44,7 @@ class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: } val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) - storage.updateProfilePicture(groupId, bytes) + storage.updateProfilePicture(groupId, bytes.copyToBytes()) storage.updateTimestampUpdated(groupId, SnodeAPI.nowWithOffset) delegate?.handleJobSucceeded(this, dispatcherName) } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 5e93a01018..ca7a05c268 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -10,7 +10,6 @@ import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.Sign import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.map import okhttp3.Headers.Companion.toHeaders @@ -39,6 +38,7 @@ import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ByteArraySlice import org.session.libsignal.utilities.removingIdPrefixIfNeeded import org.whispersystems.curve25519.Curve25519 import java.util.concurrent.TimeUnit @@ -79,7 +79,7 @@ object OpenGroupApi { object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.") } - data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) { + data class DefaultGroup(val id: String, val name: String, val image: ByteArraySlice?) { val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey" } @@ -290,7 +290,7 @@ object OpenGroupApi { return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } - private fun getResponseBody(request: Request): Promise { + private fun getResponseBody(request: Request): Promise { return send(request).map { response -> response.body ?: throw Error.ParsingFailed } @@ -417,7 +417,7 @@ object OpenGroupApi { server: String, roomID: String, imageId: String - ): Promise { + ): Promise { val request = Request( verb = GET, room = roomID, @@ -445,7 +445,7 @@ object OpenGroupApi { } } - fun download(fileId: String, room: String, server: String): Promise { + fun download(fileId: String, room: String, server: String): Promise { val request = Request( verb = GET, room = room, diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 7357c1f1aa..a6f5159fc6 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -1,6 +1,5 @@ package org.session.libsession.snode -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -27,15 +26,15 @@ import org.session.libsignal.crypto.secureRandom import org.session.libsignal.crypto.secureRandomOrNull import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Base64 -import org.session.libsignal.utilities.Broadcaster import org.session.libsignal.utilities.ForkInfo import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ByteArraySlice +import org.session.libsignal.utilities.ByteArraySlice.Companion.view import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.recover import org.session.libsignal.utilities.toHexString -import java.util.concurrent.atomic.AtomicReference import kotlin.collections.set private typealias Path = List @@ -629,7 +628,7 @@ object OnionRequestAPI { ) return deferred.reject(exception) } - deferred.resolve(OnionResponse(body, JsonUtil.toJson(body).toByteArray())) + deferred.resolve(OnionResponse(body, JsonUtil.toJson(body).toByteArray().view())) } else -> { if (statusCode != 200) { @@ -640,7 +639,7 @@ object OnionRequestAPI { ) return deferred.reject(exception) } - deferred.resolve(OnionResponse(json, JsonUtil.toJson(json).toByteArray())) + deferred.resolve(OnionResponse(json, JsonUtil.toJson(json).toByteArray().view())) } } } catch (exception: Exception) { @@ -652,17 +651,16 @@ object OnionRequestAPI { } } - private fun ByteArray.getBody(infoLength: Int, infoEndIndex: Int): ByteArray { + private fun ByteArray.getBody(infoLength: Int, infoEndIndex: Int): ByteArraySlice { // If there is no data in the response, i.e. only `l123:jsone`, then just return the ResponseInfo val infoLengthStringLength = infoLength.toString().length if (size <= infoLength + infoLengthStringLength + 2/*l and e bytes*/) { - return byteArrayOf() + return ByteArraySlice.EMPTY } // Extract the response data as well - val dataSlice = slice(infoEndIndex + 1 until size - 1) - val dataSepIdx = dataSlice.indexOfFirst { byteArrayOf(it).contentEquals(":".toByteArray()) } - val responseBody = dataSlice.slice(dataSepIdx + 1 until dataSlice.size) - return responseBody.toByteArray() + val dataSlice = view(infoEndIndex + 1 until size - 1) + val dataSepIdx = dataSlice.asList().indexOfFirst { it.toInt() == ':'.code } + return dataSlice.view(dataSepIdx + 1 until dataSlice.len) } // endregion @@ -676,7 +674,7 @@ enum class Version(val value: String) { data class OnionResponse( val info: Map<*, *>, - val body: ByteArray? = null + val body: ByteArraySlice? = null ) { val code: Int? get() = info["code"] as? Int val message: String? get() = info["message"] as? String diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index f159d20907..cf581d0f01 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -2,15 +2,14 @@ package org.session.libsession.utilities import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.snode.utilities.await import org.session.libsignal.exceptions.NonRetryableException import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.ByteArraySlice.Companion.write import java.io.File -import java.io.FileOutputStream import java.io.OutputStream object DownloadUtilities { diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index e6842bf57f..3ede3b3863 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import org.session.libsession.R +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK @@ -213,7 +214,8 @@ interface TextSecurePreferences { // This is a stop-gap solution for static access to shared preference. - internal lateinit var preferenceInstance: TextSecurePreferences + val preferenceInstance: TextSecurePreferences + get() = MessagingModuleConfiguration.shared.preferences const val DISABLE_PASSPHRASE_PREF = "pref_disable_passphrase" const val LANGUAGE_PREF = "pref_language" @@ -987,11 +989,6 @@ interface TextSecurePreferences { class AppTextSecurePreferences @Inject constructor( @ApplicationContext private val context: Context ): TextSecurePreferences { - init { - // Should remove once all static access to the companion objects is removed - TextSecurePreferences.preferenceInstance = this - } - private val localNumberState = MutableStateFlow(getStringPreference(TextSecurePreferences.LOCAL_NUMBER_PREF, null)) override var migratedToGroupV2Config: Boolean diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/ByteArraySlice.kt b/libsignal/src/main/java/org/session/libsignal/utilities/ByteArraySlice.kt new file mode 100644 index 0000000000..8bb047bbaf --- /dev/null +++ b/libsignal/src/main/java/org/session/libsignal/utilities/ByteArraySlice.kt @@ -0,0 +1,94 @@ +package org.session.libsignal.utilities + +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.io.OutputStream + +/** + * A view of a byte array with a range. This is useful for avoiding copying data when slicing a byte array. + */ +class ByteArraySlice private constructor( + val data: ByteArray, + val offset: Int, + val len: Int, +) { + init { + check(offset in 0..data.size) { "Offset $offset is not within [0..${data.size}]" } + check(len in 0..data.size) { "Length $len is not within [0..${data.size}]" } + } + + fun view(range: IntRange): ByteArraySlice { + val newOffset = offset + range.first + val newLength = range.last + 1 - range.first + return ByteArraySlice( + data = data, + offset = newOffset, + len = newLength + ) + } + + fun copyToBytes(): ByteArray { + return data.copyOfRange(offset, offset + len) + } + + operator fun get(index: Int): Byte { + return data[offset + index] + } + + fun asList(): List { + return object : AbstractList() { + override val size: Int + get() = this@ByteArraySlice.len + + override fun get(index: Int) = this@ByteArraySlice[index] + } + } + + fun decodeToString(): String { + return data.decodeToString(offset, offset + len) + } + + fun inputStream(): InputStream { + return ByteArrayInputStream(data, offset, len) + } + + fun isEmpty(): Boolean = len == 0 + fun isNotEmpty(): Boolean = len != 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ByteArraySlice) return false + + if (offset != other.offset) return false + if (len != other.len) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = offset + result = 31 * result + len + result = 31 * result + data.contentHashCode() + return result + } + + companion object { + val EMPTY = ByteArraySlice(byteArrayOf(), 0, 0) + + /** + * Create a view of a byte array + */ + fun ByteArray.view(range: IntRange = indices): ByteArraySlice { + return ByteArraySlice( + data = this, + offset = range.first, + len = range.last + 1 - range.first + ) + } + + fun OutputStream.write(view: ByteArraySlice) { + write(view.data, view.offset, view.len) + } + } +} diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/JsonUtil.java b/libsignal/src/main/java/org/session/libsignal/utilities/JsonUtil.java index 1ed3ec67f3..872b716f23 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/JsonUtil.java +++ b/libsignal/src/main/java/org/session/libsignal/utilities/JsonUtil.java @@ -31,6 +31,10 @@ public class JsonUtil { return fromJson(new String(serialized), clazz); } + public static T fromJson(ByteArraySlice serialized, Class clazz) throws IOException { + return objectMapper.readValue(serialized.getData(), serialized.getOffset(), serialized.getLen(), clazz); + } + public static T fromJson(String serialized, TypeReference typeReference) throws IOException { return objectMapper.readValue(serialized, typeReference); }