FUSE

A mount made on the server’s request, and kinds 12 to 18: the seven operations by which the server serves the files and directories the caller holds live, each answered with exactly one message.

The server makes every mount by one request on /fuse/mount. Seven asks carry the operations of the mounts made. Every ask names a mount by its id and an entry by its path. The server answers every ask with exactly one message.

KindAskPayload after the kindAnswered onAnswer
18stat[id_len: u16, big-endian][id …][path …]/fuse/stat/{channel}0 and nine bytes; 1; or 2 and a message
12read[id_len: u16, big-endian][id …][path …]/fuse/read/{channel}0 and the bytes; 1; or 2 and a message
13write[id_len: u16, big-endian][id …][path_len: u16, big-endian][path …][bytes …]/fuse/write/{channel}0; or 1 and a message
14list[id_len: u16, big-endian][id …][path …]/fuse/list/{channel}0 and the entries; 1; or 2 and a message
15remove[id_len: u16, big-endian][id …][path …]/fuse/remove/{channel}0; or 1 and a message
16rename[id_len: u16, big-endian][id …][from_len: u16, big-endian][from …][to …]/fuse/rename/{channel}0; or 1 and a message
17mkdir[id_len: u16, big-endian][id …][path …]/fuse/mkdir/{channel}0; or 1 and a message
  • The id. The id with which the mount was requested, UTF-8, carried on every ask for that mount.
  • The path. The path of the entry relative to the root of the mount: components separated by /, without a leading /, with no empty component and no . or .. component. The path is empty for a file mount and for the root of a directory mount. Where the path is the last field of the payload it runs to the end of the payload; where bytes or a second path follow it, a length prefix precedes it.
  • A file mount. For a mount requested with kind file, the proxy asks only stat, read and write, and every such ask carries the empty path. For a mount requested with kind directory, the proxy asks all seven operations, each carrying the entry’s path.
  • A read-only mount. For a mount requested with readonly true, the proxy asks no write, no remove, no rename and no mkdir.
  • One message. The server sends exactly one message, the frame its page defines, and closes the connection. A path that the proxy or the server closes cleanly with no message before the close states that the ask was not served.
  • No repetition. The proxy does not ask an operation again. An operation whose path ended abruptly, and an operation whose /requests connection ended before the server opened its path, are reported inside the container as failed.
  • A file is one message. A read answer carries the whole file in one message, and a write ask carries the whole file in one message. Neither is paged or split.

The target of stat, read, list, remove and mkdir is defined by diverge-provider-sdk/src/shared/containers/fuse/target.rs:

//! What an ask names: a mount, and an entry in it.

use super::{RequestEncodeError, RequestError, prefixed};
use crate::encode::{Encode, Writer};

/// One entry of one mount: the mount's id, and the entry's path
/// relative to the mount root — empty for a file mount, and for a
/// directory mount's root.
///
/// ```text
/// [id_len: u16 BE][id: utf8…][path: utf8…]
/// ```
///
/// The path runs to the end of the payload, so it needs no prefix and
/// takes any length. This is the whole ask for [`read`](super::read),
/// [`list`](super::list), [`remove`](super::remove) and
/// [`mkdir`](super::mkdir), each of which is answered by its own
/// frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Target<'a> {
    /// The mount's id.
    pub id: &'a str,
    /// The entry's path inside the mount, `/`-separated, no leading
    /// slash; empty is the mount itself.
    pub path: &'a str,
}

impl Encode for Target<'_> {
    /// One way to fail: an id longer than the length prefix holds.
    type Error = RequestEncodeError;

    fn encode(&self, out: &mut Writer<'_>) -> Result<(), RequestEncodeError> {
        prefixed::put(out, self.id.as_bytes()).map_err(RequestEncodeError::IdLength)?;
        out.extend_from_slice(self.path.as_bytes());
        Ok(())
    }
}

impl<'a> Target<'a> {
    /// Decode from the bytes after the ask's kind. The id and the path
    /// borrow from `bytes`.
    pub fn decode(bytes: &'a [u8]) -> Result<Self, RequestError> {
        let (id, path) = prefixed::take(bytes)?;
        Ok(Target {
            id: std::str::from_utf8(id).map_err(|_| RequestError::IdUtf8)?,
            path: std::str::from_utf8(path).map_err(|_| RequestError::PathUtf8)?,
        })
    }
}

An entry of a listing, and the Kind — one byte on these answers, and the string "file" or "directory" on a mount request — are defined by diverge-provider-sdk/src/shared/containers/fuse/entry.rs:

//! One entry of a listed directory.

use serde::{Deserialize, Serialize};

use super::{ResponseEncodeError, ResponseError, prefixed};
use crate::encode::Writer;

/// What an entry is — and, on a
/// [`mount`](crate::container_proxy::fuse::mount) request, which kind
/// of mount: one file, or a tree. One byte on the binary answers;
/// `"file"` or `"directory"` where it rides JSON.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
    /// A regular file. Kind `0`.
    File,
    /// A directory. Kind `1`.
    Directory,
}

/// Kind byte for a file.
const FILE: u8 = 0;

/// Kind byte for a directory.
const DIRECTORY: u8 = 1;

impl Kind {
    /// The one byte that says which.
    pub(crate) fn byte(self) -> u8 {
        match self {
            Kind::File => FILE,
            Kind::Directory => DIRECTORY,
        }
    }

    /// Which, from its byte.
    pub(crate) fn from_byte(byte: u8) -> Result<Self, ResponseError> {
        match byte {
            FILE => Ok(Kind::File),
            DIRECTORY => Ok(Kind::Directory),
            other => Err(ResponseError::UnknownKind(other)),
        }
    }
}

/// One entry of a directory, as a [`list`](super::list) answers it.
///
/// ```text
/// [kind: u8][name_len: u16 BE][name: utf8…]
/// ```
///
/// The name is the entry's own, one path component, never `.` or
/// `..`. What a listing says is what a `readdir` needs and no more;
/// an entry's size is a [`stat`](super::stat) of its own.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Entry<'a> {
    /// The entry's name.
    pub name: &'a str,
    /// File or directory.
    pub kind: Kind,
}

impl Entry<'_> {
    /// Write one entry.
    pub(crate) fn encode(&self, out: &mut Writer<'_>) -> Result<(), ResponseEncodeError> {
        out.extend_from_slice(&[self.kind.byte()]);
        prefixed::put(out, self.name.as_bytes()).map_err(ResponseEncodeError::NameLength)
    }
}

impl<'a> Entry<'a> {
    /// Split one entry off the front: the entry, then the rest.
    pub(crate) fn decode(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ResponseError> {
        let (kind, rest) = bytes.split_first().ok_or(ResponseError::Truncated)?;
        let kind = Kind::from_byte(*kind)?;
        let (name, rest) = prefixed::take(rest).map_err(|_| ResponseError::Truncated)?;
        Ok((
            Entry {
                name: std::str::from_utf8(name).map_err(|_| ResponseError::NameUtf8)?,
                kind,
            },
            rest,
        ))
    }
}

The nine bytes of a stat answer are defined by diverge-provider-sdk/src/shared/containers/fuse/stat/stat.rs:

//! What an entry is and how long it is.

use super::super::{Kind, ResponseError};
use crate::encode::Writer;

/// An entry's kind and size, as a [`stat`](super) answers them.
///
/// ```text
/// [kind: u8][size: u64 BE]
/// ```
///
/// The size is the file's byte length, and `0` for a directory. Nine
/// bytes, fixed: everything a `stat(2)` on the mount needs that the
/// proxy does not answer itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Stat {
    /// File or directory.
    pub kind: Kind,
    /// The file's length in bytes; `0` for a directory.
    pub size: u64,
}

/// The bytes a stat occupies.
const FIXED: usize = 1 + 8;

impl Stat {
    /// Write the nine bytes.
    pub(crate) fn encode(&self, out: &mut Writer<'_>) {
        out.extend_from_slice(&[self.kind.byte()]);
        out.extend_from_slice(&self.size.to_be_bytes());
    }

    /// Read the nine bytes off the front: the stat, then the rest.
    pub(crate) fn decode(bytes: &[u8]) -> Result<(Self, &[u8]), ResponseError> {
        let fixed = bytes.get(..FIXED).ok_or(ResponseError::Truncated)?;
        let kind = Kind::from_byte(fixed[0])?;
        let size: [u8; 8] = fixed[1..].try_into().expect("eight bytes were taken");
        Ok((
            Stat {
                kind,
                size: u64::from_be_bytes(size),
            },
            &bytes[FIXED..],
        ))
    }
}

The answer to write, remove, rename and mkdir is defined by diverge-provider-sdk/src/shared/containers/fuse/ack/frame.rs:

//! Ok, or why not.

use std::convert::Infallible;

use super::super::ResponseError;
use crate::encode::{Encode, Writer};

/// The one message that answers a mutation.
///
/// ```text
/// [kind: u8][message…]
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Frame<'a> {
    /// Kind `0`. It happened: the caller holds the result.
    Ok,
    /// Kind `1`. It did not, and this says why, for a reader rather
    /// than a program: what a caller can refuse — a read-only mount it
    /// was written to anyway, a directory it will not empty, a path it
    /// will not serve — is its policy and not this specification's.
    Error(&'a str),
}

impl Encode for Frame<'_> {
    /// [`Infallible`]: a kind byte and bytes copied.
    type Error = Infallible;

    fn encode(&self, out: &mut Writer<'_>) -> Result<(), Infallible> {
        match self {
            Frame::Ok => out.extend_from_slice(&[0]),
            Frame::Error(message) => {
                out.extend_from_slice(&[1]);
                out.extend_from_slice(message.as_bytes());
            }
        }
        Ok(())
    }
}

impl<'a> Frame<'a> {
    /// Decode the one message. The message borrows from `bytes`.
    pub fn decode(bytes: &'a [u8]) -> Result<Self, ResponseError> {
        let (kind, rest) = bytes.split_first().ok_or(ResponseError::Empty)?;
        match *kind {
            0 => Ok(Frame::Ok),
            1 => std::str::from_utf8(rest)
                .map(Frame::Error)
                .map_err(|_| ResponseError::MessageUtf8),
            other => Err(ResponseError::UnknownKind(other)),
        }
    }
}

The ways in which an ask or an answer fails to decode are defined by diverge-provider-sdk/src/shared/containers/fuse/error.rs:

//! Why a fuse ask or answer could not be read or written.

use std::error;
use std::fmt;

/// A fuse ask that could not be read.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequestError {
    /// Fewer bytes than the ask's fixed part promises — a length
    /// prefix, or the id or path a prefix said was there.
    Truncated,
    /// An id that is not UTF-8.
    IdUtf8,
    /// A path that is not UTF-8.
    PathUtf8,
}

impl fmt::Display for RequestError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RequestError::Truncated => {
                f.write_str("fuse request is shorter than it promises")
            }
            RequestError::IdUtf8 => f.write_str("fuse mount id is not utf-8"),
            RequestError::PathUtf8 => f.write_str("fuse path is not utf-8"),
        }
    }
}

impl error::Error for RequestError {}

/// A fuse ask that could not be written.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RequestEncodeError {
    /// An id of more bytes than a two-byte length prefix can say,
    /// carrying how many there were.
    IdLength(usize),
    /// A path of more bytes than a two-byte length prefix can say,
    /// carrying how many there were. Only the asks where something
    /// follows the path prefix it; a path that ends the payload takes
    /// any length.
    PathLength(usize),
}

impl fmt::Display for RequestEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RequestEncodeError::IdLength(len) => {
                write!(f, "fuse mount id is {len} bytes, more than 65535")
            }
            RequestEncodeError::PathLength(len) => {
                write!(f, "fuse path is {len} bytes, more than 65535")
            }
        }
    }
}

impl error::Error for RequestEncodeError {}

/// A fuse answer that could not be read.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResponseError {
    /// No bytes at all, so not even a kind.
    Empty,
    /// A kind this answer does not define — of the message, or of an
    /// entry in a listing.
    UnknownKind(u8),
    /// An error message that is not UTF-8.
    MessageUtf8,
    /// A listing shorter than its counts and lengths promise, or a
    /// stat shorter than its nine bytes.
    Truncated,
    /// An entry's name that is not UTF-8.
    NameUtf8,
}

impl fmt::Display for ResponseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ResponseError::Empty => f.write_str("fuse response is empty"),
            ResponseError::UnknownKind(kind) => {
                write!(f, "unknown fuse response kind {kind}")
            }
            ResponseError::MessageUtf8 => {
                f.write_str("fuse error message is not utf-8")
            }
            ResponseError::Truncated => {
                f.write_str("fuse answer is shorter than it promises")
            }
            ResponseError::NameUtf8 => f.write_str("fuse entry name is not utf-8"),
        }
    }
}

impl error::Error for ResponseError {}

/// A fuse answer that could not be written.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResponseEncodeError {
    /// An entry's name of more bytes than a two-byte length prefix can
    /// say, carrying how many there were.
    NameLength(usize),
}

impl fmt::Display for ResponseEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ResponseEncodeError::NameLength(len) => {
                write!(f, "fuse entry name is {len} bytes, more than 65535")
            }
        }
    }
}

impl error::Error for ResponseEncodeError {}