feat(mavlink): adopt the kaptan-native dialect
This commit is contained in:
1 parent
1c90870634
commit
5f91465e66
87 files changed
+7733
-6850
No files matched your search
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "kaptan-file-mavlink"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
kaptan-mavlink = { path = "../kaptan-mavlink", default-features = false }
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Bounded Kaptan virtual-file service over two custom MAVLink messages.
|
||||
//!
|
||||
//! The service owns transfer correlation, strict sequential writes, retry
|
||||
//! replay, and complete-file CRC-32C verification. File formats, resource
|
||||
//! routing, validation, persistence, and activation policy belong to the
|
||||
//! caller-provided [`FileProvider`].
|
||||
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
mod protocol;
|
||||
mod service;
|
||||
pub mod wire;
|
||||
|
||||
pub use protocol::{
|
||||
Content, ControlRequest, ControlResponse, DataRequest, DataResponse, FileFailure, FileMetadata,
|
||||
FileOperation, FilePath, FileRequest, FileResponse, FileStatus, Principal, Target,
|
||||
};
|
||||
pub use service::{FileProvider, FileService, crc32c};
|
||||
|
||||
/// Bytes transported by one `FILE_DATA` record.
|
||||
pub const DATA_BYTES: usize = 246;
|
||||
/// Maximum bytes in a canonical virtual-file path or bounded response detail.
|
||||
pub const CONTENT_BYTES: usize = 64;
|
||||
/// Time callers retain an open transfer or completed-response replay state.
|
||||
pub const TRANSFER_REPLAY_LEASE_SECONDS: u8 = 10;
|
||||
@@ -0,0 +1,350 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,317 @@
|
||||
//! Generated Kaptan-dialect translation for the two file messages.
|
||||
|
||||
use kaptan_mavlink::generated::kaptan::{
|
||||
FILE_CONTROL_DATA, FILE_DATA_DATA, FileOperation as WireFileOperation,
|
||||
FileStatus as WireFileStatus, MavMessage,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Content, ControlRequest, ControlResponse, DataRequest, DataResponse, FileOperation,
|
||||
FileRequest, FileResponse, FileStatus, Target,
|
||||
};
|
||||
|
||||
/// Failure while decoding a generated custom message as a service request.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DecodeError {
|
||||
/// The message is not one of the two file records.
|
||||
UnsupportedMessage,
|
||||
/// A response was presented to the request decoder.
|
||||
WrongDirection,
|
||||
/// The request used a broadcast or zero target.
|
||||
InvalidTarget,
|
||||
/// The control content was not canonically zero-padded.
|
||||
InvalidContent,
|
||||
}
|
||||
|
||||
/// Decodes one generated file message into the bounded service request.
|
||||
pub fn decode_request(message: &MavMessage) -> Result<FileRequest, DecodeError> {
|
||||
match message {
|
||||
MavMessage::FILE_CONTROL(data) => {
|
||||
if data.status != WireFileStatus::FILE_STATUS_UNSPECIFIED {
|
||||
return Err(DecodeError::WrongDirection);
|
||||
}
|
||||
let target = target(data.target_system, data.target_component)?;
|
||||
let content = Content::from_wire(data.content_length, data.content)
|
||||
.ok_or(DecodeError::InvalidContent)?;
|
||||
Ok(FileRequest::Control(ControlRequest {
|
||||
operation: decode_operation(data.operation),
|
||||
target,
|
||||
position: data.position,
|
||||
file_size: data.file_size,
|
||||
checksum_crc32c: data.checksum_crc32c,
|
||||
transfer_id: data.transfer_id,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
MavMessage::FILE_DATA(data) => {
|
||||
if data.status != WireFileStatus::FILE_STATUS_UNSPECIFIED {
|
||||
return Err(DecodeError::WrongDirection);
|
||||
}
|
||||
Ok(FileRequest::Data(DataRequest {
|
||||
target: target(data.target_system, data.target_component)?,
|
||||
position: data.position,
|
||||
transfer_id: data.transfer_id,
|
||||
data: data.data,
|
||||
}))
|
||||
}
|
||||
_ => Err(DecodeError::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes one generated file message as a host-side service response.
|
||||
pub fn decode_response(message: &MavMessage) -> Result<FileResponse, DecodeError> {
|
||||
match message {
|
||||
MavMessage::FILE_CONTROL(data) => {
|
||||
if data.status == WireFileStatus::FILE_STATUS_UNSPECIFIED {
|
||||
return Err(DecodeError::WrongDirection);
|
||||
}
|
||||
let target = target(data.target_system, data.target_component)?;
|
||||
let content = Content::from_wire(data.content_length, data.content)
|
||||
.ok_or(DecodeError::InvalidContent)?;
|
||||
Ok(FileResponse::Control(ControlResponse {
|
||||
operation: decode_operation(data.operation),
|
||||
target,
|
||||
position: data.position,
|
||||
file_size: data.file_size,
|
||||
checksum_crc32c: data.checksum_crc32c,
|
||||
transfer_id: data.transfer_id,
|
||||
status: decode_status(data.status),
|
||||
content,
|
||||
}))
|
||||
}
|
||||
MavMessage::FILE_DATA(data) => {
|
||||
if data.status != WireFileStatus::FILE_STATUS_OK {
|
||||
return Err(DecodeError::WrongDirection);
|
||||
}
|
||||
Ok(FileResponse::Data(DataResponse {
|
||||
target: target(data.target_system, data.target_component)?,
|
||||
position: data.position,
|
||||
transfer_id: data.transfer_id,
|
||||
data: data.data,
|
||||
}))
|
||||
}
|
||||
_ => Err(DecodeError::UnsupportedMessage),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes one service response as a generated Kaptan-dialect message.
|
||||
pub fn encode_response(response: FileResponse) -> MavMessage {
|
||||
match response {
|
||||
FileResponse::Control(response) => {
|
||||
MavMessage::FILE_CONTROL(encode_control_response(response))
|
||||
}
|
||||
FileResponse::Data(response) => MavMessage::FILE_DATA(encode_data_response(response)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodes one host request as a generated Kaptan-dialect message.
|
||||
pub fn encode_request(request: FileRequest) -> MavMessage {
|
||||
match request {
|
||||
FileRequest::Control(request) => MavMessage::FILE_CONTROL(FILE_CONTROL_DATA {
|
||||
position: request.position,
|
||||
file_size: request.file_size,
|
||||
checksum_crc32c: request.checksum_crc32c,
|
||||
transfer_id: request.transfer_id,
|
||||
target_system: request.target.system(),
|
||||
target_component: request.target.component(),
|
||||
operation: encode_operation(request.operation),
|
||||
status: WireFileStatus::FILE_STATUS_UNSPECIFIED,
|
||||
content_length: request.content.len(),
|
||||
content: request.content.wire_bytes(),
|
||||
}),
|
||||
FileRequest::Data(request) => MavMessage::FILE_DATA(FILE_DATA_DATA {
|
||||
position: request.position,
|
||||
transfer_id: request.transfer_id,
|
||||
target_system: request.target.system(),
|
||||
target_component: request.target.component(),
|
||||
status: WireFileStatus::FILE_STATUS_UNSPECIFIED,
|
||||
data: request.data,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_control_response(response: ControlResponse) -> FILE_CONTROL_DATA {
|
||||
FILE_CONTROL_DATA {
|
||||
position: response.position,
|
||||
file_size: response.file_size,
|
||||
checksum_crc32c: response.checksum_crc32c,
|
||||
transfer_id: response.transfer_id,
|
||||
target_system: response.target.system(),
|
||||
target_component: response.target.component(),
|
||||
operation: encode_operation(response.operation),
|
||||
status: encode_status(response.status),
|
||||
content_length: response.content.len(),
|
||||
content: response.content.wire_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_data_response(response: DataResponse) -> FILE_DATA_DATA {
|
||||
FILE_DATA_DATA {
|
||||
position: response.position,
|
||||
transfer_id: response.transfer_id,
|
||||
target_system: response.target.system(),
|
||||
target_component: response.target.component(),
|
||||
status: WireFileStatus::FILE_STATUS_OK,
|
||||
data: response.data,
|
||||
}
|
||||
}
|
||||
|
||||
fn target(system: u8, component: u8) -> Result<Target, DecodeError> {
|
||||
Target::try_new(system, component).ok_or(DecodeError::InvalidTarget)
|
||||
}
|
||||
|
||||
const fn decode_operation(operation: WireFileOperation) -> FileOperation {
|
||||
match operation {
|
||||
WireFileOperation::FILE_OPERATION_OPEN_READ => FileOperation::OpenRead,
|
||||
WireFileOperation::FILE_OPERATION_OPEN_WRITE => FileOperation::OpenWrite,
|
||||
WireFileOperation::FILE_OPERATION_READ => FileOperation::Read,
|
||||
WireFileOperation::FILE_OPERATION_WRITE_ACK => FileOperation::WriteAck,
|
||||
WireFileOperation::FILE_OPERATION_CLOSE => FileOperation::Close,
|
||||
WireFileOperation::FILE_OPERATION_ABORT => FileOperation::Abort,
|
||||
}
|
||||
}
|
||||
|
||||
const fn encode_operation(operation: FileOperation) -> WireFileOperation {
|
||||
match operation {
|
||||
FileOperation::OpenRead => WireFileOperation::FILE_OPERATION_OPEN_READ,
|
||||
FileOperation::OpenWrite => WireFileOperation::FILE_OPERATION_OPEN_WRITE,
|
||||
FileOperation::Read => WireFileOperation::FILE_OPERATION_READ,
|
||||
FileOperation::WriteAck => WireFileOperation::FILE_OPERATION_WRITE_ACK,
|
||||
FileOperation::Close => WireFileOperation::FILE_OPERATION_CLOSE,
|
||||
FileOperation::Abort => WireFileOperation::FILE_OPERATION_ABORT,
|
||||
}
|
||||
}
|
||||
|
||||
const fn encode_status(status: FileStatus) -> WireFileStatus {
|
||||
match status {
|
||||
FileStatus::Ok => WireFileStatus::FILE_STATUS_OK,
|
||||
FileStatus::Busy => WireFileStatus::FILE_STATUS_BUSY,
|
||||
FileStatus::InvalidTransfer => WireFileStatus::FILE_STATUS_INVALID_TRANSFER,
|
||||
FileStatus::InvalidState => WireFileStatus::FILE_STATUS_INVALID_STATE,
|
||||
FileStatus::InvalidPath => WireFileStatus::FILE_STATUS_INVALID_PATH,
|
||||
FileStatus::NotFound => WireFileStatus::FILE_STATUS_NOT_FOUND,
|
||||
FileStatus::TooLarge => WireFileStatus::FILE_STATUS_TOO_LARGE,
|
||||
FileStatus::InvalidPosition => WireFileStatus::FILE_STATUS_INVALID_POSITION,
|
||||
FileStatus::ChecksumMismatch => WireFileStatus::FILE_STATUS_CHECKSUM_MISMATCH,
|
||||
FileStatus::ContentRejected => WireFileStatus::FILE_STATUS_CONTENT_REJECTED,
|
||||
FileStatus::StorageFailure => WireFileStatus::FILE_STATUS_STORAGE_FAILURE,
|
||||
FileStatus::RequestConflict => WireFileStatus::FILE_STATUS_REQUEST_CONFLICT,
|
||||
FileStatus::Internal => WireFileStatus::FILE_STATUS_INTERNAL,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_status(status: WireFileStatus) -> FileStatus {
|
||||
match status {
|
||||
WireFileStatus::FILE_STATUS_UNSPECIFIED => {
|
||||
unreachable!("response direction was checked before status decoding")
|
||||
}
|
||||
WireFileStatus::FILE_STATUS_OK => FileStatus::Ok,
|
||||
WireFileStatus::FILE_STATUS_BUSY => FileStatus::Busy,
|
||||
WireFileStatus::FILE_STATUS_INVALID_TRANSFER => FileStatus::InvalidTransfer,
|
||||
WireFileStatus::FILE_STATUS_INVALID_STATE => FileStatus::InvalidState,
|
||||
WireFileStatus::FILE_STATUS_INVALID_PATH => FileStatus::InvalidPath,
|
||||
WireFileStatus::FILE_STATUS_NOT_FOUND => FileStatus::NotFound,
|
||||
WireFileStatus::FILE_STATUS_TOO_LARGE => FileStatus::TooLarge,
|
||||
WireFileStatus::FILE_STATUS_INVALID_POSITION => FileStatus::InvalidPosition,
|
||||
WireFileStatus::FILE_STATUS_CHECKSUM_MISMATCH => FileStatus::ChecksumMismatch,
|
||||
WireFileStatus::FILE_STATUS_CONTENT_REJECTED => FileStatus::ContentRejected,
|
||||
WireFileStatus::FILE_STATUS_STORAGE_FAILURE => FileStatus::StorageFailure,
|
||||
WireFileStatus::FILE_STATUS_REQUEST_CONFLICT => FileStatus::RequestConflict,
|
||||
WireFileStatus::FILE_STATUS_INTERNAL => FileStatus::Internal,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn file_enum_wire_values_are_stable() {
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_OPEN_READ as u8, 1);
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_OPEN_WRITE as u8, 2);
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_READ as u8, 3);
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_WRITE_ACK as u8, 4);
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_CLOSE as u8, 5);
|
||||
assert_eq!(WireFileOperation::FILE_OPERATION_ABORT as u8, 6);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_UNSPECIFIED as u8, 0);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_OK as u8, 1);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_BUSY as u8, 2);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_INVALID_TRANSFER as u8, 3);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_INVALID_STATE as u8, 4);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_INVALID_PATH as u8, 5);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_NOT_FOUND as u8, 6);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_TOO_LARGE as u8, 7);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_INVALID_POSITION as u8, 8);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_CHECKSUM_MISMATCH as u8, 9);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_CONTENT_REJECTED as u8, 10);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_STORAGE_FAILURE as u8, 11);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_REQUEST_CONFLICT as u8, 12);
|
||||
assert_eq!(WireFileStatus::FILE_STATUS_INTERNAL as u8, 13);
|
||||
assert_eq!(FileStatus::Ok as u8, 1);
|
||||
assert_eq!(FileStatus::Busy as u8, 2);
|
||||
assert_eq!(FileStatus::InvalidTransfer as u8, 3);
|
||||
assert_eq!(FileStatus::InvalidState as u8, 4);
|
||||
assert_eq!(FileStatus::InvalidPath as u8, 5);
|
||||
assert_eq!(FileStatus::NotFound as u8, 6);
|
||||
assert_eq!(FileStatus::TooLarge as u8, 7);
|
||||
assert_eq!(FileStatus::InvalidPosition as u8, 8);
|
||||
assert_eq!(FileStatus::ChecksumMismatch as u8, 9);
|
||||
assert_eq!(FileStatus::ContentRejected as u8, 10);
|
||||
assert_eq!(FileStatus::StorageFailure as u8, 11);
|
||||
assert_eq!(FileStatus::RequestConflict as u8, 12);
|
||||
assert_eq!(FileStatus::Internal as u8, 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_request_and_response_round_trip_without_untyped_fields() {
|
||||
let request = FileRequest::Control(ControlRequest {
|
||||
operation: FileOperation::OpenRead,
|
||||
target: Target::new(1, 2),
|
||||
position: 0,
|
||||
file_size: 0,
|
||||
checksum_crc32c: 0,
|
||||
transfer_id: 7,
|
||||
content: Content::try_new(b"/test/data.bin").unwrap(),
|
||||
});
|
||||
let encoded = encode_request(request);
|
||||
assert_eq!(decode_request(&encoded), Ok(request));
|
||||
assert_eq!(decode_response(&encoded), Err(DecodeError::WrongDirection));
|
||||
|
||||
let response = FileResponse::Control(ControlResponse {
|
||||
operation: FileOperation::OpenRead,
|
||||
target: Target::new(42, 7),
|
||||
position: 0,
|
||||
file_size: 901,
|
||||
checksum_crc32c: 0x1234_5678,
|
||||
transfer_id: 7,
|
||||
status: FileStatus::Ok,
|
||||
content: Content::EMPTY,
|
||||
});
|
||||
let encoded = encode_response(response);
|
||||
assert_eq!(decode_response(&encoded), Ok(response));
|
||||
assert_eq!(decode_request(&encoded), Err(DecodeError::WrongDirection));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_direction_is_explicit_and_round_trips() {
|
||||
let request = FileRequest::Data(DataRequest {
|
||||
target: Target::new(1, 2),
|
||||
position: 246,
|
||||
transfer_id: 7,
|
||||
data: [0x5a; crate::DATA_BYTES],
|
||||
});
|
||||
let encoded = encode_request(request);
|
||||
assert_eq!(decode_request(&encoded), Ok(request));
|
||||
assert_eq!(decode_response(&encoded), Err(DecodeError::WrongDirection));
|
||||
|
||||
let response = FileResponse::Data(DataResponse {
|
||||
target: Target::new(42, 7),
|
||||
position: 246,
|
||||
transfer_id: 7,
|
||||
data: [0xa5; crate::DATA_BYTES],
|
||||
});
|
||||
let encoded = encode_response(response);
|
||||
assert_eq!(decode_response(&encoded), Ok(response));
|
||||
assert_eq!(decode_request(&encoded), Err(DecodeError::WrongDirection));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user