mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
Add better logging for ovesized messages.
This commit is contained in:
committed by
Alex Hart
parent
caa743aba2
commit
ae04749336
@@ -12,8 +12,10 @@ import com.squareup.wire.Message
|
||||
import com.squareup.wire.ProtoAdapter
|
||||
import com.squareup.wire.ProtoReader
|
||||
import com.squareup.wire.ProtoWriter
|
||||
import com.squareup.wire.WireField
|
||||
import okio.Buffer
|
||||
import okio.ByteString
|
||||
import okio.utf8Size
|
||||
import org.signal.core.util.logging.Log
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
@@ -87,6 +89,96 @@ fun writeUnknownEnumValue(tag: Int, enumValue: Int): ByteString {
|
||||
return buffer.readByteString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a human-readable tree of serialized sizes for this proto message,
|
||||
* recursively descending into all nested message fields. Useful for debugging
|
||||
* oversized messages.
|
||||
*
|
||||
* Example output:
|
||||
* ```
|
||||
* Content(1234)
|
||||
* dataMessage(1100)
|
||||
* body(500)
|
||||
* groupV2(400)
|
||||
* groupChange(350)
|
||||
* attachments[2](200)
|
||||
* ```
|
||||
*/
|
||||
fun Message<*, *>.buildSizeTree(name: String): String {
|
||||
val sb = StringBuilder()
|
||||
appendSizeTree(name, 0, sb)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun Message<*, *>.appendSizeTree(name: String, depth: Int, sb: StringBuilder) {
|
||||
val totalSize = (adapter as ProtoAdapter<Message<*, *>>).encodedSize(this)
|
||||
repeat(depth) { sb.append(" ") }
|
||||
sb.append(name).append("(").append(totalSize).append(")")
|
||||
|
||||
for (field in this.javaClass.declaredFields) {
|
||||
field.getAnnotation(WireField::class.java) ?: continue
|
||||
|
||||
val value = try {
|
||||
field.get(this)
|
||||
} catch (_: IllegalAccessException) {
|
||||
continue
|
||||
} ?: continue
|
||||
|
||||
val fieldName = field.name
|
||||
|
||||
when (value) {
|
||||
is Message<*, *> -> {
|
||||
sb.append("\n")
|
||||
value.appendSizeTree(fieldName, depth + 1, sb)
|
||||
}
|
||||
is List<*> -> {
|
||||
if (value.isNotEmpty()) {
|
||||
var listTotalSize = 0
|
||||
val isMessageList = value[0] is Message<*, *>
|
||||
for (item in value) {
|
||||
when (item) {
|
||||
is Message<*, *> -> listTotalSize += (item.adapter as ProtoAdapter<Message<*, *>>).encodedSize(item)
|
||||
is ByteString -> listTotalSize += item.size
|
||||
is String -> listTotalSize += item.utf8Size().toInt()
|
||||
}
|
||||
}
|
||||
sb.append("\n")
|
||||
repeat(depth + 1) { sb.append(" ") }
|
||||
sb.append(fieldName).append("[").append(value.size).append("](").append(listTotalSize).append(")")
|
||||
|
||||
if (isMessageList) {
|
||||
for (i in value.indices) {
|
||||
sb.append("\n")
|
||||
(value[i] as Message<*, *>).appendSizeTree("${fieldName}[$i]", depth + 2, sb)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ByteString -> {
|
||||
if (value.size > 0) {
|
||||
sb.append("\n")
|
||||
repeat(depth + 1) { sb.append(" ") }
|
||||
sb.append(fieldName).append("(").append(value.size).append(")")
|
||||
}
|
||||
}
|
||||
is String -> {
|
||||
if (value.isNotEmpty()) {
|
||||
sb.append("\n")
|
||||
repeat(depth + 1) { sb.append(" ") }
|
||||
sb.append(fieldName).append("(").append(value.utf8Size().toInt()).append(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unknownFields.size > 0) {
|
||||
sb.append("\n")
|
||||
repeat(depth + 1) { sb.append(" ") }
|
||||
sb.append("unknownFields(").append(unknownFields.size).append(")")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively retrieves all inner complex proto types inside a given proto.
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.core.models.ServiceId.PNI;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.signal.core.util.ProtoUtil;
|
||||
import org.signal.core.util.UuidUtil;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
@@ -2981,6 +2982,7 @@ public class SignalServiceMessageSender {
|
||||
} else {
|
||||
message = buildContentTooLargeBreadcrumbs(content.getContent().get());
|
||||
}
|
||||
Log.w(TAG, "About to crash for exceeding max envelope size (" + size + " > " + maxEnvelopeSize + ")\n" + message);
|
||||
throw new ContentTooLargeException(size, message);
|
||||
}
|
||||
}
|
||||
@@ -2996,104 +2998,7 @@ public class SignalServiceMessageSender {
|
||||
}
|
||||
|
||||
private String buildContentTooLargeBreadcrumbs(Content content) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
|
||||
if (content.dataMessage != null) {
|
||||
message.append("Data message;");
|
||||
if (content.dataMessage.body != null && !content.dataMessage.body.isEmpty()) {
|
||||
message.append("Body(").append(content.dataMessage.body.length()).append(");");
|
||||
}
|
||||
if (content.dataMessage.groupV2 != null) {
|
||||
if (content.dataMessage.groupV2.groupChange != null) {
|
||||
message.append("GroupV2Change(").append(content.dataMessage.groupV2.groupChange.size()).append(");");
|
||||
} else {
|
||||
message.append("GroupV2NoChange;");
|
||||
}
|
||||
}
|
||||
if (content.dataMessage.giftBadge != null) {
|
||||
message.append("GiftBadge;");
|
||||
}
|
||||
if (content.dataMessage.pollCreate != null) {
|
||||
message.append("PollCreate;");
|
||||
}
|
||||
if (content.dataMessage.pollTerminate != null) {
|
||||
message.append("PollTerminate;");
|
||||
}
|
||||
if (content.dataMessage.pollVote != null) {
|
||||
message.append("PollVote;");
|
||||
}
|
||||
if (content.dataMessage.pinMessage != null) {
|
||||
message.append("PinMessage;");
|
||||
}
|
||||
if (content.dataMessage.unpinMessage != null) {
|
||||
message.append("UnpinMessage;");
|
||||
}
|
||||
if (content.dataMessage.reaction != null) {
|
||||
message.append("Reaction;");
|
||||
}
|
||||
if (content.dataMessage.profileKey != null && content.dataMessage.profileKey.size() > 0) {
|
||||
message.append("ProfileKey(").append(content.dataMessage.profileKey.size()).append(");");
|
||||
}
|
||||
if (content.dataMessage.payment != null) {
|
||||
message.append("Payment;");
|
||||
}
|
||||
if (!content.dataMessage.attachments.isEmpty()) {
|
||||
message.append("Attachments(").append(content.dataMessage.attachments.size()).append(");");
|
||||
}
|
||||
if (!content.dataMessage.contact.isEmpty()) {
|
||||
message.append("Contacts(").append(content.dataMessage.contact.size()).append(");");
|
||||
}
|
||||
if (!content.dataMessage.bodyRanges.isEmpty()) {
|
||||
message.append("Ranges(").append(content.dataMessage.bodyRanges.size()).append(");");
|
||||
}
|
||||
if (content.dataMessage.quote != null) {
|
||||
if (content.dataMessage.quote.text != null) {
|
||||
message.append("Quote(").append(content.dataMessage.quote.text.length()).append(");");
|
||||
} else {
|
||||
message.append("Quote(No text);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (content.syncMessage != null) {
|
||||
message.append("Sync message;");
|
||||
|
||||
if (content.syncMessage.sent != null) {
|
||||
if (content.syncMessage.sent.storyMessage != null) {
|
||||
message.append("StoryMessage(").append(content.syncMessage.sent.storyMessageRecipients.size()).append(");");
|
||||
}
|
||||
if (!content.syncMessage.sent.storyMessageRecipients.isEmpty()) {
|
||||
message.append("StoryRecipients(").append(content.syncMessage.sent.storyMessageRecipients.size()).append(");");
|
||||
}
|
||||
if (content.syncMessage.blocked != null) {
|
||||
message.append("Blocked-AciString(").append(content.syncMessage.blocked.acis.size()).append(");");
|
||||
message.append("Blocked-AciBinary(").append(content.syncMessage.blocked.acisBinary.size()).append(");");
|
||||
message.append("Blocked-GroupIds(").append(content.syncMessage.blocked.groupIds.size()).append(");");
|
||||
message.append("Blocked-Numbers(").append(content.syncMessage.blocked.numbers.size()).append(");");
|
||||
}
|
||||
if (content.syncMessage.outgoingPayment != null) {
|
||||
message.append("OutgoingPayment");
|
||||
}
|
||||
if (content.syncMessage.deleteForMe != null) {
|
||||
message.append("DeleteForMe-Messages(").append(content.syncMessage.deleteForMe.messageDeletes.size()).append(");");
|
||||
message.append("DeleteForMe-Attachments(").append(content.syncMessage.deleteForMe.attachmentDeletes.size()).append(");");
|
||||
message.append("DeleteForMe-Conversations(").append(content.syncMessage.deleteForMe.conversationDeletes.size()).append(");");
|
||||
}
|
||||
if (!content.syncMessage.read.isEmpty()) {
|
||||
message.append("Read(").append(content.syncMessage.read.size()).append(");");
|
||||
}
|
||||
if (!content.syncMessage.viewed.isEmpty()) {
|
||||
message.append("Viewed(").append(content.syncMessage.read.size()).append(");");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (content.receiptMessage != null) {
|
||||
message.append("ReceiptMessage(").append(content.receiptMessage.timestamp.size()).append(");");
|
||||
message.append("ReceiptMessage(").append(content.receiptMessage.type.getValue()).append(");");
|
||||
}
|
||||
|
||||
return message.toString();
|
||||
return ProtoUtil.buildSizeTree(content, "Content");
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.doesNotContain
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.startsWith
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Test
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
|
||||
class BuildSizeTreeTest {
|
||||
|
||||
@Test
|
||||
fun `empty message has zero encoded size`() {
|
||||
val msg = DataMessage()
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
assertThat(tree).isEqualTo("DataMessage(0)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `string field shows utf8 byte size`() {
|
||||
val msg = DataMessage(body = "hello")
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
|
||||
assertThat(tree).startsWith("DataMessage(")
|
||||
assertThat(tree).contains("\n body(5)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multi-byte utf8 string shows correct byte size`() {
|
||||
val msg = DataMessage(body = "\uD83D\uDE00") // emoji, 4 bytes in UTF-8
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
|
||||
assertThat(tree).contains("body(4)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ByteString field shows raw byte size`() {
|
||||
val bytes = ByteArray(100).toByteString()
|
||||
val msg = DataMessage(profileKey = bytes)
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
|
||||
assertThat(tree).contains("\n profileKey(100)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `null and empty fields are omitted`() {
|
||||
val msg = DataMessage(body = "hi")
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
|
||||
assertThat(tree).doesNotContain("profileKey")
|
||||
assertThat(tree).doesNotContain("groupV2")
|
||||
assertThat(tree).doesNotContain("attachments")
|
||||
assertThat(tree).doesNotContain("bodyRanges")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nested message field shows indented sub-tree`() {
|
||||
val msg = Content(
|
||||
dataMessage = DataMessage(body = "test")
|
||||
)
|
||||
val tree = msg.buildSizeTree("Content")
|
||||
|
||||
assertThat(tree).startsWith("Content(")
|
||||
assertThat(tree).contains("\n dataMessage(")
|
||||
assertThat(tree).contains("\n body(4)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `repeated message field shows count and total size with elements`() {
|
||||
val ranges = listOf(
|
||||
BodyRange(start = 0, length = 5),
|
||||
BodyRange(start = 10, length = 3)
|
||||
)
|
||||
val msg = DataMessage(bodyRanges = ranges)
|
||||
val tree = msg.buildSizeTree("DataMessage")
|
||||
|
||||
assertThat(tree).contains("bodyRanges[2](")
|
||||
assertThat(tree).contains("bodyRanges[0](")
|
||||
assertThat(tree).contains("bodyRanges[1](")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deeply nested structure shows correct indentation`() {
|
||||
val groupChange = ByteArray(50).toByteString()
|
||||
val masterKey = ByteArray(32).toByteString()
|
||||
val msg = Content(
|
||||
dataMessage = DataMessage(
|
||||
body = "hi",
|
||||
groupV2 = GroupContextV2(
|
||||
masterKey = masterKey,
|
||||
groupChange = groupChange,
|
||||
revision = 5
|
||||
)
|
||||
)
|
||||
)
|
||||
val tree = msg.buildSizeTree("Content")
|
||||
|
||||
// depth 0
|
||||
assertThat(tree).startsWith("Content(")
|
||||
// depth 1
|
||||
assertThat(tree).contains("\n dataMessage(")
|
||||
// depth 2
|
||||
assertThat(tree).contains("\n body(2)")
|
||||
assertThat(tree).contains("\n groupV2(")
|
||||
// depth 3
|
||||
assertThat(tree).contains("\n masterKey(32)")
|
||||
assertThat(tree).contains("\n groupChange(50)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encoded sizes match wire adapter`() {
|
||||
val msg = Content(
|
||||
dataMessage = DataMessage(body = "hello world")
|
||||
)
|
||||
val tree = msg.buildSizeTree("Content")
|
||||
|
||||
val contentSize = Content.ADAPTER.encodedSize(msg)
|
||||
val dataMessageSize = DataMessage.ADAPTER.encodedSize(msg.dataMessage!!)
|
||||
|
||||
assertThat(tree).startsWith("Content($contentSize)")
|
||||
assertThat(tree).contains("\n dataMessage($dataMessageSize)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple top-level fields are all included`() {
|
||||
val msg = Content(
|
||||
dataMessage = DataMessage(body = "hi"),
|
||||
syncMessage = SyncMessage()
|
||||
)
|
||||
val tree = msg.buildSizeTree("Content")
|
||||
|
||||
assertThat(tree).contains("dataMessage(")
|
||||
assertThat(tree).contains("syncMessage(")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom root name is used`() {
|
||||
val msg = DataMessage(body = "x")
|
||||
val tree = msg.buildSizeTree("MyCustomName")
|
||||
|
||||
assertThat(tree).startsWith("MyCustomName(")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user