1use std::{fmt, mem};
4
5use serde::Serialize;
6use serde_json::Value as JsonValue;
7
8mod macros;
9mod serializer;
10mod value;
11
12pub use self::{
13 serializer::Serializer,
14 value::{CanonicalJsonObject, CanonicalJsonValue},
15};
16#[doc(inline)]
17pub use crate::assert_to_canonical_json_eq;
18use crate::{room_version_rules::RedactionRules, serde::Raw};
19
20#[derive(Debug)]
22#[allow(clippy::exhaustive_enums)]
23pub enum CanonicalJsonError {
24 IntegerOutOfRange,
26
27 InvalidType(String),
29
30 InvalidObjectKeyType(String),
32
33 DuplicateObjectKey(String),
35
36 InvalidRawValue(serde_json::Error),
38
39 Other(String),
41}
42
43impl fmt::Display for CanonicalJsonError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::IntegerOutOfRange => f.write_str("integer is out of the range of `js_int::Int`"),
47 Self::InvalidType(ty) => write!(f, "{ty} cannot be serialized as canonical JSON"),
48 Self::InvalidObjectKeyType(ty) => {
49 write!(f, "{ty} cannot be used as an object key, expected a string type")
50 }
51 Self::InvalidRawValue(error) => {
52 write!(f, "invalid raw value: {error}")
53 }
54 Self::DuplicateObjectKey(key) => write!(f, "duplicate object key `{key}`"),
55 Self::Other(msg) => f.write_str(msg),
56 }
57 }
58}
59
60impl std::error::Error for CanonicalJsonError {}
61
62impl serde::ser::Error for CanonicalJsonError {
63 fn custom<T>(msg: T) -> Self
64 where
65 T: fmt::Display,
66 {
67 Self::Other(msg.to_string())
68 }
69}
70
71#[derive(Debug)]
73#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
74pub enum RedactionError {
75 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
77 NotOfType {
78 field: String,
80 of_type: JsonType,
82 },
83
84 JsonFieldMissingFromObject(String),
86}
87
88impl fmt::Display for RedactionError {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 RedactionError::NotOfType { field, of_type } => {
92 write!(f, "Value in {field:?} must be a JSON {of_type:?}")
93 }
94 RedactionError::JsonFieldMissingFromObject(field) => {
95 write!(f, "JSON object must contain the field {field:?}")
96 }
97 }
98 }
99}
100
101impl std::error::Error for RedactionError {}
102
103impl RedactionError {
104 fn not_of_type(target: &str, of_type: JsonType) -> Self {
105 Self::NotOfType { field: target.to_owned(), of_type }
106 }
107
108 fn field_missing_from_object(target: &str) -> Self {
109 Self::JsonFieldMissingFromObject(target.to_owned())
110 }
111}
112
113#[derive(Debug)]
115#[allow(clippy::exhaustive_enums)]
116pub enum JsonType {
117 Object,
119
120 String,
122
123 Integer,
125
126 Array,
128
129 Boolean,
131
132 Null,
134}
135
136pub fn try_from_json_map(
138 json: serde_json::Map<String, JsonValue>,
139) -> Result<CanonicalJsonObject, CanonicalJsonError> {
140 json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect()
141}
142
143pub fn to_canonical_value<T: Serialize>(
155 value: T,
156) -> Result<CanonicalJsonValue, CanonicalJsonError> {
157 value.serialize(Serializer)
158}
159
160#[derive(Clone, Debug)]
162pub struct RedactedBecause(CanonicalJsonObject);
163
164impl RedactedBecause {
165 pub fn from_json(obj: CanonicalJsonObject) -> Self {
167 Self(obj)
168 }
169
170 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
174 ev.deserialize_as_unchecked().map(Self)
175 }
176}
177
178pub trait RedactionEvent {}
180
181pub fn redact(
206 mut object: CanonicalJsonObject,
207 rules: &RedactionRules,
208 redacted_because: Option<RedactedBecause>,
209) -> Result<CanonicalJsonObject, RedactionError> {
210 redact_in_place(&mut object, rules, redacted_because)?;
211 Ok(object)
212}
213
214pub fn redact_in_place(
218 event: &mut CanonicalJsonObject,
219 rules: &RedactionRules,
220 redacted_because: Option<RedactedBecause>,
221) -> Result<(), RedactionError> {
222 let retained_event_content_keys = match event.get("type") {
225 Some(CanonicalJsonValue::String(event_type)) => {
226 retained_event_content_keys(event_type.as_ref(), rules)
227 }
228 Some(_) => return Err(RedactionError::not_of_type("type", JsonType::String)),
229 None => return Err(RedactionError::field_missing_from_object("type")),
230 };
231
232 if let Some(content_value) = event.get_mut("content") {
233 let CanonicalJsonValue::Object(content) = content_value else {
234 return Err(RedactionError::not_of_type("content", JsonType::Object));
235 };
236
237 retained_event_content_keys.apply(rules, content)?;
238 }
239
240 let retained_event_keys =
241 RetainedKeys::some(|rules, key, _value| Ok(is_event_key_retained(rules, key)));
242 retained_event_keys.apply(rules, event)?;
243
244 if let Some(redacted_because) = redacted_because {
245 let unsigned = CanonicalJsonObject::from_iter([(
246 "redacted_because".to_owned(),
247 redacted_because.0.into(),
248 )]);
249 event.insert("unsigned".to_owned(), unsigned.into());
250 }
251
252 Ok(())
253}
254
255pub fn redact_content_in_place(
260 content: &mut CanonicalJsonObject,
261 rules: &RedactionRules,
262 event_type: impl AsRef<str>,
263) -> Result<(), RedactionError> {
264 retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content)
265}
266
267type RetainKeyFn =
270 dyn Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>;
271
272enum RetainedKeys {
274 All,
276
277 Some(Box<RetainKeyFn>),
279
280 None,
282}
283
284impl RetainedKeys {
285 fn some<F>(retain_key_fn: F) -> Self
287 where
288 F: Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>
289 + 'static,
290 {
291 Self::Some(Box::new(retain_key_fn))
292 }
293
294 fn apply(
296 &self,
297 rules: &RedactionRules,
298 object: &mut CanonicalJsonObject,
299 ) -> Result<(), RedactionError> {
300 match self {
301 Self::All => {}
302 Self::Some(allow_field_fn) => {
303 let old_object = mem::take(object);
304
305 for (key, mut value) in old_object {
306 if allow_field_fn(rules, &key, &mut value)? {
307 object.insert(key, value);
308 }
309 }
310 }
311 Self::None => object.clear(),
312 }
313
314 Ok(())
315 }
316}
317
318fn is_event_key_retained(rules: &RedactionRules, key: &str) -> bool {
320 match key {
321 "event_id" | "type" | "room_id" | "sender" | "state_key" | "content" | "hashes"
322 | "signatures" | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true,
323 "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state,
324 _ => false,
325 }
326}
327
328fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
330 match event_type {
331 "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
332 "m.room.create" => room_create_content_retained_keys(rules),
333 "m.room.join_rules" => RetainedKeys::some(|rules, key, _value| {
334 is_room_join_rules_content_key_retained(rules, key)
335 }),
336 "m.room.power_levels" => RetainedKeys::some(|rules, key, _value| {
337 is_room_power_levels_content_key_retained(rules, key)
338 }),
339 "m.room.history_visibility" => RetainedKeys::some(|_rules, key, _value| {
340 is_room_history_visibility_content_key_retained(key)
341 }),
342 "m.room.redaction" => room_redaction_content_retained_keys(rules),
343 "m.room.aliases" => room_aliases_content_retained_keys(rules),
344 #[cfg(feature = "unstable-msc2870")]
345 "m.room.server_acl" => RetainedKeys::some(|rules, key, _value| {
346 is_room_server_acl_content_key_retained(rules, key)
347 }),
348 _ => RetainedKeys::None,
349 }
350}
351
352fn is_room_member_content_key_retained(
354 rules: &RedactionRules,
355 key: &str,
356 value: &mut CanonicalJsonValue,
357) -> Result<bool, RedactionError> {
358 Ok(match key {
359 "membership" => true,
360 "join_authorised_via_users_server" => {
361 rules.keep_room_member_join_authorised_via_users_server
362 }
363 "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
364 let Some(third_party_invite) = value.as_object_mut() else {
365 return Err(RedactionError::not_of_type("third_party_invite", JsonType::Object));
366 };
367
368 third_party_invite.retain(|key, _| key == "signed");
369
370 !third_party_invite.is_empty()
372 }
373 _ => false,
374 })
375}
376
377fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
379 if rules.keep_room_create_content {
380 RetainedKeys::All
381 } else {
382 RetainedKeys::some(|_rules, field, _value| Ok(field == "creator"))
383 }
384}
385
386fn is_room_join_rules_content_key_retained(
389 rules: &RedactionRules,
390 key: &str,
391) -> Result<bool, RedactionError> {
392 Ok(match key {
393 "join_rule" => true,
394 "allow" => rules.keep_room_join_rules_allow,
395 _ => false,
396 })
397}
398
399fn is_room_power_levels_content_key_retained(
402 rules: &RedactionRules,
403 key: &str,
404) -> Result<bool, RedactionError> {
405 Ok(match key {
406 "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
407 | "users_default" => true,
408 "invite" => rules.keep_room_power_levels_invite,
409 _ => false,
410 })
411}
412
413fn is_room_history_visibility_content_key_retained(key: &str) -> Result<bool, RedactionError> {
416 Ok(key == "history_visibility")
417}
418
419fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
421 if rules.keep_room_redaction_redacts {
422 RetainedKeys::some(|_rules, field, _value| Ok(field == "redacts"))
423 } else {
424 RetainedKeys::None
425 }
426}
427
428fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
430 if rules.keep_room_aliases_aliases {
431 RetainedKeys::some(|_rules, field, _value| Ok(field == "aliases"))
432 } else {
433 RetainedKeys::None
434 }
435}
436
437#[cfg(feature = "unstable-msc2870")]
440fn is_room_server_acl_content_key_retained(
441 rules: &RedactionRules,
442 key: &str,
443) -> Result<bool, RedactionError> {
444 Ok(match key {
445 "allow" | "deny" | "allow_ip_literals" => {
446 rules.keep_room_server_acl_allow_deny_allow_ip_literals
447 }
448 _ => false,
449 })
450}
451
452#[cfg(test)]
453mod tests {
454 use std::collections::BTreeMap;
455
456 use assert_matches2::assert_matches;
457 use js_int::int;
458 use serde_json::{
459 from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value,
460 value::RawValue as RawJsonValue,
461 };
462
463 use super::{
464 CanonicalJsonError, assert_to_canonical_json_eq, redact_in_place, to_canonical_value,
465 try_from_json_map, value::CanonicalJsonValue,
466 };
467 use crate::room_version_rules::RedactionRules;
468
469 #[test]
470 fn serialize_canon() {
471 let json: CanonicalJsonValue = json!({
472 "a": [1, 2, 3],
473 "other": { "stuff": "hello" },
474 "string": "Thing"
475 })
476 .try_into()
477 .unwrap();
478
479 let ser = to_json_string(&json).unwrap();
480 let back = from_json_str::<CanonicalJsonValue>(&ser).unwrap();
481
482 assert_eq!(json, back);
483 }
484
485 #[test]
486 fn check_canonical_sorts_keys() {
487 let json: CanonicalJsonValue = json!({
488 "auth": {
489 "success": true,
490 "mxid": "@john.doe:example.com",
491 "profile": {
492 "display_name": "John Doe",
493 "three_pids": [
494 {
495 "medium": "email",
496 "address": "john.doe@example.org"
497 },
498 {
499 "medium": "msisdn",
500 "address": "123456789"
501 }
502 ]
503 }
504 }
505 })
506 .try_into()
507 .unwrap();
508
509 assert_eq!(
510 to_json_string(&json).unwrap(),
511 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}}"#
512 );
513 }
514
515 #[test]
516 fn serialize_map_to_canonical() {
517 let mut expected = BTreeMap::new();
518 expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
519 expected.insert(
520 "bar".into(),
521 CanonicalJsonValue::Array(vec![
522 CanonicalJsonValue::Integer(int!(0)),
523 CanonicalJsonValue::Integer(int!(1)),
524 CanonicalJsonValue::Integer(int!(2)),
525 ]),
526 );
527
528 let mut map = serde_json::Map::new();
529 map.insert("foo".into(), json!("string"));
530 map.insert("bar".into(), json!(vec![0, 1, 2,]));
531
532 assert_eq!(try_from_json_map(map).unwrap(), expected);
533 }
534
535 #[test]
536 fn to_canonical_value_success() {
537 #[derive(Debug, serde::Serialize)]
538 struct MyStruct {
539 string: String,
540 array: Vec<u8>,
541 boolean: Option<bool>,
542 object: BTreeMap<String, MyEnum>,
543 null: (),
544 raw: Box<RawJsonValue>,
545 }
546
547 #[derive(Debug, serde::Serialize)]
548 enum MyEnum {
549 Foo,
550 #[serde(rename = "bar")]
551 Bar,
552 }
553
554 let t = MyStruct {
555 string: "string".into(),
556 array: vec![0, 1, 2],
557 boolean: Some(true),
558 object: [("foo".to_owned(), MyEnum::Foo), ("bar".to_owned(), MyEnum::Bar)].into(),
559 null: (),
560 raw: RawJsonValue::from_string(r#"{"baz":false}"#.to_owned()).unwrap(),
561 };
562
563 let mut expected = BTreeMap::new();
564 expected.insert("string".to_owned(), CanonicalJsonValue::String("string".to_owned()));
565 expected.insert(
566 "array".to_owned(),
567 CanonicalJsonValue::Array(vec![
568 CanonicalJsonValue::Integer(int!(0)),
569 CanonicalJsonValue::Integer(int!(1)),
570 CanonicalJsonValue::Integer(int!(2)),
571 ]),
572 );
573 expected.insert("boolean".to_owned(), CanonicalJsonValue::Bool(true));
574 let mut child_object = BTreeMap::new();
575 child_object.insert("foo".to_owned(), CanonicalJsonValue::String("Foo".to_owned()));
576 child_object.insert("bar".to_owned(), CanonicalJsonValue::String("bar".to_owned()));
577 expected.insert("object".to_owned(), CanonicalJsonValue::Object(child_object));
578 expected.insert("null".to_owned(), CanonicalJsonValue::Null);
579 let mut raw_object = BTreeMap::new();
580 raw_object.insert("baz".to_owned(), CanonicalJsonValue::Bool(false));
581 expected.insert("raw".to_owned(), CanonicalJsonValue::Object(raw_object));
582
583 let expected = CanonicalJsonValue::Object(expected);
584 assert_eq!(to_canonical_value(&t).unwrap(), expected);
585 assert_to_canonical_json_eq!(t, expected.into());
586 }
587
588 #[test]
589 fn to_canonical_value_out_of_range_int() {
590 #[derive(Debug, serde::Serialize)]
591 struct StructWithInt {
592 foo: i64,
593 }
594
595 let t = StructWithInt { foo: i64::MAX };
596 assert_matches!(to_canonical_value(t), Err(CanonicalJsonError::IntegerOutOfRange));
597 }
598
599 #[test]
600 fn to_canonical_value_invalid_type() {
601 #[derive(Debug, serde::Serialize)]
602 struct StructWithFloat {
603 foo: f32,
604 }
605
606 let t = StructWithFloat { foo: 10.0 };
607 assert_matches!(to_canonical_value(t), Err(CanonicalJsonError::InvalidType(_)));
608 }
609
610 #[test]
611 fn to_canonical_value_invalid_object_key_type() {
612 {
613 #[derive(Debug, serde::Serialize)]
614 struct StructWithBoolKey {
615 foo: BTreeMap<bool, String>,
616 }
617
618 let t = StructWithBoolKey { foo: [(true, "bar".to_owned())].into() };
619 assert_matches!(
620 to_canonical_value(t),
621 Err(CanonicalJsonError::InvalidObjectKeyType(_))
622 );
623 }
624
625 {
626 #[derive(Debug, serde::Serialize)]
627 struct StructWithIntKey {
628 foo: BTreeMap<i8, String>,
629 }
630
631 let t = StructWithIntKey { foo: [(4, "bar".to_owned())].into() };
632 assert_matches!(
633 to_canonical_value(t),
634 Err(CanonicalJsonError::InvalidObjectKeyType(_))
635 );
636 }
637
638 {
639 #[derive(Debug, serde::Serialize)]
640 struct StructWithUnitKey {
641 foo: BTreeMap<(), String>,
642 }
643
644 let t = StructWithUnitKey { foo: [((), "bar".to_owned())].into() };
645 assert_matches!(
646 to_canonical_value(t),
647 Err(CanonicalJsonError::InvalidObjectKeyType(_))
648 );
649 }
650
651 {
652 #[derive(Debug, serde::Serialize)]
653 struct StructWithTupleKey {
654 foo: BTreeMap<(String, String), bool>,
655 }
656
657 let t =
658 StructWithTupleKey { foo: [(("bar".to_owned(), "baz".to_owned()), false)].into() };
659 assert_matches!(
660 to_canonical_value(t),
661 Err(CanonicalJsonError::InvalidObjectKeyType(_))
662 );
663 }
664 }
665
666 #[test]
667 fn to_canonical_value_duplicate_object_key() {
668 #[derive(Debug, serde::Serialize)]
669 struct StructWithDuplicateKey {
670 foo: String,
671 #[serde(rename = "foo")]
672 bar: Vec<u8>,
673 }
674
675 let t = StructWithDuplicateKey { foo: "string".into(), bar: vec![0, 1, 2] };
676 assert_matches!(to_canonical_value(t), Err(CanonicalJsonError::DuplicateObjectKey(_)));
677 }
678
679 #[test]
680 fn redact_allowed_keys_some() {
681 let original_event = json!({
682 "content": {
683 "ban": 50,
684 "events": {
685 "m.room.avatar": 50,
686 "m.room.canonical_alias": 50,
687 "m.room.history_visibility": 100,
688 "m.room.name": 50,
689 "m.room.power_levels": 100
690 },
691 "events_default": 0,
692 "invite": 0,
693 "kick": 50,
694 "redact": 50,
695 "state_default": 50,
696 "users": {
697 "@example:localhost": 100
698 },
699 "users_default": 0
700 },
701 "event_id": "$15139375512JaHAW:localhost",
702 "origin_server_ts": 45,
703 "sender": "@example:localhost",
704 "room_id": "!room:localhost",
705 "state_key": "",
706 "type": "m.room.power_levels",
707 "unsigned": {
708 "age": 45
709 }
710 });
711
712 assert_matches!(
713 CanonicalJsonValue::try_from(original_event),
714 Ok(CanonicalJsonValue::Object(mut object))
715 );
716
717 redact_in_place(&mut object, &RedactionRules::V1, None).unwrap();
718
719 let expected = json!({
720 "content": {
721 "ban": 50,
722 "events": {
723 "m.room.avatar": 50,
724 "m.room.canonical_alias": 50,
725 "m.room.history_visibility": 100,
726 "m.room.name": 50,
727 "m.room.power_levels": 100
728 },
729 "events_default": 0,
730 "kick": 50,
731 "redact": 50,
732 "state_default": 50,
733 "users": {
734 "@example:localhost": 100
735 },
736 "users_default": 0
737 },
738 "event_id": "$15139375512JaHAW:localhost",
739 "origin_server_ts": 45,
740 "sender": "@example:localhost",
741 "room_id": "!room:localhost",
742 "state_key": "",
743 "type": "m.room.power_levels",
744 });
745
746 assert_eq!(to_json_value(&object).unwrap(), expected);
747 assert_to_canonical_json_eq!(object, expected);
748 }
749
750 #[test]
751 fn redact_allowed_keys_none() {
752 let original_event = json!({
753 "content": {
754 "aliases": ["#somewhere:localhost"]
755 },
756 "event_id": "$152037280074GZeOm:localhost",
757 "origin_server_ts": 1,
758 "sender": "@example:localhost",
759 "state_key": "room.com",
760 "room_id": "!room:room.com",
761 "type": "m.room.aliases",
762 "unsigned": {
763 "age": 1
764 }
765 });
766
767 assert_matches!(
768 CanonicalJsonValue::try_from(original_event),
769 Ok(CanonicalJsonValue::Object(mut object))
770 );
771
772 redact_in_place(&mut object, &RedactionRules::V9, None).unwrap();
773
774 let expected = json!({
775 "content": {},
776 "event_id": "$152037280074GZeOm:localhost",
777 "origin_server_ts": 1,
778 "sender": "@example:localhost",
779 "state_key": "room.com",
780 "room_id": "!room:room.com",
781 "type": "m.room.aliases",
782 });
783
784 assert_eq!(to_json_value(&object).unwrap(), expected);
785 assert_to_canonical_json_eq!(object, expected);
786 }
787
788 #[test]
789 fn redact_allowed_keys_all() {
790 let original_event = json!({
791 "content": {
792 "m.federate": true,
793 "predecessor": {
794 "event_id": "$something",
795 "room_id": "!oldroom:example.org"
796 },
797 "room_version": "11",
798 },
799 "event_id": "$143273582443PhrSn",
800 "origin_server_ts": 1_432_735,
801 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
802 "sender": "@example:example.org",
803 "state_key": "",
804 "type": "m.room.create",
805 "unsigned": {
806 "age": 1234,
807 },
808 });
809
810 assert_matches!(
811 CanonicalJsonValue::try_from(original_event),
812 Ok(CanonicalJsonValue::Object(mut object))
813 );
814
815 redact_in_place(&mut object, &RedactionRules::V11, None).unwrap();
816
817 let expected = json!({
818 "content": {
819 "m.federate": true,
820 "predecessor": {
821 "event_id": "$something",
822 "room_id": "!oldroom:example.org"
823 },
824 "room_version": "11",
825 },
826 "event_id": "$143273582443PhrSn",
827 "origin_server_ts": 1_432_735,
828 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
829 "sender": "@example:example.org",
830 "state_key": "",
831 "type": "m.room.create",
832 });
833
834 assert_eq!(to_json_value(&object).unwrap(), expected);
835 assert_to_canonical_json_eq!(object, expected);
836 }
837}