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