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