1use 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::{room_version_rules::RedactionRules, serde::Raw};
12
13#[derive(Debug)]
15#[allow(clippy::exhaustive_enums)]
16pub enum CanonicalJsonError {
17 IntConvert,
19
20 SerDe(serde_json::Error),
22}
23
24impl fmt::Display for CanonicalJsonError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 CanonicalJsonError::IntConvert => {
28 f.write_str("number found is not a valid `js_int::Int`")
29 }
30 CanonicalJsonError::SerDe(err) => write!(f, "serde Error: {err}"),
31 }
32 }
33}
34
35impl std::error::Error for CanonicalJsonError {}
36
37#[derive(Debug)]
39#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
40pub enum RedactionError {
41 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
43 NotOfType {
44 field: String,
46 of_type: JsonType,
48 },
49
50 JsonFieldMissingFromObject(String),
52}
53
54impl fmt::Display for RedactionError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match self {
57 RedactionError::NotOfType { field, of_type } => {
58 write!(f, "Value in {field:?} must be a JSON {of_type:?}")
59 }
60 RedactionError::JsonFieldMissingFromObject(field) => {
61 write!(f, "JSON object must contain the field {field:?}")
62 }
63 }
64 }
65}
66
67impl std::error::Error for RedactionError {}
68
69impl RedactionError {
70 fn not_of_type(target: &str, of_type: JsonType) -> Self {
71 Self::NotOfType { field: target.to_owned(), of_type }
72 }
73
74 fn field_missing_from_object(target: &str) -> Self {
75 Self::JsonFieldMissingFromObject(target.to_owned())
76 }
77}
78
79#[derive(Debug)]
81#[allow(clippy::exhaustive_enums)]
82pub enum JsonType {
83 Object,
85
86 String,
88
89 Integer,
91
92 Array,
94
95 Boolean,
97
98 Null,
100}
101
102pub fn try_from_json_map(
104 json: serde_json::Map<String, JsonValue>,
105) -> Result<CanonicalJsonObject, CanonicalJsonError> {
106 json.into_iter().map(|(k, v)| Ok((k, v.try_into()?))).collect()
107}
108
109pub fn to_canonical_value<T: Serialize>(
111 value: T,
112) -> Result<CanonicalJsonValue, CanonicalJsonError> {
113 serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)?.try_into()
114}
115
116#[derive(Clone, Debug)]
118pub struct RedactedBecause(CanonicalJsonObject);
119
120impl RedactedBecause {
121 pub fn from_json(obj: CanonicalJsonObject) -> Self {
123 Self(obj)
124 }
125
126 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
130 ev.deserialize_as_unchecked().map(Self)
131 }
132}
133
134pub trait RedactionEvent {}
136
137pub fn redact(
162 mut object: CanonicalJsonObject,
163 rules: &RedactionRules,
164 redacted_because: Option<RedactedBecause>,
165) -> Result<CanonicalJsonObject, RedactionError> {
166 redact_in_place(&mut object, rules, redacted_because)?;
167 Ok(object)
168}
169
170pub fn redact_in_place(
174 event: &mut CanonicalJsonObject,
175 rules: &RedactionRules,
176 redacted_because: Option<RedactedBecause>,
177) -> Result<(), RedactionError> {
178 let retained_event_content_keys = match event.get("type") {
181 Some(CanonicalJsonValue::String(event_type)) => {
182 retained_event_content_keys(event_type.as_ref(), rules)
183 }
184 Some(_) => return Err(RedactionError::not_of_type("type", JsonType::String)),
185 None => return Err(RedactionError::field_missing_from_object("type")),
186 };
187
188 if let Some(content_value) = event.get_mut("content") {
189 let CanonicalJsonValue::Object(content) = content_value else {
190 return Err(RedactionError::not_of_type("content", JsonType::Object));
191 };
192
193 retained_event_content_keys.apply(rules, content)?;
194 }
195
196 let retained_event_keys =
197 RetainedKeys::some(|rules, key, _value| Ok(is_event_key_retained(rules, key)));
198 retained_event_keys.apply(rules, event)?;
199
200 if let Some(redacted_because) = redacted_because {
201 let unsigned = CanonicalJsonObject::from_iter([(
202 "redacted_because".to_owned(),
203 redacted_because.0.into(),
204 )]);
205 event.insert("unsigned".to_owned(), unsigned.into());
206 }
207
208 Ok(())
209}
210
211pub fn redact_content_in_place(
216 content: &mut CanonicalJsonObject,
217 rules: &RedactionRules,
218 event_type: impl AsRef<str>,
219) -> Result<(), RedactionError> {
220 retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content)
221}
222
223type RetainKeyFn =
226 dyn Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>;
227
228enum RetainedKeys {
230 All,
232
233 Some(Box<RetainKeyFn>),
235
236 None,
238}
239
240impl RetainedKeys {
241 fn some<F>(retain_key_fn: F) -> Self
243 where
244 F: Fn(&RedactionRules, &str, &mut CanonicalJsonValue) -> Result<bool, RedactionError>
245 + 'static,
246 {
247 Self::Some(Box::new(retain_key_fn))
248 }
249
250 fn apply(
252 &self,
253 rules: &RedactionRules,
254 object: &mut CanonicalJsonObject,
255 ) -> Result<(), RedactionError> {
256 match self {
257 Self::All => {}
258 Self::Some(allow_field_fn) => {
259 let old_object = mem::take(object);
260
261 for (key, mut value) in old_object {
262 if allow_field_fn(rules, &key, &mut value)? {
263 object.insert(key, value);
264 }
265 }
266 }
267 Self::None => object.clear(),
268 }
269
270 Ok(())
271 }
272}
273
274fn is_event_key_retained(rules: &RedactionRules, key: &str) -> bool {
276 match key {
277 "event_id" | "type" | "room_id" | "sender" | "state_key" | "content" | "hashes"
278 | "signatures" | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true,
279 "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state,
280 _ => false,
281 }
282}
283
284fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
286 match event_type {
287 "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
288 "m.room.create" => room_create_content_retained_keys(rules),
289 "m.room.join_rules" => RetainedKeys::some(|rules, key, _value| {
290 is_room_join_rules_content_key_retained(rules, key)
291 }),
292 "m.room.power_levels" => RetainedKeys::some(|rules, key, _value| {
293 is_room_power_levels_content_key_retained(rules, key)
294 }),
295 "m.room.history_visibility" => RetainedKeys::some(|_rules, key, _value| {
296 is_room_history_visibility_content_key_retained(key)
297 }),
298 "m.room.redaction" => room_redaction_content_retained_keys(rules),
299 "m.room.aliases" => room_aliases_content_retained_keys(rules),
300 #[cfg(feature = "unstable-msc2870")]
301 "m.room.server_acl" => RetainedKeys::some(|rules, key, _value| {
302 is_room_server_acl_content_key_retained(rules, key)
303 }),
304 _ => RetainedKeys::None,
305 }
306}
307
308fn is_room_member_content_key_retained(
310 rules: &RedactionRules,
311 key: &str,
312 value: &mut CanonicalJsonValue,
313) -> Result<bool, RedactionError> {
314 Ok(match key {
315 "membership" => true,
316 "join_authorised_via_users_server" => {
317 rules.keep_room_member_join_authorised_via_users_server
318 }
319 "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
320 let Some(third_party_invite) = value.as_object_mut() else {
321 return Err(RedactionError::not_of_type("third_party_invite", JsonType::Object));
322 };
323
324 third_party_invite.retain(|key, _| key == "signed");
325
326 !third_party_invite.is_empty()
328 }
329 _ => false,
330 })
331}
332
333fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
335 if rules.keep_room_create_content {
336 RetainedKeys::All
337 } else {
338 RetainedKeys::some(|_rules, field, _value| Ok(field == "creator"))
339 }
340}
341
342fn is_room_join_rules_content_key_retained(
345 rules: &RedactionRules,
346 key: &str,
347) -> Result<bool, RedactionError> {
348 Ok(match key {
349 "join_rule" => true,
350 "allow" => rules.keep_room_join_rules_allow,
351 _ => false,
352 })
353}
354
355fn is_room_power_levels_content_key_retained(
358 rules: &RedactionRules,
359 key: &str,
360) -> Result<bool, RedactionError> {
361 Ok(match key {
362 "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
363 | "users_default" => true,
364 "invite" => rules.keep_room_power_levels_invite,
365 _ => false,
366 })
367}
368
369fn is_room_history_visibility_content_key_retained(key: &str) -> Result<bool, RedactionError> {
372 Ok(key == "history_visibility")
373}
374
375fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
377 if rules.keep_room_redaction_redacts {
378 RetainedKeys::some(|_rules, field, _value| Ok(field == "redacts"))
379 } else {
380 RetainedKeys::None
381 }
382}
383
384fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
386 if rules.keep_room_aliases_aliases {
387 RetainedKeys::some(|_rules, field, _value| Ok(field == "aliases"))
388 } else {
389 RetainedKeys::None
390 }
391}
392
393#[cfg(feature = "unstable-msc2870")]
396fn is_room_server_acl_content_key_retained(
397 rules: &RedactionRules,
398 key: &str,
399) -> Result<bool, RedactionError> {
400 Ok(match key {
401 "allow" | "deny" | "allow_ip_literals" => {
402 rules.keep_room_server_acl_allow_deny_allow_ip_literals
403 }
404 _ => false,
405 })
406}
407
408#[cfg(test)]
409mod tests {
410 use std::collections::BTreeMap;
411
412 use assert_matches2::assert_matches;
413 use js_int::int;
414 use serde_json::{
415 from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value,
416 };
417
418 use super::{
419 redact_in_place, to_canonical_value, try_from_json_map, value::CanonicalJsonValue,
420 };
421 use crate::room_version_rules::RedactionRules;
422
423 #[test]
424 fn serialize_canon() {
425 let json: CanonicalJsonValue = json!({
426 "a": [1, 2, 3],
427 "other": { "stuff": "hello" },
428 "string": "Thing"
429 })
430 .try_into()
431 .unwrap();
432
433 let ser = to_json_string(&json).unwrap();
434 let back = from_json_str::<CanonicalJsonValue>(&ser).unwrap();
435
436 assert_eq!(json, back);
437 }
438
439 #[test]
440 fn check_canonical_sorts_keys() {
441 let json: CanonicalJsonValue = json!({
442 "auth": {
443 "success": true,
444 "mxid": "@john.doe:example.com",
445 "profile": {
446 "display_name": "John Doe",
447 "three_pids": [
448 {
449 "medium": "email",
450 "address": "john.doe@example.org"
451 },
452 {
453 "medium": "msisdn",
454 "address": "123456789"
455 }
456 ]
457 }
458 }
459 })
460 .try_into()
461 .unwrap();
462
463 assert_eq!(
464 to_json_string(&json).unwrap(),
465 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}}"#
466 );
467 }
468
469 #[test]
470 fn serialize_map_to_canonical() {
471 let mut expected = BTreeMap::new();
472 expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
473 expected.insert(
474 "bar".into(),
475 CanonicalJsonValue::Array(vec![
476 CanonicalJsonValue::Integer(int!(0)),
477 CanonicalJsonValue::Integer(int!(1)),
478 CanonicalJsonValue::Integer(int!(2)),
479 ]),
480 );
481
482 let mut map = serde_json::Map::new();
483 map.insert("foo".into(), json!("string"));
484 map.insert("bar".into(), json!(vec![0, 1, 2,]));
485
486 assert_eq!(try_from_json_map(map).unwrap(), expected);
487 }
488
489 #[test]
490 fn to_canonical() {
491 #[derive(Debug, serde::Serialize)]
492 struct Thing {
493 foo: String,
494 bar: Vec<u8>,
495 }
496 let t = Thing { foo: "string".into(), bar: vec![0, 1, 2] };
497
498 let mut expected = BTreeMap::new();
499 expected.insert("foo".into(), CanonicalJsonValue::String("string".into()));
500 expected.insert(
501 "bar".into(),
502 CanonicalJsonValue::Array(vec![
503 CanonicalJsonValue::Integer(int!(0)),
504 CanonicalJsonValue::Integer(int!(1)),
505 CanonicalJsonValue::Integer(int!(2)),
506 ]),
507 );
508
509 assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected));
510 }
511
512 #[test]
513 fn redact_allowed_keys_some() {
514 let original_event = json!({
515 "content": {
516 "ban": 50,
517 "events": {
518 "m.room.avatar": 50,
519 "m.room.canonical_alias": 50,
520 "m.room.history_visibility": 100,
521 "m.room.name": 50,
522 "m.room.power_levels": 100
523 },
524 "events_default": 0,
525 "invite": 0,
526 "kick": 50,
527 "redact": 50,
528 "state_default": 50,
529 "users": {
530 "@example:localhost": 100
531 },
532 "users_default": 0
533 },
534 "event_id": "$15139375512JaHAW:localhost",
535 "origin_server_ts": 45,
536 "sender": "@example:localhost",
537 "room_id": "!room:localhost",
538 "state_key": "",
539 "type": "m.room.power_levels",
540 "unsigned": {
541 "age": 45
542 }
543 });
544
545 assert_matches!(
546 CanonicalJsonValue::try_from(original_event),
547 Ok(CanonicalJsonValue::Object(mut object))
548 );
549
550 redact_in_place(&mut object, &RedactionRules::V1, None).unwrap();
551
552 let redacted_event = to_json_value(&object).unwrap();
553
554 assert_eq!(
555 redacted_event,
556 json!({
557 "content": {
558 "ban": 50,
559 "events": {
560 "m.room.avatar": 50,
561 "m.room.canonical_alias": 50,
562 "m.room.history_visibility": 100,
563 "m.room.name": 50,
564 "m.room.power_levels": 100
565 },
566 "events_default": 0,
567 "kick": 50,
568 "redact": 50,
569 "state_default": 50,
570 "users": {
571 "@example:localhost": 100
572 },
573 "users_default": 0
574 },
575 "event_id": "$15139375512JaHAW:localhost",
576 "origin_server_ts": 45,
577 "sender": "@example:localhost",
578 "room_id": "!room:localhost",
579 "state_key": "",
580 "type": "m.room.power_levels",
581 })
582 );
583 }
584
585 #[test]
586 fn redact_allowed_keys_none() {
587 let original_event = json!({
588 "content": {
589 "aliases": ["#somewhere:localhost"]
590 },
591 "event_id": "$152037280074GZeOm:localhost",
592 "origin_server_ts": 1,
593 "sender": "@example:localhost",
594 "state_key": "room.com",
595 "room_id": "!room:room.com",
596 "type": "m.room.aliases",
597 "unsigned": {
598 "age": 1
599 }
600 });
601
602 assert_matches!(
603 CanonicalJsonValue::try_from(original_event),
604 Ok(CanonicalJsonValue::Object(mut object))
605 );
606
607 redact_in_place(&mut object, &RedactionRules::V9, None).unwrap();
608
609 let redacted_event = to_json_value(&object).unwrap();
610
611 assert_eq!(
612 redacted_event,
613 json!({
614 "content": {},
615 "event_id": "$152037280074GZeOm:localhost",
616 "origin_server_ts": 1,
617 "sender": "@example:localhost",
618 "state_key": "room.com",
619 "room_id": "!room:room.com",
620 "type": "m.room.aliases",
621 })
622 );
623 }
624
625 #[test]
626 fn redact_allowed_keys_all() {
627 let original_event = json!({
628 "content": {
629 "m.federate": true,
630 "predecessor": {
631 "event_id": "$something",
632 "room_id": "!oldroom:example.org"
633 },
634 "room_version": "11",
635 },
636 "event_id": "$143273582443PhrSn",
637 "origin_server_ts": 1_432_735,
638 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
639 "sender": "@example:example.org",
640 "state_key": "",
641 "type": "m.room.create",
642 "unsigned": {
643 "age": 1234,
644 },
645 });
646
647 assert_matches!(
648 CanonicalJsonValue::try_from(original_event),
649 Ok(CanonicalJsonValue::Object(mut object))
650 );
651
652 redact_in_place(&mut object, &RedactionRules::V11, None).unwrap();
653
654 let redacted_event = to_json_value(&object).unwrap();
655
656 assert_eq!(
657 redacted_event,
658 json!({
659 "content": {
660 "m.federate": true,
661 "predecessor": {
662 "event_id": "$something",
663 "room_id": "!oldroom:example.org"
664 },
665 "room_version": "11",
666 },
667 "event_id": "$143273582443PhrSn",
668 "origin_server_ts": 1_432_735,
669 "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
670 "sender": "@example:example.org",
671 "state_key": "",
672 "type": "m.room.create",
673 })
674 );
675 }
676}