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