Skip to main content

ripsync_core/net/
proto.rs

1//! Wire types for the ripsync↔ripsync remote-sync protocol.
2//!
3//! Both peers are ripsync; the protocol is **not** wire-compatible with real
4//! rsync. Control messages are bincode-serialized inside length-prefixed frames
5//! (see [`super::transport`]). The flow is **receiver-driven lock-step**: the
6//! side that holds the *source* ("sender") streams its file list, then becomes
7//! purely reactive — it answers one [`Request`] with one [`Data`] response at a
8//! time. The side that holds the *destination* ("receiver") drives: it requests
9//! the content it needs, applies each response, and finally sends [`Msg::Finished`]
10//! to terminate. Because exactly one side is ever waiting to read while the other
11//! computes-then-writes, the single duplex stream cannot deadlock.
12
13use std::path::PathBuf;
14
15use serde::{Deserialize, Serialize};
16
17use crate::delta::{Delta, Signature};
18use crate::report::Stats;
19
20/// Magic prefix sent raw (unframed) at the very start of a connection.
21pub const MAGIC: &[u8; 7] = b"ripsync";
22
23/// Protocol version. Bump on any incompatible wire change.
24pub const PROTO_VERSION: u32 = 1;
25
26/// Which direction the *local* (initiating) side asked for.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum Role {
29    /// Local → remote: local is the sender, the remote `--server` is the receiver.
30    Push,
31    /// Remote → local: the remote `--server` is the sender, local is the receiver.
32    Pull,
33}
34
35/// Options that affect how the transfer is performed, negotiated once.
36#[allow(clippy::struct_excessive_bools)]
37#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
38pub struct NetOptions {
39    /// Mirror deletions (remove destination entries absent from the source).
40    pub delete: bool,
41    /// Compare files by content hash rather than size+mtime.
42    pub checksum: bool,
43    /// Preserve Unix permission bits.
44    pub preserve_mode: bool,
45    /// Preserve modification times.
46    pub preserve_mtime: bool,
47    /// Always transfer whole files; never compute a delta.
48    pub whole_file: bool,
49    /// Compress whole-file payloads on the wire with zstd.
50    pub compress: bool,
51    /// zstd compression level (1–22) used when `compress` is set.
52    pub compress_level: i32,
53}
54
55/// The first framed message the initiator sends after the raw handshake: it tells
56/// the responder which role to play and over which root.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Init {
59    /// The local side's intent.
60    pub role: Role,
61    /// The remote root path (destination for [`Role::Push`], source for [`Role::Pull`]).
62    pub root: PathBuf,
63    /// Negotiated transfer options.
64    pub options: NetOptions,
65}
66
67/// What a listed entry is.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub enum NetKind {
70    /// A directory.
71    Dir,
72    /// A regular file.
73    File,
74    /// A symlink with the given verbatim target.
75    Symlink(PathBuf),
76}
77
78/// One entry in the sender's file list.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct NetEntry {
81    /// Path relative to the transfer root (never contains `..`).
82    pub rel: PathBuf,
83    /// Kind of entry.
84    pub kind: NetKind,
85    /// Byte length (0 for dirs/symlinks).
86    pub len: u64,
87    /// Modification time, whole seconds since the Unix epoch.
88    pub mtime_s: i64,
89    /// Sub-second part of the modification time, in nanoseconds.
90    pub mtime_ns: u32,
91    /// Unix permission+type bits (0 where unavailable).
92    pub mode: u32,
93}
94
95/// Receiver → sender: the content the receiver needs for one file.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub enum Request {
98    /// Send the whole file at this relative path.
99    Whole(PathBuf),
100    /// Send a delta of this file against the supplied signature of the receiver's
101    /// existing copy.
102    Delta(PathBuf, Signature),
103}
104
105/// Sender → receiver: the response to a [`Request`].
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub enum Data {
108    /// The whole file contents. `zstd` is true when `bytes` is zstd-compressed.
109    Whole {
110        /// File bytes (possibly compressed).
111        bytes: Vec<u8>,
112        /// Whether `bytes` is zstd-compressed.
113        zstd: bool,
114    },
115    /// A delta to apply to the receiver's existing copy. Deltas are mostly small
116    /// literal runs already, so they are not compressed.
117    Delta(Delta),
118    /// The sender could not read the file (it vanished or is unreadable); the
119    /// receiver records a per-entry error and moves on.
120    NotFound,
121}
122
123/// Every framed message on the wire.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub enum Msg {
126    /// Responder → initiator: version check result.
127    ServerHello {
128        /// Responder's protocol version.
129        version: u32,
130        /// Whether the responder accepts the connection.
131        ok: bool,
132    },
133    /// Initiator → responder: role, root, options.
134    Init(Init),
135    /// Responder → initiator: handshake complete.
136    InitAck,
137    /// Sender → receiver: one file-list entry.
138    Entry(NetEntry),
139    /// Sender → receiver: the file list is complete.
140    ListDone,
141    /// Receiver → sender: request content for one file.
142    Request(Request),
143    /// Sender → receiver: content for the requested file.
144    Data(Data),
145    /// Receiver → sender: terminal message carrying the final tally.
146    Finished(Stats),
147    /// Either side: a fatal error; the peer should abort.
148    Error(String),
149}