ruma_common/
canonical_json.rs

1//! Canonical JSON types and related functions.
2
3use std::{fmt, mem};
4
5use serde::Serialize;
6use serde_json::Value as JsonValue;
7
8mod value;
9
10pub use self::value::{CanonicalJsonObject, CanonicalJsonValue};
11use crate::{serde::Raw, RoomVersionId};
12
13/// The set of possible errors when serializing to canonical JSON.
14#[cfg(feature = "canonical-json")]
15#[derive(Debug)]
16#[allow(clippy::exhaustive_enums)]
17pub enum CanonicalJsonError {
18    /// The numeric value failed conversion to js_int::Int.
19    IntConvert,
20
21    /// An error occurred while serializing/deserializing.
22    SerDe(serde_json::Error),
23}
24
25impl fmt::Display for CanonicalJsonError {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            CanonicalJsonError::IntConvert => {
29                f.write_str("number found is not a valid `js_int::Int`")
30            }
31            CanonicalJsonError::SerDe(err) => write!(f, "serde Error: {err}"),
32        }
33    }
34}
35
36impl std::error::Error for CanonicalJsonError {}
37
38/// Errors that can happen in redaction.
39#[cfg(feature = "canonical-json")]
40#[derive(Debug)]
41#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
42pub enum RedactionError {
43    /// The field `field` is not of the correct type `of_type` ([`JsonType`]).
44    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
45    NotOfType {
46        /// The field name.
47        field: String,
48        /// The expected JSON type.
49        of_type: JsonType,
50    },
51
52    /// The given required field is missing from a JSON object.
53    JsonFieldMissingFromObject(String),
54}
55
56impl fmt::Display for RedactionError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            RedactionError::NotOfType { field, of_type } => {
60                write!(f, "Value in {field:?} must be a JSON {of_type:?}")
61            }
62            RedactionError::JsonFieldMissingFromObject(field) => {
63                write!(f, "JSON object must contain the field {field:?}")
64            }
65        }
66    }
67}
68
69impl std::error::Error for RedactionError {}
70
71impl RedactionError {
72    fn not_of_type(target: &str, of_type: JsonType) -> Self {
73        Self::NotOfType { field: target.to_owned(), of_type }
74    }
75
76    fn field_missing_from_object(target: &str) -> Self {
77        Self::JsonFieldMissingFromObject(target.to_owned())
78    }
79}
80
81/// A JSON type enum for [`RedactionError`] variants.
82#[derive(Debug)]
83#[allow(clippy::exhaustive_enums)]
84pub enum JsonType {
85    /// A JSON Object.
86    Object,
87
88    /// A JSON String.
89    String,
90
91    /// A JSON Integer.
92    Integer,
93
94    /// A JSON Array.
95    Array,
96
97    /// A JSON Boolean.
98    Boolean,
99
100    /// JSON Null.
101    Null,
102}
103
104/// Fallible conversion from a `serde_json::Map` to a `CanonicalJsonObject`.
105pub fn try_from_json_map(
106    json: serde_json::Map<String, JsonValue>,
107) -> Result<CanonicalJsonObject, CanonicalJsonError> {
108    json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect()
109}
110
111/// Fallible conversion from any value that impl's `Serialize` to a `CanonicalJsonValue`.
112pub fn to_canonical_value<T: Serialize>(
113    value: T,
114) -> Result<CanonicalJsonValue, CanonicalJsonError> {
115    serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)?.try_into()
116}
117
118/// The value to put in `unsigned.redacted_because`.
119#[derive(Clone, Debug)]
120pub struct RedactedBecause(CanonicalJsonObject);
121
122impl RedactedBecause {
123    /// Create a `RedactedBecause` from an arbitrary JSON object.
124    pub fn from_json(obj: CanonicalJsonObject) -> Self {
125        Self(obj)
126    }
127
128    /// Create a `RedactedBecause` from a redaction event.
129    ///
130    /// Fails if the raw event is not valid canonical JSON.
131    pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
132        ev.deserialize_as().map(Self)
133    }
134}
135
136/// Marker trait for redaction events.
137pub trait RedactionEvent {}
138
139/// Redacts an event using the rules specified in the Matrix client-server specification.
140///
141/// This is part of the process of signing an event.
142///
143/// Redaction is also suggested when verifying an event with `verify_event` returns
144/// `Verified::Signatures`. See the documentation for `Verified` for details.
145///
146/// Returns a new JSON object with all applicable fields redacted.
147///
148/// # Parameters
149///
150/// * `object`: A JSON object to redact.
151/// * `version`: The room version, determines which keys to keep for a few event types.
152/// * `redacted_because`: If this is set, an `unsigned` object with a `redacted_because` field set
153///   to the given value is added to the event after redaction.
154///
155/// # Errors
156///
157/// Returns an error if:
158///
159/// * `object` contains a field called `content` that is not a JSON object.
160/// * `object` contains a field called `hashes` that is not a JSON object.
161/// * `object` contains a field called `signatures` that is not a JSON object.
162/// * `object` is missing the `type` field or the field is not a JSON string.
163pub fn redact(
164    mut object: CanonicalJsonObject,
165    version: &RoomVersionId,
166    redacted_because: Option<RedactedBecause>,
167) -> Result<CanonicalJsonObject, RedactionError> {
168    redact_in_place(&mut object, version, redacted_because)?;
169    Ok(object)
170}
171
172/// Redacts an event using the rules specified in the Matrix client-server specification.
173///
174/// Functionally equivalent to `redact`, only this'll redact the event in-place.
175pub fn redact_in_place(
176    event: &mut CanonicalJsonObject,
177    version: &RoomVersionId,
178    redacted_because: Option<RedactedBecause>,
179) -> Result<(), RedactionError> {
180    // Get the content keys here even if they're only needed inside the branch below, because we
181    // can't teach rust that this is a disjoint borrow with `get_mut("content")`.
182    let allowed_content_keys = match event.get("type") {
183        Some(CanonicalJsonValue::String(event_type)) => {
184            allowed_content_keys_for(event_type, version)
185        }
186        Some(_) => return Err(RedactionError::not_of_type("type", JsonType::String)),
187        None => return Err(RedactionError::field_missing_from_object("type")),
188    };
189
190    if let Some(content_value) = event.get_mut("content") {
191        let content = match content_value {
192            CanonicalJsonValue::Object(map) => map,
193            _ => return Err(RedactionError::not_of_type("content", JsonType::Object)),
194        };
195
196        object_retain_keys(content, allowed_content_keys)?;
197    }
198
199    let mut old_event = mem::take(event);
200
201    for &key in allowed_event_keys_for(version) {
202        if let Some(value) = old_event.remove(key) {
203            event.insert(key.to_owned(), value);
204        }
205    }
206
207    if let Some(redacted_because) = redacted_because {
208        let unsigned = CanonicalJsonObject::from_iter([(
209            "redacted_because".to_owned(),
210            redacted_because.0.into(),
211        )]);
212        event.insert("unsigned".to_owned(), unsigned.into());
213    }
214
215    Ok(())
216}
217
218/// Redacts event content using the rules specified in the Matrix client-server specification.
219///
220/// Edits the `object` in-place.
221pub fn redact_content_in_place(
222    object: &mut CanonicalJsonObject,
223    version: &RoomVersionId,
224    event_type: impl AsRef<str>,
225) -> Result<(), RedactionError> {
226    object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version))
227}
228
229fn object_retain_keys(
230    object: &mut CanonicalJsonObject,
231    allowed_keys: &AllowedKeys,
232) -> Result<(), RedactionError> {
233    match *allowed_keys {
234        AllowedKeys::All => {}
235        AllowedKeys::Some { keys, nested } => {
236            object_retain_some_keys(object, keys, nested)?;
237        }
238        AllowedKeys::None => {
239            object.clear();
240        }
241    }
242
243    Ok(())
244}
245
246fn object_retain_some_keys(
247    object: &mut CanonicalJsonObject,
248    keys: &[&str],
249    nested: &[(&str, &AllowedKeys)],
250) -> Result<(), RedactionError> {
251    let mut old_object = mem::take(object);
252
253    for &(nested_key, nested_allowed_keys) in nested {
254        if let Some((key, mut nested_object_value)) = old_object.remove_entry(nested_key) {
255            let nested_object = match &mut nested_object_value {
256                CanonicalJsonValue::Object(map) => map,
257                _ => return Err(RedactionError::not_of_type(nested_key, JsonType::Object)),
258            };
259
260            object_retain_keys(nested_object, nested_allowed_keys)?;
261
262            // If the object is empty, it means none of the nested fields were found so we
263            // don't want to keep the object.
264            if !nested_object.is_empty() {
265                object.insert(key, nested_object_value);
266            }
267        }
268    }
269
270    for &key in keys {
271        if let Some((key, value)) = old_object.remove_entry(key) {
272            object.insert(key, value);
273        }
274    }
275
276    Ok(())
277}
278
279/// The fields that are allowed to remain in an event during redaction depending on the room
280/// version.
281fn allowed_event_keys_for(version: &RoomVersionId) -> &'static [&'static str] {
282    match version {
283        RoomVersionId::V1
284        | RoomVersionId::V2
285        | RoomVersionId::V3
286        | RoomVersionId::V4
287        | RoomVersionId::V5
288        | RoomVersionId::V6
289        | RoomVersionId::V7
290        | RoomVersionId::V8
291        | RoomVersionId::V9
292        | RoomVersionId::V10 => &[
293            "event_id",
294            "type",
295            "room_id",
296            "sender",
297            "state_key",
298            "content",
299            "hashes",
300            "signatures",
301            "depth",
302            "prev_events",
303            "prev_state",
304            "auth_events",
305            "origin",
306            "origin_server_ts",
307            "membership",
308        ],
309        _ => &[
310            "event_id",
311            "type",
312            "room_id",
313            "sender",
314            "state_key",
315            "content",
316            "hashes",
317            "signatures",
318            "depth",
319            "prev_events",
320            "auth_events",
321            "origin_server_ts",
322        ],
323    }
324}
325
326/// List of keys to preserve on an object.
327#[derive(Clone, Copy)]
328enum AllowedKeys {
329    /// All keys are preserved.
330    All,
331    /// Some keys are preserved.
332    Some {
333        /// The keys to preserve on this object.
334        keys: &'static [&'static str],
335
336        /// Keys to preserve on nested objects.
337        ///
338        /// A list of `(nested_object_key, nested_allowed_keys)`.
339        nested: &'static [(&'static str, &'static AllowedKeys)],
340    },
341    /// No keys are preserved.
342    None,
343}
344
345impl AllowedKeys {
346    /// Creates an new `AllowedKeys::Some` with the given keys at this level.
347    const fn some(keys: &'static [&'static str]) -> Self {
348        Self::Some { keys, nested: &[] }
349    }
350
351    /// Creates an new `AllowedKeys::Some` with the given keys and nested keys.
352    const fn some_nested(
353        keys: &'static [&'static str],
354        nested: &'static [(&'static str, &'static AllowedKeys)],
355    ) -> Self {
356        Self::Some { keys, nested }
357    }
358}
359
360/// Allowed keys in `m.room.member`'s content according to room version 1.
361static ROOM_MEMBER_V1: AllowedKeys = AllowedKeys::some(&["membership"]);
362/// Allowed keys in `m.room.member`'s content according to room version 9.
363static ROOM_MEMBER_V9: AllowedKeys =
364    AllowedKeys::some(&["membership", "join_authorised_via_users_server"]);
365/// Allowed keys in `m.room.member`'s content according to room version 11.
366static ROOM_MEMBER_V11: AllowedKeys = AllowedKeys::some_nested(
367    &["membership", "join_authorised_via_users_server"],
368    &[("third_party_invite", &ROOM_MEMBER_THIRD_PARTY_INVITE_V11)],
369);
370/// Allowed keys in the `third_party_invite` field of `m.room.member`'s content according to room
371/// version 11.
372static ROOM_MEMBER_THIRD_PARTY_INVITE_V11: AllowedKeys = AllowedKeys::some(&["signed"]);
373
374/// Allowed keys in `m.room.create`'s content according to room version 1.
375static ROOM_CREATE_V1: AllowedKeys = AllowedKeys::some(&["creator"]);
376
377/// Allowed keys in `m.room.join_rules`'s content according to room version 1.
378static ROOM_JOIN_RULES_V1: AllowedKeys = AllowedKeys::some(&["join_rule"]);
379/// Allowed keys in `m.room.join_rules`'s content according to room version 8.
380static ROOM_JOIN_RULES_V8: AllowedKeys = AllowedKeys::some(&["join_rule", "allow"]);
381
382/// Allowed keys in `m.room.power_levels`'s content according to room version 1.
383static ROOM_POWER_LEVELS_V1: AllowedKeys = AllowedKeys::some(&[
384    "ban",
385    "events",
386    "events_default",
387    "kick",
388    "redact",
389    "state_default",
390    "users",
391    "users_default",
392]);
393/// Allowed keys in `m.room.power_levels`'s content according to room version 11.
394static ROOM_POWER_LEVELS_V11: AllowedKeys = AllowedKeys::some(&[
395    "ban",
396    "events",
397    "events_default",
398    "invite",
399    "kick",
400    "redact",
401    "state_default",
402    "users",
403    "users_default",
404]);
405
406/// Allowed keys in `m.room.aliases`'s content according to room version 1.
407static ROOM_ALIASES_V1: AllowedKeys = AllowedKeys::some(&["aliases"]);
408
409/// Allowed keys in `m.room.server_acl`'s content according to MSC2870.
410#[cfg(feature = "unstable-msc2870")]
411static ROOM_SERVER_ACL_MSC2870: AllowedKeys =
412    AllowedKeys::some(&["allow", "deny", "allow_ip_literals"]);
413
414/// Allowed keys in `m.room.history_visibility`'s content according to room version 1.
415static ROOM_HISTORY_VISIBILITY_V1: AllowedKeys = AllowedKeys::some(&["history_visibility"]);
416
417/// Allowed keys in `m.room.redaction`'s content according to room version 11.
418static ROOM_REDACTION_V11: AllowedKeys = AllowedKeys::some(&["redacts"]);
419
420fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static AllowedKeys {
421    match event_type {
422        "m.room.member" => match version {
423            RoomVersionId::V1
424            | RoomVersionId::V2
425            | RoomVersionId::V3
426            | RoomVersionId::V4
427            | RoomVersionId::V5
428            | RoomVersionId::V6
429            | RoomVersionId::V7
430            | RoomVersionId::V8 => &ROOM_MEMBER_V1,
431            RoomVersionId::V9 | RoomVersionId::V10 => &ROOM_MEMBER_V9,
432            _ => &ROOM_MEMBER_V11,
433        },
434        "m.room.create" => match version {
435            RoomVersionId::V1
436            | RoomVersionId::V2
437            | RoomVersionId::V3
438            | RoomVersionId::V4
439            | RoomVersionId::V5
440            | RoomVersionId::V6
441            | RoomVersionId::V7
442            | RoomVersionId::V8
443            | RoomVersionId::V9
444            | RoomVersionId::V10 => &ROOM_CREATE_V1,
445            _ => &AllowedKeys::All,
446        },
447        "m.room.join_rules" => match version {
448            RoomVersionId::V1
449            | RoomVersionId::V2
450            | RoomVersionId::V3
451            | RoomVersionId::V4
452            | RoomVersionId::V5
453            | RoomVersionId::V6
454            | RoomVersionId::V7 => &ROOM_JOIN_RULES_V1,
455            _ => &ROOM_JOIN_RULES_V8,
456        },
457        "m.room.power_levels" => match version {
458            RoomVersionId::V1
459            | RoomVersionId::V2
460            | RoomVersionId::V3
461            | RoomVersionId::V4
462            | RoomVersionId::V5
463            | RoomVersionId::V6
464            | RoomVersionId::V7
465            | RoomVersionId::V8
466            | RoomVersionId::V9
467            | RoomVersionId::V10 => &ROOM_POWER_LEVELS_V1,
468            _ => &ROOM_POWER_LEVELS_V11,
469        },
470        "m.room.aliases" => match version {
471            RoomVersionId::V1
472            | RoomVersionId::V2
473            | RoomVersionId::V3
474            | RoomVersionId::V4
475            | RoomVersionId::V5 => &ROOM_ALIASES_V1,
476            // All other room versions, including custom ones, are treated by version 6 rules.
477            // TODO: Should we return an error for unknown versions instead?
478            _ => &AllowedKeys::None,
479        },
480        #[cfg(feature = "unstable-msc2870")]
481        "m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => &ROOM_SERVER_ACL_MSC2870,
482        "m.room.history_visibility" => &ROOM_HISTORY_VISIBILITY_V1,
483        "m.room.redaction" => match version {
484            RoomVersionId::V1
485            | RoomVersionId::V2
486            | RoomVersionId::V3
487            | RoomVersionId::V4
488            | RoomVersionId::V5
489            | RoomVersionId::V6
490            | RoomVersionId::V7
491            | RoomVersionId::V8
492            | RoomVersionId::V9
493            | RoomVersionId::V10 => &AllowedKeys::None,
494            _ => &ROOM_REDACTION_V11,
495        },
496        _ => &AllowedKeys::None,
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use std::collections::BTreeMap;
503
504    use assert_matches2::assert_matches;
505    use js_int::int;
506    use serde_json::{
507        from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value,
508    };
509
510    use super::{
511        redact_in_place, to_canonical_value, try_from_json_map, value::CanonicalJsonValue,
512    };
513    use crate::RoomVersionId;
514
515    #[test]
516    fn serialize_canon() {
517        let json: CanonicalJsonValue = json!({
518            "a": [1, 2, 3],
519            "other": { "stuff": "hello" },
520            "string": "Thing"
521        })
522        .try_into()
523        .unwrap();
524
525        let ser = to_json_string(&json).unwrap();
526        let back = from_json_str::<CanonicalJsonValue>(&ser).unwrap();
527
528        assert_eq!(json, back);
529    }
530
531    #[test]
532    fn check_canonical_sorts_keys() {
533        let json: CanonicalJsonValue = json!({
534            "auth": {
535                "success": true,
536                "mxid": "@john.doe:example.com",
537                "profile": {
538                    "display_name": "John Doe",
539                    "three_pids": [
540                        {
541                            "medium": "email",
542                            "address": "john.doe@example.org"
543                        },
544                        {
545                            "medium": "msisdn",
546                            "address": "123456789"
547                        }
548                    ]
549                }
550            }
551        })
552        .try_into()
553        .unwrap();
554
555        assert_eq!(
556            to_json_string(&json).unwrap(),
557            r#"{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}"#
558        );
559    }
560
561    #[test]
562    fn serialize_map_to_canonical() {
563        let mut expected = BTreeMap::new();
564        expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
565        expected.insert(
566            "bar".into(),
567            CanonicalJsonValue::Array(vec![
568                CanonicalJsonValue::Integer(int!(0)),
569                CanonicalJsonValue::Integer(int!(1)),
570                CanonicalJsonValue::Integer(int!(2)),
571            ]),
572        );
573
574        let mut map = serde_json::Map::new();
575        map.insert("foo".into(), json!("string"));
576        map.insert("bar".into(), json!(vec![0, 1, 2,]));
577
578        assert_eq!(try_from_json_map(map).unwrap(), expected);
579    }
580
581    #[test]
582    fn to_canonical() {
583        #[derive(Debug, serde::Serialize)]
584        struct Thing {
585            foo: String,
586            bar: Vec<u8>,
587        }
588        let t = Thing { foo: "string".into(), bar: vec![0, 1, 2] };
589
590        let mut expected = BTreeMap::new();
591        expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
592        expected.insert(
593            "bar".into(),
594            CanonicalJsonValue::Array(vec![
595                CanonicalJsonValue::Integer(int!(0)),
596                CanonicalJsonValue::Integer(int!(1)),
597                CanonicalJsonValue::Integer(int!(2)),
598            ]),
599        );
600
601        assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected));
602    }
603
604    #[test]
605    fn redact_allowed_keys_some() {
606        let original_event = json!({
607            "content": {
608                "ban": 50,
609                "events": {
610                    "m.room.avatar": 50,
611                    "m.room.canonical_alias": 50,
612                    "m.room.history_visibility": 100,
613                    "m.room.name": 50,
614                    "m.room.power_levels": 100
615                },
616                "events_default": 0,
617                "invite": 0,
618                "kick": 50,
619                "redact": 50,
620                "state_default": 50,
621                "users": {
622                    "@example:localhost": 100
623                },
624                "users_default": 0
625            },
626            "event_id": "$15139375512JaHAW:localhost",
627            "origin_server_ts": 45,
628            "sender": "@example:localhost",
629            "room_id": "!room:localhost",
630            "state_key": "",
631            "type": "m.room.power_levels",
632            "unsigned": {
633                "age": 45
634            }
635        });
636
637        assert_matches!(
638            CanonicalJsonValue::try_from(original_event),
639            Ok(CanonicalJsonValue::Object(mut object))
640        );
641
642        redact_in_place(&mut object, &RoomVersionId::V1, None).unwrap();
643
644        let redacted_event = to_json_value(&object).unwrap();
645
646        assert_eq!(
647            redacted_event,
648            json!({
649                "content": {
650                    "ban": 50,
651                    "events": {
652                        "m.room.avatar": 50,
653                        "m.room.canonical_alias": 50,
654                        "m.room.history_visibility": 100,
655                        "m.room.name": 50,
656                        "m.room.power_levels": 100
657                    },
658                    "events_default": 0,
659                    "kick": 50,
660                    "redact": 50,
661                    "state_default": 50,
662                    "users": {
663                        "@example:localhost": 100
664                    },
665                    "users_default": 0
666                },
667                "event_id": "$15139375512JaHAW:localhost",
668                "origin_server_ts": 45,
669                "sender": "@example:localhost",
670                "room_id": "!room:localhost",
671                "state_key": "",
672                "type": "m.room.power_levels",
673            })
674        );
675    }
676
677    #[test]
678    fn redact_allowed_keys_none() {
679        let original_event = json!({
680            "content": {
681                "aliases": ["#somewhere:localhost"]
682            },
683            "event_id": "$152037280074GZeOm:localhost",
684            "origin_server_ts": 1,
685            "sender": "@example:localhost",
686            "state_key": "room.com",
687            "room_id": "!room:room.com",
688            "type": "m.room.aliases",
689            "unsigned": {
690                "age": 1
691            }
692        });
693
694        assert_matches!(
695            CanonicalJsonValue::try_from(original_event),
696            Ok(CanonicalJsonValue::Object(mut object))
697        );
698
699        redact_in_place(&mut object, &RoomVersionId::V10, None).unwrap();
700
701        let redacted_event = to_json_value(&object).unwrap();
702
703        assert_eq!(
704            redacted_event,
705            json!({
706                "content": {},
707                "event_id": "$152037280074GZeOm:localhost",
708                "origin_server_ts": 1,
709                "sender": "@example:localhost",
710                "state_key": "room.com",
711                "room_id": "!room:room.com",
712                "type": "m.room.aliases",
713            })
714        );
715    }
716
717    #[test]
718    fn redact_allowed_keys_all() {
719        let original_event = json!({
720            "content": {
721              "m.federate": true,
722              "predecessor": {
723                "event_id": "$something",
724                "room_id": "!oldroom:example.org"
725              },
726              "room_version": "11",
727            },
728            "event_id": "$143273582443PhrSn",
729            "origin_server_ts": 1_432_735,
730            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
731            "sender": "@example:example.org",
732            "state_key": "",
733            "type": "m.room.create",
734            "unsigned": {
735              "age": 1234,
736            },
737        });
738
739        assert_matches!(
740            CanonicalJsonValue::try_from(original_event),
741            Ok(CanonicalJsonValue::Object(mut object))
742        );
743
744        redact_in_place(&mut object, &RoomVersionId::V11, None).unwrap();
745
746        let redacted_event = to_json_value(&object).unwrap();
747
748        assert_eq!(
749            redacted_event,
750            json!({
751                "content": {
752                  "m.federate": true,
753                  "predecessor": {
754                    "event_id": "$something",
755                    "room_id": "!oldroom:example.org"
756                  },
757                  "room_version": "11",
758                },
759                "event_id": "$143273582443PhrSn",
760                "origin_server_ts": 1_432_735,
761                "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
762                "sender": "@example:example.org",
763                "state_key": "",
764                "type": "m.room.create",
765            })
766        );
767    }
768}