@@ -46,6 +46,7 @@ use ruma::{
46
46
CanonicalJsonObject , EventId , MxcUri , OwnedEventId , OwnedUserId , RoomId , RoomVersionId , UserId ,
47
47
} ;
48
48
use serde:: { de:: DeserializeOwned , Deserialize , Serialize } ;
49
+ use serde_json:: value:: { RawValue as RawJsonValue , Value as JsonValue } ;
49
50
use tracing:: { debug, warn} ;
50
51
use wasm_bindgen:: JsValue ;
51
52
use web_sys:: IdbKeyRange ;
@@ -109,7 +110,7 @@ impl From<IndexeddbStateStoreError> for StoreError {
109
110
mod KEYS {
110
111
// STORES
111
112
112
- pub const CURRENT_DB_VERSION : f64 = 1.1 ;
113
+ pub const CURRENT_DB_VERSION : f64 = 1.2 ;
113
114
pub const CURRENT_META_DB_VERSION : f64 = 2.0 ;
114
115
115
116
pub const INTERNAL_STATE : & str = "matrix-sdk-state" ;
@@ -237,6 +238,74 @@ async fn backup(source: &IdbDatabase, meta: &IdbDatabase) -> Result<()> {
237
238
Ok ( ( ) )
238
239
}
239
240
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
+
240
309
#[ derive( Builder , Debug , PartialEq , Eq ) ]
241
310
#[ builder( name = "IndexeddbStateStoreBuilder" , build_fn( skip) ) ]
242
311
pub struct IndexeddbStateStoreBuilderConfig {
@@ -311,7 +380,8 @@ impl IndexeddbStateStoreBuilder {
311
380
None
312
381
} ;
313
382
314
- let recreate_stores = {
383
+ let mut recreate_stores = false ;
384
+ {
315
385
// checkup up in a separate call, whether we have to backup or do anything else
316
386
// to the db. Unfortunately the set_on_upgrade_needed doesn't allow async fn
317
387
// which we need to execute the backup.
@@ -337,15 +407,16 @@ impl IndexeddbStateStoreBuilder {
337
407
let old_version = pre_db. version ( ) ;
338
408
339
409
if created. load ( Ordering :: Relaxed ) {
340
- // this is a fresh DB, return
341
- false
410
+ // this is a fresh DB, nothing to do
342
411
} else if old_version == 1.0 && has_store_cipher {
343
412
match migration_strategy {
344
413
MigrationConflictStrategy :: BackupAndDrop => {
345
414
backup ( & pre_db, & meta_db) . await ?;
346
- true
415
+ recreate_stores = true ;
416
+ }
417
+ MigrationConflictStrategy :: Drop => {
418
+ recreate_stores = true ;
347
419
}
348
- MigrationConflictStrategy :: Drop => true ,
349
420
MigrationConflictStrategy :: Raise => {
350
421
return Err ( IndexeddbStateStoreError :: MigrationConflict {
351
422
name,
@@ -354,11 +425,12 @@ impl IndexeddbStateStoreBuilder {
354
425
} )
355
426
}
356
427
}
428
+ } else if old_version < 1.2 {
429
+ migrate_to_v1_2 ( & pre_db, store_cipher. as_deref ( ) ) . await ?;
357
430
} else {
358
431
// Nothing to be done
359
- false
360
432
}
361
- } ;
433
+ }
362
434
363
435
let mut db_req: OpenDbRequest = IdbDatabase :: open_f64 ( & name, KEYS :: CURRENT_DB_VERSION ) ?;
364
436
db_req. set_on_upgrade_needed ( Some (
@@ -421,17 +493,11 @@ impl IndexeddbStateStore {
421
493
}
422
494
423
495
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)
428
497
}
429
498
430
499
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)
435
501
}
436
502
437
503
fn encode_key < T > ( & self , table_name : & str , key : T ) -> JsValue
@@ -1451,15 +1517,21 @@ mod migration_tests {
1451
1517
1452
1518
use indexed_db_futures:: prelude:: * ;
1453
1519
use matrix_sdk_test:: async_test;
1520
+ use ruma:: {
1521
+ events:: { AnySyncStateEvent , StateEventType } ,
1522
+ room_id,
1523
+ } ;
1524
+ use serde_json:: json;
1454
1525
use uuid:: Uuid ;
1455
1526
use wasm_bindgen:: JsValue ;
1456
1527
1457
1528
use super :: {
1458
- IndexeddbStateStore , IndexeddbStateStoreError , MigrationConflictStrategy , Result ,
1459
- ALL_STORES ,
1529
+ serialize_event , IndexeddbStateStore , IndexeddbStateStoreError , MigrationConflictStrategy ,
1530
+ Result , ALL_STORES , KEYS ,
1460
1531
} ;
1532
+ use crate :: safe_encode:: SafeEncode ;
1461
1533
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 > {
1463
1535
let mut db_req: OpenDbRequest = IdbDatabase :: open_f64 ( name, version) ?;
1464
1536
db_req. set_on_upgrade_needed ( Some (
1465
1537
move |evt : & IdbVersionChangeEvent | -> Result < ( ) , JsValue > {
@@ -1471,8 +1543,7 @@ mod migration_tests {
1471
1543
Ok ( ( ) )
1472
1544
} ,
1473
1545
) ) ;
1474
- db_req. into_future ( ) . await ?;
1475
- Ok ( ( ) )
1546
+ db_req. into_future ( ) . await . map_err ( Into :: into)
1476
1547
}
1477
1548
1478
1549
#[ async_test]
@@ -1565,4 +1636,52 @@ mod migration_tests {
1565
1636
}
1566
1637
Ok ( ( ) )
1567
1638
}
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
+ }
1568
1687
}
0 commit comments