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