aboutsummaryrefslogtreecommitdiffstats
path: root/src/document/sync.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 01:40:44 +0100
committertv <tv@krebsco.de>2026-11-07 01:40:44 +0100
commite50a5ced65a1f03ebdbe5b0d3b09a57b11b7edbe (patch)
tree88493cdc06a841dab564cee46a989945a89530fa /src/document/sync.rs
parent4eb8eda89db812c3aa40f0226853aa9a20809a92 (diff)
refuse misplaced authors
Diffstat (limited to 'src/document/sync.rs')
-rw-r--r--src/document/sync.rs49
1 files changed, 41 insertions, 8 deletions
diff --git a/src/document/sync.rs b/src/document/sync.rs
index dab43b4..8cb740b 100644
--- a/src/document/sync.rs
+++ b/src/document/sync.rs
@@ -10,7 +10,6 @@ use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
-use automerge::AutomergeError;
use automerge::sync::{self, SyncDoc};
use tokio::time::Instant;
use tracing::debug;
@@ -302,23 +301,29 @@ impl SyncSession {
/// Applies a sync message from the peer to `doc`. Doesn't settle the awaited
/// answer: the caller measures the exchange first (`reply`).
#[cfg(test)]
- pub(crate) fn receive(
- &mut self,
- doc: &mut Document,
- message: Received,
- ) -> Result<(), AutomergeError> {
+ pub(crate) fn receive(&mut self, doc: &mut Document, message: Received) -> anyhow::Result<()> {
self.receive_logged(doc, message, &mut automerge::PatchLog::inactive())
}
/// Applies a sync message from the peer to `doc`, what it changes recorded in `log`
/// (`Replica::logged`). Doesn't settle the awaited answer: the caller measures the
- /// exchange first (`reply`).
+ /// exchange first (`reply`). A message carrying a change that names an author though
+ /// it isn't its actor's first is refused whole, before Automerge sees it
+ /// (`changes::misplaced_author`).
pub(crate) fn receive_logged(
&mut self,
doc: &mut Document,
message: Received,
log: &mut automerge::PatchLog,
- ) -> Result<(), AutomergeError> {
+ ) -> anyhow::Result<()> {
+ if let Some(misplaced) = message
+ .0
+ .changes
+ .iter()
+ .find_map(crate::document::changes::misplaced_author)
+ {
+ anyhow::bail!("Refused a sync message: {misplaced}");
+ }
self.delta_offered = false;
self.before_receive = Some(self.peer_view());
let said = message.0.heads.clone();
@@ -1909,6 +1914,34 @@ mod tests {
assert_eq!(theirs.get_heads(), ours.get_heads(), "both in one message");
}
+ /// A change naming an author though it isn't its actor's first is refused before
+ /// Automerge sees it (`changes::misplaced_author`), and the document stays as it was.
+ #[test]
+ fn a_message_with_a_misplaced_author_is_refused() {
+ use crate::document::testing::{change, with_author_footer};
+ use automerge::{ActorId, ROOT, transaction::Transactable};
+ let mut writer = Document::new().with_actor(ActorId::from(b"writer".to_vec()));
+ change(&mut writer, |doc| doc.put(ROOT, "x", 1).unwrap());
+ change(&mut writer, |doc| doc.put(ROOT, "x", 2).unwrap());
+ let changes = writer.get_changes(&[]);
+ let first = changes[0].raw_bytes().to_vec();
+ let crafted = with_author_footer(changes[1].raw_bytes(), b"mallory");
+ let (mut ours, mut theirs) = (Document::new(), Document::new());
+ let (mut a, _) = in_step(&mut ours, &mut theirs);
+ a.receive(
+ &mut ours,
+ Received::pushed(Vec::new(), vec![first]).unwrap(),
+ )
+ .unwrap();
+ let heads = ours.get_heads();
+ let refused = a.receive(
+ &mut ours,
+ Received::pushed(Vec::new(), vec![crafted]).unwrap(),
+ );
+ assert!(refused.is_err());
+ assert_eq!(ours.get_heads(), heads);
+ }
+
/// Pushed changes (`push`) do what the sync message they stand for would: the peer
/// applies them, is level with us, and has nothing to answer; nothing goes twice.
#[test]