From f4002850bb76812f2f42d294d42e3a929b3e0946 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 23 Feb 2022 18:22:58 -0500 Subject: [PATCH] Add a ColumnTransformer system to Spinner. --- .../securesms/SpinnerApplicationContext.kt | 17 ++- .../MessageBitmaskColumnTransformer.kt | 122 ++++++++++++++++++ .../org/signal/spinnertest/MainActivity.kt | 4 +- spinner/lib/src/main/assets/browse.hbs | 2 +- .../org/signal/spinner/ColumnTransformer.kt | 18 +++ .../spinner/DefaultColumnTransformer.kt | 20 +++ .../java/org/signal/spinner/MessageUtil.kt | 117 ----------------- .../main/java/org/signal/spinner/Spinner.kt | 11 +- .../java/org/signal/spinner/SpinnerServer.kt | 67 +++++----- 9 files changed, 215 insertions(+), 163 deletions(-) create mode 100644 app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt create mode 100644 spinner/lib/src/main/java/org/signal/spinner/ColumnTransformer.kt create mode 100644 spinner/lib/src/main/java/org/signal/spinner/DefaultColumnTransformer.kt delete mode 100644 spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt index e60846227..f09fb3158 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt @@ -4,12 +4,14 @@ import android.content.ContentValues import android.os.Build import leakcanary.LeakCanary import org.signal.spinner.Spinner +import org.signal.spinner.Spinner.DatabaseConfig import org.thoughtcrime.securesms.database.DatabaseMonitor import org.thoughtcrime.securesms.database.JobDatabase import org.thoughtcrime.securesms.database.KeyValueDatabase import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.database.MegaphoneDatabase +import org.thoughtcrime.securesms.database.MessageBitmaskColumnTransformer import org.thoughtcrime.securesms.database.QueryMonitor import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.util.AppSignatureUtil @@ -27,12 +29,15 @@ class SpinnerApplicationContext : ApplicationContext() { appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.CANONICAL_VERSION_CODE}, ${BuildConfig.GIT_HASH})" ), linkedMapOf( - "signal" to SignalDatabase.rawDatabase, - "jobmanager" to JobDatabase.getInstance(this).sqlCipherDatabase, - "keyvalue" to KeyValueDatabase.getInstance(this).sqlCipherDatabase, - "megaphones" to MegaphoneDatabase.getInstance(this).sqlCipherDatabase, - "localmetrics" to LocalMetricsDatabase.getInstance(this).sqlCipherDatabase, - "logs" to LogDatabase.getInstance(this).sqlCipherDatabase, + "signal" to DatabaseConfig( + db = SignalDatabase.rawDatabase, + columnTransformers = listOf(MessageBitmaskColumnTransformer) + ), + "jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase), + "keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase), + "megaphones" to DatabaseConfig(db = MegaphoneDatabase.getInstance(this).sqlCipherDatabase), + "localmetrics" to DatabaseConfig(db = LocalMetricsDatabase.getInstance(this).sqlCipherDatabase), + "logs" to DatabaseConfig(db = LogDatabase.getInstance(this).sqlCipherDatabase), ) ) diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt new file mode 100644 index 000000000..22895b7ef --- /dev/null +++ b/app/src/spinner/java/org/thoughtcrime/securesms/database/MessageBitmaskColumnTransformer.kt @@ -0,0 +1,122 @@ +package org.thoughtcrime.securesms.database + +import android.database.Cursor +import org.signal.spinner.ColumnTransformer +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BAD_DECRYPT_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_DRAFT_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_INBOX_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_OUTBOX_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_PENDING_INSECURE_SMS_FALLBACK +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_PENDING_SECURE_SMS_FALLBACK +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENDING_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENT_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_TYPE_MASK +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BOOST_REQUEST_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.CHANGE_NUMBER_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_DUPLICATE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_FAILED_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_LEGACY_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_NO_SESSION_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.END_SESSION_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_LEAVE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_UPDATE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GV1_MIGRATION_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INCOMING_AUDIO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INCOMING_VIDEO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INVALID_MESSAGE_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.JOINED_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_BUNDLE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_CONTENT_FORMAT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_CORRUPTED_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_INVALID_VERSION_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MESSAGE_FORCE_SMS_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MESSAGE_RATE_LIMITED_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MISSED_AUDIO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MISSED_VIDEO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_AUDIO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_VIDEO_CALL_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PROFILE_CHANGE_TYPE +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT +import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE + +object MessageBitmaskColumnTransformer : ColumnTransformer { + + override fun matches(tableName: String?, columnName: String): Boolean { + return columnName == "type" || columnName == "msg_box" + } + + override fun transform(tableName: String?, columnName: String, cursor: Cursor): String { + val type = cursor.requireLong(columnName) + + val describe = """ + isOutgoingMessageType:${isOutgoingMessageType(type)} + isForcedSms:${type and MESSAGE_FORCE_SMS_BIT != 0L} + isDraftMessageType:${type and BASE_TYPE_MASK == BASE_DRAFT_TYPE} + isFailedMessageType:${type and BASE_TYPE_MASK == BASE_SENT_FAILED_TYPE} + isPendingMessageType:${type and BASE_TYPE_MASK == BASE_OUTBOX_TYPE || type and BASE_TYPE_MASK == BASE_SENDING_TYPE } + isSentType:${type and BASE_TYPE_MASK == BASE_SENT_TYPE} + isPendingSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK || type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK} + isPendingSecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK} + isPendingInsecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK} + isInboxType:${type and BASE_TYPE_MASK == BASE_INBOX_TYPE} + isJoinedType:${type and BASE_TYPE_MASK == JOINED_TYPE} + isUnsupportedMessageType:${type and BASE_TYPE_MASK == UNSUPPORTED_MESSAGE_TYPE} + isInvalidMessageType:${type and BASE_TYPE_MASK == INVALID_MESSAGE_TYPE} + isBadDecryptType:${type and BASE_TYPE_MASK == BAD_DECRYPT_TYPE} + isSecureType:${type and SECURE_MESSAGE_BIT != 0L} + isPushType:${type and PUSH_MESSAGE_BIT != 0L} + isEndSessionType:${type and END_SESSION_BIT != 0L} + isKeyExchangeType:${type and KEY_EXCHANGE_BIT != 0L} + isIdentityVerified:${type and KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L} + isIdentityDefault:${type and KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L} + isCorruptedKeyExchange:${type and KEY_EXCHANGE_CORRUPTED_BIT != 0L} + isInvalidVersionKeyExchange:${type and KEY_EXCHANGE_INVALID_VERSION_BIT != 0L} + isBundleKeyExchange:${type and KEY_EXCHANGE_BUNDLE_BIT != 0L} + isContentBundleKeyExchange:${type and KEY_EXCHANGE_CONTENT_FORMAT != 0L} + isIdentityUpdate:${type and KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L} + isRateLimited:${type and MESSAGE_RATE_LIMITED_BIT != 0L} + isExpirationTimerUpdate:${type and EXPIRATION_TIMER_UPDATE_BIT != 0L} + isIncomingAudioCall:${type == INCOMING_AUDIO_CALL_TYPE} + isIncomingVideoCall:${type == INCOMING_VIDEO_CALL_TYPE} + isOutgoingAudioCall:${type == OUTGOING_AUDIO_CALL_TYPE} + isOutgoingVideoCall:${type == OUTGOING_VIDEO_CALL_TYPE} + isMissedAudioCall:${type == MISSED_AUDIO_CALL_TYPE} + isMissedVideoCall:${type == MISSED_VIDEO_CALL_TYPE} + isGroupCall:${type == GROUP_CALL_TYPE} + isGroupUpdate:${type and GROUP_UPDATE_BIT != 0L} + isGroupV2:${type and GROUP_V2_BIT != 0L} + isGroupQuit:${type and GROUP_LEAVE_BIT != 0L && type and GROUP_V2_BIT == 0L} + isChatSessionRefresh:${type and ENCRYPTION_REMOTE_FAILED_BIT != 0L} + isDuplicateMessageType:${type and ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L} + isDecryptInProgressType:${type and 0x40000000 != 0L} + isNoRemoteSessionType:${type and ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L} + isLegacyType:${type and ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and ENCRYPTION_REMOTE_BIT != 0L} + isProfileChange:${type == PROFILE_CHANGE_TYPE} + isGroupV1MigrationEvent:${type == GV1_MIGRATION_TYPE} + isChangeNumber:${type == CHANGE_NUMBER_TYPE} + isBoostRequest:${type == BOOST_REQUEST_TYPE} + isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS} + """.trimIndent() + + return "$type

" + describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "
") + } + + private fun isOutgoingMessageType(type: Long): Boolean { + for (outgoingType in OUTGOING_MESSAGE_TYPES) { + if (type and BASE_TYPE_MASK == outgoingType) return true + } + return false + } +} diff --git a/spinner/app/src/main/java/org/signal/spinnertest/MainActivity.kt b/spinner/app/src/main/java/org/signal/spinnertest/MainActivity.kt index 5376955f0..23b3a404d 100644 --- a/spinner/app/src/main/java/org/signal/spinnertest/MainActivity.kt +++ b/spinner/app/src/main/java/org/signal/spinnertest/MainActivity.kt @@ -18,13 +18,13 @@ class MainActivity : AppCompatActivity() { // insertMockData(db.writableDatabase) Spinner.init( - this, + application, Spinner.DeviceInfo( name = "${Build.MODEL} (API ${Build.VERSION.SDK_INT})", packageName = packageName, appVersion = "0.1" ), - mapOf("main" to db) + mapOf("main" to Spinner.DatabaseConfig(db = db)) ) } diff --git a/spinner/lib/src/main/assets/browse.hbs b/spinner/lib/src/main/assets/browse.hbs index 5602f5730..ecf4296ad 100644 --- a/spinner/lib/src/main/assets/browse.hbs +++ b/spinner/lib/src/main/assets/browse.hbs @@ -47,7 +47,7 @@ {{#each queryResult.rows}} {{#each this}} - {{this}} + {{{this}}} {{/each}} {{/each}} diff --git a/spinner/lib/src/main/java/org/signal/spinner/ColumnTransformer.kt b/spinner/lib/src/main/java/org/signal/spinner/ColumnTransformer.kt new file mode 100644 index 000000000..95d45ac86 --- /dev/null +++ b/spinner/lib/src/main/java/org/signal/spinner/ColumnTransformer.kt @@ -0,0 +1,18 @@ +package org.signal.spinner + +import android.database.Cursor + +/** + * An interface to transform on column value into another. Useful for making certain data fields (like bitmasks) more readable. + */ +interface ColumnTransformer { + /** + * In certain circumstances (like some queries), the table name may not be guaranteed. + */ + fun matches(tableName: String?, columnName: String): Boolean + + /** + * In certain circumstances (like some queries), the table name may not be guaranteed. + */ + fun transform(tableName: String?, columnName: String, cursor: Cursor): String +} diff --git a/spinner/lib/src/main/java/org/signal/spinner/DefaultColumnTransformer.kt b/spinner/lib/src/main/java/org/signal/spinner/DefaultColumnTransformer.kt new file mode 100644 index 000000000..d1060a284 --- /dev/null +++ b/spinner/lib/src/main/java/org/signal/spinner/DefaultColumnTransformer.kt @@ -0,0 +1,20 @@ +package org.signal.spinner + +import android.database.Cursor +import android.util.Base64 + +internal object DefaultColumnTransformer : ColumnTransformer { + override fun matches(tableName: String?, columnName: String): Boolean { + return true + } + + override fun transform(tableName: String?, columnName: String, cursor: Cursor): String { + val index = cursor.getColumnIndex(columnName) + val data: String? = when (cursor.getType(index)) { + Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0) + else -> cursor.getString(index) + } + + return data ?: "null" + } +} diff --git a/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt b/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt deleted file mode 100644 index 0e0b36938..000000000 --- a/spinner/lib/src/main/java/org/signal/spinner/MessageUtil.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.signal.spinner - -object MessageUtil { - private const val BASE_TYPE_MASK: Long = 0x1F - private const val INCOMING_AUDIO_CALL_TYPE: Long = 1 - private const val OUTGOING_AUDIO_CALL_TYPE: Long = 2 - private const val MISSED_AUDIO_CALL_TYPE: Long = 3 - private const val JOINED_TYPE: Long = 4 - private const val UNSUPPORTED_MESSAGE_TYPE: Long = 5 - private const val INVALID_MESSAGE_TYPE: Long = 6 - private const val PROFILE_CHANGE_TYPE: Long = 7 - private const val MISSED_VIDEO_CALL_TYPE: Long = 8 - private const val GV1_MIGRATION_TYPE: Long = 9 - private const val INCOMING_VIDEO_CALL_TYPE: Long = 10 - private const val OUTGOING_VIDEO_CALL_TYPE: Long = 11 - private const val GROUP_CALL_TYPE: Long = 12 - private const val BAD_DECRYPT_TYPE: Long = 13 - private const val CHANGE_NUMBER_TYPE: Long = 14 - private const val BOOST_REQUEST_TYPE: Long = 15 - private const val BASE_INBOX_TYPE: Long = 20 - private const val BASE_OUTBOX_TYPE: Long = 21 - private const val outgoingSmsMessageType: Long = 22 - private const val BASE_SENT_TYPE: Long = 23 - private const val BASE_SENT_FAILED_TYPE: Long = 24 - private const val BASE_PENDING_SECURE_SMS_FALLBACK: Long = 25 - private const val BASE_PENDING_INSECURE_SMS_FALLBACK: Long = 26 - private const val BASE_DRAFT_TYPE: Long = 27 - private val OUTGOING_MESSAGE_TYPES = longArrayOf(BASE_OUTBOX_TYPE, BASE_SENT_TYPE, outgoingSmsMessageType, BASE_SENT_FAILED_TYPE, BASE_PENDING_SECURE_SMS_FALLBACK, BASE_PENDING_INSECURE_SMS_FALLBACK, OUTGOING_AUDIO_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE) - private const val MESSAGE_RATE_LIMITED_BIT: Long = 0x80 - private const val MESSAGE_FORCE_SMS_BIT: Long = 0x40 - private const val KEY_EXCHANGE_BIT: Long = 0x8000 - private const val KEY_EXCHANGE_IDENTITY_VERIFIED_BIT: Long = 0x4000 - private const val KEY_EXCHANGE_IDENTITY_DEFAULT_BIT: Long = 0x2000 - private const val KEY_EXCHANGE_CORRUPTED_BIT: Long = 0x1000 - private const val KEY_EXCHANGE_INVALID_VERSION_BIT: Long = 0x800 - private const val KEY_EXCHANGE_BUNDLE_BIT: Long = 0x400 - private const val KEY_EXCHANGE_IDENTITY_UPDATE_BIT: Long = 0x200 - private const val KEY_EXCHANGE_CONTENT_FORMAT: Long = 0x100 - private const val SECURE_MESSAGE_BIT: Long = 0x800000 - private const val END_SESSION_BIT: Long = 0x400000 - private const val PUSH_MESSAGE_BIT: Long = 0x200000 - private const val GROUP_UPDATE_BIT: Long = 0x10000 - private const val GROUP_LEAVE_BIT: Long = 0x20000 - private const val EXPIRATION_TIMER_UPDATE_BIT: Long = 0x40000 - private const val GROUP_V2_BIT: Long = 0x80000 - private const val GROUP_V2_LEAVE_BITS = GROUP_V2_BIT or GROUP_LEAVE_BIT or GROUP_UPDATE_BIT - private const val ENCRYPTION_REMOTE_BIT: Long = 0x20000000 - private const val ENCRYPTION_REMOTE_FAILED_BIT: Long = 0x10000000 - private const val ENCRYPTION_REMOTE_NO_SESSION_BIT: Long = 0x08000000 - private const val ENCRYPTION_REMOTE_DUPLICATE_BIT: Long = 0x04000000 - private const val ENCRYPTION_REMOTE_LEGACY_BIT: Long = 0x02000000 - - fun String.isMessageType(): Boolean { - return this == "type" || this == "msg_box" - } - - private fun isOutgoingMessageType(type: Long): Boolean { - for (outgoingType in OUTGOING_MESSAGE_TYPES) { - if (type and BASE_TYPE_MASK == outgoingType) return true - } - return false - } - - fun describeMessageType(type: Long): String { - val describe = """ - isOutgoingMessageType:${isOutgoingMessageType(type)} - isForcedSms:${type and MESSAGE_FORCE_SMS_BIT != 0L} - isDraftMessageType:${type and BASE_TYPE_MASK == BASE_DRAFT_TYPE} - isFailedMessageType:${type and BASE_TYPE_MASK == BASE_SENT_FAILED_TYPE} - isPendingMessageType:${type and BASE_TYPE_MASK == BASE_OUTBOX_TYPE || type and BASE_TYPE_MASK == outgoingSmsMessageType} - isSentType:${type and BASE_TYPE_MASK == BASE_SENT_TYPE} - isPendingSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK || type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK} - isPendingSecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK} - isPendingInsecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK} - isInboxType:${type and BASE_TYPE_MASK == BASE_INBOX_TYPE} - isJoinedType:${type and BASE_TYPE_MASK == JOINED_TYPE} - isUnsupportedMessageType:${type and BASE_TYPE_MASK == UNSUPPORTED_MESSAGE_TYPE} - isInvalidMessageType:${type and BASE_TYPE_MASK == INVALID_MESSAGE_TYPE} - isBadDecryptType:${type and BASE_TYPE_MASK == BAD_DECRYPT_TYPE} - isSecureType:${type and SECURE_MESSAGE_BIT != 0L} - isPushType:${type and PUSH_MESSAGE_BIT != 0L} - isEndSessionType:${type and END_SESSION_BIT != 0L} - isKeyExchangeType:${type and KEY_EXCHANGE_BIT != 0L} - isIdentityVerified:${type and KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L} - isIdentityDefault:${type and KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L} - isCorruptedKeyExchange:${type and KEY_EXCHANGE_CORRUPTED_BIT != 0L} - isInvalidVersionKeyExchange:${type and KEY_EXCHANGE_INVALID_VERSION_BIT != 0L} - isBundleKeyExchange:${type and KEY_EXCHANGE_BUNDLE_BIT != 0L} - isContentBundleKeyExchange:${type and KEY_EXCHANGE_CONTENT_FORMAT != 0L} - isIdentityUpdate:${type and KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L} - isRateLimited:${type and MESSAGE_RATE_LIMITED_BIT != 0L} - isExpirationTimerUpdate:${type and EXPIRATION_TIMER_UPDATE_BIT != 0L} - isIncomingAudioCall:${type == INCOMING_AUDIO_CALL_TYPE} - isIncomingVideoCall:${type == INCOMING_VIDEO_CALL_TYPE} - isOutgoingAudioCall:${type == OUTGOING_AUDIO_CALL_TYPE} - isOutgoingVideoCall:${type == OUTGOING_VIDEO_CALL_TYPE} - isMissedAudioCall:${type == MISSED_AUDIO_CALL_TYPE} - isMissedVideoCall:${type == MISSED_VIDEO_CALL_TYPE} - isGroupCall:${type == GROUP_CALL_TYPE} - isGroupUpdate:${type and GROUP_UPDATE_BIT != 0L} - isGroupV2:${type and GROUP_V2_BIT != 0L} - isGroupQuit:${type and GROUP_LEAVE_BIT != 0L && type and GROUP_V2_BIT == 0L} - isChatSessionRefresh:${type and ENCRYPTION_REMOTE_FAILED_BIT != 0L} - isDuplicateMessageType:${type and ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L} - isDecryptInProgressType:${type and 0x40000000 != 0L} - isNoRemoteSessionType:${type and ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L} - isLegacyType:${type and ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and ENCRYPTION_REMOTE_BIT != 0L} - isProfileChange:${type == PROFILE_CHANGE_TYPE} - isGroupV1MigrationEvent:${type == GV1_MIGRATION_TYPE} - isChangeNumber:${type == CHANGE_NUMBER_TYPE} - isBoostRequest:${type == BOOST_REQUEST_TYPE} - isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS} - """.trimIndent() - - return describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "
") - } -} diff --git a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt index d69493f0b..9988764cf 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/Spinner.kt @@ -1,7 +1,7 @@ package org.signal.spinner +import android.app.Application import android.content.ContentValues -import android.content.Context import android.database.sqlite.SQLiteQueryBuilder import androidx.sqlite.db.SupportSQLiteDatabase import org.signal.core.util.logging.Log @@ -15,9 +15,9 @@ object Spinner { private lateinit var server: SpinnerServer - fun init(context: Context, deviceInfo: DeviceInfo, databases: Map) { + fun init(application: Application, deviceInfo: DeviceInfo, databases: Map) { try { - server = SpinnerServer(context, deviceInfo, databases) + server = SpinnerServer(application, deviceInfo, databases) server.start() } catch (e: IOException) { Log.w(TAG, "Spinner server hit IO exception!", e) @@ -92,4 +92,9 @@ object Spinner { val packageName: String, val appVersion: String ) + + data class DatabaseConfig( + val db: SupportSQLiteDatabase, + val columnTransformers: List = emptyList() + ) } diff --git a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt index c4c4c3be3..c40f8160f 100644 --- a/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt +++ b/spinner/lib/src/main/java/org/signal/spinner/SpinnerServer.kt @@ -1,8 +1,7 @@ package org.signal.spinner -import android.content.Context +import android.app.Application import android.database.Cursor -import android.util.Base64 import androidx.sqlite.db.SupportSQLiteDatabase import com.github.jknack.handlebars.Handlebars import com.github.jknack.handlebars.Template @@ -10,7 +9,7 @@ import com.github.jknack.handlebars.helper.ConditionalHelpers import fi.iki.elonen.NanoHTTPD import org.signal.core.util.ExceptionUtil import org.signal.core.util.logging.Log -import org.signal.spinner.MessageUtil.isMessageType +import org.signal.spinner.Spinner.DatabaseConfig import java.lang.IllegalArgumentException import java.text.SimpleDateFormat import java.util.Date @@ -26,16 +25,16 @@ import kotlin.math.min * to [renderTemplate]. */ internal class SpinnerServer( - private val context: Context, + private val application: Application, private val deviceInfo: Spinner.DeviceInfo, - private val databases: Map + private val databases: Map ) : NanoHTTPD(5000) { companion object { private val TAG = Log.tag(SpinnerServer::class.java) } - private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(context)).apply { + private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(application)).apply { registerHelper("eq", ConditionalHelpers.eq) registerHelper("neq", ConditionalHelpers.neq) } @@ -50,18 +49,18 @@ internal class SpinnerServer( } val dbParam: String = session.queryParam("db") ?: session.parameters["db"]?.toString() ?: databases.keys.first() - val db: SupportSQLiteDatabase = databases[dbParam] ?: return internalError(IllegalArgumentException("Invalid db param!")) + val dbConfig: DatabaseConfig = databases[dbParam] ?: return internalError(IllegalArgumentException("Invalid db param!")) try { return when { session.method == Method.GET && session.uri == "/css/main.css" -> newFileResponse("css/main.css", "text/css") session.method == Method.GET && session.uri == "/js/main.js" -> newFileResponse("js/main.js", "text/javascript") - session.method == Method.GET && session.uri == "/" -> getIndex(dbParam, db) - session.method == Method.GET && session.uri == "/browse" -> getBrowse(dbParam, db) - session.method == Method.POST && session.uri == "/browse" -> postBrowse(dbParam, db, session) - session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, db) - session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, db, session) - session.method == Method.GET && session.uri == "/recent" -> getRecent(dbParam, db) + session.method == Method.GET && session.uri == "/" -> getIndex(dbParam, dbConfig.db) + session.method == Method.GET && session.uri == "/browse" -> getBrowse(dbParam, dbConfig.db) + session.method == Method.POST && session.uri == "/browse" -> postBrowse(dbParam, dbConfig, session) + session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, dbConfig.db) + session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, dbConfig, session) + session.method == Method.GET && session.uri == "/recent" -> getRecent(dbParam, dbConfig.db) else -> newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found") } } catch (t: Throwable) { @@ -108,13 +107,13 @@ internal class SpinnerServer( ) } - private fun postBrowse(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response { + private fun postBrowse(dbName: String, dbConfig: DatabaseConfig, session: IHTTPSession): Response { val table: String = session.parameters["table"]?.get(0).toString() val pageSize: Int = session.parameters["pageSize"]?.get(0)?.toInt() ?: 1000 var pageIndex: Int = session.parameters["pageIndex"]?.get(0)?.toInt() ?: 0 val action: String? = session.parameters["action"]?.get(0) - val rowCount = db.getTableRowCount(table) + val rowCount = dbConfig.db.getTableRowCount(table) val pageCount = ceil(rowCount.toFloat() / pageSize.toFloat()).toInt() when (action) { @@ -125,7 +124,7 @@ internal class SpinnerServer( } val query = "select * from $table limit $pageSize offset ${pageSize * pageIndex}" - val queryResult = db.query(query).toQueryResult() + val queryResult = dbConfig.db.query(query).toQueryResult(columnTransformers = dbConfig.columnTransformers) return renderTemplate( "browse", @@ -133,7 +132,7 @@ internal class SpinnerServer( deviceInfo = deviceInfo, database = dbName, databases = databases.keys.toList(), - tableNames = db.getTableNames(), + tableNames = dbConfig.db.getTableNames(), table = table, queryResult = queryResult, pagingData = PagingData( @@ -180,7 +179,7 @@ internal class SpinnerServer( ) } - private fun postQuery(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response { + private fun postQuery(dbName: String, dbConfig: DatabaseConfig, session: IHTTPSession): Response { val action: String = session.parameters["action"]?.get(0).toString() val rawQuery: String = session.parameters["query"]?.get(0).toString() val query = if (action == "analyze") "EXPLAIN QUERY PLAN $rawQuery" else rawQuery @@ -193,7 +192,7 @@ internal class SpinnerServer( database = dbName, databases = databases.keys.toList(), query = rawQuery, - queryResult = db.query(query).toQueryResult(startTime) + queryResult = dbConfig.db.query(query).toQueryResult(queryStartTime = startTime, columnTransformers = dbConfig.columnTransformers) ) ) } @@ -218,20 +217,26 @@ internal class SpinnerServer( return newChunkedResponse( Response.Status.OK, mimeType, - context.assets.open(assetPath) + application.assets.open(assetPath) ) } - private fun Cursor.toQueryResult(queryStartTime: Long = 0): QueryResult { + private fun Cursor.toQueryResult(queryStartTime: Long = 0, columnTransformers: List = emptyList()): QueryResult { val numColumns = this.columnCount - val columns = mutableListOf() + val transformers = mutableListOf() + for (i in 0 until numColumns) { val columnName = getColumnName(i) - columns += columnName - if (columnName.isMessageType()) { - columns += "meta_type" + val customTransformer: ColumnTransformer? = columnTransformers.find { it.matches(null, columnName) } + + columns += if (customTransformer != null) { + "$columnName *" + } else { + columnName } + + transformers += customTransformer ?: DefaultColumnTransformer } var timeOfFirstRow = 0L @@ -243,16 +248,10 @@ internal class SpinnerServer( val row = mutableListOf() for (i in 0 until numColumns) { - val data: String? = when (getType(i)) { - Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(getBlob(i), 0) - else -> getString(i) - } - row += data ?: "null" - - if (getColumnName(i).isMessageType()) { - row += MessageUtil.describeMessageType(getLong(i)) - } + val columnName: String = getColumnName(i) + row += transformers[i].transform(null, columnName, this) } + rows += row }