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