Response

The server sends exactly one response, whose payload begins with the byte 0 followed by the stat in the postcard wire format or with the byte 1 followed by an error as one JSON value, and the response finish follows it.

The server sends exactly one response on the scope. The response finish follows the response, and no frame follows the finish.

[0][stat, postcard …]
[1][error JSON …]
  • The sequence. Exactly one response precedes the response finish, and nothing follows the finish. A response finish that no response precedes states that the request was not served, as Endpoints provides.
  • The stat. A payload whose first byte is 0 carries the stat after that byte, running to the end of the payload. The stat is the Stat defined below, encoded as the postcard reading of Notation provides: name as a varint byte length followed by that many bytes of UTF-8, followed by bytes as a varint, followed by created as a varint, followed by bytes_used as a varint, followed by dirhash as a varint byte length followed by that many bytes of UTF-8.
  • name, bytes, created. The volume as the client’s listing reports it, in the form the volumes::list response defines. The three values equal those the listing reports for the volume at the time of the response.
  • bytes_used. The number of bytes of the volume in use, at the time of the response.
  • dirhash. The hash of the content of the volume, at the time of the response. It is the SHA-256 of the manifest of the volume, encoded as base64url without padding. The manifest is one line per file of the volume, each line <hash> <size> <path>: <hash> is the SHA-256 of the bytes of the file, encoded as base64url without padding; <size> is the length of the file in bytes, in decimal; <path> is the path of the file relative to the root of the volume, its components separated by /. The lines are sorted bytewise. A volume with no file has the manifest of zero bytes, and its dirhash is 47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU. Two volumes with one dirhash have one content; two volumes with different dirhash values have different content.
  • The error. A payload whose first byte is 1 carries an error after that byte, running to the end of the payload. The error is exactly one JSON value, in the form defined on the volumes::list response page. This revision prescribes nothing about its content.
  • Malformed. A payload with no byte, a payload whose first byte is neither 0 nor 1, a payload whose bytes after 0 do not decode as the stat, and a payload whose bytes after 1 are not one JSON value are malformed.

The response frame is defined by diverge-provider-sdk/src/endpoints/volumes/stat/server/response/frame.rs:

//! What a server's response frame carries for a volume stat.

use std::fmt;

use super::Stat;
use crate::decode::Decode;
use crate::encode::{Encode, Writer};
use crate::shared::error::Error;

/// The volume examined, or the news that it could not be.
///
/// One of these on channel `0`, then the scope finishes. A payload
/// leads with one byte saying which — `0` for [`Stat`](Self::Stat),
/// `1` for [`Error`](Self::Error) — and the rest is that variant's
/// own bytes.
///
/// A stat is not a stream: a provider walks the volume once and says
/// what it found, so there is nothing to discover incrementally and
/// nothing to hold a channel open for.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// The volume examined. Tag `0`.
    Stat(Stat),
    /// A failure. Tag `1`.
    ///
    /// A volume the caller cannot see is one, and so is a walk the
    /// provider could not finish. See
    /// [`shared::error::Error`](crate::shared::error::Error) for why
    /// it says so little.
    Error(Error),
}

/// Tag for [`Frame::Stat`].
const STAT: u8 = 0;

/// Tag for [`Frame::Error`].
const ERROR: u8 = 1;

/// Two variants, two formats, and the tag chooses between them.
///
/// The stat is **postcard**, matching the rest of
/// [`volumes`](crate::endpoints::volumes): this relays nothing, so no
/// byte of it has to survive a round trip unchanged, and nothing
/// downstream reads it as text.
///
/// The error is JSON, and has to be. A
/// [`serde_json::Value`] deserializes through `deserialize_any`, which
/// a format with no self-description cannot answer — so postcard can
/// carry the stat and cannot carry the failure, and each variant gets
/// the format it needs.
impl Encode for Frame {
    /// One failure per half, and they are different libraries'.
    type Error = FrameEncodeError;

    // Spelled out rather than `Self::Error`: this enum has a variant
    // called `Error`, so the associated type is ambiguous by that name.
    fn encode(
        &self,
        out: &mut Writer<'_>,
    ) -> Result<(), FrameEncodeError> {
        match self {
            Frame::Stat(stat) => {
                out.extend_from_slice(&[STAT]);
                postcard::to_io(stat, &mut *out)
                    .map(|_| ())
                    .map_err(FrameEncodeError::Stat)
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out).map_err(FrameEncodeError::Error)
            }
        }
    }
}

impl Decode<'_> for Frame {
    /// Four ways to fail, and each names which half failed.
    type Error = FrameDecodeError;

    // Spelled out for the same reason as `encode` above.
    fn decode(bytes: &[u8]) -> Result<Self, FrameDecodeError> {
        let (tag, rest) = bytes.split_first().ok_or(FrameDecodeError::Empty)?;
        match *tag {
            STAT => postcard::from_bytes(rest)
                .map(Frame::Stat)
                .map_err(FrameDecodeError::Stat),
            ERROR => Error::decode(rest)
                .map(Frame::Error)
                .map_err(FrameDecodeError::Error),
            tag => Err(FrameDecodeError::UnknownTag(tag)),
        }
    }
}

/// A volume stat that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
    /// The stat did not serialize.
    Stat(postcard::Error),
    /// The error did not serialize.
    Error(serde_json::Error),
}

impl fmt::Display for FrameEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameEncodeError::Stat(error) => {
                write!(f, "volume stat did not serialize: {error}")
            }
            FrameEncodeError::Error(error) => {
                write!(f, "volume stat error did not serialize: {error}")
            }
        }
    }
}

impl std::error::Error for FrameEncodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameEncodeError::Stat(error) => Some(error),
            FrameEncodeError::Error(error) => Some(error),
        }
    }
}

/// A volume stat that could not be read.
#[derive(Debug)]
pub enum FrameDecodeError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is neither of this frame's two.
    UnknownTag(u8),
    /// The stat did not parse.
    Stat(postcard::Error),
    /// The error did not parse.
    Error(serde_json::Error),
}

impl fmt::Display for FrameDecodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameDecodeError::Empty => {
                f.write_str("volume stat response frame is empty")
            }
            FrameDecodeError::UnknownTag(tag) => {
                write!(f, "unknown volume stat response frame tag {tag}")
            }
            FrameDecodeError::Stat(error) => {
                write!(f, "volume stat did not parse: {error}")
            }
            FrameDecodeError::Error(error) => {
                write!(f, "volume stat error did not parse: {error}")
            }
        }
    }
}

impl std::error::Error for FrameDecodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameDecodeError::Stat(error) => Some(error),
            FrameDecodeError::Error(error) => Some(error),
            FrameDecodeError::Empty | FrameDecodeError::UnknownTag(_) => None,
        }
    }
}

The stat is defined by diverge-provider-sdk/src/endpoints/volumes/stat/server/response/stat.rs. Its volume field is the Volume defined on the volumes::list response page:

//! One volume, examined.

use serde::{Deserialize, Serialize};

use crate::endpoints::volumes::list::server::response::Volume;

/// What a listing says about a volume, and the two things it does not.
///
/// [`volume`](Self::volume) is the listing's own record of it, the same
/// three fields a [`list`](crate::endpoints::volumes::list) reports;
/// [`bytes_used`](Self::bytes_used) and [`dirhash`](Self::dirhash) are
/// what a listing leaves out, because each costs a walk of the volume
/// and a stat pays for one volume's walk on purpose.
///
/// # Nested rather than flattened
///
/// The listing's [`Volume`] sits inside this as a field rather than
/// having its fields copied in, so there is one definition of what a
/// listing says. Postcard writes a nested struct as its fields in
/// place, so the wire reads `name`, `bytes`, `created`, then the two
/// fields of its own — a listing's record with two fields appended.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Stat {
    /// The volume as a listing reports it: its name, its size, and
    /// when it came into being.
    pub volume: Volume,
    /// How much of it is in use, in BYTES.
    ///
    /// The one field of a volume that changes on its own: writing
    /// inside the volume moves it, and nothing in this protocol
    /// reports when. It is what a stat was asked for, as of the
    /// moment the provider walked the volume.
    pub bytes_used: u64,
    /// The hash of the volume's content, at the time of the stat.
    ///
    /// The base64url SHA-256, unpadded, of the volume's manifest: one
    /// `<hash> <size> <path>` line per file, `<hash>` the base64url
    /// SHA-256 of the file's bytes, `<size>` its length in bytes,
    /// `<path>` relative to the volume's root and `/`-separated, the
    /// lines sorted bytewise. It is the hash half of the directory
    /// identity an
    /// [`IdentityMount`](crate::shared::containers::request::IdentityMount)
    /// carries, without the size — the listing already reports size.
    ///
    /// A volume with no file has the hash of the empty manifest,
    /// `47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU`, which is what a
    /// [`create`](crate::endpoints::volumes::create) just made.
    ///
    /// # Why a hash rather than a version
    ///
    /// Two stats with one `dirhash` have one content between them;
    /// two with different ones do not. That is the whole of what it
    /// says, and a counter could not say it across providers.
    pub dirhash: String,
}