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}