@@ -46,6 +46,7 @@ use ruma::{
4646 CanonicalJsonObject , EventId , MxcUri , OwnedEventId , OwnedUserId , RoomId , RoomVersionId , UserId ,
4747} ;
4848use serde:: { de:: DeserializeOwned , Deserialize , Serialize } ;
49+ use serde_json:: value:: { RawValue as RawJsonValue , Value as JsonValue } ;
4950use tracing:: { debug, warn} ;
5051use wasm_bindgen:: JsValue ;
5152use web_sys:: IdbKeyRange ;
@@ -109,7 +110,7 @@ impl From<IndexeddbStateStoreError> for StoreError {
109110mod KEYS {
110111 // STORES
111112
112- pub const CURRENT_DB_VERSION : f64 = 1.1 ;
113+ pub const CURRENT_DB_VERSION : f64 = 1.2 ;
113114 pub const CURRENT_META_DB_VERSION : f64 = 2.0 ;
114115
115116 pub const INTERNAL_STATE : & str = "matrix-sdk-state" ;
@@ -237,6 +238,74 @@ async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> {
237238 Ok ( ( ) )
238239}
239240
241+ fn serialize_event ( store_cipher : Option < & StoreCipher > , event : & impl Serialize ) -> Result < JsValue > {
242+ Ok ( match store_cipher {
243+ Some ( cipher) => JsValue :: from_serde ( & cipher. encrypt_value_typed ( event) ?) ?,
244+ None => JsValue :: from_serde ( event) ?,
245+ } )
246+ }
247+
248+ fn deserialize_event < T : DeserializeOwned > (
249+ store_cipher : Option < & StoreCipher > ,
250+ event : JsValue ,
251+ ) -> Result < T > {
252+ match store_cipher {
253+ Some ( cipher) => Ok ( cipher. decrypt_value_typed ( event. into_serde ( ) ?) ?) ,
254+ None => Ok ( event. into_serde ( ) ?) ,
255+ }
256+ }
257+
258+ async fn v1_2_fix_store (
259+ store : & IdbObjectStore < ' _ > ,
260+ store_cipher : Option < & StoreCipher > ,
261+ ) -> Result < ( ) > {
262+ fn maybe_fix_json ( raw_json : & RawJsonValue ) -> Result < Option < JsonValue > > {
263+ let json = raw_json. get ( ) ;
264+
265+ if json. contains ( r#""content":null"# ) {
266+ let mut value: JsonValue = serde_json:: from_str ( json) ?;
267+ if let Some ( content) = value. get_mut ( "content" ) {
268+ if matches ! ( content, JsonValue :: Null ) {
269+ * content = JsonValue :: Object ( Default :: default ( ) ) ;
270+ return Ok ( Some ( value) ) ;
271+ }
272+ }
273+ }
274+
275+ Ok ( None )
276+ }
277+
278+ let cursor = store. open_cursor ( ) ?. await ?;
279+
280+ if let Some ( cursor) = cursor {
281+ loop {
282+ let raw_json: Box < RawJsonValue > = deserialize_event ( store_cipher, cursor. value ( ) ) ?;
283+
284+ if let Some ( fixed_json) = maybe_fix_json ( & raw_json) ? {
285+ cursor. update ( & serialize_event ( store_cipher, & fixed_json) ?) ?. await ?;
286+ }
287+
288+ if !cursor. continue_cursor ( ) ?. await ? {
289+ break ;
290+ }
291+ }
292+ }
293+
294+ Ok ( ( ) )
295+ }
296+
297+ async fn migrate_to_v1_2 ( db : & IdbDatabase , store_cipher : Option < & StoreCipher > ) -> Result < ( ) > {
298+ let tx = db. transaction_on_multi_with_mode (
299+ & [ KEYS :: ROOM_STATE , KEYS :: ROOM_INFOS ] ,
300+ IdbTransactionMode :: Readwrite ,
301+ ) ?;
302+
303+ v1_2_fix_store ( & tx. object_store ( KEYS :: ROOM_STATE ) ?, store_cipher) . await ?;
304+ v1_2_fix_store ( & tx. object_store ( KEYS :: ROOM_INFOS ) ?, store_cipher) . await ?;
305+
306+ tx. await . into_result ( ) . map_err ( |e| e. into ( ) )
307+ }
308+
240309#[ derive( Builder , Debug , PartialEq , Eq ) ]
241310#[ builder( name = "IndexeddbStateStoreBuilder" , build_fn( skip) ) ]
242311pub struct IndexeddbStateStoreBuilderConfig {
@@ -311,7 +380,8 @@ impl IndexeddbStateStoreBuilder {
311380 None
312381 } ;
313382
314- let recreate_stores = {
383+ let mut recreate_stores = false ;
384+ {
315385 // checkup up in a separate call, whether we have to backup or do anything else
316386 // to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn
317387 // which we need to execute the backup.
@@ -337,15 +407,16 @@ impl IndexeddbStateStoreBuilder {
337407 let old_version = pre_db. version ( ) ;
338408
339409 if created. load ( Ordering :: Relaxed ) {
340- // this is a fresh DB, return
341- false
410+ // this is a fresh DB, nothing to do
342411 } else if old_version == 1.0 && has_store_cipher {
343412 match migration_strategy {
344413 MigrationConflictStrategy :: BackupAndDrop => {
345414 backup ( & pre_db, & meta_db) . await ?;
346- true
415+ recreate_stores = true ;
416+ }
417+ MigrationConflictStrategy :: Drop => {
418+ recreate_stores = true ;
347419 }
348- MigrationConflictStrategy :: Drop => true ,
349420 MigrationConflictStrategy :: Raise => {
350421 return Err ( IndexeddbStateStoreError :: MigrationConflict {
351422 name,
@@ -354,11 +425,12 @@ impl IndexeddbStateStoreBuilder {
354425 } )
355426 }
356427 }
428+ } else if old_version < 1.2 {
429+ migrate_to_v1_2 ( & pre_db, store_cipher. as_deref ( ) ) . await ?;
357430 } else {
358431 // Nothing to be done
359- false
360432 }
361- } ;
433+ }
362434
363435 let mut db_req: OpenDbRequest = IdbDatabase :: open_f64 ( & name, KEYS :: CURRENT_DB_VERSION ) ?;
364436 db_req. set_on_upgrade_needed ( Some (
@@ -421,17 +493,11 @@ impl IndexeddbStateStore {
421493 }
422494
423495 fn serialize_event ( & self , event : & impl Serialize ) -> Result < JsValue > {
424- Ok ( match & self . store_cipher {
425- Some ( cipher) => JsValue :: from_serde ( & cipher. encrypt_value_typed ( event) ?) ?,
426- None => JsValue :: from_serde ( event) ?,
427- } )
496+ serialize_event ( self . store_cipher . as_deref ( ) , event)
428497 }
429498
430499 fn deserialize_event < T : DeserializeOwned > ( & self , event : JsValue ) -> Result < T > {
431- match & self . store_cipher {
432- Some ( cipher) => Ok ( cipher. decrypt_value_typed ( event. into_serde ( ) ?) ?) ,
433- None => Ok ( event. into_serde ( ) ?) ,
434- }
500+ deserialize_event ( self . store_cipher . as_deref ( ) , event)
435501 }
436502
437503 fn encode_key < T > ( & self , table_name : & str , key : T ) -> JsValue
@@ -1451,15 +1517,21 @@ mod migration_tests {
14511517
14521518 use indexed_db_futures:: prelude:: * ;
14531519 use matrix_sdk_test:: async_test;
1520+ use ruma:: {
1521+ events:: { AnySyncStateEvent , StateEventType } ,
1522+ room_id,
1523+ } ;
1524+ use serde_json:: json;
14541525 use uuid:: Uuid ;
14551526 use wasm_bindgen:: JsValue ;
14561527
14571528 use super :: {
1458- IndexeddbStateStore , IndexeddbStateStoreError , MigrationConflictStrategy , Result ,
1459- ALL_STORES ,
1529+ serialize_event , IndexeddbStateStore , IndexeddbStateStoreError , MigrationConflictStrategy ,
1530+ Result , ALL_STORES , KEYS ,
14601531 } ;
1532+ use crate :: safe_encode:: SafeEncode ;
14611533
1462- pub async fn create_fake_db ( name : & str , version : f64 ) -> Result < ( ) > {
1534+ pub async fn create_fake_db ( name : & str , version : f64 ) -> Result < IdbDatabase > {
14631535 let mut db_req: OpenDbRequest = IdbDatabase :: open_f64 ( name, version) ?;
14641536 db_req. set_on_upgrade_needed ( Some (
14651537 move |evt : & IdbVersionChangeEvent | -> Result < ( ) , JsValue > {
@@ -1471,8 +1543,7 @@ mod migration_tests {
14711543 Ok ( ( ) )
14721544 } ,
14731545 ) ) ;
1474- db_req. into_future ( ) . await ?;
1475- Ok ( ( ) )
1546+ db_req. into_future ( ) . await . map_err ( Into :: into)
14761547 }
14771548
14781549 #[ async_test]
@@ -1565,4 +1636,52 @@ mod migration_tests {
15651636 }
15661637 Ok ( ( ) )
15671638 }
1639+
1640+ #[ async_test]
1641+ pub async fn test_migrating_to_v1_2 ( ) -> Result < ( ) > {
1642+ let name = format ! ( "migrating-1.2-{}" , Uuid :: new_v4( ) . as_hyphenated( ) . to_string( ) ) ;
1643+ // An event that fails to deserialize.
1644+ let wrong_redacted_state_event = json ! ( {
1645+ "content" : null,
1646+ "event_id" : "$wrongevent" ,
1647+ "origin_server_ts" : 1673887516047_u64 ,
1648+ "sender" : "@example:localhost" ,
1649+ "state_key" : "" ,
1650+ "type" : "m.room.topic" ,
1651+ "unsigned" : {
1652+ "redacted_because" : {
1653+ "type" : "m.room.redaction" ,
1654+ "sender" : "@example:localhost" ,
1655+ "content" : { } ,
1656+ "redacts" : "$wrongevent" ,
1657+ "origin_server_ts" : 1673893816047_u64 ,
1658+ "unsigned" : { } ,
1659+ "event_id" : "$redactionevent" ,
1660+ } ,
1661+ } ,
1662+ } ) ;
1663+ serde_json:: from_value :: < AnySyncStateEvent > ( wrong_redacted_state_event. clone ( ) )
1664+ . unwrap_err ( ) ;
1665+
1666+ let room_id = room_id ! ( "!some_room:localhost" ) ;
1667+
1668+ // Populate DB with wrong event.
1669+ {
1670+ let db = create_fake_db ( & name, 1.1 ) . await ?;
1671+ let tx =
1672+ db. transaction_on_one_with_mode ( KEYS :: ROOM_STATE , IdbTransactionMode :: Readwrite ) ?;
1673+ let state = tx. object_store ( KEYS :: ROOM_STATE ) ?;
1674+ let key = ( room_id, StateEventType :: RoomTopic , "" ) . encode ( ) ;
1675+ state. put_key_val ( & key, & serialize_event ( None , & wrong_redacted_state_event) ?) ?;
1676+ tx. await . into_result ( ) ?;
1677+ }
1678+
1679+ // this transparently migrates to the latest version
1680+ let store = IndexeddbStateStore :: builder ( ) . name ( name) . build ( ) . await ?;
1681+ let event =
1682+ store. get_state_event ( room_id, StateEventType :: RoomTopic , "" ) . await . unwrap ( ) . unwrap ( ) ;
1683+ event. deserialize ( ) . unwrap ( ) ;
1684+
1685+ Ok ( ( ) )
1686+ }
15681687}
0 commit comments