Skip to main content

commy_wire/
lib.rs

1//! The Commy wire contract: the binary range frame and the error-code vocabulary.
2//!
3//! The specification is in `spec/`, the language-neutral golden vectors are in
4//! `vectors/`, and every SDK runs the same vectors in its own CI. This crate is
5//! the Rust side of that contract and is tested against those exact files.
6
7use serde::{Deserialize, Serialize};
8
9/// First byte of a binary range frame.
10pub const KIND_RANGE: u8 = 1;
11
12/// Bytes before the payload: kind (1) + file_id (4) + seq (8) + offset (8) + len (4).
13pub const HEADER_LEN: usize = 25;
14
15/// Largest payload one frame may carry. A bigger change is split into several frames.
16pub const MAX_PAYLOAD: usize = 1024 * 1024;
17
18/// Why a binary frame was refused.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum FrameError {
22    /// Fewer bytes than the header needs.
23    Truncated,
24    /// The first byte is not a known frame kind.
25    BadKind,
26    /// The `len` field disagrees with the number of payload bytes.
27    LengthMismatch,
28    /// A range of zero bytes carries nothing and is refused.
29    EmptyRange,
30    /// `offset + len` does not fit in a u64.
31    OffsetOverflow,
32    /// `len` exceeds [`MAX_PAYLOAD`].
33    TooLarge,
34}
35
36/// A run of bytes that changed in one service file. All integers are little-endian.
37#[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    /// Split one changed run into frames of at most [`MAX_PAYLOAD`] bytes. All share `seq`.
47    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    /// Encode to wire bytes. `data` must be 1 to [`MAX_PAYLOAD`] bytes; use [`RangeFrame::split`].
60    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/// The only spellings an error code may have on the wire, from server and SDKs alike.
105#[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    /// Every code, in the order `spec/errors.md` lists them.
120    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/// The protocol version a client sends in [`Authenticate`] and this crate describes.
144pub const PROTOCOL_VERSION: u32 = 1;
145
146/// Longest `client_id` accepted.
147pub const MAX_CLIENT_ID_LEN: usize = 128;
148
149/// Longest `tenant_id` accepted.
150pub const MAX_TENANT_ID_LEN: usize = 64;
151
152/// How a client proves who it is. Tagged by `method`; the fields sit beside the tag in
153/// [`Authenticate`]. Secrets are redacted from `Debug` output.
154#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(tag = "method")]
156pub enum Credentials {
157    /// A bearer API key.
158    #[serde(rename = "api_key")]
159    ApiKey { key: String },
160    /// A JSON Web Token.
161    #[serde(rename = "jwt")]
162    Jwt { token: String },
163    /// A user name and password.
164    #[serde(rename = "basic")]
165    Basic { username: String, password: String },
166    /// Anything a custom verifier on the server understands.
167    #[serde(rename = "custom")]
168    Custom { data: serde_json::Value },
169}
170
171impl std::fmt::Debug for Credentials {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        match self {
174            Credentials::ApiKey { .. } => f
175                .debug_struct("ApiKey")
176                .field("key", &"<redacted>")
177                .finish(),
178            Credentials::Jwt { .. } => f.debug_struct("Jwt").field("token", &"<redacted>").finish(),
179            Credentials::Basic { username, .. } => f
180                .debug_struct("Basic")
181                .field("username", username)
182                .field("password", &"<redacted>")
183                .finish(),
184            Credentials::Custom { .. } => f
185                .debug_struct("Custom")
186                .field("data", &"<redacted>")
187                .finish(),
188        }
189    }
190}
191
192impl Credentials {
193    /// The wire name of the method.
194    pub fn method(&self) -> &'static str {
195        match self {
196            Credentials::ApiKey { .. } => "api_key",
197            Credentials::Jwt { .. } => "jwt",
198            Credentials::Basic { .. } => "basic",
199            Credentials::Custom { .. } => "custom",
200        }
201    }
202}
203
204/// Client to server: authenticate to a tenant. The first message on a connection.
205///
206/// `client_id` is a **hint for development servers only**: a production server takes the client's
207/// identity from the credential (its subject) and ignores or refuses a self-claimed one. `dev_scopes`
208/// is likewise development-only: ask for exactly these scopes (`[]` for none) instead of administrator.
209/// Unknown fields are ignored, because the credential fields are flattened next to the others.
210#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
211pub struct Authenticate {
212    pub request_id: String,
213    pub protocol_version: u32,
214    pub tenant_id: String,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub client_version: Option<String>,
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub client_id: Option<String>,
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub dev_scopes: Option<Vec<String>>,
221    #[serde(flatten)]
222    pub credentials: Credentials,
223}
224
225impl Authenticate {
226    /// Check the fields. A request that fails is answered `INVALID_REQUEST`.
227    pub fn validate(&self) -> Result<(), &'static str> {
228        let t = &self.tenant_id;
229        if t.is_empty()
230            || t.len() > MAX_TENANT_ID_LEN
231            || !t
232                .chars()
233                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
234        {
235            return Err("tenant_id must be 1 to 64 characters of letters, digits, _ and -");
236        }
237        if let Some(c) = &self.client_id {
238            if c.is_empty()
239                || c.len() > MAX_CLIENT_ID_LEN
240                || !c.chars().all(|ch| {
241                    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | '@' | ':')
242                })
243            {
244                return Err(
245                    "client_id must be 1 to 128 characters of letters, digits and _ - . @ :",
246                );
247            }
248        }
249        match &self.credentials {
250            Credentials::ApiKey { key } if key.is_empty() => Err("the api key must not be empty"),
251            Credentials::Jwt { token } if token.is_empty() => Err("the token must not be empty"),
252            Credentials::Basic { username, password }
253                if username.is_empty() || password.is_empty() =>
254            {
255                Err("username and password must not be empty")
256            }
257            _ => Ok(()),
258        }
259    }
260}
261
262/// Server to client: the answer to [`Authenticate`]. Never carries a secret.
263#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(deny_unknown_fields)]
265pub struct AuthenticationResult {
266    pub request_id: String,
267    pub success: bool,
268    pub message: String,
269    pub server_version: String,
270    /// The identity this session's grants are filed under (the credential's subject).
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub client_id: Option<String>,
273    /// The scopes the session holds, e.g. `service:read` (informational).
274    #[serde(default, skip_serializing_if = "Option::is_none")]
275    pub permissions: Option<Vec<String>>,
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub expires_in_seconds: Option<u64>,
278}
279
280/// Server to client: the flush identified by `request_id` is applied; `seq` is the new sequence.
281#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(deny_unknown_fields)]
283pub struct FlushAck {
284    pub request_id: String,
285    pub file_id: u32,
286    pub seq: u64,
287}
288
289/// Client to server: send the whole file again (no replay log in v1).
290#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(deny_unknown_fields)]
292pub struct Resync {
293    pub request_id: String,
294    pub file_id: u32,
295}
296
297/// Server to client: the last range frame of the snapshot has been sent.
298#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(deny_unknown_fields)]
300pub struct ResyncDone {
301    pub request_id: String,
302    pub file_id: u32,
303    pub seq: u64,
304    pub size: u64,
305}
306
307/// Server to client: a request failed.
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct ErrorReply {
311    pub code: ErrorCode,
312    pub message: String,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub request_id: Option<String>,
315}
316
317/// Client to server: may this service file be mapped locally?
318#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(deny_unknown_fields)]
320pub struct GetServiceFilePath {
321    pub request_id: String,
322    pub service_id: String,
323}
324
325/// Server to client: the `file_id` that range frames use for this service. `path` and `nonce_path`
326/// are both present (local mapping offered) or both absent (stay on WSS ranges). They are absent
327/// unless the service is open to local mapping and the caller holds a write grant on it.
328#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(deny_unknown_fields)]
330pub struct ServiceFilePath {
331    pub request_id: String,
332    pub service_id: String,
333    pub file_id: u32,
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub path: Option<String>,
336    #[serde(default, skip_serializing_if = "Option::is_none")]
337    pub nonce_path: Option<String>,
338}
339
340/// Client to server: the nonce read from `nonce_path`, proving the same filesystem view.
341#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[serde(deny_unknown_fields)]
343pub struct ConfirmLocal {
344    pub request_id: String,
345    pub file_id: u32,
346    pub nonce: String,
347}
348
349impl std::fmt::Debug for ConfirmLocal {
350    // The nonce is a secret for the life of the connection; never print it.
351    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352        f.debug_struct("ConfirmLocal")
353            .field("request_id", &self.request_id)
354            .field("file_id", &self.file_id)
355            .field("nonce", &"<redacted>")
356            .finish()
357    }
358}
359
360/// Server to client: the nonce matched; the server deletes the nonce file. A mismatch is an
361/// `Error` with `PERMISSION_DENIED`.
362#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct ConfirmLocalAck {
365    pub request_id: String,
366    pub file_id: u32,
367}
368
369/// Why a service file is being replaced.
370#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
371#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
372#[non_exhaustive]
373pub enum RotateReason {
374    /// The file changes size. A drain barrier runs first; old-offset writes are never replayed.
375    Resize,
376    /// A client is removed. Same size and shape; old bytes are kept; no drain barrier.
377    Revocation,
378}
379
380/// Client to server: replace a service's file. `new_size` is required for `RESIZE` and forbidden
381/// for `REVOCATION`; `revoke_client_id` is the reverse. See [`RotateRequest::validate`].
382#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(deny_unknown_fields)]
384pub struct RotateRequest {
385    pub request_id: String,
386    pub service_id: String,
387    pub reason: RotateReason,
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub new_size: Option<u64>,
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub revoke_client_id: Option<String>,
392}
393
394impl RotateRequest {
395    /// Check the fields agree with `reason`. A request that fails is answered `INVALID_REQUEST`.
396    pub fn validate(&self) -> Result<(), &'static str> {
397        match self.reason {
398            RotateReason::Resize => match (self.new_size, &self.revoke_client_id) {
399                (Some(n), None) if n > 0 => Ok(()),
400                (Some(0), _) => Err("new_size must be at least 1"),
401                (None, _) => Err("RESIZE needs new_size"),
402                (_, Some(_)) => Err("RESIZE must not name a client"),
403                _ => Err("invalid RESIZE request"),
404            },
405            RotateReason::Revocation => match (self.new_size, &self.revoke_client_id) {
406                (None, Some(c)) if !c.is_empty() => Ok(()),
407                (_, None) => Err("REVOCATION needs revoke_client_id"),
408                (Some(_), _) => Err("REVOCATION must not set new_size"),
409                _ => Err("invalid REVOCATION request"),
410            },
411        }
412    }
413}
414
415/// Server to client: the request was accepted. The rotation ends with [`RotationCommit`].
416#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(deny_unknown_fields)]
418pub struct RotationAccepted {
419    pub request_id: String,
420    pub rotation_id: String,
421}
422
423/// Server to every SDK on the service (resize only): the drain barrier is open. Stop issuing write
424/// guards, wait for outstanding ones, flush into the old file, then send [`RotationDrained`] within
425/// `deadline_ms`. An SDK that does not is dropped with a `ROTATION_STRAGGLER` [`Loss`]. After the
426/// barrier the requester fills the new file; the rotation ends with a [`RotationCommit`] or a
427/// [`RotationAborted`] within `fill_ms` more (see `spec/rotation.md`).
428#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
429#[serde(deny_unknown_fields)]
430pub struct RotationPending {
431    pub rotation_id: String,
432    pub service_id: String,
433    pub file_id: u32,
434    pub deadline_ms: u64,
435    pub fill_ms: u64,
436}
437
438/// Server to the requester only, after the drain barrier (resize only): the new file exists at
439/// `new_file_id`, zero filled, and is not live yet. Fill it from the old file in the new shape, then
440/// send [`RotationFilled`]. A local requester gets `path` and writes it directly; a remote requester
441/// has no `path` and sends range frames for `new_file_id`, ending with `FlushEnd`.
442#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(deny_unknown_fields)]
444pub struct RotationPrepared {
445    pub rotation_id: String,
446    pub service_id: String,
447    pub old_file_id: u32,
448    pub new_file_id: u32,
449    pub size: u64,
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub path: Option<String>,
452}
453
454/// Requester to server: the new file is filled; commit the rotation.
455#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(deny_unknown_fields)]
457pub struct RotationFilled {
458    pub request_id: String,
459    pub rotation_id: String,
460}
461
462/// Requester to server: give up; keep the old file.
463#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
464#[serde(deny_unknown_fields)]
465pub struct RotationAbort {
466    pub request_id: String,
467    pub rotation_id: String,
468}
469
470/// Why a rotation was abandoned.
471#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
472#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
473#[non_exhaustive]
474pub enum AbortReason {
475    /// The requester sent `RotationAbort`.
476    RequesterAbort,
477    /// The requester did not send `RotationFilled` within `fill_ms`.
478    FillTimeout,
479    /// The server could not build or switch to the new file.
480    ServerError,
481    /// A reason this version does not know. Treat the rotation as abandoned.
482    #[serde(other)]
483    Unknown,
484}
485
486/// Server to every member (and the requester): the rotation is abandoned. The old file stays live;
487/// writers resume on it. Nothing was replayed or lost.
488#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
489#[serde(deny_unknown_fields)]
490pub struct RotationAborted {
491    pub rotation_id: String,
492    pub service_id: String,
493    pub file_id: u32,
494    pub reason: AbortReason,
495}
496
497/// Client to server: everything written so far is flushed; `seq` is the last sequence flushed.
498#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
499#[serde(deny_unknown_fields)]
500pub struct RotationDrained {
501    pub request_id: String,
502    pub rotation_id: String,
503    pub file_id: u32,
504    pub seq: u64,
505}
506
507/// Server to every SDK on the service: the new file is live. `new_file_id` differs from
508/// `old_file_id` and is the handle for the new mapping; fetch it with `GetServiceFilePath` (local)
509/// or `Resync` (remote). After a revocation, each remaining SDK writes its unflushed ranges into
510/// the new file; after a resize, old-offset writes are never replayed.
511#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
512#[serde(deny_unknown_fields)]
513pub struct RotationCommit {
514    pub rotation_id: String,
515    pub service_id: String,
516    pub reason: RotateReason,
517    pub old_file_id: u32,
518    pub new_file_id: u32,
519    pub generation: u64,
520    pub size: u64,
521}
522
523/// What a program lost.
524#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
525#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
526#[non_exhaustive]
527pub enum LossKind {
528    /// It did not drain in time (or kept writing to the old mapping); those writes are not replayed.
529    RotationStraggler,
530    /// A resync overwrote local changes; `ranges` names them.
531    ResyncOverwrote,
532    /// Its access was revoked; unflushed writes are dropped.
533    Revoked,
534    /// A kind this version does not know. Treat it as a loss of the whole file.
535    #[serde(other)]
536    Unknown,
537}
538
539/// A byte range inside a file.
540#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
541#[serde(deny_unknown_fields)]
542pub struct LossRange {
543    pub offset: u64,
544    pub len: u64,
545}
546
547/// Server to a client: writes of its own were lost, and why. `ranges` are in old-file coordinates;
548/// empty means the whole file. A receiver treats an unknown `kind` as a loss of the whole file.
549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(deny_unknown_fields)]
551pub struct Loss {
552    pub service_id: String,
553    pub file_id: u32,
554    pub kind: LossKind,
555    #[serde(default, skip_serializing_if = "Vec::is_empty")]
556    pub ranges: Vec<LossRange>,
557}
558
559/// A run of bytes inside a file, named without its data.
560#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct ByteRange {
563    pub offset: u64,
564    pub len: u64,
565}
566
567/// Most ranges one `RangesChanged` or `RangesUpdated` may carry. A flush with more is split into
568/// several messages, each getting its own `seq`.
569pub const MAX_RANGES_PER_MESSAGE: usize = 1024;
570
571/// Check a range list: 1 to [`MAX_RANGES_PER_MESSAGE`] ranges, none empty, none whose end overflows a
572/// u64, sorted by offset and not overlapping (adjacent is fine). A message that fails is answered
573/// `INVALID_REQUEST` (or dropped, when it came from the server).
574pub fn validate_ranges(ranges: &[ByteRange]) -> Result<(), &'static str> {
575    if ranges.is_empty() {
576        return Err("ranges must not be empty");
577    }
578    if ranges.len() > MAX_RANGES_PER_MESSAGE {
579        return Err("too many ranges in one message");
580    }
581    let mut prev_end = 0u64;
582    for r in ranges {
583        if r.len == 0 {
584            return Err("a range must cover at least one byte");
585        }
586        let Some(end) = r.offset.checked_add(r.len) else {
587            return Err("offset plus len overflows");
588        };
589        if r.offset < prev_end {
590            return Err("ranges must be sorted and must not overlap");
591        }
592        prev_end = end;
593    }
594    Ok(())
595}
596
597/// Client to server, from a **local** sender (it wrote into the shared mapping): these ranges
598/// changed. No data travels; the server reads the latest bytes from the file when it forwards.
599/// Answered with `FlushAck`.
600#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(deny_unknown_fields)]
602pub struct RangesChanged {
603    pub request_id: String,
604    pub file_id: u32,
605    pub ranges: Vec<ByteRange>,
606}
607
608/// Server to a **local** receiver: these ranges of the file changed, at `seq`. No data; the receiver
609/// already sees the bytes through its own mapping.
610#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
611#[serde(deny_unknown_fields)]
612pub struct RangesUpdated {
613    pub file_id: u32,
614    pub seq: u64,
615    pub ranges: Vec<ByteRange>,
616}
617
618/// Client to server, from a **remote** sender: the range frames for one flush are complete. The
619/// server assigns the flush one new `seq`, forwards it, and answers `FlushAck`.
620#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
621#[serde(deny_unknown_fields)]
622pub struct FlushEnd {
623    pub request_id: String,
624    pub file_id: u32,
625}
626
627/// Client to server: give `client_id` permissions. With `service_id` they apply to that service only;
628/// without it they are tenant-wide, shorthand for every service of the tenant.
629/// Permission names are `tenant:read|write|admin`, `service:read|write|admin|resize` and so on.
630#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(deny_unknown_fields)]
632pub struct Grant {
633    pub request_id: String,
634    pub client_id: String,
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub service_id: Option<String>,
637    pub permissions: Vec<String>,
638}
639
640impl Grant {
641    /// Check the fields. A request that fails is answered `INVALID_REQUEST`.
642    pub fn validate(&self) -> Result<(), &'static str> {
643        validate_grant_fields(&self.client_id, Some(&self.permissions), true)
644    }
645}
646
647/// Client to server: take permissions away. An empty `permissions` list removes every permission of
648/// `client_id` at that scope. Revoking a client from a **service** also triggers a revocation rotation
649/// of that service alone: the client is removed from the file and gets `Loss{REVOKED}`.
650#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
651#[serde(deny_unknown_fields)]
652pub struct Revoke {
653    pub request_id: String,
654    pub client_id: String,
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub service_id: Option<String>,
657    #[serde(default, skip_serializing_if = "Vec::is_empty")]
658    pub permissions: Vec<String>,
659}
660
661impl Revoke {
662    /// Check the fields. A request that fails is answered `INVALID_REQUEST`.
663    pub fn validate(&self) -> Result<(), &'static str> {
664        validate_grant_fields(&self.client_id, Some(&self.permissions), false)
665    }
666}
667
668fn validate_grant_fields(
669    client_id: &str,
670    permissions: Option<&[String]>,
671    need_permissions: bool,
672) -> Result<(), &'static str> {
673    if client_id.trim().is_empty() {
674        return Err("client_id must not be empty");
675    }
676    if let Some(p) = permissions {
677        if need_permissions && p.is_empty() {
678            return Err("permissions must not be empty");
679        }
680        if p.iter().any(|name| name.trim().is_empty()) {
681            return Err("a permission name must not be blank");
682        }
683    }
684    Ok(())
685}
686
687/// Server to client: the `Grant` or `Revoke` with this `request_id` took effect.
688#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
689#[serde(deny_unknown_fields)]
690pub struct GrantAck {
691    pub request_id: String,
692}
693
694/// Client to server: list grants, optionally of one client and/or one service.
695#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(deny_unknown_fields)]
697pub struct ListGrants {
698    pub request_id: String,
699    #[serde(default, skip_serializing_if = "Option::is_none")]
700    pub client_id: Option<String>,
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub service_id: Option<String>,
703}
704
705/// One client's permissions at one scope (`service_id` absent = tenant-wide).
706#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
707#[serde(deny_unknown_fields)]
708pub struct GrantEntry {
709    pub client_id: String,
710    #[serde(default, skip_serializing_if = "Option::is_none")]
711    pub service_id: Option<String>,
712    pub permissions: Vec<String>,
713}
714
715/// Server to client: the answer to `ListGrants`.
716#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
717#[serde(deny_unknown_fields)]
718pub struct Grants {
719    pub request_id: String,
720    pub entries: Vec<GrantEntry>,
721}
722
723/// Client to server: open (or close) a service to local mapping. Needs `service:admin` on the
724/// service. A service is closed to local mapping until an administrator opens it, because the server
725/// cannot see stores into a mapping: only operating-system file permissions guard it. While open, a
726/// client with a write grant is offered `path` and `nonce_path` by `GetServiceFilePath`; closing it
727/// stops future offers (an existing mapping ends at the next rotation).
728#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
729#[serde(deny_unknown_fields)]
730pub struct SetLocalMapping {
731    pub request_id: String,
732    pub service_id: String,
733    pub enabled: bool,
734}
735
736/// Server to client: the `SetLocalMapping` with this `request_id` took effect.
737#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
738#[serde(deny_unknown_fields)]
739pub struct LocalMappingAck {
740    pub request_id: String,
741    pub service_id: String,
742    pub enabled: bool,
743}
744
745/// JSON control messages, sent as WebSocket text frames. Adjacently tagged
746/// (`{"type": ..., "data": {...}}`); unknown fields and unknown types are refused.
747#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
748#[serde(tag = "type", content = "data")]
749#[non_exhaustive]
750pub enum ControlMessage {
751    Authenticate(Authenticate),
752    AuthenticationResult(AuthenticationResult),
753    FlushAck(FlushAck),
754    Resync(Resync),
755    ResyncDone(ResyncDone),
756    Error(ErrorReply),
757    GetServiceFilePath(GetServiceFilePath),
758    ServiceFilePath(ServiceFilePath),
759    ConfirmLocal(ConfirmLocal),
760    ConfirmLocalAck(ConfirmLocalAck),
761    RotateRequest(RotateRequest),
762    RotationAccepted(RotationAccepted),
763    RotationPending(RotationPending),
764    RotationPrepared(RotationPrepared),
765    RotationFilled(RotationFilled),
766    RotationAbort(RotationAbort),
767    RotationAborted(RotationAborted),
768    RotationDrained(RotationDrained),
769    RotationCommit(RotationCommit),
770    Loss(Loss),
771    RangesChanged(RangesChanged),
772    RangesUpdated(RangesUpdated),
773    FlushEnd(FlushEnd),
774    Grant(Grant),
775    Revoke(Revoke),
776    GrantAck(GrantAck),
777    ListGrants(ListGrants),
778    Grants(Grants),
779    SetLocalMapping(SetLocalMapping),
780    LocalMappingAck(LocalMappingAck),
781}