Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alter the AppExitInfoService to encode NDK protobuf info in escaped chars #22

Merged
merged 3 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
encode AEI strings
  • Loading branch information
fractalwrench committed Oct 27, 2023
commit ca9f6656477c7f8c534926079a58035c0e57e60a
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import io.embrace.android.embracesdk.logging.InternalStaticEmbraceLogger.Compani
import io.embrace.android.embracesdk.payload.AppExitInfoData
import io.embrace.android.embracesdk.prefs.PreferencesService
import java.io.IOException
import java.util.Base64
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
Expand Down Expand Up @@ -176,14 +175,13 @@ internal class EmbraceApplicationExitInfoService constructor(

private fun collectExitInfoTrace(appExitInfo: ApplicationExitInfo): AppExitInfoBehavior.CollectTracesResult? {
try {
val encoder = Base64.getEncoder()
val bytes = appExitInfo.traceInputStream?.readBytes()

if (bytes == null) {
logDebug("AEI - No info trace collected")
return null
}
val trace = encoder.encodeToString(bytes)
val trace = bytesToUTF8String(bytes)

val traceMaxLimit = configService.appExitInfoBehavior.getTraceMaxLimit()
if (trace.length > traceMaxLimit) {
Expand All @@ -204,6 +202,26 @@ internal class EmbraceApplicationExitInfoService constructor(
}
}

/**
* Converts a byte array to a UTF-8 string, escaping non-encodable bytes as
* \Uxxxx characters. This allows us to send arbitrary binary data from the NDK
fractalwrench marked this conversation as resolved.
Show resolved Hide resolved
* protobuf file without needing to encode it as Base64 (which compresses poorly).
*/
private fun bytesToUTF8String(bytes: ByteArray): String {
val encoded = ByteArray(bytes.size * 2)
var i = 0
for (b in bytes) {
val u = b.toInt() and 0xFF
if (u < 128) {
encoded[i++] = u.toByte()
continue
}
encoded[i++] = (0xC0 or (u shr 6)).toByte()
encoded[i++] = (0x80 or (u and 0x3F)).toByte()
}
return String(encoded.copyOf(i), Charsets.UTF_8)
}

private fun getSessionIdValidationError(sid: String): String {
return if (sid.isEmpty() || sid.matches(Regex("^[0-9a-fA-F]{32}\$"))) {
""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
import java.io.InputStream
import java.util.Base64

/**
* Verifies that the protobuf file containing the NDK crash details can be sent by our SDK.
Expand All @@ -37,21 +36,6 @@ internal class AeiNdkCrashProtobufSendTest {
assertProtobufIsReadable(stream)
}

/**
* Sends the protobuf file to the delivery service. This involves the protobuf file being read
* and included into the payload. It is not a full test of how JSON is serialized & received
* by our server.
*/
@Test
fun testDeliveredProtobufIsReadable() {
val deliveryService = FakeDeliveryService()
createAeiService(deliveryService)

val obj = deliveryService.getAeiObject()
val byteStream = obj.asStream()
assertProtobufIsReadable(byteStream)
}

/**
* Serializes then deserializes a protobuf in the payload. This ensures that information is
* not lost when encoding the protobuf into JSON that is sent to the server.
Expand All @@ -61,8 +45,11 @@ internal class AeiNdkCrashProtobufSendTest {
val deliveryService = FakeDeliveryService()
createAeiService(deliveryService)

// serialize then deserialize to/from JSON
// sending through the delivery service does not corrupt the protobuf
val obj = deliveryService.getAeiObject()
assertProtobufIsReadable(obj.asStream())

// JSON serialization does not corrupt the protobuf
val json = Gson().toJson(obj)
val aei = Gson().fromJson(json, AppExitInfoData::class.java)
val byteStream = aei.asStream()
Expand All @@ -73,9 +60,36 @@ internal class AeiNdkCrashProtobufSendTest {
* Gets an inputstream of the protobuf file that was encoded in the AEI object
*/
private fun AppExitInfoData.asStream(): InputStream {
val decoder = Base64.getDecoder()
val contents = decoder.decode(trace)
return contents.inputStream()
val contents = trace ?: error("No trace found")
return utf8StringToBytes(contents).inputStream()
}

/**
* Decodes a UTF-8 string with arbitrary binary data encoded in \Uxxxx characters.
*/
private fun utf8StringToBytes(utf8String: String): ByteArray {
val encoded = utf8String.toByteArray(Charsets.UTF_8)
val decoded = ByteArray(encoded.size)

var decodedIndex = 0
var k = 0
while (k < encoded.size) {
val u = encoded[k].toInt() and 0xFF
if (u < 128) {
decoded[decodedIndex++] = u.toByte()
k++
} else {
val a = u and 0x1F
k++
if (k >= encoded.size) {
error("Invalid UTF-8 encoding")
}
val b = encoded[k].toInt() and 0x3F
k++
decoded[decodedIndex++] = ((a shl 6) or b).toByte()
}
}
return decoded.copyOf(decodedIndex)
}

/**
Expand Down