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