From 9247e3980130ebfed9ef265e996e47bf1f6c852d Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 14:35:55 +0000 Subject: transport: a pushed change written out by its size, never its bytes Review 4, F3: the console writes each message out with `{:#?}`, and `SyncBody` derived `Debug`, so `Changes` (changes pushed as they are) printed every byte of every change. Pushes are the small messages, and Automerge deflates no change under 256 bytes: a short value's path and value went into the console in clear, which any user with any capability reads (the page's `messages` events, and since 1266 a subscribed node's `messages/own/{seq}` too), paths it may not read included. `SyncBody` now writes itself out with sizes for the changes, as `SyncPayload` does for an Automerge message; heads and a delta's spec stay as they were. --- src/transport/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) (limited to 'src/transport/mod.rs') diff --git a/src/transport/mod.rs b/src/transport/mod.rs index d794271..e2135d1 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -779,8 +779,10 @@ impl std::fmt::Display for EpochTag { } } -/// What a sync envelope carries. -#[derive(Debug, Clone, Serialize, Deserialize)] +/// What a sync envelope carries. Written out (`Debug`, the dashboard's console) with +/// sizes for its bytes, never the bytes: a change holds the paths and values written, +/// which whoever looks at the console may not read. +#[derive(Clone, Serialize, Deserialize)] pub(crate) enum SyncBody { /// An Automerge sync message. Message(#[serde(with = "crate::transport::wire::codec::sync_message")] SyncPayload), @@ -845,6 +847,27 @@ impl DeltaSpec { pub(crate) struct SyncPayload(pub(crate) bytes::Bytes); /// Sizes instead of contents: logged messages stay short. +impl std::fmt::Debug for SyncBody { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Message(payload) => f.debug_tuple("Message").field(payload).finish(), + Self::Deflated(payload) => f.debug_tuple("Deflated").field(payload).finish(), + Self::Changes { heads, changes } => f + .debug_struct("Changes") + .field("heads", heads) + .field( + "changes", + &changes + .iter() + .map(|change| format!("{} bytes", change.len())) + .collect::>(), + ) + .finish(), + Self::Delta(spec) => f.debug_tuple("Delta").field(spec).finish(), + } + } +} + impl std::fmt::Debug for SyncPayload { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SyncPayload({} bytes)", self.0.len()) @@ -1146,6 +1169,20 @@ pub(crate) trait Transport: Send + Sync { #[cfg(test)] mod tests { use super::*; + + /// A pushed change is written out by its size, never its bytes (the console). + #[test] + fn pushed_changes_written_out_by_size() { + let body = SyncBody::Changes { + heads: Vec::new(), + changes: vec![serde_bytes::ByteBuf::from( + b"secret-path secret-value".to_vec(), + )], + }; + let text = format!("{body:#?}"); + assert!(text.contains("24 bytes"), "{text}"); + assert!(!text.contains("115"), "{text}"); + } use crate::testrand::for_seeds; /// Two summaries are equal exactly when the heads are, in any order, whichever form -- cgit v1.3.1