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