Skip to main content

matrix_sdk_indexeddb/state_store/
migrations.rs

1// Copyright 2021 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::{HashMap, HashSet},
17    sync::Arc,
18};
19
20use gloo_utils::format::JsValueSerdeExt;
21use indexed_db_futures::{
22    database::{Database, VersionChangeEvent},
23    error::Error,
24    future::OpenDbRequest,
25    object_store::ObjectStore,
26    prelude::*,
27    transaction::{Transaction, TransactionMode},
28};
29use js_sys::Date as JsDate;
30use matrix_sdk_base::{
31    StateStoreDataKey, deserialized_responses::SyncOrStrippedState,
32    store::migration_helpers::RoomInfoV1,
33};
34use matrix_sdk_store_encryption::StoreCipher;
35use ruma::{
36    events::{
37        StateEventType,
38        room::{
39            create::RoomCreateEventContent,
40            member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
41        },
42    },
43    serde::Raw,
44};
45use serde::{Deserialize, Serialize};
46use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue};
47use wasm_bindgen::JsValue;
48
49use super::{
50    ALL_STORES, Result, RoomMember, deserialize_value, encode_key, encode_to_range, keys,
51    serialize_value,
52};
53use crate::IndexeddbStateStoreError;
54
55const CURRENT_DB_VERSION: u32 = 14;
56const CURRENT_META_DB_VERSION: u32 = 2;
57
58/// Sometimes Migrations can't proceed without having to drop existing
59/// data. This allows you to configure, how these cases should be handled.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum MigrationConflictStrategy {
62    /// Just drop the data, we don't care that we have to sync again
63    Drop,
64    /// Raise a [`IndexeddbStateStoreError::MigrationConflict`] error with the
65    /// path to the DB in question. The caller then has to take care about
66    /// what they want to do and try again after.
67    Raise,
68    /// Default.
69    BackupAndDrop,
70}
71
72#[derive(Clone, Serialize, Deserialize)]
73struct StoreKeyWrapper(Vec<u8>);
74
75mod old_keys {
76    pub const SESSION: &str = "session";
77    pub const SYNC_TOKEN: &str = "sync_token";
78    pub const MEMBERS: &str = "members";
79    pub const STRIPPED_MEMBERS: &str = "stripped_members";
80    pub const JOINED_USER_IDS: &str = "joined_user_ids";
81    pub const INVITED_USER_IDS: &str = "invited_user_ids";
82    pub const STRIPPED_JOINED_USER_IDS: &str = "stripped_joined_user_ids";
83    pub const STRIPPED_INVITED_USER_IDS: &str = "stripped_invited_user_ids";
84    pub const STRIPPED_ROOM_INFOS: &str = "stripped_room_infos";
85    pub const MEDIA: &str = "media";
86}
87
88pub async fn upgrade_meta_db(
89    meta_name: &str,
90    passphrase: Option<&str>,
91) -> Result<(Database, Option<Arc<StoreCipher>>)> {
92    // Meta database.
93    let db_req: OpenDbRequest = Database::open(meta_name)
94        .with_version(CURRENT_META_DB_VERSION)
95        .with_on_upgrade_needed(
96            |evt: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
97                let db = tx.db();
98                let old_version = evt.old_version() as u32;
99
100                if old_version < 1 {
101                    db.create_object_store(keys::INTERNAL_STATE).build()?;
102                }
103
104                if old_version < 2 {
105                    db.create_object_store(keys::BACKUPS_META).build()?;
106                }
107
108                Ok(())
109            },
110        )
111        .build()?;
112
113    let meta_db: Database = db_req.await?;
114
115    let store_cipher = if let Some(passphrase) = passphrase {
116        let tx: Transaction<'_> = meta_db
117            .transaction(keys::INTERNAL_STATE)
118            .with_mode(TransactionMode::Readwrite)
119            .build()?;
120        let ob = tx.object_store(keys::INTERNAL_STATE)?;
121
122        let cipher = if let Some(StoreKeyWrapper(inner)) = ob
123            .get(&JsValue::from_str(keys::STORE_KEY))
124            .await?
125            .map(|v: JsValue| v.into_serde())
126            .transpose()?
127        {
128            StoreCipher::import(passphrase, &inner)?
129        } else {
130            let cipher = StoreCipher::new()?;
131            #[cfg(not(test))]
132            let export = cipher.export(passphrase)?;
133            #[cfg(test)]
134            let export = cipher._insecure_export_fast_for_testing(passphrase)?;
135            ob.put(&StoreKeyWrapper(export)).with_key(keys::STORE_KEY.to_owned()).serde()?.await?;
136            cipher
137        };
138
139        tx.commit().await?;
140        Some(Arc::new(cipher))
141    } else {
142        None
143    };
144
145    Ok((meta_db, store_cipher))
146}
147
148/// Helper struct for upgrading the inner DB.
149#[derive(Debug, Clone, Default)]
150pub struct OngoingMigration {
151    /// Names of stores to drop.
152    drop_stores: HashSet<&'static str>,
153    /// Names of stores to create.
154    create_stores: HashSet<&'static str>,
155    /// Store name => key-value data to add.
156    data: HashMap<&'static str, Vec<(JsValue, JsValue)>>,
157}
158
159pub async fn upgrade_inner_db(
160    name: &str,
161    store_cipher: Option<&StoreCipher>,
162    migration_strategy: MigrationConflictStrategy,
163    meta_db: &Database,
164) -> Result<Database> {
165    let mut db = Database::open(name).await?;
166
167    // Even if the web-sys bindings expose the version as a f64, the IndexedDB API
168    // works with an unsigned integer.
169    // See <https://github.com/rustwasm/wasm-bindgen/issues/1149>
170    let mut old_version = db.version() as u32;
171
172    if old_version < CURRENT_DB_VERSION {
173        // This is a hack, we need to open the database a first time to get the current
174        // version.
175        // The indexed_db_futures crate doesn't let us access the transaction so we
176        // can't migrate data inside the `onupgradeneeded` callback. Instead we see if
177        // we need to migrate some data before the upgrade, then let the store process
178        // the upgrade.
179        // See <https://github.com/Alorel/rust-indexed-db/issues/20>
180        let has_store_cipher = store_cipher.is_some();
181
182        // Inside the `onupgradeneeded` callback we would know whether it's a new DB
183        // because the old version would be set to 0, here it is already set to 1 so we
184        // check if the stores exist.
185        if old_version == 1 && db.object_store_names().next().is_none() {
186            old_version = 0;
187        }
188
189        // Upgrades to v1 and v2 (re)create empty stores, while the other upgrades
190        // change data that is already in the stores, so we use exclusive branches here.
191        if old_version == 0 {
192            let migration = OngoingMigration {
193                create_stores: ALL_STORES.iter().copied().collect(),
194                ..Default::default()
195            };
196            db = apply_migration(db, CURRENT_DB_VERSION, migration).await?;
197        } else if old_version < 2 && has_store_cipher {
198            match migration_strategy {
199                MigrationConflictStrategy::BackupAndDrop => {
200                    backup_v1(&db, meta_db).await?;
201                }
202                MigrationConflictStrategy::Drop => {}
203                MigrationConflictStrategy::Raise => {
204                    return Err(IndexeddbStateStoreError::MigrationConflict {
205                        name: name.to_owned(),
206                        old_version,
207                        new_version: CURRENT_DB_VERSION,
208                    });
209                }
210            }
211
212            let migration = OngoingMigration {
213                drop_stores: V1_STORES.iter().copied().collect(),
214                create_stores: ALL_STORES.iter().copied().collect(),
215                ..Default::default()
216            };
217            db = apply_migration(db, CURRENT_DB_VERSION, migration).await?;
218        } else {
219            if old_version < 3 {
220                db = migrate_to_v3(db, store_cipher).await?;
221            }
222            if old_version < 4 {
223                db = migrate_to_v4(db, store_cipher).await?;
224            }
225            if old_version < 5 {
226                db = migrate_to_v5(db, store_cipher).await?;
227            }
228            if old_version < 6 {
229                db = migrate_to_v6(db, store_cipher).await?;
230            }
231            if old_version < 7 {
232                db = migrate_to_v7(db, store_cipher).await?;
233            }
234            if old_version < 8 {
235                db = migrate_to_v8(db, store_cipher).await?;
236            }
237            if old_version < 9 {
238                db = migrate_to_v9(db).await?;
239            }
240            if old_version < 10 {
241                db = migrate_to_v10(db).await?;
242            }
243            if old_version < 11 {
244                db = migrate_to_v11(db).await?;
245            }
246            if old_version < 12 {
247                db = migrate_to_v12(db).await?;
248            }
249            if old_version < 13 {
250                db = migrate_to_v13(db).await?;
251            }
252            if old_version < 14 {
253                db = migrate_to_v14(db).await?;
254            }
255        }
256
257        db.close();
258
259        let db_req: OpenDbRequest = Database::open(name)
260            .with_version(CURRENT_DB_VERSION)
261            .with_on_upgrade_needed(
262                move |evt: VersionChangeEvent, _: &Transaction<'_>| -> Result<(), Error> {
263                    // Sanity check.
264                    // There should be no upgrade needed since the database should have already been
265                    // upgraded to the latest version.
266                    panic!(
267                        "Opening database that was not fully upgraded: \
268                     DB version: {}; latest version: {CURRENT_DB_VERSION}",
269                        evt.old_version()
270                    )
271                },
272            )
273            .build()?;
274        db = db_req.await?;
275    }
276
277    Ok(db)
278}
279
280/// Apply the given migration by upgrading the database with the given name to
281/// the given version.
282async fn apply_migration(
283    db: Database,
284    version: u32,
285    migration: OngoingMigration,
286) -> Result<Database> {
287    let name = db.name();
288    db.close();
289
290    let db_req: OpenDbRequest = Database::open(&name)
291        .with_version(version)
292        .with_on_upgrade_needed(
293            move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
294                // Changing the format can only happen in the upgrade procedure
295                for store in &migration.drop_stores {
296                    tx.db().delete_object_store(store)?;
297                }
298                for store in &migration.create_stores {
299                    tx.db().create_object_store(store).build()?;
300                }
301
302                Ok(())
303            },
304        )
305        .build()?;
306
307    let db = db_req.await?;
308
309    // Finally, we can add data to the newly created tables if needed.
310    if !migration.data.is_empty() {
311        let stores: Vec<_> = migration.data.keys().copied().collect();
312        let tx = db.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
313
314        for (name, data) in migration.data {
315            let store = tx.object_store(name)?;
316            for (key, value) in data {
317                store.put(&value).with_key(key).await?;
318            }
319        }
320
321        tx.commit().await?;
322    }
323
324    Ok(db)
325}
326
327pub const V1_STORES: &[&str] = &[
328    old_keys::SESSION,
329    keys::ACCOUNT_DATA,
330    old_keys::MEMBERS,
331    keys::PROFILES,
332    keys::DISPLAY_NAMES,
333    old_keys::JOINED_USER_IDS,
334    old_keys::INVITED_USER_IDS,
335    keys::ROOM_STATE,
336    keys::ROOM_INFOS,
337    keys::PRESENCE,
338    keys::ROOM_ACCOUNT_DATA,
339    old_keys::STRIPPED_ROOM_INFOS,
340    old_keys::STRIPPED_MEMBERS,
341    keys::STRIPPED_ROOM_STATE,
342    old_keys::STRIPPED_JOINED_USER_IDS,
343    old_keys::STRIPPED_INVITED_USER_IDS,
344    keys::ROOM_USER_RECEIPTS,
345    keys::ROOM_EVENT_RECEIPTS,
346    old_keys::MEDIA,
347    keys::CUSTOM,
348    old_keys::SYNC_TOKEN,
349];
350
351async fn backup_v1(source: &Database, meta: &Database) -> Result<()> {
352    let now = JsDate::now();
353    let backup_name = format!("backup-{}-{now}", source.name());
354
355    let db_req: OpenDbRequest = Database::open(&backup_name)
356        .with_version(source.version())
357        .with_on_upgrade_needed(
358            move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
359                // migrating to version 1
360                let db = tx.db();
361                for name in V1_STORES {
362                    db.create_object_store(name).build()?;
363                }
364                Ok(())
365            },
366        )
367        .build()?;
368    let target = db_req.await?;
369
370    for name in V1_STORES {
371        let source_tx = source.transaction(*name).with_mode(TransactionMode::Readonly).build()?;
372        let source_obj = source_tx.object_store(name)?;
373        let Some(mut curs) = source_obj.open_cursor().await? else {
374            continue;
375        };
376
377        let mut data = vec![];
378        while let Some(value) = curs.next_record::<JsValue>().await? {
379            if let Some(key) = curs.key::<JsValue>()? {
380                data.push((key, value));
381            }
382        }
383
384        let target_tx = target.transaction(*name).with_mode(TransactionMode::Readwrite).build()?;
385        let target_obj = target_tx.object_store(name)?;
386
387        for (key, value) in data {
388            target_obj.put(value).with_key(key).await?;
389        }
390
391        target_tx.commit().await?;
392    }
393
394    let tx = meta.transaction(keys::BACKUPS_META).with_mode(TransactionMode::Readwrite).build()?;
395    let backup_store = tx.object_store(keys::BACKUPS_META)?;
396    backup_store.put(&backup_name).with_key(now).await?;
397
398    tx.commit().await?;
399
400    target.close();
401
402    Ok(())
403}
404
405async fn v3_fix_store(store: &ObjectStore<'_>, store_cipher: Option<&StoreCipher>) -> Result<()> {
406    fn maybe_fix_json(raw_json: &RawJsonValue) -> Result<Option<JsonValue>> {
407        let json = raw_json.get();
408
409        if json.contains(r#""content":null"#) {
410            let mut value: JsonValue = serde_json::from_str(json)?;
411            if let Some(content) = value.get_mut("content")
412                && matches!(content, JsonValue::Null)
413            {
414                *content = JsonValue::Object(Default::default());
415                return Ok(Some(value));
416            }
417        }
418
419        Ok(None)
420    }
421
422    let cursor = store.open_cursor().await?;
423
424    if let Some(mut cursor) = cursor {
425        while let Some(value) = cursor.next_record().await? {
426            let raw_json: Box<RawJsonValue> = deserialize_value(store_cipher, &value)?;
427
428            if let Some(fixed_json) = maybe_fix_json(&raw_json)? {
429                cursor.update(&serialize_value(store_cipher, &fixed_json)?).await?;
430            }
431        }
432    }
433
434    Ok(())
435}
436
437/// Fix serialized redacted state events.
438async fn migrate_to_v3(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
439    let tx = db
440        .transaction([keys::ROOM_STATE, keys::ROOM_INFOS])
441        .with_mode(TransactionMode::Readwrite)
442        .build()?;
443
444    v3_fix_store(&tx.object_store(keys::ROOM_STATE)?, store_cipher).await?;
445    v3_fix_store(&tx.object_store(keys::ROOM_INFOS)?, store_cipher).await?;
446
447    tx.commit().await?;
448
449    let name = db.name();
450    db.close();
451
452    // Update the version of the database.
453    Ok(Database::open(&name).with_version(3u32).await?)
454}
455
456/// Move the content of the SYNC_TOKEN and SESSION stores to the new KV store.
457async fn migrate_to_v4(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
458    let tx = db
459        .transaction([old_keys::SYNC_TOKEN, old_keys::SESSION])
460        .with_mode(TransactionMode::Readonly)
461        .build()?;
462    let mut values = Vec::new();
463
464    // Sync token
465    let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
466    let sync_token = sync_token_store.get(&JsValue::from_str(old_keys::SYNC_TOKEN)).await?;
467
468    if let Some(sync_token) = sync_token {
469        values.push((
470            encode_key(store_cipher, StateStoreDataKey::SYNC_TOKEN, StateStoreDataKey::SYNC_TOKEN),
471            sync_token,
472        ));
473    }
474
475    // Filters
476    let session_store = tx.object_store(old_keys::SESSION)?;
477    let range = encode_to_range(store_cipher, StateStoreDataKey::FILTER, StateStoreDataKey::FILTER);
478    if let Some(mut cursor) = session_store.open_cursor().with_query(&range).await? {
479        while let Some(value) = cursor.next_record().await? {
480            let Some(key) = cursor.key()? else {
481                break;
482            };
483            values.push((key, value));
484        }
485    }
486
487    tx.commit().await?;
488
489    let mut data = HashMap::new();
490    if !values.is_empty() {
491        data.insert(keys::KV, values);
492    }
493
494    let migration = OngoingMigration {
495        drop_stores: [old_keys::SYNC_TOKEN, old_keys::SESSION].into_iter().collect(),
496        create_stores: [keys::KV].into_iter().collect(),
497        data,
498    };
499    apply_migration(db, 4, migration).await
500}
501
502/// Move the member events with other state events.
503async fn migrate_to_v5(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
504    let tx = db
505        .transaction([
506            old_keys::MEMBERS,
507            old_keys::STRIPPED_MEMBERS,
508            keys::ROOM_STATE,
509            keys::STRIPPED_ROOM_STATE,
510            keys::ROOM_INFOS,
511            old_keys::STRIPPED_ROOM_INFOS,
512        ])
513        .with_mode(TransactionMode::Readwrite)
514        .build()?;
515
516    let members_store = tx.object_store(old_keys::MEMBERS)?;
517    let state_store = tx.object_store(keys::ROOM_STATE)?;
518    let room_infos = tx
519        .object_store(keys::ROOM_INFOS)?
520        .get_all()
521        .await?
522        .filter_map(Result::ok)
523        .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
524        .collect::<Vec<_>>();
525
526    for room_info in room_infos {
527        let room_id = room_info.room_id();
528        let range = encode_to_range(store_cipher, old_keys::MEMBERS, room_id);
529        for result in members_store.get_all().with_query(&range).await? {
530            let value = result?;
531            let raw_member_event =
532                deserialize_value::<Raw<SyncRoomMemberEvent>>(store_cipher, &value)?;
533            let state_key = raw_member_event.get_field::<String>("state_key")?.unwrap_or_default();
534            let key = encode_key(
535                store_cipher,
536                keys::ROOM_STATE,
537                (room_id, StateEventType::RoomMember, state_key),
538            );
539
540            state_store.add(&value).with_key(key).build()?.await?;
541        }
542    }
543
544    let stripped_members_store = tx.object_store(old_keys::STRIPPED_MEMBERS)?;
545    let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
546    let stripped_room_infos = tx
547        .object_store(old_keys::STRIPPED_ROOM_INFOS)?
548        .get_all()
549        .await?
550        .filter_map(Result::ok)
551        .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
552        .collect::<Vec<_>>();
553
554    for room_info in stripped_room_infos {
555        let room_id = room_info.room_id();
556        let range = encode_to_range(store_cipher, old_keys::STRIPPED_MEMBERS, room_id);
557        for result in stripped_members_store.get_all().with_query(&range).await? {
558            let value = result?;
559            let raw_member_event =
560                deserialize_value::<Raw<StrippedRoomMemberEvent>>(store_cipher, &value)?;
561            let state_key = raw_member_event.get_field::<String>("state_key")?.unwrap_or_default();
562            let key = encode_key(
563                store_cipher,
564                keys::STRIPPED_ROOM_STATE,
565                (room_id, StateEventType::RoomMember, state_key),
566            );
567
568            stripped_state_store.add(&value).with_key(key).build()?.await?;
569        }
570    }
571
572    tx.commit().await?;
573
574    let migration = OngoingMigration {
575        drop_stores: [old_keys::MEMBERS, old_keys::STRIPPED_MEMBERS].into_iter().collect(),
576        create_stores: Default::default(),
577        data: Default::default(),
578    };
579    apply_migration(db, 5, migration).await
580}
581
582/// Remove the old user IDs stores and populate the new ones.
583async fn migrate_to_v6(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
584    // We only have joined and invited user IDs in the old store, so instead we will
585    // use the room member events to populate the new store.
586    let tx = db
587        .transaction([
588            keys::ROOM_STATE,
589            keys::ROOM_INFOS,
590            keys::STRIPPED_ROOM_STATE,
591            old_keys::STRIPPED_ROOM_INFOS,
592        ])
593        .with_mode(TransactionMode::Readonly)
594        .build()?;
595
596    let state_store = tx.object_store(keys::ROOM_STATE)?;
597    let room_infos = tx
598        .object_store(keys::ROOM_INFOS)?
599        .get_all()
600        .await?
601        .filter_map(Result::ok)
602        .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
603        .collect::<Vec<_>>();
604    let mut values = Vec::new();
605
606    for room_info in room_infos {
607        let room_id = room_info.room_id();
608        let range =
609            encode_to_range(store_cipher, keys::ROOM_STATE, (room_id, StateEventType::RoomMember));
610        for result in state_store.get_all().with_query(&range).await? {
611            let value = result?;
612            let member_event = deserialize_value::<Raw<SyncRoomMemberEvent>>(store_cipher, &value)?
613                .deserialize()?;
614            let key = encode_key(store_cipher, keys::USER_IDS, (room_id, member_event.state_key()));
615            let value = serialize_value(store_cipher, &RoomMember::from(&member_event))?;
616
617            values.push((key, value));
618        }
619    }
620
621    let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
622    let stripped_room_infos = tx
623        .object_store(old_keys::STRIPPED_ROOM_INFOS)?
624        .get_all()
625        .await?
626        .filter_map(Result::ok)
627        .filter_map(|f| deserialize_value::<RoomInfoV1>(store_cipher, &f).ok())
628        .collect::<Vec<_>>();
629    let mut stripped_values = Vec::new();
630
631    for room_info in stripped_room_infos {
632        let room_id = room_info.room_id();
633        let range = encode_to_range(
634            store_cipher,
635            keys::STRIPPED_ROOM_STATE,
636            (room_id, StateEventType::RoomMember),
637        );
638        for result in stripped_state_store.get_all().with_query(&range).await? {
639            let value = result?;
640            let stripped_member_event =
641                deserialize_value::<Raw<StrippedRoomMemberEvent>>(store_cipher, &value)?
642                    .deserialize()?;
643            let key = encode_key(
644                store_cipher,
645                keys::STRIPPED_USER_IDS,
646                (room_id, &stripped_member_event.state_key),
647            );
648            let value = serialize_value(store_cipher, &RoomMember::from(&stripped_member_event))?;
649
650            stripped_values.push((key, value));
651        }
652    }
653
654    tx.commit().await?;
655
656    let mut data = HashMap::new();
657    if !values.is_empty() {
658        data.insert(keys::USER_IDS, values);
659    }
660    if !stripped_values.is_empty() {
661        data.insert(keys::STRIPPED_USER_IDS, stripped_values);
662    }
663
664    let migration = OngoingMigration {
665        drop_stores: HashSet::from_iter([
666            old_keys::JOINED_USER_IDS,
667            old_keys::INVITED_USER_IDS,
668            old_keys::STRIPPED_JOINED_USER_IDS,
669            old_keys::STRIPPED_INVITED_USER_IDS,
670        ]),
671        create_stores: HashSet::from_iter([keys::USER_IDS, keys::STRIPPED_USER_IDS]),
672        data,
673    };
674    apply_migration(db, 6, migration).await
675}
676
677/// Remove the stripped room infos store and migrate the data with the other
678/// room infos, as well as .
679async fn migrate_to_v7(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
680    let tx = db
681        .transaction([old_keys::STRIPPED_ROOM_INFOS])
682        .with_mode(TransactionMode::Readonly)
683        .build()?;
684
685    let room_infos = tx
686        .object_store(old_keys::STRIPPED_ROOM_INFOS)?
687        .get_all()
688        .await?
689        .filter_map(Result::ok)
690        .filter_map(|value| {
691            deserialize_value::<RoomInfoV1>(store_cipher, &value)
692                .ok()
693                .map(|info| (encode_key(store_cipher, keys::ROOM_INFOS, info.room_id()), value))
694        })
695        .collect::<Vec<_>>();
696
697    tx.commit().await?;
698
699    let mut data = HashMap::new();
700    if !room_infos.is_empty() {
701        data.insert(keys::ROOM_INFOS, room_infos);
702    }
703
704    let migration = OngoingMigration {
705        drop_stores: HashSet::from_iter([old_keys::STRIPPED_ROOM_INFOS]),
706        data,
707        ..Default::default()
708    };
709    apply_migration(db, 7, migration).await
710}
711
712/// Change the format of the room infos.
713async fn migrate_to_v8(db: Database, store_cipher: Option<&StoreCipher>) -> Result<Database> {
714    let tx = db
715        .transaction([keys::ROOM_STATE, keys::STRIPPED_ROOM_STATE, keys::ROOM_INFOS])
716        .with_mode(TransactionMode::Readwrite)
717        .build()?;
718
719    let room_state_store = tx.object_store(keys::ROOM_STATE)?;
720    let stripped_room_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
721    let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
722
723    let room_infos_v1 = room_infos_store
724        .get_all()
725        .build()?
726        .await?
727        .map(|value| deserialize_value::<RoomInfoV1>(store_cipher, &value?))
728        .collect::<Result<Vec<_>, _>>()?;
729
730    for room_info_v1 in room_infos_v1 {
731        let create = if let Some(event) = stripped_room_state_store
732            .get(&encode_key(
733                store_cipher,
734                keys::STRIPPED_ROOM_STATE,
735                (room_info_v1.room_id(), &StateEventType::RoomCreate, ""),
736            ))
737            .await?
738            .map(|f| deserialize_value(store_cipher, &f))
739            .transpose()?
740        {
741            Some(SyncOrStrippedState::<RoomCreateEventContent>::Stripped(event))
742        } else {
743            room_state_store
744                .get(&encode_key(
745                    store_cipher,
746                    keys::ROOM_STATE,
747                    (room_info_v1.room_id(), &StateEventType::RoomCreate, ""),
748                ))
749                .await?
750                .map(|f| deserialize_value(store_cipher, &f))
751                .transpose()?
752                .map(SyncOrStrippedState::<RoomCreateEventContent>::Sync)
753        };
754
755        let room_info = room_info_v1.migrate(create.as_ref());
756        room_infos_store
757            .put(&serialize_value(store_cipher, &room_info)?)
758            .with_key(encode_key(store_cipher, keys::ROOM_INFOS, room_info.room_id()))
759            .build()?;
760    }
761
762    tx.commit().await?;
763
764    let name = db.name();
765    db.close();
766
767    // Update the version of the database.
768    Ok(Database::open(&name).with_version(8u32).build()?.await?)
769}
770
771/// Add the new [`keys::ROOM_SEND_QUEUE`] table.
772async fn migrate_to_v9(db: Database) -> Result<Database> {
773    let migration = OngoingMigration {
774        drop_stores: [].into(),
775        create_stores: [keys::ROOM_SEND_QUEUE].into_iter().collect(),
776        data: Default::default(),
777    };
778    apply_migration(db, 9, migration).await
779}
780
781/// Add the new [`keys::DEPENDENT_SEND_QUEUE`] table.
782async fn migrate_to_v10(db: Database) -> Result<Database> {
783    let migration = OngoingMigration {
784        drop_stores: [].into(),
785        create_stores: [keys::DEPENDENT_SEND_QUEUE].into_iter().collect(),
786        data: Default::default(),
787    };
788    apply_migration(db, 10, migration).await
789}
790
791/// Drop the [`old_keys::MEDIA`] table.
792async fn migrate_to_v11(db: Database) -> Result<Database> {
793    let migration = OngoingMigration {
794        drop_stores: [old_keys::MEDIA].into(),
795        create_stores: Default::default(),
796        data: Default::default(),
797    };
798    apply_migration(db, 11, migration).await
799}
800
801/// The format of data serialized into the send queue and dependent send queue
802/// tables have changed, clear both.
803async fn migrate_to_v12(db: Database) -> Result<Database> {
804    let store_keys = [keys::DEPENDENT_SEND_QUEUE, keys::ROOM_SEND_QUEUE];
805    let tx = db.transaction(store_keys).with_mode(TransactionMode::Readwrite).build()?;
806
807    for store_name in store_keys {
808        let store = tx.object_store(store_name)?;
809        store.clear()?;
810    }
811
812    tx.commit().await?;
813
814    let name = db.name();
815    db.close();
816
817    // Update the version of the database.
818    Ok(Database::open(&name).with_version(12u32).build()?.await?)
819}
820
821/// Add the thread subscriptions table.
822async fn migrate_to_v13(db: Database) -> Result<Database> {
823    let migration = OngoingMigration {
824        drop_stores: [].into(),
825        create_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
826        data: Default::default(),
827    };
828    apply_migration(db, 13, migration).await
829}
830
831/// Empty the thread subscriptions table, because the serialized format has
832/// changed (from storing only the subscription to storing the
833/// `StoredThreadSubscription`).
834async fn migrate_to_v14(db: Database) -> Result<Database> {
835    let migration = OngoingMigration {
836        drop_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
837        create_stores: [keys::THREAD_SUBSCRIPTIONS].into_iter().collect(),
838        data: Default::default(),
839    };
840    apply_migration(db, 14, migration).await
841}
842
843#[cfg(all(test, target_family = "wasm"))]
844mod tests {
845    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
846
847    use assert_matches::assert_matches;
848    use assert_matches2::assert_let;
849    use indexed_db_futures::{
850        database::{Database, VersionChangeEvent},
851        error::Error,
852        object_store::ObjectStore,
853        prelude::*,
854        transaction::{Transaction, TransactionMode},
855    };
856    use matrix_sdk_base::{
857        RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError,
858        deserialized_responses::RawMemberEvent,
859        store::{RoomLoadSettings, StateStoreExt},
860        sync::UnreadNotificationsCount,
861    };
862    use matrix_sdk_test::{async_test, event_factory::EventFactory};
863    use ruma::{
864        EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, UserId,
865        events::{
866            AnySyncStateEvent, StateEventType,
867            room::{
868                create::RoomCreateEventContent,
869                member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
870            },
871        },
872        owned_user_id, room_id,
873        serde::Raw,
874        server_name, user_id,
875    };
876    use serde_json::json;
877    use uuid::Uuid;
878    use wasm_bindgen::JsValue;
879
880    use super::{CURRENT_DB_VERSION, CURRENT_META_DB_VERSION, MigrationConflictStrategy, old_keys};
881    use crate::{
882        IndexeddbStateStore, IndexeddbStateStoreError,
883        serializer::safe_encode::traits::SafeEncode,
884        state_store::{Result, encode_key, keys, serialize_value},
885    };
886
887    const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key";
888    const CUSTOM_DATA: &[u8] = b"some_custom_data";
889
890    pub async fn create_fake_db(name: &str, version: u32) -> Result<Database> {
891        Database::open(name)
892            .with_version(version)
893            .with_on_upgrade_needed(
894                move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
895                    let db = tx.db();
896
897                    // Stores common to all versions.
898                    let common_stores = &[
899                        keys::ACCOUNT_DATA,
900                        keys::PROFILES,
901                        keys::DISPLAY_NAMES,
902                        keys::ROOM_STATE,
903                        keys::ROOM_INFOS,
904                        keys::PRESENCE,
905                        keys::ROOM_ACCOUNT_DATA,
906                        keys::STRIPPED_ROOM_STATE,
907                        keys::ROOM_USER_RECEIPTS,
908                        keys::ROOM_EVENT_RECEIPTS,
909                        keys::CUSTOM,
910                    ];
911
912                    for name in common_stores {
913                        db.create_object_store(name).build()?;
914                    }
915
916                    if version < 4 {
917                        for name in [old_keys::SYNC_TOKEN, old_keys::SESSION] {
918                            db.create_object_store(name).build()?;
919                        }
920                    }
921                    if version >= 4 {
922                        db.create_object_store(keys::KV).build()?;
923                    }
924                    if version < 5 {
925                        for name in [old_keys::MEMBERS, old_keys::STRIPPED_MEMBERS] {
926                            db.create_object_store(name).build()?;
927                        }
928                    }
929                    if version < 6 {
930                        for name in [
931                            old_keys::INVITED_USER_IDS,
932                            old_keys::JOINED_USER_IDS,
933                            old_keys::STRIPPED_INVITED_USER_IDS,
934                            old_keys::STRIPPED_JOINED_USER_IDS,
935                        ] {
936                            db.create_object_store(name).build()?;
937                        }
938                    }
939                    if version >= 6 {
940                        for name in [keys::USER_IDS, keys::STRIPPED_USER_IDS] {
941                            db.create_object_store(name).build()?;
942                        }
943                    }
944                    if version < 7 {
945                        db.create_object_store(old_keys::STRIPPED_ROOM_INFOS).build()?;
946                    }
947                    if version < 11 {
948                        db.create_object_store(old_keys::MEDIA).build()?;
949                    }
950
951                    Ok(())
952                },
953            )
954            .build()?
955            .await
956            .map_err(Into::into)
957    }
958
959    fn room_info_v1_json(
960        room_id: &RoomId,
961        state: RoomState,
962        name: Option<&str>,
963        creator: Option<&UserId>,
964    ) -> serde_json::Value {
965        // Test with name set or not.
966        let name_content = match name {
967            Some(name) => json!({ "name": name }),
968            None => json!({ "name": null }),
969        };
970        // Test with creator set or not.
971        let create_content = match creator {
972            Some(creator) => RoomCreateEventContent::new_v1(creator.to_owned()),
973            None => RoomCreateEventContent::new_v11(),
974        };
975
976        json!({
977            "room_id": room_id,
978            "room_type": state,
979            "notification_counts": UnreadNotificationsCount::default(),
980            "summary": {
981                "heroes": [],
982                "joined_member_count": 0,
983                "invited_member_count": 0,
984            },
985            "members_synced": false,
986            "base_info": {
987                "dm_targets": [],
988                "max_power_level": 100,
989                "name": {
990                    "Original": {
991                        "content": name_content,
992                    },
993                },
994                "create": {
995                    "Original": {
996                        "content": create_content,
997                    }
998                }
999            },
1000        })
1001    }
1002
1003    #[async_test]
1004    pub async fn test_new_store() -> Result<()> {
1005        let name = format!("new-store-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1006
1007        // this transparently migrates to the latest version
1008        let store = IndexeddbStateStore::builder().name(name).build().await?;
1009        // this didn't create any backup
1010        assert_eq!(store.has_backups().await?, false);
1011        // simple check that the layout exists.
1012        assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1013
1014        // Check versions.
1015        assert_eq!(store.version(), CURRENT_DB_VERSION);
1016        assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1017
1018        Ok(())
1019    }
1020
1021    #[async_test]
1022    pub async fn test_migrating_v1_to_v2_plain() -> Result<()> {
1023        let name = format!("migrating-v2-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1024
1025        // Create and populate db.
1026        {
1027            let db = create_fake_db(&name, 1).await?;
1028            let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1029            let custom = tx.object_store(keys::CUSTOM)?;
1030            let jskey = JsValue::from_str(
1031                core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1032            );
1033            custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1034            tx.commit().await?;
1035            db.close();
1036        }
1037
1038        // this transparently migrates to the latest version
1039        let store = IndexeddbStateStore::builder().name(name).build().await?;
1040        // this didn't create any backup
1041        assert_eq!(store.has_backups().await?, false);
1042        // Custom data is still there.
1043        assert_let!(Some(stored_data) = store.get_custom_value(CUSTOM_DATA_KEY).await?);
1044        assert_eq!(stored_data, CUSTOM_DATA);
1045
1046        // Check versions.
1047        assert_eq!(store.version(), CURRENT_DB_VERSION);
1048        assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1049
1050        Ok(())
1051    }
1052
1053    #[async_test]
1054    pub async fn test_migrating_v1_to_v2_with_pw() -> Result<()> {
1055        let name =
1056            format!("migrating-v2-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1057        let passphrase = "somepassphrase".to_owned();
1058
1059        // Create and populate db.
1060        {
1061            let db = create_fake_db(&name, 1).await?;
1062            let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1063            let custom = tx.object_store(keys::CUSTOM)?;
1064            let jskey = JsValue::from_str(
1065                core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1066            );
1067            custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1068            tx.commit().await?;
1069            db.close();
1070        }
1071
1072        // this transparently migrates to the latest version
1073        let store =
1074            IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?;
1075        // this creates a backup by default
1076        assert_eq!(store.has_backups().await?, true);
1077        assert!(store.latest_backup().await?.is_some(), "No backup_found");
1078        // the data is gone
1079        assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1080
1081        // Check versions.
1082        assert_eq!(store.version(), CURRENT_DB_VERSION);
1083        assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1084
1085        Ok(())
1086    }
1087
1088    #[async_test]
1089    pub async fn test_migrating_v1_to_v2_with_pw_drops() -> Result<()> {
1090        let name = format!(
1091            "migrating-v2-with-cipher-drops-{}",
1092            Uuid::new_v4().as_hyphenated().to_string()
1093        );
1094        let passphrase = "some-other-passphrase".to_owned();
1095
1096        // Create and populate db.
1097        {
1098            let db = create_fake_db(&name, 1).await?;
1099            let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1100            let custom = tx.object_store(keys::CUSTOM)?;
1101            let jskey = JsValue::from_str(
1102                core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1103            );
1104            custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1105            tx.commit().await?;
1106            db.close();
1107        }
1108
1109        // this transparently migrates to the latest version
1110        let store = IndexeddbStateStore::builder()
1111            .name(name)
1112            .passphrase(passphrase)
1113            .migration_conflict_strategy(MigrationConflictStrategy::Drop)
1114            .build()
1115            .await?;
1116        // this doesn't create a backup
1117        assert_eq!(store.has_backups().await?, false);
1118        // the data is gone
1119        assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1120
1121        // Check versions.
1122        assert_eq!(store.version(), CURRENT_DB_VERSION);
1123        assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1124
1125        Ok(())
1126    }
1127
1128    #[async_test]
1129    pub async fn test_migrating_v1_to_v2_with_pw_raise() -> Result<()> {
1130        let name = format!(
1131            "migrating-v2-with-cipher-raises-{}",
1132            Uuid::new_v4().as_hyphenated().to_string()
1133        );
1134        let passphrase = "some-other-passphrase".to_owned();
1135
1136        // Create and populate db.
1137        {
1138            let db = create_fake_db(&name, 1).await?;
1139            let tx = db.transaction(keys::CUSTOM).with_mode(TransactionMode::Readwrite).build()?;
1140            let custom = tx.object_store(keys::CUSTOM)?;
1141            let jskey = JsValue::from_str(
1142                core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1143            );
1144            custom.put(&serialize_value(None, &CUSTOM_DATA)?).with_key(jskey).await?;
1145            tx.commit().await?;
1146            db.close();
1147        }
1148
1149        // this transparently migrates to the latest version
1150        let store_res = IndexeddbStateStore::builder()
1151            .name(name)
1152            .passphrase(passphrase)
1153            .migration_conflict_strategy(MigrationConflictStrategy::Raise)
1154            .build()
1155            .await;
1156
1157        assert_matches!(store_res, Err(IndexeddbStateStoreError::MigrationConflict { .. }));
1158
1159        Ok(())
1160    }
1161
1162    #[async_test]
1163    pub async fn test_migrating_to_v3() -> Result<()> {
1164        let name = format!("migrating-v3-{}", Uuid::new_v4().as_hyphenated().to_string());
1165
1166        // An event that fails to deserialize.
1167        let wrong_redacted_state_event = json!({
1168            "content": null,
1169            "event_id": "$wrongevent",
1170            "origin_server_ts": 1673887516047_u64,
1171            "sender": "@example:localhost",
1172            "state_key": "",
1173            "type": "m.room.topic",
1174            "unsigned": {
1175                "redacted_because": {
1176                    "type": "m.room.redaction",
1177                    "sender": "@example:localhost",
1178                    "content": {},
1179                    "redacts": "$wrongevent",
1180                    "origin_server_ts": 1673893816047_u64,
1181                    "unsigned": {},
1182                    "event_id": "$redactionevent",
1183                },
1184            },
1185        });
1186        serde_json::from_value::<AnySyncStateEvent>(wrong_redacted_state_event.clone())
1187            .unwrap_err();
1188
1189        let room_id = room_id!("!some_room:localhost");
1190
1191        // Populate DB with wrong event.
1192        {
1193            let db = create_fake_db(&name, 2).await?;
1194            let tx =
1195                db.transaction(keys::ROOM_STATE).with_mode(TransactionMode::Readwrite).build()?;
1196            let state = tx.object_store(keys::ROOM_STATE)?;
1197            let key: JsValue = (room_id, StateEventType::RoomTopic, "").as_encoded_string().into();
1198            state.put(&serialize_value(None, &wrong_redacted_state_event)?).with_key(key).await?;
1199            tx.commit().await?;
1200            db.close();
1201        }
1202
1203        // this transparently migrates to the latest version
1204        let store = IndexeddbStateStore::builder().name(name).build().await?;
1205        let event =
1206            store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap();
1207        event.deserialize().unwrap();
1208
1209        // Check versions.
1210        assert_eq!(store.version(), CURRENT_DB_VERSION);
1211        assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1212
1213        Ok(())
1214    }
1215
1216    #[async_test]
1217    pub async fn test_migrating_to_v4() -> Result<()> {
1218        let name = format!("migrating-v4-{}", Uuid::new_v4().as_hyphenated().to_string());
1219
1220        let sync_token = "a_very_unique_string";
1221        let filter_1 = "filter_1";
1222        let filter_1_id = "filter_1_id";
1223        let filter_2 = "filter_2";
1224        let filter_2_id = "filter_2_id";
1225
1226        // Populate DB with old table.
1227        {
1228            let db = create_fake_db(&name, 3).await?;
1229            let tx = db
1230                .transaction([old_keys::SYNC_TOKEN, old_keys::SESSION])
1231                .with_mode(TransactionMode::Readwrite)
1232                .build()?;
1233
1234            let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
1235            sync_token_store
1236                .put(&serialize_value(None, &sync_token)?)
1237                .with_key(JsValue::from_str(old_keys::SYNC_TOKEN))
1238                .build()?;
1239
1240            let session_store = tx.object_store(old_keys::SESSION)?;
1241            session_store
1242                .put(&serialize_value(None, &filter_1_id)?)
1243                .with_key(encode_key(
1244                    None,
1245                    StateStoreDataKey::FILTER,
1246                    (StateStoreDataKey::FILTER, filter_1),
1247                ))
1248                .build()?;
1249            session_store
1250                .put(&serialize_value(None, &filter_2_id)?)
1251                .with_key(encode_key(
1252                    None,
1253                    StateStoreDataKey::FILTER,
1254                    (StateStoreDataKey::FILTER, filter_2),
1255                ))
1256                .build()?;
1257
1258            tx.commit().await?;
1259            db.close();
1260        }
1261
1262        // this transparently migrates to the latest version
1263        let store = IndexeddbStateStore::builder().name(name).build().await?;
1264
1265        let stored_sync_token = store
1266            .get_kv_data(StateStoreDataKey::SyncToken)
1267            .await?
1268            .unwrap()
1269            .into_sync_token()
1270            .unwrap();
1271        assert_eq!(stored_sync_token, sync_token);
1272
1273        let stored_filter_1_id = store
1274            .get_kv_data(StateStoreDataKey::Filter(filter_1))
1275            .await?
1276            .unwrap()
1277            .into_filter()
1278            .unwrap();
1279        assert_eq!(stored_filter_1_id, filter_1_id);
1280
1281        let stored_filter_2_id = store
1282            .get_kv_data(StateStoreDataKey::Filter(filter_2))
1283            .await?
1284            .unwrap()
1285            .into_filter()
1286            .unwrap();
1287        assert_eq!(stored_filter_2_id, filter_2_id);
1288
1289        Ok(())
1290    }
1291
1292    #[async_test]
1293    pub async fn test_migrating_to_v5() -> Result<()> {
1294        let name = format!("migrating-v5-{}", Uuid::new_v4().as_hyphenated().to_string());
1295
1296        let room_id = room_id!("!room:localhost");
1297        let user_id = user_id!("@invited:localhost");
1298        let f = EventFactory::new().room(room_id).sender(user_id!("@example:localhost"));
1299        let member_event: Raw<SyncRoomMemberEvent> = f
1300            .member(user_id)
1301            .invited(user_id)
1302            .display_name("example")
1303            .avatar_url(ruma::mxc_uri!("mxc://localhost/SEsfnsuifSDFSSEF"))
1304            .reason("Looking for support")
1305            .into();
1306
1307        let stripped_room_id = room_id!("!stripped_room:localhost");
1308        let stripped_user_id = user_id!("@example:localhost");
1309        let stripped_member_event: Raw<StrippedRoomMemberEvent> =
1310            f.member(stripped_user_id).display_name("example").into();
1311
1312        // Populate DB with old table.
1313        {
1314            let db = create_fake_db(&name, 4).await?;
1315            let tx = db
1316                .transaction([
1317                    old_keys::MEMBERS,
1318                    keys::ROOM_INFOS,
1319                    old_keys::STRIPPED_MEMBERS,
1320                    old_keys::STRIPPED_ROOM_INFOS,
1321                ])
1322                .with_mode(TransactionMode::Readwrite)
1323                .build()?;
1324
1325            let members_store = tx.object_store(old_keys::MEMBERS)?;
1326            members_store
1327                .put(&serialize_value(None, &member_event)?)
1328                .with_key(encode_key(None, old_keys::MEMBERS, (room_id, user_id)))
1329                .build()?;
1330            let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1331            let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1332            room_infos_store
1333                .put(&serialize_value(None, &room_info)?)
1334                .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1335                .build()?;
1336
1337            let stripped_members_store = tx.object_store(old_keys::STRIPPED_MEMBERS)?;
1338            stripped_members_store
1339                .put(&serialize_value(None, &stripped_member_event)?)
1340                .with_key(encode_key(
1341                    None,
1342                    old_keys::STRIPPED_MEMBERS,
1343                    (stripped_room_id, stripped_user_id),
1344                ))
1345                .build()?;
1346            let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1347            let stripped_room_info =
1348                room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1349            stripped_room_infos_store
1350                .put(&serialize_value(None, &stripped_room_info)?)
1351                .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1352                .build()?;
1353
1354            tx.commit().await?;
1355            db.close();
1356        }
1357
1358        // this transparently migrates to the latest version
1359        let store = IndexeddbStateStore::builder().name(name).build().await?;
1360
1361        assert_let!(
1362            Ok(Some(RawMemberEvent::Sync(stored_member_event))) =
1363                store.get_member_event(room_id, user_id).await
1364        );
1365        assert_eq!(stored_member_event.json().get(), member_event.json().get());
1366
1367        assert_let!(
1368            Ok(Some(RawMemberEvent::Stripped(stored_stripped_member_event))) =
1369                store.get_member_event(stripped_room_id, stripped_user_id).await
1370        );
1371        assert_eq!(stored_stripped_member_event.json().get(), stripped_member_event.json().get());
1372
1373        Ok(())
1374    }
1375
1376    #[async_test]
1377    pub async fn test_migrating_to_v6() -> Result<()> {
1378        let name = format!("migrating-v6-{}", Uuid::new_v4().as_hyphenated().to_string());
1379
1380        let room_id = room_id!("!room:localhost");
1381        let invite_user_id = user_id!("@invited:localhost");
1382        let ban_user_id = user_id!("@banned:localhost");
1383
1384        let f = EventFactory::new().room(room_id).sender(user_id!("@example:localhost"));
1385        let invite_member_event: Raw<SyncRoomMemberEvent> = f
1386            .member(invite_user_id)
1387            .invited(invite_user_id)
1388            .display_name("example")
1389            .avatar_url(ruma::mxc_uri!("mxc://localhost/SEsfnsuifSDFSSEF"))
1390            .reason("Looking for support")
1391            .into();
1392        let ban_member_event: Raw<SyncRoomMemberEvent> =
1393            f.member(ban_user_id).banned(ban_user_id).display_name("example").into();
1394
1395        let stripped_room_id = room_id!("!stripped_room:localhost");
1396        let stripped_user_id = user_id!("@example:localhost");
1397        let stripped_member_event: Raw<StrippedRoomMemberEvent> =
1398            f.member(stripped_user_id).display_name("example").into();
1399
1400        // Populate DB with old table.
1401        {
1402            let db = create_fake_db(&name, 5).await?;
1403            let tx = db
1404                .transaction([
1405                    keys::ROOM_STATE,
1406                    keys::ROOM_INFOS,
1407                    keys::STRIPPED_ROOM_STATE,
1408                    old_keys::STRIPPED_ROOM_INFOS,
1409                    old_keys::INVITED_USER_IDS,
1410                    old_keys::JOINED_USER_IDS,
1411                    old_keys::STRIPPED_INVITED_USER_IDS,
1412                    old_keys::STRIPPED_JOINED_USER_IDS,
1413                ])
1414                .with_mode(TransactionMode::Readwrite)
1415                .build()?;
1416
1417            let state_store = tx.object_store(keys::ROOM_STATE)?;
1418            state_store
1419                .put(&serialize_value(None, &invite_member_event)?)
1420                .with_key(encode_key(
1421                    None,
1422                    keys::ROOM_STATE,
1423                    (room_id, StateEventType::RoomMember, invite_user_id),
1424                ))
1425                .build()?;
1426            state_store
1427                .put(&serialize_value(None, &ban_member_event)?)
1428                .with_key(encode_key(
1429                    None,
1430                    keys::ROOM_STATE,
1431                    (room_id, StateEventType::RoomMember, ban_user_id),
1432                ))
1433                .build()?;
1434            let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1435            let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1436            room_infos_store
1437                .put(&serialize_value(None, &room_info)?)
1438                .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1439                .build()?;
1440
1441            let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
1442            stripped_state_store
1443                .put(&serialize_value(None, &stripped_member_event)?)
1444                .with_key(encode_key(
1445                    None,
1446                    keys::STRIPPED_ROOM_STATE,
1447                    (stripped_room_id, StateEventType::RoomMember, stripped_user_id),
1448                ))
1449                .build()?;
1450            let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1451            let stripped_room_info =
1452                room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1453            stripped_room_infos_store
1454                .put(&serialize_value(None, &stripped_room_info)?)
1455                .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1456                .build()?;
1457
1458            // Populate the old user IDs stores to check the data is not reused.
1459            let joined_user_id = user_id!("@joined_user:localhost");
1460            tx.object_store(old_keys::JOINED_USER_IDS)?
1461                .put(&serialize_value(None, &joined_user_id)?)
1462                .with_key(encode_key(None, old_keys::JOINED_USER_IDS, (room_id, joined_user_id)))
1463                .build()?;
1464            let invited_user_id = user_id!("@invited_user:localhost");
1465            tx.object_store(old_keys::INVITED_USER_IDS)?
1466                .put(&serialize_value(None, &invited_user_id)?)
1467                .with_key(encode_key(None, old_keys::INVITED_USER_IDS, (room_id, invited_user_id)))
1468                .build()?;
1469            let stripped_joined_user_id = user_id!("@stripped_joined_user:localhost");
1470            tx.object_store(old_keys::STRIPPED_JOINED_USER_IDS)?
1471                .put(&serialize_value(None, &stripped_joined_user_id)?)
1472                .with_key(encode_key(
1473                    None,
1474                    old_keys::STRIPPED_JOINED_USER_IDS,
1475                    (room_id, stripped_joined_user_id),
1476                ))
1477                .build()?;
1478            let stripped_invited_user_id = user_id!("@stripped_invited_user:localhost");
1479            tx.object_store(old_keys::STRIPPED_INVITED_USER_IDS)?
1480                .put(&serialize_value(None, &stripped_invited_user_id)?)
1481                .with_key(encode_key(
1482                    None,
1483                    old_keys::STRIPPED_INVITED_USER_IDS,
1484                    (room_id, stripped_invited_user_id),
1485                ))
1486                .build()?;
1487
1488            tx.commit().await?;
1489            db.close();
1490        }
1491
1492        // this transparently migrates to the latest version
1493        let store = IndexeddbStateStore::builder().name(name).build().await?;
1494
1495        assert_eq!(store.get_user_ids(room_id, RoomMemberships::JOIN).await.unwrap().len(), 0);
1496        assert_eq!(
1497            store.get_user_ids(room_id, RoomMemberships::INVITE).await.unwrap().as_slice(),
1498            [invite_user_id.to_owned()]
1499        );
1500        let user_ids = store.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
1501        assert_eq!(user_ids.len(), 2);
1502        assert!(user_ids.contains(&invite_user_id.to_owned()));
1503        assert!(user_ids.contains(&ban_user_id.to_owned()));
1504
1505        assert_eq!(
1506            store.get_user_ids(stripped_room_id, RoomMemberships::JOIN).await.unwrap().as_slice(),
1507            [stripped_user_id.to_owned()]
1508        );
1509        assert_eq!(
1510            store.get_user_ids(stripped_room_id, RoomMemberships::INVITE).await.unwrap().len(),
1511            0
1512        );
1513        assert_eq!(
1514            store
1515                .get_user_ids(stripped_room_id, RoomMemberships::empty())
1516                .await
1517                .unwrap()
1518                .as_slice(),
1519            [stripped_user_id.to_owned()]
1520        );
1521
1522        Ok(())
1523    }
1524
1525    #[async_test]
1526    pub async fn test_migrating_to_v7() -> Result<()> {
1527        let name = format!("migrating-v7-{}", Uuid::new_v4().as_hyphenated().to_string());
1528
1529        let room_id = room_id!("!room:localhost");
1530        let stripped_room_id = room_id!("!stripped_room:localhost");
1531
1532        // Populate DB with old table.
1533        {
1534            let db = create_fake_db(&name, 6).await?;
1535            let tx = db
1536                .transaction([keys::ROOM_INFOS, old_keys::STRIPPED_ROOM_INFOS])
1537                .with_mode(TransactionMode::Readwrite)
1538                .build()?;
1539
1540            let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1541            let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1542            room_infos_store
1543                .put(&serialize_value(None, &room_info)?)
1544                .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1545                .build()?;
1546
1547            let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1548            let stripped_room_info =
1549                room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1550            stripped_room_infos_store
1551                .put(&serialize_value(None, &stripped_room_info)?)
1552                .with_key(encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id))
1553                .build()?;
1554
1555            tx.commit().await?;
1556            db.close();
1557        }
1558
1559        // this transparently migrates to the latest version
1560        let store = IndexeddbStateStore::builder().name(name).build().await?;
1561
1562        assert_eq!(store.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 2);
1563
1564        Ok(())
1565    }
1566
1567    // Add a room in version 7 format of the state store.
1568    fn add_room_v7(
1569        room_infos_store: &ObjectStore<'_>,
1570        room_state_store: &ObjectStore<'_>,
1571        room_id: &RoomId,
1572        name: Option<&str>,
1573        create_creator: Option<OwnedUserId>,
1574        create_sender: Option<&UserId>,
1575    ) -> Result<()> {
1576        let room_info_json =
1577            room_info_v1_json(room_id, RoomState::Joined, name, create_creator.as_deref());
1578
1579        room_infos_store
1580            .put(&serialize_value(None, &room_info_json)?)
1581            .with_key(encode_key(None, keys::ROOM_INFOS, room_id))
1582            .build()?;
1583
1584        // Test with or without `m.room.create` event in the room state.
1585        let Some(create_sender) = create_sender else {
1586            return Ok(());
1587        };
1588
1589        let create_content = match create_creator {
1590            Some(creator) => RoomCreateEventContent::new_v1(creator),
1591            None => RoomCreateEventContent::new_v11(),
1592        };
1593
1594        let event_id = EventId::new(server_name!("dummy.local"));
1595        let create_event = json!({
1596            "content": create_content,
1597            "event_id": event_id,
1598            "sender": create_sender,
1599            "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
1600            "state_key": "",
1601            "type": "m.room.create",
1602            "unsigned": {},
1603        });
1604
1605        room_state_store
1606            .put(&serialize_value(None, &create_event)?)
1607            .with_key(encode_key(
1608                None,
1609                keys::ROOM_STATE,
1610                (room_id, &StateEventType::RoomCreate, ""),
1611            ))
1612            .build()?;
1613
1614        Ok(())
1615    }
1616
1617    #[async_test]
1618    pub async fn test_migrating_to_v8() -> Result<()> {
1619        let name = format!("migrating-v8-{}", Uuid::new_v4().as_hyphenated().to_string());
1620
1621        // Room A: with name, creator and sender.
1622        let room_a_id = room_id!("!room_a:dummy.local");
1623        let room_a_name = "Room A";
1624        let room_a_creator = owned_user_id!("@creator:dummy.local");
1625        // Use a different sender to check that sender is used over creator in
1626        // migration.
1627        let room_a_create_sender = owned_user_id!("@sender:dummy.local");
1628
1629        // Room B: without name, creator and sender.
1630        let room_b_id = room_id!("!room_b:dummy.local");
1631
1632        // Room C: only with sender.
1633        let room_c_id = room_id!("!room_c:dummy.local");
1634        let room_c_create_sender = owned_user_id!("@creator:dummy.local");
1635
1636        // Create and populate db.
1637        {
1638            let db = create_fake_db(&name, 6).await?;
1639            let tx = db
1640                .transaction([keys::ROOM_INFOS, keys::ROOM_STATE])
1641                .with_mode(TransactionMode::Readwrite)
1642                .build()?;
1643
1644            let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1645            let room_state_store = tx.object_store(keys::ROOM_STATE)?;
1646
1647            add_room_v7(
1648                &room_infos_store,
1649                &room_state_store,
1650                room_a_id,
1651                Some(room_a_name),
1652                Some(room_a_creator),
1653                Some(&room_a_create_sender),
1654            )?;
1655            add_room_v7(&room_infos_store, &room_state_store, room_b_id, None, None, None)?;
1656            add_room_v7(
1657                &room_infos_store,
1658                &room_state_store,
1659                room_c_id,
1660                None,
1661                None,
1662                Some(&room_c_create_sender),
1663            )?;
1664
1665            tx.commit().await?;
1666            db.close();
1667        }
1668
1669        // This transparently migrates to the latest version.
1670        let store = IndexeddbStateStore::builder().name(name).build().await?;
1671
1672        // Check all room infos are there.
1673        let room_infos = store.get_room_infos(&RoomLoadSettings::default()).await?;
1674        assert_eq!(room_infos.len(), 3);
1675
1676        let room_a = room_infos.iter().find(|r| r.room_id() == room_a_id).unwrap();
1677        assert_eq!(room_a.name(), Some(room_a_name));
1678        assert_eq!(room_a.creators(), Some(vec![room_a_create_sender]));
1679
1680        let room_b = room_infos.iter().find(|r| r.room_id() == room_b_id).unwrap();
1681        assert_eq!(room_b.name(), None);
1682        assert_eq!(room_b.creators(), None);
1683
1684        let room_c = room_infos.iter().find(|r| r.room_id() == room_c_id).unwrap();
1685        assert_eq!(room_c.name(), None);
1686        assert_eq!(room_c.creators(), Some(vec![room_c_create_sender]));
1687
1688        Ok(())
1689    }
1690}