351 lines
10 KiB
Rust
351 lines
10 KiB
Rust
//! Transport-sized request, response, and provider boundary types.
|
|
|
|
use crate::{CONTENT_BYTES, DATA_BYTES};
|
|
|
|
/// Exact MAVLink endpoint that owns the virtual-file service.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct Target {
|
|
system: u8,
|
|
component: u8,
|
|
}
|
|
|
|
impl Target {
|
|
/// Creates one exact, non-broadcast endpoint.
|
|
pub const fn new(system: u8, component: u8) -> Self {
|
|
assert!(system != 0 && component != 0, "file target must be exact");
|
|
Self { system, component }
|
|
}
|
|
|
|
/// Parses an exact endpoint from an untrusted request.
|
|
pub const fn try_new(system: u8, component: u8) -> Option<Self> {
|
|
if system == 0 || component == 0 {
|
|
None
|
|
} else {
|
|
Some(Self { system, component })
|
|
}
|
|
}
|
|
|
|
/// Target system identity.
|
|
pub const fn system(self) -> u8 {
|
|
self.system
|
|
}
|
|
|
|
/// Target component identity.
|
|
pub const fn component(self) -> u8 {
|
|
self.component
|
|
}
|
|
}
|
|
|
|
/// Exact MAVLink source that owns an open transfer.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct Principal {
|
|
system: u8,
|
|
component: u8,
|
|
}
|
|
|
|
impl Principal {
|
|
/// Creates one exact source identity.
|
|
pub const fn new(system: u8, component: u8) -> Self {
|
|
assert!(
|
|
system != 0 && component != 0,
|
|
"file principal must be exact"
|
|
);
|
|
Self { system, component }
|
|
}
|
|
|
|
/// Parses an exact source identity from an untrusted frame.
|
|
pub const fn try_new(system: u8, component: u8) -> Option<Self> {
|
|
if system == 0 || component == 0 {
|
|
None
|
|
} else {
|
|
Some(Self { system, component })
|
|
}
|
|
}
|
|
|
|
pub(crate) const fn target(self) -> Target {
|
|
Target {
|
|
system: self.system,
|
|
component: self.component,
|
|
}
|
|
}
|
|
|
|
/// Source system identity.
|
|
pub const fn system(self) -> u8 {
|
|
self.system
|
|
}
|
|
|
|
/// Source component identity.
|
|
pub const fn component(self) -> u8 {
|
|
self.component
|
|
}
|
|
}
|
|
|
|
/// File operation carried by `FILE_CONTROL`.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
#[repr(u8)]
|
|
pub enum FileOperation {
|
|
/// Opens a stable read snapshot selected by the supplied path.
|
|
OpenRead = 1,
|
|
/// Opens a bounded write staging area selected by the supplied path.
|
|
OpenWrite = 2,
|
|
/// Requests the chunk beginning at `position`.
|
|
Read = 3,
|
|
/// Acknowledges one accepted upload chunk.
|
|
WriteAck = 4,
|
|
/// Completes an open transfer.
|
|
Close = 5,
|
|
/// Discards an open transfer.
|
|
Abort = 6,
|
|
}
|
|
|
|
/// Authoritative result carried by a control response.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
#[repr(u8)]
|
|
pub enum FileStatus {
|
|
/// The operation succeeded.
|
|
Ok = 1,
|
|
/// Another principal owns the single transfer slot.
|
|
Busy = 2,
|
|
/// The transfer identity is absent, stale, or owned by another principal.
|
|
InvalidTransfer = 3,
|
|
/// The operation is not valid for the transfer's current mode or state.
|
|
InvalidState = 4,
|
|
/// The open request did not carry one canonical bounded path.
|
|
InvalidPath = 5,
|
|
/// No virtual file exists at the supplied path.
|
|
NotFound = 6,
|
|
/// The declared complete file exceeds the provider's bound.
|
|
TooLarge = 7,
|
|
/// A chunk or read did not start at an admissible position.
|
|
InvalidPosition = 8,
|
|
/// The complete sequential upload disagrees with its declared CRC-32C.
|
|
ChecksumMismatch = 9,
|
|
/// The provider rejected the complete file contents.
|
|
ContentRejected = 10,
|
|
/// Durable storage failed; the resulting durable state may be unknown.
|
|
StorageFailure = 11,
|
|
/// A retry reused an operation identity with different content.
|
|
RequestConflict = 12,
|
|
/// A provider or service invariant failed.
|
|
Internal = 13,
|
|
}
|
|
|
|
/// Bounded opaque response detail or open-request path storage.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct Content {
|
|
length: u8,
|
|
bytes: [u8; CONTENT_BYTES],
|
|
}
|
|
|
|
impl Content {
|
|
/// Empty content.
|
|
pub const EMPTY: Self = Self {
|
|
length: 0,
|
|
bytes: [0; CONTENT_BYTES],
|
|
};
|
|
|
|
/// Copies a bounded byte sequence.
|
|
pub fn try_new(bytes: &[u8]) -> Option<Self> {
|
|
if bytes.len() > CONTENT_BYTES {
|
|
return None;
|
|
}
|
|
let mut content = Self::EMPTY;
|
|
content.bytes[..bytes.len()].copy_from_slice(bytes);
|
|
content.length = bytes.len() as u8;
|
|
Some(content)
|
|
}
|
|
|
|
/// Parses canonical length-delimited wire content.
|
|
pub(crate) fn from_wire(length: u8, bytes: [u8; CONTENT_BYTES]) -> Option<Self> {
|
|
let length = usize::from(length);
|
|
if length > CONTENT_BYTES || bytes[length..].iter().any(|byte| *byte != 0) {
|
|
return None;
|
|
}
|
|
Some(Self {
|
|
length: length as u8,
|
|
bytes,
|
|
})
|
|
}
|
|
|
|
/// Logical initialized bytes.
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
&self.bytes[..usize::from(self.length)]
|
|
}
|
|
|
|
/// Logical byte count.
|
|
pub(crate) const fn len(self) -> u8 {
|
|
self.length
|
|
}
|
|
|
|
/// Whether the logical content is empty.
|
|
pub(crate) const fn is_empty(self) -> bool {
|
|
self.length == 0
|
|
}
|
|
|
|
/// Zero-padded wire storage.
|
|
pub(crate) const fn wire_bytes(self) -> [u8; CONTENT_BYTES] {
|
|
self.bytes
|
|
}
|
|
}
|
|
|
|
/// Canonical absolute ASCII virtual-file path.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct FilePath(Content);
|
|
|
|
impl FilePath {
|
|
/// Validates a bounded absolute path without normalization or allocation.
|
|
pub(crate) fn try_from_content(content: Content) -> Option<Self> {
|
|
let bytes = content.as_bytes();
|
|
if bytes.len() < 2 || bytes[0] != b'/' || bytes[bytes.len() - 1] == b'/' {
|
|
return None;
|
|
}
|
|
let mut segment_start = 1;
|
|
for (index, byte) in bytes.iter().copied().enumerate().skip(1) {
|
|
if byte == b'/' {
|
|
if index == segment_start || reserved_segment(&bytes[segment_start..index]) {
|
|
return None;
|
|
}
|
|
segment_start = index + 1;
|
|
} else if !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'_' | b'-')
|
|
{
|
|
return None;
|
|
}
|
|
}
|
|
if reserved_segment(&bytes[segment_start..]) {
|
|
return None;
|
|
}
|
|
Some(Self(content))
|
|
}
|
|
|
|
/// Canonical path bytes.
|
|
pub fn as_bytes(&self) -> &[u8] {
|
|
self.0.as_bytes()
|
|
}
|
|
}
|
|
|
|
fn reserved_segment(segment: &[u8]) -> bool {
|
|
segment == b"." || segment == b".."
|
|
}
|
|
|
|
/// One decoded control request.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ControlRequest {
|
|
/// Request-selected operation.
|
|
pub operation: FileOperation,
|
|
/// Exact addressed service endpoint.
|
|
pub target: Target,
|
|
/// Transfer-relative byte position.
|
|
pub position: u32,
|
|
/// Complete file size declared by an open-write request.
|
|
pub file_size: u32,
|
|
/// Complete CRC-32C declared by an open-write request.
|
|
pub checksum_crc32c: u32,
|
|
/// Nonzero client-selected transfer identity.
|
|
pub transfer_id: u16,
|
|
/// Canonical path bytes for opens; empty for all other requests.
|
|
pub content: Content,
|
|
}
|
|
|
|
/// One decoded upload data request.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct DataRequest {
|
|
/// Exact addressed service endpoint.
|
|
pub target: Target,
|
|
/// Transfer-relative byte position.
|
|
pub position: u32,
|
|
/// Nonzero client-selected transfer identity.
|
|
pub transfer_id: u16,
|
|
/// Fixed wire chunk. Its logical length comes from the open file size.
|
|
pub data: [u8; DATA_BYTES],
|
|
}
|
|
|
|
/// Request accepted by [`crate::FileService`].
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum FileRequest {
|
|
/// Control operation.
|
|
Control(ControlRequest),
|
|
/// Write data.
|
|
Data(DataRequest),
|
|
}
|
|
|
|
/// One control response.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ControlResponse {
|
|
/// Operation whose outcome is reported.
|
|
pub operation: FileOperation,
|
|
/// Requester addressed by the response.
|
|
pub target: Target,
|
|
/// Next accepted position or request position on failure.
|
|
pub position: u32,
|
|
/// Complete opened file size.
|
|
pub file_size: u32,
|
|
/// Complete opened file CRC-32C.
|
|
pub checksum_crc32c: u32,
|
|
/// Client-selected transfer identity.
|
|
pub transfer_id: u16,
|
|
/// Authoritative outcome.
|
|
pub status: FileStatus,
|
|
/// Bounded provider detail.
|
|
pub content: Content,
|
|
}
|
|
|
|
/// One read-data response.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct DataResponse {
|
|
/// Requester addressed by the response.
|
|
pub target: Target,
|
|
/// Position of the first logical byte.
|
|
pub position: u32,
|
|
/// Transfer identity.
|
|
pub transfer_id: u16,
|
|
/// Zero-padded wire data; logical length derives from the opened file size.
|
|
pub data: [u8; DATA_BYTES],
|
|
}
|
|
|
|
/// Response produced by [`crate::FileService`].
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum FileResponse {
|
|
/// Control outcome.
|
|
Control(ControlResponse),
|
|
/// Read data.
|
|
Data(DataResponse),
|
|
}
|
|
|
|
/// Stable metadata returned when a provider pins a readable file.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct FileMetadata {
|
|
/// Exact complete byte length.
|
|
pub size: u32,
|
|
/// CRC-32C of the complete pinned bytes.
|
|
pub checksum_crc32c: u32,
|
|
}
|
|
|
|
/// Format-agnostic provider rejection.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct FileFailure {
|
|
/// Stable generic failure classification.
|
|
status: FileStatus,
|
|
/// Bounded format-specific detail.
|
|
content: Content,
|
|
}
|
|
|
|
impl FileFailure {
|
|
/// Constructs a provider failure. Success statuses are rejected.
|
|
pub const fn new(status: FileStatus, content: Content) -> Option<Self> {
|
|
match status {
|
|
FileStatus::Ok => None,
|
|
_ => Some(Self { status, content }),
|
|
}
|
|
}
|
|
|
|
/// Stable generic failure classification.
|
|
pub(crate) const fn status(self) -> FileStatus {
|
|
self.status
|
|
}
|
|
|
|
/// Bounded format-specific detail.
|
|
pub(crate) const fn content(self) -> Content {
|
|
self.content
|
|
}
|
|
}
|