1use kevy_resp::Argv;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct HandshakeReq {
35 pub generation: u64,
38 pub from_offset: u64,
41 pub replica_id: String,
44}
45
46#[derive(Debug, PartialEq, Eq)]
48pub enum HandshakeError {
49 BadCommand,
51 WrongArity(usize),
53 BadFromKeyword,
55 BadGeneration,
57 BadOffset,
59 BadIdKeyword,
61 BadReplicaId,
63}
64
65impl std::fmt::Display for HandshakeError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::BadCommand => write!(f, "expected REPLICATE command"),
69 Self::WrongArity(n) => write!(f, "REPLICATE expects 6 args, got {n}"),
70 Self::BadFromKeyword => write!(f, "expected 'FROM' keyword"),
71 Self::BadGeneration => write!(f, "generation must be an unsigned decimal"),
72 Self::BadOffset => write!(f, "from-offset must be an unsigned decimal"),
73 Self::BadIdKeyword => write!(f, "expected 'ID' keyword"),
74 Self::BadReplicaId => write!(f, "replica id must be non-empty UTF-8"),
75 }
76 }
77}
78
79impl std::error::Error for HandshakeError {}
80
81#[allow(clippy::missing_panics_doc)]
87pub fn parse_replicate_from(argv: &Argv) -> Result<HandshakeReq, HandshakeError> {
88 if argv.len() != 6 {
89 return Err(HandshakeError::WrongArity(argv.len()));
90 }
91 if !eq_ascii_ci(argv.get(0).unwrap(), b"REPLICATE") {
92 return Err(HandshakeError::BadCommand);
93 }
94 if !eq_ascii_ci(argv.get(1).unwrap(), b"FROM") {
95 return Err(HandshakeError::BadFromKeyword);
96 }
97 let generation =
98 parse_decimal_u64(argv.get(2).unwrap()).ok_or(HandshakeError::BadGeneration)?;
99 let from_offset = parse_decimal_u64(argv.get(3).unwrap()).ok_or(HandshakeError::BadOffset)?;
100 if !eq_ascii_ci(argv.get(4).unwrap(), b"ID") {
101 return Err(HandshakeError::BadIdKeyword);
102 }
103 let id_bytes = argv.get(5).unwrap();
104 if id_bytes.is_empty() {
105 return Err(HandshakeError::BadReplicaId);
106 }
107 let replica_id =
108 std::str::from_utf8(id_bytes).map_err(|_| HandshakeError::BadReplicaId)?.to_string();
109 Ok(HandshakeReq { generation, from_offset, replica_id })
110}
111
112pub fn encode_ack(generation: u64, current_offset: u64) -> Vec<u8> {
117 let mut out = Vec::with_capacity(8 + 20 + 20 + 3);
118 out.extend_from_slice(b"+ACK ");
119 push_u64(&mut out, generation);
120 out.push(b' ');
121 push_u64(&mut out, current_offset);
122 out.extend_from_slice(b"\r\n");
123 out
124}
125
126fn eq_ascii_ci(a: &[u8], b: &[u8]) -> bool {
127 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_ignore_ascii_case(y))
128}
129
130fn parse_decimal_u64(bytes: &[u8]) -> Option<u64> {
131 if bytes.is_empty() {
132 return None;
133 }
134 let mut n: u64 = 0;
135 for &b in bytes {
136 if !b.is_ascii_digit() {
137 return None;
138 }
139 n = n.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
140 }
141 Some(n)
142}
143
144fn push_u64(out: &mut Vec<u8>, n: u64) {
145 if n == 0 {
146 out.push(b'0');
147 return;
148 }
149 let mut tmp = [0u8; 20];
150 let mut i = tmp.len();
151 let mut v = n;
152 while v != 0 {
153 i -= 1;
154 tmp[i] = b'0' + (v % 10) as u8;
155 v /= 10;
156 }
157 out.extend_from_slice(&tmp[i..]);
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn argv(args: &[&[u8]]) -> Argv {
165 let mut a = Argv::default();
166 for arg in args {
167 a.push(arg);
168 }
169 a
170 }
171
172 #[test]
173 fn parses_fresh_replica_from_zero() {
174 let req =
175 parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"0", b"0", b"ID", b"replica-a"]))
176 .unwrap();
177 assert_eq!(req.generation, 0);
178 assert_eq!(req.from_offset, 0);
179 assert_eq!(req.replica_id, "replica-a");
180 }
181
182 #[test]
183 fn parses_reconnect_with_generation_and_large_offset() {
184 let req = parse_replicate_from(&argv(&[
185 b"REPLICATE",
186 b"FROM",
187 b"7",
188 b"4294967296", b"ID",
190 b"node-7",
191 ]))
192 .unwrap();
193 assert_eq!(req.generation, 7);
194 assert_eq!(req.from_offset, 4_294_967_296);
195 assert_eq!(req.replica_id, "node-7");
196 }
197
198 #[test]
199 fn keywords_are_case_insensitive() {
200 let req =
201 parse_replicate_from(&argv(&[b"replicate", b"from", b"2", b"1", b"id", b"x"])).unwrap();
202 assert_eq!(req.generation, 2);
203 assert_eq!(req.from_offset, 1);
204 assert_eq!(req.replica_id, "x");
205 }
206
207 #[test]
208 fn wrong_arity_rejected_with_actual_count() {
209 let err =
212 parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"0", b"ID", b"a"])).unwrap_err();
213 assert_eq!(err, HandshakeError::WrongArity(5));
214 }
215
216 #[test]
217 fn wrong_command_rejected() {
218 let err = parse_replicate_from(&argv(&[b"SUBSCRIBE", b"FROM", b"0", b"0", b"ID", b"a"]))
219 .unwrap_err();
220 assert_eq!(err, HandshakeError::BadCommand);
221 }
222
223 #[test]
224 fn wrong_from_keyword_rejected() {
225 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"AT", b"0", b"0", b"ID", b"a"]))
226 .unwrap_err();
227 assert_eq!(err, HandshakeError::BadFromKeyword);
228 }
229
230 #[test]
231 fn wrong_id_keyword_rejected() {
232 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"0", b"0", b"NAME", b"a"]))
233 .unwrap_err();
234 assert_eq!(err, HandshakeError::BadIdKeyword);
235 }
236
237 #[test]
238 fn non_decimal_generation_rejected() {
239 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"NaN", b"0", b"ID", b"a"]))
240 .unwrap_err();
241 assert_eq!(err, HandshakeError::BadGeneration);
242 }
243
244 #[test]
245 fn non_decimal_offset_rejected() {
246 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"1", b"NaN", b"ID", b"a"]))
247 .unwrap_err();
248 assert_eq!(err, HandshakeError::BadOffset);
249 }
250
251 #[test]
252 fn negative_offset_rejected_as_bad_offset() {
253 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"1", b"-1", b"ID", b"a"]))
254 .unwrap_err();
255 assert_eq!(err, HandshakeError::BadOffset);
256 }
257
258 #[test]
259 fn empty_replica_id_rejected() {
260 let err = parse_replicate_from(&argv(&[b"REPLICATE", b"FROM", b"0", b"0", b"ID", b""]))
261 .unwrap_err();
262 assert_eq!(err, HandshakeError::BadReplicaId);
263 }
264
265 #[test]
266 fn non_utf8_replica_id_rejected() {
267 let err = parse_replicate_from(&argv(&[
268 b"REPLICATE",
269 b"FROM",
270 b"0",
271 b"0",
272 b"ID",
273 &[0xFF, 0xFE, 0xFD], ]))
275 .unwrap_err();
276 assert_eq!(err, HandshakeError::BadReplicaId);
277 }
278
279 #[test]
280 fn ack_format_for_zero() {
281 assert_eq!(encode_ack(1, 0), b"+ACK 1 0\r\n");
282 }
283
284 #[test]
285 fn ack_format_for_large_values() {
286 assert_eq!(encode_ack(12, 987_654_321), b"+ACK 12 987654321\r\n");
287 }
288}