diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index e2618daa78..558de499cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -62,6 +62,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -597,23 +598,44 @@ public class FullBackupExporter extends FullBackupBase { } private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull Cursor cursor) { - long id = CursorUtil.requireLong(cursor, MessageTable.ID); - long expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED); - long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN); - long latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID); + return isNonExpiringMessage(db, + CursorUtil.requireLong(cursor, MessageTable.ID), + CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED), + CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN), + CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID)); + } + + /** + * Corrupt data can contain a cycle in {@link MessageTable#LATEST_REVISION_ID}, so the walk tracks visited ids. + */ + private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, long id, long expireStarted, long expiresIn, long latestRevisionId) { + String[] columns = new String[] { MessageTable.ID, MessageTable.EXPIRE_STARTED, MessageTable.EXPIRES_IN, MessageTable.LATEST_REVISION_ID }; + String where = MessageTable.ID + " = ?"; + + Set visited = new HashSet<>(); + + while (latestRevisionId > 0 && latestRevisionId != id) { + if (!visited.add(id)) { + Log.w(TAG, "Detected a cycle in the message revision chain! Stopping the walk at " + id); + break; + } + + try (Cursor cursor = db.query(MessageTable.TABLE_NAME, columns, where, SqlUtil.buildArgs(latestRevisionId), null, null, null)) { + if (cursor == null || !cursor.moveToFirst()) { + return false; + } + + id = CursorUtil.requireLong(cursor, MessageTable.ID); + expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED); + expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN); + latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID); + } + } long expiresAt = expireStarted + expiresIn; long timeRemaining = expiresAt - System.currentTimeMillis(); - if (latestRevisionId > 0 && latestRevisionId != id ) { - return isForNonExpiringMessage(db, latestRevisionId); - } - - if (expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD) { - return false; - } - - return true; + return !(expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD); } private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long messageId) {