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,
805 store::{RoomLoadSettings, StateStoreExt},
806 sync::UnreadNotificationsCount,
807 RoomMemberships, RoomState, StateStore, StateStoreDataKey, StoreError,
808 };
809 use matrix_sdk_test::{async_test, test_json};
810 use ruma::{
811 events::{
812 room::{
813 create::RoomCreateEventContent,
814 member::{StrippedRoomMemberEvent, SyncRoomMemberEvent},
815 },
816 AnySyncStateEvent, StateEventType,
817 },
818 room_id,
819 serde::Raw,
820 server_name, user_id, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId,
821 };
822 use serde_json::json;
823 use uuid::Uuid;
824 use wasm_bindgen::JsValue;
825
826 use super::{old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION};
827 use crate::{
828 safe_encode::SafeEncode,
829 state_store::{encode_key, keys, serialize_value, Result},
830 IndexeddbStateStore, IndexeddbStateStoreError,
831 };
832
833 const CUSTOM_DATA_KEY: &[u8] = b"custom_data_key";
834 const CUSTOM_DATA: &[u8] = b"some_custom_data";
835
836 pub async fn create_fake_db(name: &str, version: u32) -> Result<IdbDatabase> {
837 let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?;
838 db_req.set_on_upgrade_needed(Some(
839 move |evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
840 let db = evt.db();
841
842 let common_stores = &[
844 keys::ACCOUNT_DATA,
845 keys::PROFILES,
846 keys::DISPLAY_NAMES,
847 keys::ROOM_STATE,
848 keys::ROOM_INFOS,
849 keys::PRESENCE,
850 keys::ROOM_ACCOUNT_DATA,
851 keys::STRIPPED_ROOM_STATE,
852 keys::ROOM_USER_RECEIPTS,
853 keys::ROOM_EVENT_RECEIPTS,
854 keys::CUSTOM,
855 ];
856
857 for name in common_stores {
858 db.create_object_store(name)?;
859 }
860
861 if version < 4 {
862 for name in [old_keys::SYNC_TOKEN, old_keys::SESSION] {
863 db.create_object_store(name)?;
864 }
865 }
866 if version >= 4 {
867 db.create_object_store(keys::KV)?;
868 }
869 if version < 5 {
870 for name in [old_keys::MEMBERS, old_keys::STRIPPED_MEMBERS] {
871 db.create_object_store(name)?;
872 }
873 }
874 if version < 6 {
875 for name in [
876 old_keys::INVITED_USER_IDS,
877 old_keys::JOINED_USER_IDS,
878 old_keys::STRIPPED_INVITED_USER_IDS,
879 old_keys::STRIPPED_JOINED_USER_IDS,
880 ] {
881 db.create_object_store(name)?;
882 }
883 }
884 if version >= 6 {
885 for name in [keys::USER_IDS, keys::STRIPPED_USER_IDS] {
886 db.create_object_store(name)?;
887 }
888 }
889 if version < 7 {
890 db.create_object_store(old_keys::STRIPPED_ROOM_INFOS)?;
891 }
892 if version < 11 {
893 db.create_object_store(old_keys::MEDIA)?;
894 }
895
896 Ok(())
897 },
898 ));
899 db_req.await.map_err(Into::into)
900 }
901
902 fn room_info_v1_json(
903 room_id: &RoomId,
904 state: RoomState,
905 name: Option<&str>,
906 creator: Option<&UserId>,
907 ) -> serde_json::Value {
908 let name_content = match name {
910 Some(name) => json!({ "name": name }),
911 None => json!({ "name": null }),
912 };
913 let create_content = match creator {
915 Some(creator) => RoomCreateEventContent::new_v1(creator.to_owned()),
916 None => RoomCreateEventContent::new_v11(),
917 };
918
919 json!({
920 "room_id": room_id,
921 "room_type": state,
922 "notification_counts": UnreadNotificationsCount::default(),
923 "summary": {
924 "heroes": [],
925 "joined_member_count": 0,
926 "invited_member_count": 0,
927 },
928 "members_synced": false,
929 "base_info": {
930 "dm_targets": [],
931 "max_power_level": 100,
932 "name": {
933 "Original": {
934 "content": name_content,
935 },
936 },
937 "create": {
938 "Original": {
939 "content": create_content,
940 }
941 }
942 },
943 })
944 }
945
946 #[async_test]
947 pub async fn test_new_store() -> Result<()> {
948 let name = format!("new-store-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
949
950 let store = IndexeddbStateStore::builder().name(name).build().await?;
952 assert_eq!(store.has_backups().await?, false);
954 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
956
957 assert_eq!(store.version(), CURRENT_DB_VERSION);
959 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
960
961 Ok(())
962 }
963
964 #[async_test]
965 pub async fn test_migrating_v1_to_v2_plain() -> Result<()> {
966 let name = format!("migrating-v2-no-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
967
968 {
970 let db = create_fake_db(&name, 1).await?;
971 let tx =
972 db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
973 let custom = tx.object_store(keys::CUSTOM)?;
974 let jskey = JsValue::from_str(
975 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
976 );
977 custom.put_key_val(&jskey, &serialize_value(None, &CUSTOM_DATA)?)?;
978 tx.await.into_result()?;
979 db.close();
980 }
981
982 let store = IndexeddbStateStore::builder().name(name).build().await?;
984 assert_eq!(store.has_backups().await?, false);
986 assert_let!(Some(stored_data) = store.get_custom_value(CUSTOM_DATA_KEY).await?);
988 assert_eq!(stored_data, CUSTOM_DATA);
989
990 assert_eq!(store.version(), CURRENT_DB_VERSION);
992 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
993
994 Ok(())
995 }
996
997 #[async_test]
998 pub async fn test_migrating_v1_to_v2_with_pw() -> Result<()> {
999 let name =
1000 format!("migrating-v2-with-cipher-{}", Uuid::new_v4().as_hyphenated().to_string());
1001 let passphrase = "somepassphrase".to_owned();
1002
1003 {
1005 let db = create_fake_db(&name, 1).await?;
1006 let tx =
1007 db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
1008 let custom = tx.object_store(keys::CUSTOM)?;
1009 let jskey = JsValue::from_str(
1010 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1011 );
1012 custom.put_key_val(&jskey, &serialize_value(None, &CUSTOM_DATA)?)?;
1013 tx.await.into_result()?;
1014 db.close();
1015 }
1016
1017 let store =
1019 IndexeddbStateStore::builder().name(name).passphrase(passphrase).build().await?;
1020 assert_eq!(store.has_backups().await?, true);
1022 assert!(store.latest_backup().await?.is_some(), "No backup_found");
1023 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1025
1026 assert_eq!(store.version(), CURRENT_DB_VERSION);
1028 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1029
1030 Ok(())
1031 }
1032
1033 #[async_test]
1034 pub async fn test_migrating_v1_to_v2_with_pw_drops() -> Result<()> {
1035 let name = format!(
1036 "migrating-v2-with-cipher-drops-{}",
1037 Uuid::new_v4().as_hyphenated().to_string()
1038 );
1039 let passphrase = "some-other-passphrase".to_owned();
1040
1041 {
1043 let db = create_fake_db(&name, 1).await?;
1044 let tx =
1045 db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
1046 let custom = tx.object_store(keys::CUSTOM)?;
1047 let jskey = JsValue::from_str(
1048 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1049 );
1050 custom.put_key_val(&jskey, &serialize_value(None, &CUSTOM_DATA)?)?;
1051 tx.await.into_result()?;
1052 db.close();
1053 }
1054
1055 let store = IndexeddbStateStore::builder()
1057 .name(name)
1058 .passphrase(passphrase)
1059 .migration_conflict_strategy(MigrationConflictStrategy::Drop)
1060 .build()
1061 .await?;
1062 assert_eq!(store.has_backups().await?, false);
1064 assert_eq!(store.get_custom_value(CUSTOM_DATA_KEY).await?, None);
1066
1067 assert_eq!(store.version(), CURRENT_DB_VERSION);
1069 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1070
1071 Ok(())
1072 }
1073
1074 #[async_test]
1075 pub async fn test_migrating_v1_to_v2_with_pw_raise() -> Result<()> {
1076 let name = format!(
1077 "migrating-v2-with-cipher-raises-{}",
1078 Uuid::new_v4().as_hyphenated().to_string()
1079 );
1080 let passphrase = "some-other-passphrase".to_owned();
1081
1082 {
1084 let db = create_fake_db(&name, 1).await?;
1085 let tx =
1086 db.transaction_on_one_with_mode(keys::CUSTOM, IdbTransactionMode::Readwrite)?;
1087 let custom = tx.object_store(keys::CUSTOM)?;
1088 let jskey = JsValue::from_str(
1089 core::str::from_utf8(CUSTOM_DATA_KEY).map_err(StoreError::Codec)?,
1090 );
1091 custom.put_key_val(&jskey, &serialize_value(None, &CUSTOM_DATA)?)?;
1092 tx.await.into_result()?;
1093 db.close();
1094 }
1095
1096 let store_res = IndexeddbStateStore::builder()
1098 .name(name)
1099 .passphrase(passphrase)
1100 .migration_conflict_strategy(MigrationConflictStrategy::Raise)
1101 .build()
1102 .await;
1103
1104 assert_matches!(store_res, Err(IndexeddbStateStoreError::MigrationConflict { .. }));
1105
1106 Ok(())
1107 }
1108
1109 #[async_test]
1110 pub async fn test_migrating_to_v3() -> Result<()> {
1111 let name = format!("migrating-v3-{}", Uuid::new_v4().as_hyphenated().to_string());
1112
1113 let wrong_redacted_state_event = json!({
1115 "content": null,
1116 "event_id": "$wrongevent",
1117 "origin_server_ts": 1673887516047_u64,
1118 "sender": "@example:localhost",
1119 "state_key": "",
1120 "type": "m.room.topic",
1121 "unsigned": {
1122 "redacted_because": {
1123 "type": "m.room.redaction",
1124 "sender": "@example:localhost",
1125 "content": {},
1126 "redacts": "$wrongevent",
1127 "origin_server_ts": 1673893816047_u64,
1128 "unsigned": {},
1129 "event_id": "$redactionevent",
1130 },
1131 },
1132 });
1133 serde_json::from_value::<AnySyncStateEvent>(wrong_redacted_state_event.clone())
1134 .unwrap_err();
1135
1136 let room_id = room_id!("!some_room:localhost");
1137
1138 {
1140 let db = create_fake_db(&name, 2).await?;
1141 let tx =
1142 db.transaction_on_one_with_mode(keys::ROOM_STATE, IdbTransactionMode::Readwrite)?;
1143 let state = tx.object_store(keys::ROOM_STATE)?;
1144 let key: JsValue = (room_id, StateEventType::RoomTopic, "").as_encoded_string().into();
1145 state.put_key_val(&key, &serialize_value(None, &wrong_redacted_state_event)?)?;
1146 tx.await.into_result()?;
1147 db.close();
1148 }
1149
1150 let store = IndexeddbStateStore::builder().name(name).build().await?;
1152 let event =
1153 store.get_state_event(room_id, StateEventType::RoomTopic, "").await.unwrap().unwrap();
1154 event.deserialize().unwrap();
1155
1156 assert_eq!(store.version(), CURRENT_DB_VERSION);
1158 assert_eq!(store.meta_version(), CURRENT_META_DB_VERSION);
1159
1160 Ok(())
1161 }
1162
1163 #[async_test]
1164 pub async fn test_migrating_to_v4() -> Result<()> {
1165 let name = format!("migrating-v4-{}", Uuid::new_v4().as_hyphenated().to_string());
1166
1167 let sync_token = "a_very_unique_string";
1168 let filter_1 = "filter_1";
1169 let filter_1_id = "filter_1_id";
1170 let filter_2 = "filter_2";
1171 let filter_2_id = "filter_2_id";
1172
1173 {
1175 let db = create_fake_db(&name, 3).await?;
1176 let tx = db.transaction_on_multi_with_mode(
1177 &[old_keys::SYNC_TOKEN, old_keys::SESSION],
1178 IdbTransactionMode::Readwrite,
1179 )?;
1180
1181 let sync_token_store = tx.object_store(old_keys::SYNC_TOKEN)?;
1182 sync_token_store.put_key_val(
1183 &JsValue::from_str(old_keys::SYNC_TOKEN),
1184 &serialize_value(None, &sync_token)?,
1185 )?;
1186
1187 let session_store = tx.object_store(old_keys::SESSION)?;
1188 session_store.put_key_val(
1189 &encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_1)),
1190 &serialize_value(None, &filter_1_id)?,
1191 )?;
1192 session_store.put_key_val(
1193 &encode_key(None, StateStoreDataKey::FILTER, (StateStoreDataKey::FILTER, filter_2)),
1194 &serialize_value(None, &filter_2_id)?,
1195 )?;
1196
1197 tx.await.into_result()?;
1198 db.close();
1199 }
1200
1201 let store = IndexeddbStateStore::builder().name(name).build().await?;
1203
1204 let stored_sync_token = store
1205 .get_kv_data(StateStoreDataKey::SyncToken)
1206 .await?
1207 .unwrap()
1208 .into_sync_token()
1209 .unwrap();
1210 assert_eq!(stored_sync_token, sync_token);
1211
1212 let stored_filter_1_id = store
1213 .get_kv_data(StateStoreDataKey::Filter(filter_1))
1214 .await?
1215 .unwrap()
1216 .into_filter()
1217 .unwrap();
1218 assert_eq!(stored_filter_1_id, filter_1_id);
1219
1220 let stored_filter_2_id = store
1221 .get_kv_data(StateStoreDataKey::Filter(filter_2))
1222 .await?
1223 .unwrap()
1224 .into_filter()
1225 .unwrap();
1226 assert_eq!(stored_filter_2_id, filter_2_id);
1227
1228 Ok(())
1229 }
1230
1231 #[async_test]
1232 pub async fn test_migrating_to_v5() -> Result<()> {
1233 let name = format!("migrating-v5-{}", Uuid::new_v4().as_hyphenated().to_string());
1234
1235 let room_id = room_id!("!room:localhost");
1236 let member_event =
1237 Raw::new(&*test_json::MEMBER_INVITE).unwrap().cast::<SyncRoomMemberEvent>();
1238 let user_id = user_id!("@invited:localhost");
1239
1240 let stripped_room_id = room_id!("!stripped_room:localhost");
1241 let stripped_member_event =
1242 Raw::new(&*test_json::MEMBER_STRIPPED).unwrap().cast::<StrippedRoomMemberEvent>();
1243 let stripped_user_id = user_id!("@example:localhost");
1244
1245 {
1247 let db = create_fake_db(&name, 4).await?;
1248 let tx = db.transaction_on_multi_with_mode(
1249 &[
1250 old_keys::MEMBERS,
1251 keys::ROOM_INFOS,
1252 old_keys::STRIPPED_MEMBERS,
1253 old_keys::STRIPPED_ROOM_INFOS,
1254 ],
1255 IdbTransactionMode::Readwrite,
1256 )?;
1257
1258 let members_store = tx.object_store(old_keys::MEMBERS)?;
1259 members_store.put_key_val(
1260 &encode_key(None, old_keys::MEMBERS, (room_id, user_id)),
1261 &serialize_value(None, &member_event)?,
1262 )?;
1263 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1264 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1265 room_infos_store.put_key_val(
1266 &encode_key(None, keys::ROOM_INFOS, room_id),
1267 &serialize_value(None, &room_info)?,
1268 )?;
1269
1270 let stripped_members_store = tx.object_store(old_keys::STRIPPED_MEMBERS)?;
1271 stripped_members_store.put_key_val(
1272 &encode_key(None, old_keys::STRIPPED_MEMBERS, (stripped_room_id, stripped_user_id)),
1273 &serialize_value(None, &stripped_member_event)?,
1274 )?;
1275 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1276 let stripped_room_info =
1277 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1278 stripped_room_infos_store.put_key_val(
1279 &encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id),
1280 &serialize_value(None, &stripped_room_info)?,
1281 )?;
1282
1283 tx.await.into_result()?;
1284 db.close();
1285 }
1286
1287 let store = IndexeddbStateStore::builder().name(name).build().await?;
1289
1290 assert_let!(
1291 Ok(Some(RawMemberEvent::Sync(stored_member_event))) =
1292 store.get_member_event(room_id, user_id).await
1293 );
1294 assert_eq!(stored_member_event.json().get(), member_event.json().get());
1295
1296 assert_let!(
1297 Ok(Some(RawMemberEvent::Stripped(stored_stripped_member_event))) =
1298 store.get_member_event(stripped_room_id, stripped_user_id).await
1299 );
1300 assert_eq!(stored_stripped_member_event.json().get(), stripped_member_event.json().get());
1301
1302 Ok(())
1303 }
1304
1305 #[async_test]
1306 pub async fn test_migrating_to_v6() -> Result<()> {
1307 let name = format!("migrating-v6-{}", Uuid::new_v4().as_hyphenated().to_string());
1308
1309 let room_id = room_id!("!room:localhost");
1310 let invite_member_event =
1311 Raw::new(&*test_json::MEMBER_INVITE).unwrap().cast::<SyncRoomMemberEvent>();
1312 let invite_user_id = user_id!("@invited:localhost");
1313 let ban_member_event =
1314 Raw::new(&*test_json::MEMBER_BAN).unwrap().cast::<SyncRoomMemberEvent>();
1315 let ban_user_id = user_id!("@banned:localhost");
1316
1317 let stripped_room_id = room_id!("!stripped_room:localhost");
1318 let stripped_member_event =
1319 Raw::new(&*test_json::MEMBER_STRIPPED).unwrap().cast::<StrippedRoomMemberEvent>();
1320 let stripped_user_id = user_id!("@example:localhost");
1321
1322 {
1324 let db = create_fake_db(&name, 5).await?;
1325 let tx = db.transaction_on_multi_with_mode(
1326 &[
1327 keys::ROOM_STATE,
1328 keys::ROOM_INFOS,
1329 keys::STRIPPED_ROOM_STATE,
1330 old_keys::STRIPPED_ROOM_INFOS,
1331 old_keys::INVITED_USER_IDS,
1332 old_keys::JOINED_USER_IDS,
1333 old_keys::STRIPPED_INVITED_USER_IDS,
1334 old_keys::STRIPPED_JOINED_USER_IDS,
1335 ],
1336 IdbTransactionMode::Readwrite,
1337 )?;
1338
1339 let state_store = tx.object_store(keys::ROOM_STATE)?;
1340 state_store.put_key_val(
1341 &encode_key(
1342 None,
1343 keys::ROOM_STATE,
1344 (room_id, StateEventType::RoomMember, invite_user_id),
1345 ),
1346 &serialize_value(None, &invite_member_event)?,
1347 )?;
1348 state_store.put_key_val(
1349 &encode_key(
1350 None,
1351 keys::ROOM_STATE,
1352 (room_id, StateEventType::RoomMember, ban_user_id),
1353 ),
1354 &serialize_value(None, &ban_member_event)?,
1355 )?;
1356 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1357 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1358 room_infos_store.put_key_val(
1359 &encode_key(None, keys::ROOM_INFOS, room_id),
1360 &serialize_value(None, &room_info)?,
1361 )?;
1362
1363 let stripped_state_store = tx.object_store(keys::STRIPPED_ROOM_STATE)?;
1364 stripped_state_store.put_key_val(
1365 &encode_key(
1366 None,
1367 keys::STRIPPED_ROOM_STATE,
1368 (stripped_room_id, StateEventType::RoomMember, stripped_user_id),
1369 ),
1370 &serialize_value(None, &stripped_member_event)?,
1371 )?;
1372 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1373 let stripped_room_info =
1374 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1375 stripped_room_infos_store.put_key_val(
1376 &encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id),
1377 &serialize_value(None, &stripped_room_info)?,
1378 )?;
1379
1380 let joined_user_id = user_id!("@joined_user:localhost");
1382 tx.object_store(old_keys::JOINED_USER_IDS)?.put_key_val(
1383 &encode_key(None, old_keys::JOINED_USER_IDS, (room_id, joined_user_id)),
1384 &serialize_value(None, &joined_user_id)?,
1385 )?;
1386 let invited_user_id = user_id!("@invited_user:localhost");
1387 tx.object_store(old_keys::INVITED_USER_IDS)?.put_key_val(
1388 &encode_key(None, old_keys::INVITED_USER_IDS, (room_id, invited_user_id)),
1389 &serialize_value(None, &invited_user_id)?,
1390 )?;
1391 let stripped_joined_user_id = user_id!("@stripped_joined_user:localhost");
1392 tx.object_store(old_keys::STRIPPED_JOINED_USER_IDS)?.put_key_val(
1393 &encode_key(
1394 None,
1395 old_keys::STRIPPED_JOINED_USER_IDS,
1396 (room_id, stripped_joined_user_id),
1397 ),
1398 &serialize_value(None, &stripped_joined_user_id)?,
1399 )?;
1400 let stripped_invited_user_id = user_id!("@stripped_invited_user:localhost");
1401 tx.object_store(old_keys::STRIPPED_INVITED_USER_IDS)?.put_key_val(
1402 &encode_key(
1403 None,
1404 old_keys::STRIPPED_INVITED_USER_IDS,
1405 (room_id, stripped_invited_user_id),
1406 ),
1407 &serialize_value(None, &stripped_invited_user_id)?,
1408 )?;
1409
1410 tx.await.into_result()?;
1411 db.close();
1412 }
1413
1414 let store = IndexeddbStateStore::builder().name(name).build().await?;
1416
1417 assert_eq!(store.get_user_ids(room_id, RoomMemberships::JOIN).await.unwrap().len(), 0);
1418 assert_eq!(
1419 store.get_user_ids(room_id, RoomMemberships::INVITE).await.unwrap().as_slice(),
1420 [invite_user_id.to_owned()]
1421 );
1422 let user_ids = store.get_user_ids(room_id, RoomMemberships::empty()).await.unwrap();
1423 assert_eq!(user_ids.len(), 2);
1424 assert!(user_ids.contains(&invite_user_id.to_owned()));
1425 assert!(user_ids.contains(&ban_user_id.to_owned()));
1426
1427 assert_eq!(
1428 store.get_user_ids(stripped_room_id, RoomMemberships::JOIN).await.unwrap().as_slice(),
1429 [stripped_user_id.to_owned()]
1430 );
1431 assert_eq!(
1432 store.get_user_ids(stripped_room_id, RoomMemberships::INVITE).await.unwrap().len(),
1433 0
1434 );
1435 assert_eq!(
1436 store
1437 .get_user_ids(stripped_room_id, RoomMemberships::empty())
1438 .await
1439 .unwrap()
1440 .as_slice(),
1441 [stripped_user_id.to_owned()]
1442 );
1443
1444 Ok(())
1445 }
1446
1447 #[async_test]
1448 pub async fn test_migrating_to_v7() -> Result<()> {
1449 let name = format!("migrating-v7-{}", Uuid::new_v4().as_hyphenated().to_string());
1450
1451 let room_id = room_id!("!room:localhost");
1452 let stripped_room_id = room_id!("!stripped_room:localhost");
1453
1454 {
1456 let db = create_fake_db(&name, 6).await?;
1457 let tx = db.transaction_on_multi_with_mode(
1458 &[keys::ROOM_INFOS, old_keys::STRIPPED_ROOM_INFOS],
1459 IdbTransactionMode::Readwrite,
1460 )?;
1461
1462 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1463 let room_info = room_info_v1_json(room_id, RoomState::Joined, None, None);
1464 room_infos_store.put_key_val(
1465 &encode_key(None, keys::ROOM_INFOS, room_id),
1466 &serialize_value(None, &room_info)?,
1467 )?;
1468
1469 let stripped_room_infos_store = tx.object_store(old_keys::STRIPPED_ROOM_INFOS)?;
1470 let stripped_room_info =
1471 room_info_v1_json(stripped_room_id, RoomState::Invited, None, None);
1472 stripped_room_infos_store.put_key_val(
1473 &encode_key(None, old_keys::STRIPPED_ROOM_INFOS, stripped_room_id),
1474 &serialize_value(None, &stripped_room_info)?,
1475 )?;
1476
1477 tx.await.into_result()?;
1478 db.close();
1479 }
1480
1481 let store = IndexeddbStateStore::builder().name(name).build().await?;
1483
1484 assert_eq!(store.get_room_infos(&RoomLoadSettings::default()).await.unwrap().len(), 2);
1485
1486 Ok(())
1487 }
1488
1489 fn add_room_v7(
1491 room_infos_store: &IdbObjectStore<'_>,
1492 room_state_store: &IdbObjectStore<'_>,
1493 room_id: &RoomId,
1494 name: Option<&str>,
1495 create_creator: Option<&UserId>,
1496 create_sender: Option<&UserId>,
1497 ) -> Result<()> {
1498 let room_info_json = room_info_v1_json(room_id, RoomState::Joined, name, create_creator);
1499
1500 room_infos_store.put_key_val(
1501 &encode_key(None, keys::ROOM_INFOS, room_id),
1502 &serialize_value(None, &room_info_json)?,
1503 )?;
1504
1505 let Some(create_sender) = create_sender else {
1507 return Ok(());
1508 };
1509
1510 let create_content = match create_creator {
1511 Some(creator) => RoomCreateEventContent::new_v1(creator.to_owned()),
1512 None => RoomCreateEventContent::new_v11(),
1513 };
1514
1515 let event_id = EventId::new(server_name!("dummy.local"));
1516 let create_event = json!({
1517 "content": create_content,
1518 "event_id": event_id,
1519 "sender": create_sender.to_owned(),
1520 "origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
1521 "state_key": "",
1522 "type": "m.room.create",
1523 "unsigned": {},
1524 });
1525
1526 room_state_store.put_key_val(
1527 &encode_key(None, keys::ROOM_STATE, (room_id, &StateEventType::RoomCreate, "")),
1528 &serialize_value(None, &create_event)?,
1529 )?;
1530
1531 Ok(())
1532 }
1533
1534 #[async_test]
1535 pub async fn test_migrating_to_v8() -> Result<()> {
1536 let name = format!("migrating-v8-{}", Uuid::new_v4().as_hyphenated().to_string());
1537
1538 let room_a_id = room_id!("!room_a:dummy.local");
1540 let room_a_name = "Room A";
1541 let room_a_creator = user_id!("@creator:dummy.local");
1542 let room_a_create_sender = user_id!("@sender:dummy.local");
1545
1546 let room_b_id = room_id!("!room_b:dummy.local");
1548
1549 let room_c_id = room_id!("!room_c:dummy.local");
1551 let room_c_create_sender = user_id!("@creator:dummy.local");
1552
1553 {
1555 let db = create_fake_db(&name, 6).await?;
1556 let tx = db.transaction_on_multi_with_mode(
1557 &[keys::ROOM_INFOS, keys::ROOM_STATE],
1558 IdbTransactionMode::Readwrite,
1559 )?;
1560
1561 let room_infos_store = tx.object_store(keys::ROOM_INFOS)?;
1562 let room_state_store = tx.object_store(keys::ROOM_STATE)?;
1563
1564 add_room_v7(
1565 &room_infos_store,
1566 &room_state_store,
1567 room_a_id,
1568 Some(room_a_name),
1569 Some(room_a_creator),
1570 Some(room_a_create_sender),
1571 )?;
1572 add_room_v7(&room_infos_store, &room_state_store, room_b_id, None, None, None)?;
1573 add_room_v7(
1574 &room_infos_store,
1575 &room_state_store,
1576 room_c_id,
1577 None,
1578 None,
1579 Some(room_c_create_sender),
1580 )?;
1581
1582 tx.await.into_result()?;
1583 db.close();
1584 }
1585
1586 let store = IndexeddbStateStore::builder().name(name).build().await?;
1588
1589 let room_infos = store.get_room_infos(&RoomLoadSettings::default()).await?;
1591 assert_eq!(room_infos.len(), 3);
1592
1593 let room_a = room_infos.iter().find(|r| r.room_id() == room_a_id).unwrap();
1594 assert_eq!(room_a.name(), Some(room_a_name));
1595 assert_eq!(room_a.creator(), Some(room_a_create_sender));
1596
1597 let room_b = room_infos.iter().find(|r| r.room_id() == room_b_id).unwrap();
1598 assert_eq!(room_b.name(), None);
1599 assert_eq!(room_b.creator(), None);
1600
1601 let room_c = room_infos.iter().find(|r| r.room_id() == room_c_id).unwrap();
1602 assert_eq!(room_c.name(), None);
1603 assert_eq!(room_c.creator(), Some(room_c_create_sender));
1604
1605 Ok(())
1606 }
1607}