Skip to main content

matrix_sdk_common/
edit_validation.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::{
16    events::{AnySyncTimelineEvent, relation::RelationType},
17    serde::Raw,
18};
19use serde::Deserialize;
20
21use crate::deserialized_responses::EncryptionInfo;
22
23/// Represents all possible validation errors that can occur when processing an
24/// event edit.
25///
26/// These errors ensure that a replacement event complies with the rules
27/// required to safely and correctly modify an existing event.
28///
29/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
30#[derive(Debug, thiserror::Error)]
31pub enum EditValidityError {
32    /// Occurs when the sender of the replacement event does not match the
33    /// sender of the original event.
34    ///
35    /// Only the original sender is allowed to edit their own event.
36    #[error(
37        "the sender of the original event isn't the same as the sender of the replacement event"
38    )]
39    InvalidSender,
40
41    /// Occurs when either the original event or the replacement event contains
42    /// a state key.
43    ///
44    /// State events are not allowed to be edited.
45    #[error("the original event or the replacement event contains a state key")]
46    StateKeyPresent,
47
48    /// Occurs when the content type of the original event differs from that of
49    /// the replacement event.
50    ///
51    /// Edits must not change the event’s content type, as this would introduce
52    /// semantic inconsistencies.
53    #[error(
54        "the content type of the original event is `{content_type}` while the replacement is a `{replacement_type}`"
55    )]
56    MismatchContentType {
57        /// The content type of the original event.
58        content_type: String,
59
60        /// The content type of the replacement event.
61        replacement_type: String,
62    },
63
64    /// Occurs when the original event is itself already a replacement (edit).
65    #[error("the original event is an edit as well")]
66    OriginalEventIsReplacement,
67
68    /// Occurs when the replacement event is not a replacement for the original
69    /// event.
70    #[error("the replacement event is not a replacement for the original event")]
71    NotReplacement,
72
73    /// Occurs when a required field is missing from either the original or the
74    /// replacement event.
75    ///
76    /// The event is considered malformed and cannot be validated.
77    #[error("the event was encrypted, as such it should have an `m.new_content` field")]
78    MissingNewContent,
79
80    #[error(transparent)]
81    InvalidJson(#[from] serde_json::Error),
82
83    /// Occurs when the original event is encrypted but the replacement event is
84    /// not.
85    #[error("the original event was encrypted while the replacement is not")]
86    ReplacementNotEncrypted,
87}
88
89/// This implements the Matrix spec rule set for validity of replacement events
90/// (edits). Invalid replacements must be ignored.
91///
92/// This function implements the steps documented in the [spec] with one
93/// exception, the step to check if the room IDs match isn't done. The JSON of
94/// the event might not contain the room ID if it wasn received over a `/sync`
95/// request.
96///
97/// _Warning_: Callers must ensure that the original event and replacement event
98/// belong to the same room, that is, they have the same room ID.
99///
100/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
101pub fn check_validity_of_replacement_events(
102    original_json: &Raw<AnySyncTimelineEvent>,
103    original_encryption_info: Option<&EncryptionInfo>,
104    replacement_json: &Raw<AnySyncTimelineEvent>,
105    replacement_encryption_info: Option<&EncryptionInfo>,
106) -> Result<(), EditValidityError> {
107    #[derive(Debug, Deserialize)]
108    struct MinimalEvent<'a> {
109        sender: &'a str,
110        event_id: &'a str,
111        #[serde(rename = "type")]
112        event_type: &'a str,
113        state_key: Option<&'a str>,
114        content: MinimalContent<'a>,
115    }
116
117    #[derive(Debug, Deserialize)]
118    struct MinimalContent<'a> {
119        #[serde(borrow, rename = "m.relates_to")]
120        relates_to: Option<MinimalRelatesTo<'a>>,
121        #[serde(rename = "m.new_content")]
122        new_content: Option<MinimalNewContent>,
123    }
124
125    #[derive(Debug, Deserialize)]
126    struct MinimalNewContent {}
127
128    #[derive(Debug, Deserialize)]
129    struct MinimalRelatesTo<'a> {
130        rel_type: Option<RelationType>,
131        event_id: Option<&'a str>,
132    }
133
134    let original_event = original_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
135    let replacement_event = replacement_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
136
137    // We don't check the room ID here since this event might have been received
138    // over /sync, in this case the JSON likely won't contain the room ID field.
139
140    // The original event and replacement event must have the same sender (i.e.
141    // you cannot edit someone else’s messages).
142    if original_event.sender != replacement_event.sender {
143        return Err(EditValidityError::InvalidSender);
144    }
145
146    // This check isn't part of the list in the spec, but it makes sense to
147    // check if the replacement event is has the correct rel_type and if it's an
148    // edit for the original event.
149    if let Some(relates_to) = replacement_event.content.relates_to {
150        if relates_to.rel_type != Some(RelationType::Replacement)
151            || relates_to.event_id != Some(original_event.event_id)
152        {
153            return Err(EditValidityError::NotReplacement);
154        }
155    } else {
156        return Err(EditValidityError::NotReplacement);
157    }
158
159    // The replacement and original events must have the same type (i.e. you
160    // cannot change the original event’s type).
161    if original_event.event_type != replacement_event.event_type {
162        return Err(EditValidityError::MismatchContentType {
163            content_type: original_event.event_type.to_owned(),
164            replacement_type: replacement_event.event_type.to_owned(),
165        });
166    }
167
168    // The replacement and original events must not have a state_key property
169    // (i.e. you cannot edit state events at all).
170    if original_event.state_key.is_some() || replacement_event.state_key.is_some() {
171        return Err(EditValidityError::StateKeyPresent);
172    }
173
174    // The original event must not, itself, have a rel_type of m.replace (i.e.
175    // you cannot edit an edit — though you can send multiple edits for a single
176    // original event).
177    if let Some(relates_to) = original_event.content.relates_to
178        && relates_to.rel_type == Some(RelationType::Replacement)
179    {
180        return Err(EditValidityError::OriginalEventIsReplacement);
181    }
182
183    // The replacement event (once decrypted, if appropriate) must have an
184    // m.new_content property.
185    if replacement_encryption_info.is_some() && replacement_event.content.new_content.is_none() {
186        return Err(EditValidityError::MissingNewContent);
187    }
188
189    // If the original event was encrypted, the replacement should be too.
190    if original_encryption_info.is_some() && replacement_encryption_info.is_none() {
191        return Err(EditValidityError::ReplacementNotEncrypted);
192    }
193
194    Ok(())
195}