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 size: u64,
520}
521
522/// What a program lost.
523#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
524#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
525#[non_exhaustive]
526pub enum LossKind {
527    /// It did not drain in time (or kept writing to the old mapping); those writes are not replayed.
528    RotationStraggler,
529    /// A resync overwrote local changes; `ranges` names them.
530    ResyncOverwrote,
531    /// Its access was revoked; unflushed writes are dropped.
532    Revoked,
533    /// A kind this version does not know. Treat it as a loss of the whole file.
534    #[serde(other)]
535    Unknown,
536}
537
538/// A byte range inside a file.
539#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(deny_unknown_fields)]
541pub struct LossRange {
542    pub offset: u64,
543    pub len: u64,
544}
545
546/// Server to a client: writes of its own were lost, and why. `ranges` are in old-file coordinates;
547/// empty means the whole file. A receiver treats an unknown `kind` as a loss of the whole file.
548#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
549#[serde(deny_unknown_fields)]
550pub struct Loss {
551    pub service_id: String,
552    pub file_id: u32,
553    pub kind: LossKind,
554    #[serde(default, skip_serializing_if = "Vec::is_empty")]
555    pub ranges: Vec<LossRange>,
556}
557
558/// A run of bytes inside a file, named without its data.
559#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
560#[serde(deny_unknown_fields)]
561pub struct ByteRange {
562    pub offset: u64,
563    pub len: u64,
564}
565
566/// Most ranges one `RangesChanged` or `RangesUpdated` may carry. A flush with more is split into
567/// several messages, each getting its own `seq`.
568pub const MAX_RANGES_PER_MESSAGE: usize = 1024;
569
570/// Check a range list: 1 to [`MAX_RANGES_PER_MESSAGE`] ranges, none empty, none whose end overflows a
571/// u64, sorted by offset and not overlapping (adjacent is fine). A message that fails is answered
572/// `INVALID_REQUEST` (or dropped, when it came from the server).
573pub fn validate_ranges(ranges: &[ByteRange]) -> Result<(), &'static str> {
574    if ranges.is_empty() {
575        return Err("ranges must not be empty");
576    }
577    if ranges.len() > MAX_RANGES_PER_MESSAGE {
578        return Err("too many ranges in one message");
579    }
580    let mut prev_end = 0u64;
581    for r in ranges {
582        if r.len == 0 {
583            return Err("a range must cover at least one byte");
584        }
585        let Some(end) = r.offset.checked_add(r.len) else {
586            return Err("offset plus len overflows");
587        };
588        if r.offset < prev_end {
589            return Err("ranges must be sorted and must not overlap");
590        }
591        prev_end = end;
592    }
593    Ok(())
594}
595
596/// Client to server, from a **local** sender (it wrote into the shared mapping): these ranges
597/// changed. No data travels; the server reads the latest bytes from the file when it forwards.
598/// Answered with `FlushAck`.
599#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
600#[serde(deny_unknown_fields)]
601pub struct RangesChanged {
602    pub request_id: String,
603    pub file_id: u32,
604    pub ranges: Vec<ByteRange>,
605}
606
607/// Server to a **local** receiver: these ranges of the file changed, at `seq`. No data; the receiver
608/// already sees the bytes through its own mapping.
609#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
610#[serde(deny_unknown_fields)]
611pub struct RangesUpdated {
612    pub file_id: u32,
613    pub seq: u64,
614    pub ranges: Vec<ByteRange>,
615}
616
617/// Client to server, from a **remote** sender: the range frames for one flush are complete. The
618/// server assigns the flush one new `seq`, forwards it, and answers `FlushAck`.
619#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
620#[serde(deny_unknown_fields)]
621pub struct FlushEnd {
622    pub request_id: String,
623    pub file_id: u32,
624}
625
626/// Client to server: give `client_id` permissions. With `service_id` they apply to that service only;
627/// without it they are tenant-wide, shorthand for every service of the tenant.
628/// Permission names are `tenant:read|write|admin`, `service:read|write|admin|resize` and so on.
629#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(deny_unknown_fields)]
631pub struct Grant {
632    pub request_id: String,
633    pub client_id: String,
634    #[serde(default, skip_serializing_if = "Option::is_none")]
635    pub service_id: Option<String>,
636    pub permissions: Vec<String>,
637}
638
639impl Grant {
640    /// Check the fields. A request that fails is answered `INVALID_REQUEST`.
641    pub fn validate(&self) -> Result<(), &'static str> {
642        validate_grant_fields(&self.client_id, Some(&self.permissions), true)
643    }
644}
645
646/// Client to server: take permissions away. An empty `permissions` list removes every permission of
647/// `client_id` at that scope. Revoking a client from a **service** also triggers a revocation rotation
648/// of that service alone: the client is removed from the file and gets `Loss{REVOKED}`.
649#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650#[serde(deny_unknown_fields)]
651pub struct Revoke {
652    pub request_id: String,
653    pub client_id: String,
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub service_id: Option<String>,
656    #[serde(default, skip_serializing_if = "Vec::is_empty")]
657    pub permissions: Vec<String>,
658}
659
660impl Revoke {
661    /// Check the fields. A request that fails is answered `INVALID_REQUEST`.
662    pub fn validate(&self) -> Result<(), &'static str> {
663        validate_grant_fields(&self.client_id, Some(&self.permissions), false)
664    }
665}
666
667fn validate_grant_fields(
668    client_id: &str,
669    permissions: Option<&[String]>,
670    need_permissions: bool,
671) -> Result<(), &'static str> {
672    if client_id.trim().is_empty() {
673        return Err("client_id must not be empty");
674    }
675    if let Some(p) = permissions {
676        if need_permissions && p.is_empty() {
677            return Err("permissions must not be empty");
678        }
679        if p.iter().any(|name| name.trim().is_empty()) {
680            return Err("a permission name must not be blank");
681        }
682    }
683    Ok(())
684}
685
686/// Server to client: the `Grant` or `Revoke` with this `request_id` took effect.
687#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
688#[serde(deny_unknown_fields)]
689pub struct GrantAck {
690    pub request_id: String,
691}
692
693/// Client to server: list grants, optionally of one client and/or one service.
694#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
695#[serde(deny_unknown_fields)]
696pub struct ListGrants {
697    pub request_id: String,
698    #[serde(default, skip_serializing_if = "Option::is_none")]
699    pub client_id: Option<String>,
700    #[serde(default, skip_serializing_if = "Option::is_none")]
701    pub service_id: Option<String>,
702}
703
704/// One client's permissions at one scope (`service_id` absent = tenant-wide).
705#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
706#[serde(deny_unknown_fields)]
707pub struct GrantEntry {
708    pub client_id: String,
709    #[serde(default, skip_serializing_if = "Option::is_none")]
710    pub service_id: Option<String>,
711    pub permissions: Vec<String>,
712}
713
714/// Server to client: the answer to `ListGrants`.
715#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(deny_unknown_fields)]
717pub struct Grants {
718    pub request_id: String,
719    pub entries: Vec<GrantEntry>,
720}
721
722/// Client to server: open (or close) a service to local mapping. Needs `service:admin` on the
723/// service. A service is closed to local mapping until an administrator opens it, because the server
724/// cannot see stores into a mapping: only operating-system file permissions guard it. While open, a
725/// client with a write grant is offered `path` and `nonce_path` by `GetServiceFilePath`; closing it
726/// stops future offers (an existing mapping ends at the next rotation).
727#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
728#[serde(deny_unknown_fields)]
729pub struct SetLocalMapping {
730    pub request_id: String,
731    pub service_id: String,
732    pub enabled: bool,
733}
734
735/// Server to client: the `SetLocalMapping` with this `request_id` took effect.
736#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(deny_unknown_fields)]
738pub struct LocalMappingAck {
739    pub request_id: String,
740    pub service_id: String,
741    pub enabled: bool,
742}
743
744/// JSON control messages, sent as WebSocket text frames. Adjacently tagged
745/// (`{"type": ..., "data": {...}}`); unknown fields and unknown types are refused.
746#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
747#[serde(tag = "type", content = "data")]
748#[non_exhaustive]
749pub enum ControlMessage {
750    Authenticate(Authenticate),
751    AuthenticationResult(AuthenticationResult),
752    FlushAck(FlushAck),
753    Resync(Resync),
754    ResyncDone(ResyncDone),
755    Error(ErrorReply),
756    GetServiceFilePath(GetServiceFilePath),
757    ServiceFilePath(ServiceFilePath),
758    ConfirmLocal(ConfirmLocal),
759    ConfirmLocalAck(ConfirmLocalAck),
760    RotateRequest(RotateRequest),
761    RotationAccepted(RotationAccepted),
762    RotationPending(RotationPending),
763    RotationPrepared(RotationPrepared),
764    RotationFilled(RotationFilled),
765    RotationAbort(RotationAbort),
766    RotationAborted(RotationAborted),
767    RotationDrained(RotationDrained),
768    RotationCommit(RotationCommit),
769    Loss(Loss),
770    RangesChanged(RangesChanged),
771    RangesUpdated(RangesUpdated),
772    FlushEnd(FlushEnd),
773    Grant(Grant),
774    Revoke(Revoke),
775    GrantAck(GrantAck),
776    ListGrants(ListGrants),
777    Grants(Grants),
778    SetLocalMapping(SetLocalMapping),
779    LocalMappingAck(LocalMappingAck),
780}