1use serde::{Deserialize, Serialize};
8
9pub const KIND_RANGE: u8 = 1;
11
12pub const HEADER_LEN: usize = 25;
14
15pub const MAX_PAYLOAD: usize = 1024 * 1024;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum FrameError {
22 Truncated,
24 BadKind,
26 LengthMismatch,
28 EmptyRange,
30 OffsetOverflow,
32 TooLarge,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct RangeFrame {
39 pub file_id: u32,
40 pub seq: u64,
41 pub offset: u64,
42 pub data: Vec<u8>,
43}
44
45impl RangeFrame {
46 pub fn split(file_id: u32, seq: u64, offset: u64, data: &[u8]) -> Vec<RangeFrame> {
48 data.chunks(MAX_PAYLOAD)
49 .enumerate()
50 .map(|(i, chunk)| RangeFrame {
51 file_id,
52 seq,
53 offset: offset + (i * MAX_PAYLOAD) as u64,
54 data: chunk.to_vec(),
55 })
56 .collect()
57 }
58
59 pub fn encode(&self) -> Vec<u8> {
61 let mut out = Vec::with_capacity(HEADER_LEN + self.data.len());
62 out.push(KIND_RANGE);
63 out.extend_from_slice(&self.file_id.to_le_bytes());
64 out.extend_from_slice(&self.seq.to_le_bytes());
65 out.extend_from_slice(&self.offset.to_le_bytes());
66 out.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
67 out.extend_from_slice(&self.data);
68 out
69 }
70
71 pub fn decode(bytes: &[u8]) -> Result<RangeFrame, FrameError> {
72 if bytes.len() < HEADER_LEN {
73 return Err(FrameError::Truncated);
74 }
75 if bytes[0] != KIND_RANGE {
76 return Err(FrameError::BadKind);
77 }
78 let file_id = u32::from_le_bytes(bytes[1..5].try_into().unwrap());
79 let seq = u64::from_le_bytes(bytes[5..13].try_into().unwrap());
80 let offset = u64::from_le_bytes(bytes[13..21].try_into().unwrap());
81 let len = u32::from_le_bytes(bytes[21..25].try_into().unwrap());
82 if len as usize > MAX_PAYLOAD {
83 return Err(FrameError::TooLarge);
84 }
85 let data = &bytes[HEADER_LEN..];
86 if data.len() as u64 != len as u64 {
87 return Err(FrameError::LengthMismatch);
88 }
89 if len == 0 {
90 return Err(FrameError::EmptyRange);
91 }
92 if offset.checked_add(len as u64).is_none() {
93 return Err(FrameError::OffsetOverflow);
94 }
95 Ok(RangeFrame {
96 file_id,
97 seq,
98 offset,
99 data: data.to_vec(),
100 })
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
106#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
107#[non_exhaustive]
108pub enum ErrorCode {
109 InvalidRequest,
110 Unauthenticated,
111 PermissionDenied,
112 NotFound,
113 RateLimited,
114 UnsupportedVersion,
115 InternalError,
116}
117
118impl ErrorCode {
119 pub const ALL: [ErrorCode; 7] = [
121 ErrorCode::InvalidRequest,
122 ErrorCode::Unauthenticated,
123 ErrorCode::PermissionDenied,
124 ErrorCode::NotFound,
125 ErrorCode::RateLimited,
126 ErrorCode::UnsupportedVersion,
127 ErrorCode::InternalError,
128 ];
129
130 pub fn as_str(&self) -> &'static str {
131 match self {
132 ErrorCode::InvalidRequest => "INVALID_REQUEST",
133 ErrorCode::Unauthenticated => "UNAUTHENTICATED",
134 ErrorCode::PermissionDenied => "PERMISSION_DENIED",
135 ErrorCode::NotFound => "NOT_FOUND",
136 ErrorCode::RateLimited => "RATE_LIMITED",
137 ErrorCode::UnsupportedVersion => "UNSUPPORTED_VERSION",
138 ErrorCode::InternalError => "INTERNAL_ERROR",
139 }
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct Authenticate {
147 pub request_id: String,
148 pub protocol_version: u32,
149 pub tenant_id: String,
150 pub client_id: String,
151 pub auth_method: String,
152 pub credentials: String,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(deny_unknown_fields)]
158pub struct FlushAck {
159 pub request_id: String,
160 pub file_id: u32,
161 pub seq: u64,
162}
163
164#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(deny_unknown_fields)]
167pub struct Resync {
168 pub request_id: String,
169 pub file_id: u32,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(deny_unknown_fields)]
175pub struct ResyncDone {
176 pub request_id: String,
177 pub file_id: u32,
178 pub seq: u64,
179 pub size: u64,
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct ErrorReply {
186 pub code: ErrorCode,
187 pub message: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub request_id: Option<String>,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct GetServiceFilePath {
196 pub request_id: String,
197 pub service_id: String,
198}
199
200#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(deny_unknown_fields)]
205pub struct ServiceFilePath {
206 pub request_id: String,
207 pub service_id: String,
208 pub file_id: u32,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub path: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub nonce_path: Option<String>,
213}
214
215#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(deny_unknown_fields)]
218pub struct ConfirmLocal {
219 pub request_id: String,
220 pub file_id: u32,
221 pub nonce: String,
222}
223
224impl std::fmt::Debug for ConfirmLocal {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("ConfirmLocal")
228 .field("request_id", &self.request_id)
229 .field("file_id", &self.file_id)
230 .field("nonce", &"<redacted>")
231 .finish()
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(deny_unknown_fields)]
239pub struct ConfirmLocalAck {
240 pub request_id: String,
241 pub file_id: u32,
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
246#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
247#[non_exhaustive]
248pub enum RotateReason {
249 Resize,
251 Revocation,
253}
254
255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(deny_unknown_fields)]
259pub struct RotateRequest {
260 pub request_id: String,
261 pub service_id: String,
262 pub reason: RotateReason,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub new_size: Option<u64>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub revoke_client_id: Option<String>,
267}
268
269impl RotateRequest {
270 pub fn validate(&self) -> Result<(), &'static str> {
272 match self.reason {
273 RotateReason::Resize => match (self.new_size, &self.revoke_client_id) {
274 (Some(n), None) if n > 0 => Ok(()),
275 (Some(0), _) => Err("new_size must be at least 1"),
276 (None, _) => Err("RESIZE needs new_size"),
277 (_, Some(_)) => Err("RESIZE must not name a client"),
278 _ => Err("invalid RESIZE request"),
279 },
280 RotateReason::Revocation => match (self.new_size, &self.revoke_client_id) {
281 (None, Some(c)) if !c.is_empty() => Ok(()),
282 (_, None) => Err("REVOCATION needs revoke_client_id"),
283 (Some(_), _) => Err("REVOCATION must not set new_size"),
284 _ => Err("invalid REVOCATION request"),
285 },
286 }
287 }
288}
289
290#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
292#[serde(deny_unknown_fields)]
293pub struct RotationAccepted {
294 pub request_id: String,
295 pub rotation_id: String,
296}
297
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(deny_unknown_fields)]
305pub struct RotationPending {
306 pub rotation_id: String,
307 pub service_id: String,
308 pub file_id: u32,
309 pub deadline_ms: u64,
310 pub fill_ms: u64,
311}
312
313#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
318#[serde(deny_unknown_fields)]
319pub struct RotationPrepared {
320 pub rotation_id: String,
321 pub service_id: String,
322 pub old_file_id: u32,
323 pub new_file_id: u32,
324 pub size: u64,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub path: Option<String>,
327}
328
329#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct RotationFilled {
333 pub request_id: String,
334 pub rotation_id: String,
335}
336
337#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(deny_unknown_fields)]
340pub struct RotationAbort {
341 pub request_id: String,
342 pub rotation_id: String,
343}
344
345#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
347#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
348#[non_exhaustive]
349pub enum AbortReason {
350 RequesterAbort,
352 FillTimeout,
354 ServerError,
356 #[serde(other)]
358 Unknown,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(deny_unknown_fields)]
365pub struct RotationAborted {
366 pub rotation_id: String,
367 pub service_id: String,
368 pub file_id: u32,
369 pub reason: AbortReason,
370}
371
372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(deny_unknown_fields)]
375pub struct RotationDrained {
376 pub request_id: String,
377 pub rotation_id: String,
378 pub file_id: u32,
379 pub seq: u64,
380}
381
382#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(deny_unknown_fields)]
388pub struct RotationCommit {
389 pub rotation_id: String,
390 pub service_id: String,
391 pub reason: RotateReason,
392 pub old_file_id: u32,
393 pub new_file_id: u32,
394 pub generation: u64,
395 pub size: u64,
396}
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
400#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
401#[non_exhaustive]
402pub enum LossKind {
403 RotationStraggler,
405 ResyncOverwrote,
407 Revoked,
409 #[serde(other)]
411 Unknown,
412}
413
414#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(deny_unknown_fields)]
417pub struct LossRange {
418 pub offset: u64,
419 pub len: u64,
420}
421
422#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(deny_unknown_fields)]
426pub struct Loss {
427 pub service_id: String,
428 pub file_id: u32,
429 pub kind: LossKind,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
431 pub ranges: Vec<LossRange>,
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(deny_unknown_fields)]
437pub struct ByteRange {
438 pub offset: u64,
439 pub len: u64,
440}
441
442pub const MAX_RANGES_PER_MESSAGE: usize = 1024;
445
446pub fn validate_ranges(ranges: &[ByteRange]) -> Result<(), &'static str> {
450 if ranges.is_empty() {
451 return Err("ranges must not be empty");
452 }
453 if ranges.len() > MAX_RANGES_PER_MESSAGE {
454 return Err("too many ranges in one message");
455 }
456 let mut prev_end = 0u64;
457 for r in ranges {
458 if r.len == 0 {
459 return Err("a range must cover at least one byte");
460 }
461 let Some(end) = r.offset.checked_add(r.len) else {
462 return Err("offset plus len overflows");
463 };
464 if r.offset < prev_end {
465 return Err("ranges must be sorted and must not overlap");
466 }
467 prev_end = end;
468 }
469 Ok(())
470}
471
472#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(deny_unknown_fields)]
477pub struct RangesChanged {
478 pub request_id: String,
479 pub file_id: u32,
480 pub ranges: Vec<ByteRange>,
481}
482
483#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(deny_unknown_fields)]
487pub struct RangesUpdated {
488 pub file_id: u32,
489 pub seq: u64,
490 pub ranges: Vec<ByteRange>,
491}
492
493#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(deny_unknown_fields)]
497pub struct FlushEnd {
498 pub request_id: String,
499 pub file_id: u32,
500}
501
502#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
505#[serde(tag = "type", content = "data")]
506#[non_exhaustive]
507pub enum ControlMessage {
508 Authenticate(Authenticate),
509 FlushAck(FlushAck),
510 Resync(Resync),
511 ResyncDone(ResyncDone),
512 Error(ErrorReply),
513 GetServiceFilePath(GetServiceFilePath),
514 ServiceFilePath(ServiceFilePath),
515 ConfirmLocal(ConfirmLocal),
516 ConfirmLocalAck(ConfirmLocalAck),
517 RotateRequest(RotateRequest),
518 RotationAccepted(RotationAccepted),
519 RotationPending(RotationPending),
520 RotationPrepared(RotationPrepared),
521 RotationFilled(RotationFilled),
522 RotationAbort(RotationAbort),
523 RotationAborted(RotationAborted),
524 RotationDrained(RotationDrained),
525 RotationCommit(RotationCommit),
526 Loss(Loss),
527 RangesChanged(RangesChanged),
528 RangesUpdated(RangesUpdated),
529 FlushEnd(FlushEnd),
530}