mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-27 22:43:52 +00:00
Add kotlin/proto level message processing.
This commit is contained in:
committed by
Alex Hart
parent
28f27915c5
commit
2e45bd719a
@@ -0,0 +1,382 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toSingleLine
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.MessageTypes.isOutgoingMessageType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.Optional
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageContentProcessorTestV2 {
|
||||
|
||||
companion object {
|
||||
private val TAGS = listOf(MessageContentProcessor.TAG, MessageContentProcessorV2.TAG, AttachmentTable.TAG)
|
||||
|
||||
private val GENERALIZE_TAG = mapOf(
|
||||
MessageContentProcessor.TAG to "MCP",
|
||||
MessageContentProcessorV2.TAG to "MCP",
|
||||
AttachmentTable.TAG to AttachmentTable.TAG
|
||||
)
|
||||
|
||||
private val IGNORE_MESSAGE_COLUMNS = listOf(
|
||||
MessageTable.DATE_RECEIVED,
|
||||
MessageTable.NOTIFIED_TIMESTAMP,
|
||||
MessageTable.REACTIONS_LAST_SEEN,
|
||||
MessageTable.NOTIFIED
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV1: MessageContentProcessor
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV1 = MessageContentProcessor(harness.context)
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val messages: List<TestMessage> = (0 until 1).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.fuzzMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(messages)
|
||||
testResult.runV1(messages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val messages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.fuzzMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val moreMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageWithBody(messages),
|
||||
metadata = MessageContentFuzzer.fuzzMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val evenMoreMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoContent(messages + moreMessages),
|
||||
metadata = MessageContentFuzzer.fuzzMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val evenMoreMoreMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoText(messages + moreMessages),
|
||||
metadata = MessageContentFuzzer.fuzzMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(messages + moreMessages + evenMoreMessages + evenMoreMoreMessages)
|
||||
testResult.runV1(messages + moreMessages + evenMoreMessages + evenMoreMoreMessages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun fuzzIt() {
|
||||
// MessageContentFuzzer.fuzzProto(SignalServiceProtos.DataMessage.Contact.Name::class)
|
||||
// MessageContentFuzzer.fuzzProto(SignalServiceProtos.DataMessage.Contact.Avatar::class)
|
||||
// MessageContentFuzzer.fuzzProto(SignalServiceProtos.DataMessage.Contact.Email::class)
|
||||
// }
|
||||
|
||||
private inner class TestResults {
|
||||
|
||||
private lateinit var v1Logs: List<Entry>
|
||||
private lateinit var v1Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v1Attachments: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Logs: List<Entry>
|
||||
private lateinit var v2Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Attachments: List<List<Pair<String, String?>>>
|
||||
|
||||
fun runV1(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV1.process(
|
||||
MessageContentProcessor.MessageState.DECRYPTED_OK,
|
||||
toSignalServiceContent(envelope, content, metadata, serverDeliveredTimestamp),
|
||||
null,
|
||||
envelope.timestamp,
|
||||
-1
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v1Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v1Messages = dumpMessages()
|
||||
v1Attachments = dumpAttachments()
|
||||
}
|
||||
|
||||
fun runV2(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
serverDeliveredTimestamp,
|
||||
false
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v2Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v2Messages = dumpMessages()
|
||||
v2Attachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
SignalDatabase.rawDatabase.withinTransaction { db ->
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
|
||||
}
|
||||
}
|
||||
|
||||
fun assert() {
|
||||
v2Logs.zip(v1Logs)
|
||||
.forEach { (v2, v1) ->
|
||||
GENERALIZE_TAG[v2.tag]!!.assertIs(GENERALIZE_TAG[v1.tag]!!)
|
||||
|
||||
if (v2.tag != AttachmentTable.TAG) {
|
||||
if (v2.message?.startsWith("[") == true && v1.message?.startsWith("[") == false) {
|
||||
v2.message!!.substring(v2.message!!.indexOf(']') + 2).assertIs(v1.message)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
} else {
|
||||
if (v2.message?.startsWith("Inserted attachment at ID: AttachmentId::") == true) {
|
||||
v2.message!!
|
||||
.substring(0, v2.message!!.indexOf(','))
|
||||
.assertIs(
|
||||
v1.message!!
|
||||
.substring(0, v1.message!!.indexOf(','))
|
||||
)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
}
|
||||
v2.throwable.assertIs(v1.throwable)
|
||||
}
|
||||
|
||||
v2Messages.zip(v1Messages)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
|
||||
v2Attachments.zip(v1Attachments)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InMemoryLogger.logs(): List<Entry> {
|
||||
return entries()
|
||||
.filter { TAGS.contains(it.tag) }
|
||||
}
|
||||
|
||||
private fun dumpMessages(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(MessageTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(AttachmentTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == "type") {
|
||||
data = thing(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSignalServiceContent(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long): SignalServiceContent {
|
||||
val localAddress = SignalServiceAddress(metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
|
||||
val signalServiceMetadata = SignalServiceMetadata(
|
||||
SignalServiceAddress(metadata.sourceServiceId, Optional.ofNullable(metadata.sourceE164)),
|
||||
metadata.sourceDeviceId,
|
||||
envelope.timestamp,
|
||||
envelope.serverTimestamp,
|
||||
serverDeliveredTimestamp,
|
||||
metadata.sealedSender,
|
||||
envelope.serverGuid,
|
||||
Optional.ofNullable(metadata.groupId),
|
||||
metadata.destinationServiceId.toString()
|
||||
)
|
||||
|
||||
val contentProto = SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
|
||||
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(signalServiceMetadata))
|
||||
.setContent(content)
|
||||
.build()
|
||||
|
||||
return SignalServiceContent.createFromProto(contentProto)!!
|
||||
}
|
||||
|
||||
fun thing(type: Long): String {
|
||||
return """
|
||||
isOutgoingMessageType:${isOutgoingMessageType(type)}
|
||||
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
|
||||
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
|
||||
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
|
||||
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
|
||||
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
|
||||
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
|
||||
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
|
||||
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
|
||||
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
|
||||
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
|
||||
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
|
||||
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
|
||||
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
|
||||
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
|
||||
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
|
||||
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
|
||||
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
|
||||
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
|
||||
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
|
||||
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
|
||||
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
|
||||
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
|
||||
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
|
||||
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
|
||||
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
|
||||
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
|
||||
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
|
||||
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
|
||||
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
|
||||
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
|
||||
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
|
||||
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
|
||||
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
|
||||
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
|
||||
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
|
||||
isDecryptInProgressType:${type and 0x40000000 != 0L}
|
||||
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
|
||||
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
|
||||
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
|
||||
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
|
||||
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
|
||||
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
|
||||
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
|
||||
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
|
||||
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
|
||||
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
|
||||
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
|
||||
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
|
||||
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
|
||||
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
|
||||
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
|
||||
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").toSingleLine()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
data class TestMessage(
|
||||
val envelope: SignalServiceProtos.Envelope,
|
||||
val content: SignalServiceProtos.Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
val serverDeliveredTimestamp: Long
|
||||
)
|
||||
@@ -30,6 +30,16 @@ class InMemoryLogger : Log.Logger() {
|
||||
latch.await()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val latch = CountDownLatch(1)
|
||||
executor.execute {
|
||||
predicates.clear()
|
||||
logEntries.clear()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
private fun add(entry: Entry) {
|
||||
executor.execute {
|
||||
logEntries += entry
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.declaredFunctions
|
||||
import kotlin.reflect.full.functions
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
object MessageContentFuzzer {
|
||||
|
||||
private val mediaTypes = listOf("image/png", "image/jpeg", "image/heic", "image/heif", "image/avif", "image/webp", "image/gif", "audio/aac", "audio/*", "video/mp4", "video/*", "text/x-vcard", "text/x-signal-plain", "application/x-signal-view-once", "*/*", "application/octet-stream")
|
||||
private val emojis = listOf("😂", "❤️", "🔥", "😍", "👀", "🤔", "🙏", "👍", "🤷", "🥺")
|
||||
|
||||
private val random = Random(1)
|
||||
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 5)
|
||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun fuzzTextMessage(): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
addBodyRanges(
|
||||
SignalServiceProtos.BodyRange.newBuilder().run {
|
||||
start = 0
|
||||
length = 1
|
||||
style = SignalServiceProtos.BodyRange.Style.BOLD
|
||||
build()
|
||||
}
|
||||
)
|
||||
}
|
||||
build()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextBoolean()) {
|
||||
body = string()
|
||||
}
|
||||
|
||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||
body = string()
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().run {
|
||||
id = quoted.envelope.timestamp
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
||||
type = DataMessage.Quote.Type.NORMAL
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().run {
|
||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val total = random.nextInt(1, 2)
|
||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val reactTo = previousMessages.random(random)
|
||||
reaction = DataMessage.Reaction.newBuilder().run {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorUuid = reactTo.metadata.sourceServiceId.toString()
|
||||
targetSentTimestamp = reactTo.envelope.timestamp
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().run {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.newBuilder().run {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
build()
|
||||
}
|
||||
}
|
||||
build()
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
fun string(length: Int = 10, allowNullString: Boolean = false): String {
|
||||
var string = ""
|
||||
|
||||
if (allowNullString && random.nextBoolean()) {
|
||||
return string
|
||||
}
|
||||
|
||||
for (i in 0 until length) {
|
||||
string += random.nextInt(65..90).toChar()
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
fun byteString(length: Int = 512): ByteString {
|
||||
return random.nextBytes(512).toProtoByteString()
|
||||
}
|
||||
|
||||
fun attachmentPointer(): SignalServiceProtos.AttachmentPointer {
|
||||
return SignalServiceProtos.AttachmentPointer.newBuilder().run {
|
||||
cdnKey = string()
|
||||
contentType = mediaTypes.random(random)
|
||||
key = byteString()
|
||||
size = random.nextInt(1024 * 1024 * 50)
|
||||
thumbnail = byteString()
|
||||
digest = byteString()
|
||||
fileName = string()
|
||||
flags = 0
|
||||
width = random.nextInt(until = 1024)
|
||||
height = random.nextInt(until = 1024)
|
||||
caption = string(allowNullString = true)
|
||||
blurHash = string()
|
||||
uploadTimestamp = random.nextLong()
|
||||
cdnNumber = 1
|
||||
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
||||
return envelopeTimestamp + 10
|
||||
}
|
||||
|
||||
fun fuzzMetadata(source: RecipientId, destination: RecipientId): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sealedSender = true,
|
||||
groupId = null,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
)
|
||||
}
|
||||
|
||||
fun <T : Any> fuzzProto(protoClazz: KClass<T>) {
|
||||
val newBuilder: Any = protoClazz.declaredFunctions.first { it.name == "newBuilder" }.call()!!
|
||||
|
||||
val setters: List<KFunction<*>> = newBuilder::class.functions.filter { it.name.startsWith("set") && !it.name.contains("Bytes") }
|
||||
|
||||
for (setter in setters) {
|
||||
val type = setter.parameters[1].type.jvmErasure
|
||||
when {
|
||||
type == String::class -> setter.call(newBuilder, string())
|
||||
type == Int::class -> setter.call(newBuilder, random.nextInt())
|
||||
type == Long::class -> setter.call(newBuilder, random.nextLong())
|
||||
type == AttachmentPointer::class -> setter.call(newBuilder, attachmentPointer())
|
||||
type == Boolean::class -> setter.call(newBuilder, random.nextBoolean())
|
||||
// type.superclasses.contains(EnumLite::class) ->
|
||||
else -> Log.e("CODY", "WHAT!?!?!?! ${setter.parameters[1].type.jvmErasure}")
|
||||
}
|
||||
}
|
||||
|
||||
Log.e("CODY", newBuilder::class.functions.first { it.name == "build" }.call(newBuilder).toString())
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
SignalStore.settings().isMessageNotificationsEnabled = false
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ fun <T : Any?> T.assertIsNotNull() {
|
||||
assertThat(this, notNullValue())
|
||||
}
|
||||
|
||||
infix fun <T : Any> T.assertIs(expected: T) {
|
||||
infix fun <T : Any?> T.assertIs(expected: T) {
|
||||
assertThat(this, `is`(expected))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user