Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
Merge branch 'molly-7.15'
Browse files Browse the repository at this point in the history
  • Loading branch information
valldrac committed Sep 10, 2024
2 parents 6ecf712 + b342b50 commit 33f79cf
Show file tree
Hide file tree
Showing 564 changed files with 37,789 additions and 20,419 deletions.
34 changes: 27 additions & 7 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ apply {
from("fix-profm.gradle")
}

val canonicalVersionCode = 1443
val canonicalVersionName = "7.13.4"
val currentHotfixVersion = 1
val canonicalVersionCode = 1451
val canonicalVersionName = "7.15.4"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val mollyRevision = 1

Expand Down Expand Up @@ -127,14 +127,30 @@ android {
targetCompatibility = signalJavaVersion
}

packagingOptions {
resources {
excludes += setOf("LICENSE.txt", "LICENSE", "NOTICE", "asm-license.txt", "META-INF/LICENSE", "META-INF/LICENSE.md", "META-INF/NOTICE", "META-INF/LICENSE-notice.md", "META-INF/proguard/androidx-annotations.pro", "libsignal_jni.dylib", "signal_jni.dll", "libsignal_jni_testing.dylib", "signal_jni_testing.dll")
}
packaging {
jniLibs {
excludes += setOf(
"**/*.dylib",
"**/*.dll"
)
// MOLLY: Compress native libs by default as APK is not split on ABIs
useLegacyPackaging = true
}
resources {
excludes += setOf(
"LICENSE.txt",
"LICENSE",
"NOTICE",
"asm-license.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.md",
"META-INF/NOTICE",
"META-INF/LICENSE-notice.md",
"META-INF/proguard/androidx-annotations.pro",
"**/*.dylib",
"**/*.dll"
)
}
}

buildFeatures {
Expand Down Expand Up @@ -441,6 +457,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
Expand Down Expand Up @@ -501,6 +518,7 @@ dependencies {
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlin.reflect)
"gmsImplementation"(libs.kotlinx.coroutines.play.services)
implementation(libs.kotlinx.coroutines.rx3)
implementation(libs.jackson.module.kotlin)
implementation(libs.rxjava3.rxandroid)
implementation(libs.rxjava3.rxkotlin)
Expand All @@ -513,6 +531,8 @@ dependencies {
implementation(libs.gosimple.nbvcxz)
"fossImplementation"("org.osmdroid:osmdroid-android:6.1.16")

"gmsImplementation"(project(":billing"))

"spinnerImplementation"(project(":spinner"))

"instrumentationImplementation"(libs.androidx.fragment.testing) {
Expand Down
2 changes: 2 additions & 0 deletions app/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@
</issue>

<issue id="OptionalUsedAsFieldOrParameterType" severity="ignore" />

<issue id="ForegroundServiceType" severity="warning" />
</lint>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.thoughtcrime.securesms.backup.v2

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.difflib.DiffUtils
import com.github.difflib.UnifiedDiffUtils
import junit.framework.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.signal.libsignal.messagebackup.ComparableBackup
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.ByteArrayInputStream
import java.util.UUID

@RunWith(AndroidJUnit4::class)
class ArchiveImportExportTests {

companion object {
const val TAG = "ImportExport"
const val TESTS_FOLDER = "backupTests"

val SELF_ACI = ServiceId.ACI.from(UUID(100, 100))
val SELF_PNI = ServiceId.PNI.from(UUID(101, 101))
val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY: ByteArray = Base64.decode("YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=")
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
}

@Test
fun all() {
runTests()
}

@Ignore("Just for debugging")
@Test
fun accountData() {
runTests { it.startsWith("account_data_") }
}

@Ignore("Just for debugging")
@Test
fun recipientContacts() {
runTests { it.startsWith("recipient_contacts_") }
}

@Ignore("Just for debugging")
@Test
fun recipientDistributionLists() {
runTests { it.startsWith("recipient_distribution_list_") }
}

@Ignore("Just for debugging")
@Test
fun recipientGroups() {
runTests { it.startsWith("recipient_groups_") }
}

@Ignore("Just for debugging")
@Test
fun chatStandardMessageTextOnly() {
runTests { it.startsWith("chat_standard_message_text_only_") }
}

@Ignore("Just for debugging")
@Test
fun chatStandardMessageFormattedText() {
runTests { it.startsWith("chat_standard_message_formatted_text_") }
}

@Ignore("Just for debugging")
@Test
fun chatStandardMessageLongText() {
runTests { it.startsWith("chat_standard_message_long_text_") }
}

@Ignore("Just for debugging")
@Test
fun chatStandardMessageStandardAttachments() {
runTests { it.startsWith("chat_standard_message_standard_attachments_") }
}

@Ignore("Just for debugging")
@Test
fun chatStandardMessageSpecialAttachments() {
runTests { it.startsWith("chat_standard_message_special_attachments_") }
}

@Ignore("Just for debugging")
@Test
fun chatSimpleUpdates() {
runTests { it.startsWith("chat_simple_updates_") }
}

@Ignore("Just for debugging")
@Test
fun chatContactMessage() {
runTests { it.startsWith("chat_contact_message_") }
}

private fun runTests(predicate: (String) -> Boolean = { true }) {
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!!.filter(predicate)
val results: MutableList<TestResult> = mutableListOf()

Log.d(TAG, "About to run ${testFiles.size} tests.")

for (filename in testFiles) {
Log.d(TAG, "> $filename")
val startTime = System.currentTimeMillis()
val result = test(filename)
results += result

if (result is TestResult.Success) {
Log.d(TAG, " \uD83D\uDFE2 Passed in ${System.currentTimeMillis() - startTime} ms")
} else {
Log.d(TAG, " \uD83D\uDD34 Failed in ${System.currentTimeMillis() - startTime} ms")
}
}

results
.filterIsInstance<TestResult.Failure>()
.forEach {
Log.e(TAG, "Failure: ${it.name}\n${it.message}")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
}

if (results.any { it is TestResult.Failure }) {
val successCount = results.count { it is TestResult.Success }
val failingTestNames = results.filterIsInstance<TestResult.Failure>().joinToString(separator = "\n") { " \uD83D\uDD34 ${it.name}" }
val message = "Some tests failed! Only $successCount/${results.size} passed. Failure details are above. Failing tests:\n$failingTestNames"

Log.d(TAG, message)
throw AssertionError(message)
} else {
Log.d(TAG, "All ${results.size} tests passed!")
}
}

private fun test(filename: String): TestResult {
resetAllData()

val inputFileBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("$TESTS_FOLDER/$filename").readFully(true)

val importResult = import(inputFileBytes)
assertTrue(importResult is ImportResult.Success)
val success = importResult as ImportResult.Success

val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime)
checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it }

return TestResult.Success(filename)
}

private fun resetAllData() {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")

SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
.sorted()
.forEach { table ->
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
SqlUtil.resetAutoIncrementValue(SignalDatabase.rawDatabase, table)
}

AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
RecipientId.clearCache()

KeyValueDatabase.getInstance(AppDependencies.application).clear()
SignalStore.resetCache()

SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalStore.backup.backupTier = MessageBackupTier.PAID
}

private fun import(importData: ByteArray): ImportResult {
return BackupRepository.import(
length = importData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(importData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
plaintext = true
)
}

private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
try {
BackupRepository.validate(
length = generatedBackupData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
)
} catch (e: Exception) {
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
}

return null
}

private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
val importComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Imported backup hit a validation error: ${e.message}")
}

val exportComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}")
}

if (importComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}

if (exportComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}

val canonicalImport = importComparable.comparableString
val canonicalExport = exportComparable.comparableString

if (canonicalImport != canonicalExport) {
val importLines = canonicalImport.lines()
val exportLines = canonicalExport.lines()

val patch = DiffUtils.diff(importLines, exportLines)
val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n")

val importFrames = import.toFrames()
val exportFrames = export.toFrames()

val importGroupFramesByMasterKey = importFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
val exportGroupFramesByMasterKey = exportFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }

val groupErrorMessage = StringBuilder()

for ((importKey, importValue) in importGroupFramesByMasterKey) {
if (exportGroupFramesByMasterKey[importKey]?.let { it.snapshot != importValue.snapshot } == true) {
groupErrorMessage.append("[$importKey] Snapshot mismatch.\nImport:\n${importValue}\n\nExport:\n${exportGroupFramesByMasterKey[importKey]}\n\n")
}
}

return TestResult.Failure(testName, "Imported backup does not match exported backup. Diff:\n$diff\n$groupErrorMessage")
}

return null
}

fun ByteArray.toFrames(): List<Frame> {
return PlainTextBackupReader(this.inputStream(), this.size.toLong()).use { it.asSequence().toList() }
}

private sealed class TestResult(val name: String) {
class Success(name: String) : TestResult(name)
class Failure(name: String, val message: String) : TestResult(name)
}
}
Loading

0 comments on commit 33f79cf

Please sign in to comment.