1use 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#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum MigrationConflictStrategy {
56 Drop,
58 Raise,
62 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 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#[derive(Debug, Clone, Default)]
141pub struct OngoingMigration {
142 drop_stores: HashSet<&'static str>,
144 create_stores: HashSet<&'static str>,
146 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 let mut old_version = db.version() as u32;
162
163 if old_version < CURRENT_DB_VERSION {
164 let has_store_cipher = store_cipher.is_some();
172
173 if old_version == 1 && db.object_store_names().next().is_none() {
177 old_version = 0;
178 }
179
180 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 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
269async 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 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 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 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
422async 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 Ok(IdbDatabase::open_u32(&name, 3)?.await?)
439}
440
441async 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 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 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
487async 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
565async fn migrate_to_v6(db: IdbDatabase, store_cipher: Option<&StoreCipher>) -> Result<IdbDatabase> {
567 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
658async 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
693async 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 Ok(IdbDatabase::open_u32(&name, 8)?.await?)
750}
751
752async 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
762async 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
772async 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
782async 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 Ok(IdbDatabase::open_u32(&name, 12)?.await?)
800}
801
802async 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
812async 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 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 let name_content = match name {
938 Some(name) => json!({ "name": name }),
939 None => json!({ "name": null }),
940 };
941 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 let store = IndexeddbStateStore::builder().name(name).build().await?;
980 assert_eq!(store.has_backups().await?, false);
982 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
984
985 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 {
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 let store = IndexeddbStateStore::builder().name(name).build().await?;
1012 assert_eq!(store.has_backups().await?, false);
1014 assert_let!(Some(stored_data) = store.get_custom_value(CUSTOM_DATA_KEY).await?);
1016 assert_eq!(stored_data, CUSTOM_DATA);
1017
1018 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 {
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 let store =
1047 IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?;
1048 assert_eq!(store.has_backups().await?, true);
1050 assert!(store.latest_backup().await?.is_some(), "No backup_found");
1051 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1053
1054 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 {
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 let store = IndexeddbStateStore::builder()
1085 .name(name)
1086 .passphrase(passphrase)
1087 .migration_conflict_strategy(MigrationConflictStrategy::Drop)
1088 .build()
1089 .await?;
1090 assert_eq!(store.has_backups().await?, false);
1092 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1094
1095 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 {
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 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 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 {
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 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 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 {
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 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 {
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 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 {
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 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 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 {
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 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 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 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 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 let room_a_create_sender = owned_user_id!("@sender:dummy.local");
1576
1577 let room_b_id = room_id!("!room_b:dummy.local");
1579
1580 let room_c_id = room_id!("!room_c:dummy.local");
1582 let room_c_create_sender = owned_user_id!("@creator:dummy.local");
1583
1584 {
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 let store = IndexeddbStateStore::builder().name(name).build().await?;
1619
1620 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}