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