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