Fix received stickers for installed packs without a data_hash_end.

This commit is contained in:
Cody Henthorne
2025-08-28 12:04:43 -04:00
committed by GitHub
parent 8ce17e3e2d
commit 3bcfb5ab61
3 changed files with 358 additions and 2 deletions

View File

@@ -142,6 +142,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V284_SetPlaceholder
import org.thoughtcrime.securesms.database.helpers.migration.V285_AddEpochToCallLinksTable
import org.thoughtcrime.securesms.database.helpers.migration.V286_FixRemoteKeyEncoding
import org.thoughtcrime.securesms.database.helpers.migration.V287_FixInvalidArchiveState
import org.thoughtcrime.securesms.database.helpers.migration.V288_CopyStickerDataHashStartToEnd
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -289,10 +290,11 @@ object SignalDatabaseMigrations {
284 to V284_SetPlaceholderGroupFlag,
285 to V285_AddEpochToCallLinksTable,
286 to V286_FixRemoteKeyEncoding,
287 to V287_FixInvalidArchiveState
287 to V287_FixInvalidArchiveState,
288 to V288_CopyStickerDataHashStartToEnd
)
const val DATABASE_VERSION = 287
const val DATABASE_VERSION = 288
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Copy data_hash_start to data_hash_end for sticker attachments that have completed transfer.
*/
@Suppress("ClassName")
object V288_CopyStickerDataHashStartToEnd : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"UPDATE attachment SET data_hash_end = data_hash_start WHERE sticker_pack_id IS NOT NULL AND data_hash_start IS NOT NULL AND data_hash_end IS NULL AND transfer_state = 0"
)
}
}

View File

@@ -0,0 +1,333 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import android.content.ContentValues
import androidx.test.core.app.ApplicationProvider
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule
@Suppress("ClassName")
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class V288_CopyStickerDataHashStartToEndTest {
@get:Rule val signalDatabaseRule = SignalDatabaseMigrationRule(287)
// Constants copied from AttachmentTable to ensure test stability
companion object {
private const val TABLE_NAME = "attachment"
private const val ID = "_id"
private const val DATA_FILE = "data_file"
private const val DATA_RANDOM = "data_random"
private const val TRANSFER_STATE = "transfer_state"
private const val STICKER_PACK_ID = "sticker_pack_id"
private const val STICKER_PACK_KEY = "sticker_pack_key"
private const val STICKER_ID = "sticker_id"
private const val STICKER_EMOJI = "sticker_emoji"
private const val DATA_HASH_START = "data_hash_start"
private const val DATA_HASH_END = "data_hash_end"
private const val TRANSFER_PROGRESS_DONE = 0
}
@Test
fun migrate_whenStickerHasDataHashStartButNoDataHashEndAndTransferDone_copiesDataHashStartToEnd() {
val stickerAttachmentId = insertStickerAttachment(
stickerPackId = "test-pack-id",
dataHashStart = "abc123def456",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(stickerAttachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo("abc123def456")
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo("abc123def456")
}
}
@Test
fun migrate_whenStickerAlreadyHasDataHashEnd_doesNotOverwrite() {
val stickerAttachmentId = insertStickerAttachment(
stickerPackId = "test-pack-id",
dataHashStart = "abc123def456",
dataHashEnd = "existing-hash-end",
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(stickerAttachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo("abc123def456")
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo("existing-hash-end")
}
}
@Test
fun migrate_whenStickerHasNoDataHashStart_doesNothing() {
val stickerAttachmentId = insertStickerAttachment(
stickerPackId = "test-pack-id",
dataHashStart = null,
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(stickerAttachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo(true)
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo(true)
}
}
@Test
fun migrate_whenNonStickerAttachmentHasDataHashStart_doesNotCopy() {
val regularAttachmentId = insertRegularAttachment(
dataHashStart = "regular-hash-start",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(regularAttachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo("regular-hash-start")
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo(true)
}
}
@Test
fun migrate_whenMultipleStickerAttachmentsWithMixedStates_onlyCopiesWhenNeeded() {
// Should copy (sticker with data_hash_start, no data_hash_end, transfer done)
val copyId1 = insertStickerAttachment(
stickerPackId = "pack-1",
dataHashStart = "hash-start-1",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val copyId2 = insertStickerAttachment(
stickerPackId = "pack-2",
dataHashStart = "hash-start-2",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
// Should NOT copy (already has data_hash_end)
val noOverwriteId = insertStickerAttachment(
stickerPackId = "pack-3",
dataHashStart = "hash-start-3",
dataHashEnd = "existing-end-3",
transferState = TRANSFER_PROGRESS_DONE
)
// Should NOT copy (no data_hash_start)
val noDataHashStartId = insertStickerAttachment(
stickerPackId = "pack-4",
dataHashStart = null,
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
// Should NOT copy (not a sticker)
val nonStickerID = insertRegularAttachment(
dataHashStart = "regular-hash",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
// Check that data_hash_start was copied to data_hash_end
assertDataHashState(copyId1, expectedStart = "hash-start-1", expectedEnd = "hash-start-1")
assertDataHashState(copyId2, expectedStart = "hash-start-2", expectedEnd = "hash-start-2")
// Check that existing data_hash_end was not overwritten
assertDataHashState(noOverwriteId, expectedStart = "hash-start-3", expectedEnd = "existing-end-3")
// Check that null values remain null
assertDataHashState(noDataHashStartId, expectedStart = null, expectedEnd = null)
// Check that non-sticker attachment was not affected
assertDataHashState(nonStickerID, expectedStart = "regular-hash", expectedEnd = null)
}
@Test
fun migrate_whenNoStickersMatchCriteria_noChanges() {
val noStickerPackId = insertRegularAttachment(
dataHashStart = "hash1",
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val stickerButNoHashStart = insertStickerAttachment(
stickerPackId = "pack-1",
dataHashStart = null,
dataHashEnd = null,
transferState = TRANSFER_PROGRESS_DONE
)
val stickerWithExistingEnd = insertStickerAttachment(
stickerPackId = "pack-2",
dataHashStart = "start-hash",
dataHashEnd = "end-hash",
transferState = TRANSFER_PROGRESS_DONE
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
// Check no changes were made
assertDataHashState(noStickerPackId, expectedStart = "hash1", expectedEnd = null)
assertDataHashState(stickerButNoHashStart, expectedStart = null, expectedEnd = null)
assertDataHashState(stickerWithExistingEnd, expectedStart = "start-hash", expectedEnd = "end-hash")
}
@Test
fun migrate_whenStickerTransferNotDone_doesNotCopy() {
val stickerInProgressId = insertStickerAttachment(
stickerPackId = "test-pack-id",
dataHashStart = "abc123def456",
dataHashEnd = null,
transferState = 1 // TRANSFER_PROGRESS_STARTED
)
val db = signalDatabaseRule.database
V288_CopyStickerDataHashStartToEnd.migrate(ApplicationProvider.getApplicationContext(), db, 287, 288)
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(stickerInProgressId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo("abc123def456")
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo(true)
}
}
private fun insertStickerAttachment(stickerPackId: String, dataHashStart: String?, dataHashEnd: String?, transferState: Int = TRANSFER_PROGRESS_DONE): Long {
val db = signalDatabaseRule.database
val values = ContentValues().apply {
put(DATA_FILE, "/fake/path/sticker.webp")
put(DATA_RANDOM, "/fake/path/sticker.webp".toByteArray())
put(TRANSFER_STATE, transferState)
put(STICKER_PACK_ID, stickerPackId)
put(STICKER_PACK_KEY, "test-pack-key")
put(STICKER_ID, 1)
put(STICKER_EMOJI, "😀")
put(DATA_HASH_START, dataHashStart)
put(DATA_HASH_END, dataHashEnd)
}
return db.insert(TABLE_NAME, null, values)
}
private fun insertRegularAttachment(dataHashStart: String?, dataHashEnd: String?, transferState: Int = TRANSFER_PROGRESS_DONE): Long {
val db = signalDatabaseRule.database
val values = ContentValues().apply {
put(DATA_FILE, "/fake/path/regular.jpg")
put(DATA_RANDOM, "/fake/path/regular.jpg".toByteArray())
put(TRANSFER_STATE, transferState)
put(DATA_HASH_START, dataHashStart)
put(DATA_HASH_END, dataHashEnd)
// No sticker fields - this makes it a regular attachment
}
return db.insert(TABLE_NAME, null, values)
}
private fun assertDataHashState(attachmentId: Long, expectedStart: String?, expectedEnd: String?) {
val db = signalDatabaseRule.database
val cursor = db.query(
TABLE_NAME,
arrayOf(DATA_HASH_START, DATA_HASH_END),
"$ID = ?",
arrayOf(attachmentId.toString()),
null,
null,
null
)
cursor.use {
assertThat(it.moveToFirst()).isEqualTo(true)
if (expectedStart == null) {
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo(true)
} else {
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_START))).isEqualTo(expectedStart)
}
if (expectedEnd == null) {
assertThat(it.isNull(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo(true)
} else {
assertThat(it.getString(it.getColumnIndexOrThrow(DATA_HASH_END))).isEqualTo(expectedEnd)
}
}
}
}