feat(firmware): validate configuration and migrate sensor-driven flight APIs

Validate configuration through its owning schemas and keep board
declarations separate from shared F722 resource ownership. Bound USB
transitions, isolate transport failures and retain restart-only activation.

Migrate navigation and flight consumers with the sensor and schema
contracts. Require explicit installation data and synchronous output
authority; board motor outputs remain inhibited.

Validation: assembled-prefix host tests, strict Clippy, platform/USB tests
and both ARM release builds. Final-source coverage and scoped Miri/Kani
evidence are retained separately from hardware qualification.
This commit is contained in:
Onur Özkan committed 2026-11-07 18:12:11 +03:00
1 parent fb1b8af2d1
commit f9a61a086f
211 files changed
+45657 -12740

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ pub use protocol::{
Content, ControlRequest, ControlResponse, DataRequest, DataResponse, FileFailure, FileMetadata,
FileOperation, FilePath, FileRequest, FileResponse, FileStatus, Principal, Target,
};
pub use service::{FileProvider, FileService, crc32c};
pub use service::{DispatchResult, FileProvider, FileService, IngressId, TransferPeer, crc32c};
/// Bytes transported by one `FILE_DATA` record.
pub const DATA_BYTES: usize = 246;
@@ -36,7 +36,7 @@ impl Target {
}
}
/// Exact MAVLink source that owns an open transfer.
/// Claimed MAVLink source identity; these wire fields are not authentication.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Principal {
system: u8,
@@ -104,9 +104,9 @@ pub enum FileOperation {
pub enum FileStatus {
/// The operation succeeded.
Ok = 1,
/// Another principal owns the single transfer slot.
/// Another peer owns the single transfer slot.
Busy = 2,
/// The transfer identity is absent, stale, or owned by another principal.
/// The transfer identity is absent, stale, or owned by another peer.
InvalidTransfer = 3,
/// The operation is not valid for the transfer's current mode or state.
InvalidState = 4,
+269 -183
View File
@@ -6,6 +6,46 @@ use crate::{
Principal, Target,
};
/// Caller-assigned ingress identity, supplied out of band rather than on the wire.
///
/// Distinct receive paths must use distinct, stable IDs while a lease is retained.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IngressId(pub u8);
/// Transfer owner, including the receive path of the claimed MAVLink identity.
///
/// This isolates ingresses; it does not authenticate the principal on an ingress.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TransferPeer {
pub principal: Principal,
pub ingress: IngressId,
}
/// Response and the service's authoritative lease-renewal decision.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DispatchResult {
pub response: FileResponse,
/// Accepted owner activity or an exact accepted retry, including a cached
/// terminal failure. Rejections never renew the lease, regardless of status.
pub refresh_lease: bool,
}
impl DispatchResult {
fn control(response: ControlResponse, refresh_lease: bool) -> Self {
Self {
response: FileResponse::Control(response),
refresh_lease,
}
}
fn data(response: DataResponse) -> Self {
Self {
response: FileResponse::Data(response),
refresh_lease: true,
}
}
}
/// Format-agnostic owner of the one file currently pinned or staged.
///
/// The service calls `abort` after any provider failure, explicit abort, lease
@@ -69,7 +109,7 @@ struct LastChunk {
}
struct Session {
owner: Principal,
owner: TransferPeer,
transfer_id: u16,
path: FilePath,
mode: Mode,
@@ -81,7 +121,7 @@ struct Session {
#[derive(Clone, Copy)]
struct Completed {
owner: Principal,
owner: TransferPeer,
transfer_id: u16,
request: FileRequest,
response: ControlResponse,
@@ -108,17 +148,19 @@ impl FileService {
}
}
/// Handles one already integrity-checked request.
/// Handles one already integrity-checked request from an explicit ingress.
///
/// Untargeted requests are silently discarded. The caller may retry a
/// response whose transmission was not admitted: every state-changing
/// request has an exact bounded replay path.
/// request has an exact bounded replay path. Renew the open/replay lease only
/// when `refresh_lease` is true, independently of response delivery. The peer
/// must come from the received frame's identity and caller-known ingress.
pub fn dispatch<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: FileRequest,
) -> Option<FileResponse> {
) -> Option<DispatchResult> {
let addressed = match request {
FileRequest::Control(request) => request.target,
FileRequest::Data(request) => request.target,
@@ -128,16 +170,15 @@ impl FileService {
}
Some(match request {
FileRequest::Control(request) if request.operation == FileOperation::Read => {
self.read(provider, principal, request)
self.read(provider, peer, request)
}
FileRequest::Control(request) => {
FileResponse::Control(self.control(provider, principal, request))
}
FileRequest::Data(request) => self.data(provider, principal, request),
FileRequest::Control(request) => self.control(provider, peer, request),
FileRequest::Data(request) => self.data(provider, peer, request),
})
}
/// Aborts the open transfer, if any, and clears replay state.
/// Aborts the open transfer, if any, and clears owner and replay state.
/// Returns whether an open provider transfer was aborted.
pub fn expire<P: FileProvider>(&mut self, provider: &mut P) -> bool {
let expired = matches!(self.state, TransferState::Open(_));
if expired {
@@ -150,28 +191,32 @@ impl FileService {
fn control<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: ControlRequest,
) -> ControlResponse {
) -> DispatchResult {
match request.operation {
FileOperation::OpenRead => self.open(provider, principal, request, Mode::Read),
FileOperation::OpenWrite => self.open(provider, principal, request, Mode::Write),
FileOperation::OpenRead => self.open(provider, peer, request, Mode::Read),
FileOperation::OpenWrite => self.open(provider, peer, request, Mode::Write),
FileOperation::Read => unreachable!("read requests are dispatched as data responses"),
FileOperation::Close => self.close(provider, principal, request),
FileOperation::Abort => self.abort(provider, principal, request),
FileOperation::WriteAck => {
self.failure(principal, request, FileStatus::InvalidState, Content::EMPTY)
}
FileOperation::Close => self.close(provider, peer, request),
FileOperation::Abort => self.abort(provider, peer, request),
FileOperation::WriteAck => self.failure(
peer.principal,
request,
FileStatus::InvalidState,
Content::EMPTY,
),
}
}
fn open<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: ControlRequest,
mode: Mode,
) -> ControlResponse {
) -> DispatchResult {
let principal = peer.principal;
if request.transfer_id == 0 || request.position != 0 {
return self.failure(
principal,
@@ -188,7 +233,7 @@ impl FileService {
};
let replace_open = if let TransferState::Open(session) = &self.state {
if session.owner != principal {
if session.owner != peer {
return self.failure(principal, request, FileStatus::Busy, Content::EMPTY);
}
let same = session.transfer_id == request.transfer_id
@@ -229,7 +274,7 @@ impl FileService {
self.state = TransferState::Idle;
}
if let TransferState::Completed(completed) = &self.state {
if completed.owner != principal {
if completed.owner != peer {
return self.failure(principal, request, FileStatus::Busy, Content::EMPTY);
}
if completed.transfer_id == request.transfer_id {
@@ -258,7 +303,7 @@ impl FileService {
}
};
self.state = TransferState::Open(Session {
owner: principal,
owner: peer,
transfer_id: request.transfer_id,
path,
mode,
@@ -276,44 +321,37 @@ impl FileService {
fn read<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: ControlRequest,
) -> FileResponse {
) -> DispatchResult {
let principal = peer.principal;
if !canonical_non_open(&request) {
return FileResponse::Control(self.failure(
principal,
request,
FileStatus::InvalidState,
Content::EMPTY,
));
return self.failure(principal, request, FileStatus::InvalidState, Content::EMPTY);
}
let TransferState::Open(session) = &mut self.state else {
return FileResponse::Control(self.replay_or_failure(principal, request));
return self.replay_or_failure(peer, request);
};
if session.owner != principal || session.transfer_id != request.transfer_id {
return FileResponse::Control(response(
if session.owner != peer || session.transfer_id != request.transfer_id {
return self.failure(
principal,
request.operation,
request.transfer_id,
request.position,
FileMetadata {
size: request.file_size,
checksum_crc32c: request.checksum_crc32c,
},
request,
FileStatus::InvalidTransfer,
Content::EMPTY,
));
);
}
if session.mode != Mode::Read {
return FileResponse::Control(response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidState,
Content::EMPTY,
));
return DispatchResult::control(
response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidState,
Content::EMPTY,
),
false,
);
}
if request.position != session.position {
if let Some(last) = session
@@ -321,44 +359,53 @@ impl FileService {
.as_ref()
.filter(|last| last.position == request.position)
{
return FileResponse::Data(DataResponse {
return DispatchResult::data(DataResponse {
target: principal.target(),
position: request.position,
transfer_id: request.transfer_id,
data: last.data,
});
}
return FileResponse::Control(response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
));
return DispatchResult::control(
response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
),
false,
);
}
let Some(logical) = chunk_length(session.metadata.size, request.position) else {
return FileResponse::Control(response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
));
return DispatchResult::control(
response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
),
false,
);
};
if logical == 0 {
return FileResponse::Control(response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
));
return DispatchResult::control(
response(
principal,
request.operation,
request.transfer_id,
request.position,
session.metadata,
FileStatus::InvalidPosition,
Content::EMPTY,
),
false,
);
}
let mut data = [0_u8; DATA_BYTES];
if let Err(error) = provider.read(request.position, &mut data[..logical]) {
@@ -373,15 +420,14 @@ impl FileService {
error.status(),
error.content(),
);
self.remember(principal, FileRequest::Control(request), response);
return FileResponse::Control(response);
return self.remember(peer, FileRequest::Control(request), response);
}
session.last_chunk = Some(LastChunk {
position: request.position,
data,
});
session.position += logical as u32;
FileResponse::Data(DataResponse {
DispatchResult::data(DataResponse {
target: principal.target(),
position: request.position,
transfer_id: request.transfer_id,
@@ -392,66 +438,64 @@ impl FileService {
fn data<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: DataRequest,
) -> FileResponse {
) -> DispatchResult {
let principal = peer.principal;
let TransferState::Open(session) = &mut self.state else {
return FileResponse::Control(self.replay_data_or_failure(principal, request));
return self.replay_data_or_failure(peer, request);
};
if session.owner != principal || session.transfer_id != request.transfer_id {
return FileResponse::Control(data_failure(
principal,
request,
FileStatus::InvalidTransfer,
));
if session.owner != peer || session.transfer_id != request.transfer_id {
return DispatchResult::control(
data_failure(principal, request, FileStatus::InvalidTransfer),
false,
);
}
if session.mode != Mode::Write {
return FileResponse::Control(data_failure(
principal,
request,
FileStatus::InvalidState,
));
return DispatchResult::control(
data_failure(principal, request, FileStatus::InvalidState),
false,
);
}
let logical = match chunk_length(session.metadata.size, request.position) {
Some(length) if length != 0 => length,
_ => {
return FileResponse::Control(data_failure(
principal,
request,
FileStatus::InvalidPosition,
));
return DispatchResult::control(
data_failure(principal, request, FileStatus::InvalidPosition),
false,
);
}
};
if request.data[logical..].iter().any(|byte| *byte != 0) {
return FileResponse::Control(data_failure(
principal,
request,
FileStatus::RequestConflict,
));
return DispatchResult::control(
data_failure(principal, request, FileStatus::RequestConflict),
false,
);
}
if request.position < session.position {
let replayed = session
.last_chunk
.as_ref()
.is_some_and(|last| last.position == request.position && last.data == request.data);
return FileResponse::Control(if replayed {
write_ack(principal, session)
} else {
data_failure(principal, request, FileStatus::RequestConflict)
});
return DispatchResult::control(
if replayed {
write_ack(principal, session)
} else {
data_failure(principal, request, FileStatus::RequestConflict)
},
replayed,
);
}
if request.position != session.position {
return FileResponse::Control(data_failure(
principal,
request,
FileStatus::InvalidPosition,
));
return DispatchResult::control(
data_failure(principal, request, FileStatus::InvalidPosition),
false,
);
}
if let Err(error) = provider.write(request.position, &request.data[..logical]) {
provider.abort();
let response = data_provider_failure(principal, request, error);
self.remember(principal, FileRequest::Data(request), response);
return FileResponse::Control(response);
return self.remember(peer, FileRequest::Data(request), response);
}
session.checksum.update(&request.data[..logical]);
session.position += logical as u32;
@@ -459,22 +503,23 @@ impl FileService {
position: request.position,
data: request.data,
});
FileResponse::Control(write_ack(principal, session))
DispatchResult::control(write_ack(principal, session), true)
}
fn close<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: ControlRequest,
) -> ControlResponse {
) -> DispatchResult {
let principal = peer.principal;
if !canonical_non_open(&request) {
return self.failure(principal, request, FileStatus::InvalidState, Content::EMPTY);
}
let TransferState::Open(session) = &self.state else {
return self.replay_or_failure(principal, request);
return self.replay_or_failure(peer, request);
};
if session.owner != principal || session.transfer_id != request.transfer_id {
if session.owner != peer || session.transfer_id != request.transfer_id {
return self.failure(
principal,
request,
@@ -494,14 +539,19 @@ impl FileService {
&& session.checksum.checksum() != session.metadata.checksum_crc32c
{
provider.abort();
let response = self.failure(
let response = response(
principal,
request,
request.operation,
request.transfer_id,
request.position,
FileMetadata {
size: request.file_size,
checksum_crc32c: request.checksum_crc32c,
},
FileStatus::ChecksumMismatch,
Content::EMPTY,
);
self.remember(principal, FileRequest::Control(request), response);
return response;
return self.remember(peer, FileRequest::Control(request), response);
}
let completion = provider.close();
let response = match completion {
@@ -527,23 +577,23 @@ impl FileService {
)
}
};
self.remember(principal, FileRequest::Control(request), response);
response
self.remember(peer, FileRequest::Control(request), response)
}
fn abort<P: FileProvider>(
&mut self,
provider: &mut P,
principal: Principal,
peer: TransferPeer,
request: ControlRequest,
) -> ControlResponse {
) -> DispatchResult {
let principal = peer.principal;
if !canonical_non_open(&request) {
return self.failure(principal, request, FileStatus::InvalidState, Content::EMPTY);
}
let TransferState::Open(session) = &self.state else {
return self.replay_or_failure(principal, request);
return self.replay_or_failure(peer, request);
};
if session.owner != principal || session.transfer_id != request.transfer_id {
if session.owner != peer || session.transfer_id != request.transfer_id {
return self.failure(
principal,
request,
@@ -562,8 +612,7 @@ impl FileService {
FileStatus::Ok,
Content::EMPTY,
);
self.remember(principal, FileRequest::Control(request), response);
response
self.remember(peer, FileRequest::Control(request), response)
}
fn open_response(
@@ -571,15 +620,18 @@ impl FileService {
principal: Principal,
operation: FileOperation,
session: &Session,
) -> ControlResponse {
response(
principal,
operation,
session.transfer_id,
session.position,
session.metadata,
FileStatus::Ok,
Content::EMPTY,
) -> DispatchResult {
DispatchResult::control(
response(
principal,
operation,
session.transfer_id,
session.position,
session.metadata,
FileStatus::Ok,
Content::EMPTY,
),
true,
)
}
@@ -589,32 +641,36 @@ impl FileService {
request: ControlRequest,
status: FileStatus,
content: Content,
) -> ControlResponse {
response(
principal,
request.operation,
request.transfer_id,
request.position,
FileMetadata {
size: request.file_size,
checksum_crc32c: request.checksum_crc32c,
},
status,
content,
) -> DispatchResult {
DispatchResult::control(
response(
principal,
request.operation,
request.transfer_id,
request.position,
FileMetadata {
size: request.file_size,
checksum_crc32c: request.checksum_crc32c,
},
status,
content,
),
false,
)
}
fn replay_or_failure(&self, principal: Principal, request: ControlRequest) -> ControlResponse {
fn replay_or_failure(&self, peer: TransferPeer, request: ControlRequest) -> DispatchResult {
let principal = peer.principal;
match &self.state {
TransferState::Completed(completed)
if completed.owner == principal
if completed.owner == peer
&& completed.transfer_id == request.transfer_id
&& completed.request == FileRequest::Control(request) =>
{
completed.response
DispatchResult::control(completed.response, true)
}
TransferState::Completed(completed)
if completed.owner == principal && completed.transfer_id == request.transfer_id =>
if completed.owner == peer && completed.transfer_id == request.transfer_id =>
{
self.failure(
principal,
@@ -633,31 +689,39 @@ impl FileService {
}
}
fn replay_data_or_failure(
&self,
principal: Principal,
request: DataRequest,
) -> ControlResponse {
fn replay_data_or_failure(&self, peer: TransferPeer, request: DataRequest) -> DispatchResult {
let principal = peer.principal;
match &self.state {
TransferState::Completed(completed)
if completed.owner == principal
if completed.owner == peer
&& completed.transfer_id == request.transfer_id
&& completed.request == FileRequest::Data(request) =>
{
completed.response
DispatchResult::control(completed.response, true)
}
TransferState::Completed(completed)
if completed.owner == principal && completed.transfer_id == request.transfer_id =>
if completed.owner == peer && completed.transfer_id == request.transfer_id =>
{
data_failure(principal, request, FileStatus::RequestConflict)
DispatchResult::control(
data_failure(principal, request, FileStatus::RequestConflict),
false,
)
}
TransferState::Idle | TransferState::Open(_) | TransferState::Completed(_) => {
data_failure(principal, request, FileStatus::InvalidTransfer)
DispatchResult::control(
data_failure(principal, request, FileStatus::InvalidTransfer),
false,
)
}
}
}
fn remember(&mut self, owner: Principal, request: FileRequest, response: ControlResponse) {
fn remember(
&mut self,
owner: TransferPeer,
request: FileRequest,
response: ControlResponse,
) -> DispatchResult {
let transfer_id = match request {
FileRequest::Control(request) => request.transfer_id,
FileRequest::Data(request) => request.transfer_id,
@@ -668,6 +732,7 @@ impl FileService {
request,
response,
});
DispatchResult::control(response, true)
}
}
@@ -753,7 +818,10 @@ mod tests {
use super::*;
const SERVICE: Target = Target::new(1, 1);
const CLIENT: Principal = Principal::new(42, 7);
const CLIENT: TransferPeer = TransferPeer {
principal: Principal::new(42, 7),
ingress: IngressId(0),
};
const PATH: &[u8] = b"/test/data.bin";
#[derive(Clone, Copy, Eq, PartialEq)]
@@ -903,8 +971,12 @@ mod tests {
})
}
fn control_response(response: Option<FileResponse>) -> ControlResponse {
let Some(FileResponse::Control(response)) = response else {
fn control_response(response: Option<DispatchResult>) -> ControlResponse {
let Some(DispatchResult {
response: FileResponse::Control(response),
..
}) = response
else {
panic!("expected control response");
};
response
@@ -960,20 +1032,28 @@ mod tests {
assert_eq!(opened.file_size, 300);
let first = service.dispatch(&mut provider, CLIENT, control(FileOperation::Read, 9, 0));
let Some(FileResponse::Data(first)) = first else {
let Some(DispatchResult {
response: FileResponse::Data(first),
refresh_lease: true,
}) = first
else {
panic!("expected data");
};
assert_eq!(&first.data, &[0x5a; DATA_BYTES]);
let retry = service.dispatch(&mut provider, CLIENT, control(FileOperation::Read, 9, 0));
assert_eq!(retry, Some(FileResponse::Data(first)));
assert_eq!(retry, Some(DispatchResult::data(first)));
assert_eq!(provider.reads, 1);
let Some(FileResponse::Data(last)) = service.dispatch(
let Some(DispatchResult {
response: FileResponse::Data(last),
refresh_lease: true,
}) = service.dispatch(
&mut provider,
CLIENT,
control(FileOperation::Read, 9, DATA_BYTES as u32),
) else {
)
else {
panic!("expected final data");
};
assert_eq!(&last.data[..54], &source[DATA_BYTES..]);
@@ -1001,7 +1081,10 @@ mod tests {
);
assert!(matches!(
service.dispatch(&mut provider, CLIENT, control(FileOperation::Read, 1, 0),),
Some(FileResponse::Data(_))
Some(DispatchResult {
response: FileResponse::Data(_),
refresh_lease: true
})
));
assert_eq!(
control_response(service.dispatch(
@@ -1035,7 +1118,10 @@ mod tests {
assert!(matches!(
service.dispatch(&mut provider, CLIENT, control(FileOperation::Read, 2, 0)),
Some(FileResponse::Data(_))
Some(DispatchResult {
response: FileResponse::Data(_),
refresh_lease: true
})
));
assert_eq!(
control_response(service.dispatch(
@@ -0,0 +1,632 @@
use kaptan_file_mavlink::{
Content, ControlRequest, ControlResponse, DATA_BYTES, DataRequest, DispatchResult, FileFailure,
FileMetadata, FileOperation, FilePath, FileProvider, FileRequest, FileResponse, FileService,
FileStatus, IngressId, Principal, Target, TransferPeer, crc32c, wire,
};
const TARGET: Target = Target::new(1, 1);
const UART: TransferPeer = TransferPeer {
principal: Principal::new(42, 7),
ingress: IngressId(0),
};
const USB: TransferPeer = TransferPeer {
ingress: IngressId(1),
..UART
};
const ID: u16 = 9;
#[derive(Default)]
struct Provider {
bytes: Vec<u8>,
opens: usize,
reads: usize,
writes: usize,
closes: usize,
aborts: usize,
fail_open: bool,
fail_read: bool,
fail_write: bool,
fail_close: bool,
}
fn failure(status: FileStatus) -> FileFailure {
FileFailure::new(status, Content::try_new(b"provider detail").unwrap()).unwrap()
}
impl FileProvider for Provider {
fn open_read(&mut self, _: &FilePath) -> Result<FileMetadata, FileFailure> {
self.opens += 1;
if self.fail_open {
return Err(failure(FileStatus::NotFound));
}
Ok(FileMetadata {
size: self.bytes.len() as u32,
checksum_crc32c: crc32c(&self.bytes),
})
}
fn open_write(&mut self, path: &FilePath, _: FileMetadata) -> Result<(), FileFailure> {
self.open_read(path)?;
self.bytes.clear();
Ok(())
}
fn read(&mut self, position: u32, output: &mut [u8]) -> Result<(), FileFailure> {
self.reads += 1;
if self.fail_read {
return Err(failure(FileStatus::InvalidPosition));
}
let start = position as usize;
output.copy_from_slice(&self.bytes[start..start + output.len()]);
Ok(())
}
fn write(&mut self, position: u32, data: &[u8]) -> Result<(), FileFailure> {
self.writes += 1;
if self.fail_write {
return Err(failure(FileStatus::InvalidTransfer));
}
assert_eq!(position as usize, self.bytes.len());
self.bytes.extend_from_slice(data);
Ok(())
}
fn close(&mut self) -> Result<Content, FileFailure> {
self.closes += 1;
if self.fail_close {
return Err(failure(FileStatus::StorageFailure));
}
Ok(Content::try_new(b"committed").unwrap())
}
fn abort(&mut self) {
self.aborts += 1;
}
}
fn control(operation: FileOperation, position: u32) -> FileRequest {
FileRequest::Control(ControlRequest {
operation,
target: TARGET,
position,
file_size: 0,
checksum_crc32c: 0,
transfer_id: ID,
content: Content::EMPTY,
})
}
fn alter(request: FileRequest, change: impl FnOnce(&mut ControlRequest)) -> FileRequest {
let FileRequest::Control(mut request) = request else {
panic!("expected control request");
};
change(&mut request);
FileRequest::Control(request)
}
fn open(operation: FileOperation, bytes: &[u8]) -> FileRequest {
alter(control(operation, 0), |request| {
request.content = Content::try_new(b"/test/data.bin").unwrap();
if operation == FileOperation::OpenWrite {
request.file_size = bytes.len() as u32;
request.checksum_crc32c = crc32c(bytes);
}
})
}
fn data(position: u32, bytes: &[u8]) -> FileRequest {
let mut data = [0; DATA_BYTES];
data[..bytes.len()].copy_from_slice(bytes);
FileRequest::Data(DataRequest {
target: TARGET,
transfer_id: ID,
position,
data,
})
}
// Exercise the real message codecs while keeping ingress entirely out of band.
fn exchange(
service: &mut FileService,
provider: &mut Provider,
peer: TransferPeer,
request: FileRequest,
) -> DispatchResult {
let decoded = wire::decode_request(&wire::encode_request(request)).unwrap();
let result = service.dispatch(provider, peer, decoded).unwrap();
assert_eq!(
wire::decode_response(&wire::encode_response(result.response)),
Ok(result.response)
);
let target = match result.response {
FileResponse::Control(response) => response.target,
FileResponse::Data(response) => response.target,
};
assert_eq!(
target,
Target::new(peer.principal.system(), peer.principal.component())
);
result
}
fn expect_control(result: DispatchResult, status: FileStatus, refresh: bool) -> ControlResponse {
assert_eq!(result.refresh_lease, refresh);
let FileResponse::Control(response) = result.response else {
panic!("expected control response");
};
assert_eq!(response.status, status);
response
}
#[test]
fn equal_wire_identities_cannot_modify_or_replay_another_ingress_upload() {
for (owner, other_ingress) in [(UART, USB), (USB, UART)] {
let mut service = FileService::new(TARGET);
let mut provider = Provider::default();
let start = open(FileOperation::OpenWrite, b"abc");
let chunk = data(0, b"abc");
let close = control(FileOperation::Close, 3);
let opened = exchange(&mut service, &mut provider, owner, start);
expect_control(opened, FileStatus::Ok, true);
assert_eq!(exchange(&mut service, &mut provider, owner, start), opened);
assert_eq!(provider.opens, 1);
// Check both components of ownership, including identical transfer IDs.
for foreign in [
other_ingress,
TransferPeer {
principal: Principal::new(43, 7),
..owner
},
] {
for (request, status) in [
(start, FileStatus::Busy),
(alter(start, |r| r.transfer_id += 1), FileStatus::Busy),
(chunk, FileStatus::InvalidTransfer),
(control(FileOperation::Read, 0), FileStatus::InvalidTransfer),
(
control(FileOperation::Abort, 0),
FileStatus::InvalidTransfer,
),
(close, FileStatus::InvalidTransfer),
(
control(FileOperation::WriteAck, 0),
FileStatus::InvalidState,
),
] {
expect_control(
exchange(&mut service, &mut provider, foreign, request),
status,
false,
);
}
}
assert_eq!(
(
provider.opens,
provider.reads,
provider.writes,
provider.closes,
provider.aborts
),
(1, 0, 0, 0, 0)
);
let ack = exchange(&mut service, &mut provider, owner, chunk);
expect_control(ack, FileStatus::Ok, true);
expect_control(
exchange(&mut service, &mut provider, other_ingress, chunk),
FileStatus::InvalidTransfer,
false,
);
assert_eq!(exchange(&mut service, &mut provider, owner, chunk), ack);
assert_eq!(provider.writes, 1);
// Lose the durable close reply, then try to steal, evict, or corrupt it.
let lost_reply = exchange(&mut service, &mut provider, owner, close);
expect_control(lost_reply, FileStatus::Ok, true);
for (request, status) in [
(close, FileStatus::InvalidTransfer),
(chunk, FileStatus::InvalidTransfer),
(start, FileStatus::Busy),
(alter(start, |r| r.transfer_id += 1), FileStatus::Busy),
(
control(FileOperation::Abort, 3),
FileStatus::InvalidTransfer,
),
(
control(FileOperation::WriteAck, 3),
FileStatus::InvalidState,
),
] {
expect_control(
exchange(&mut service, &mut provider, other_ingress, request),
status,
false,
);
}
expect_control(
exchange(
&mut service,
&mut provider,
owner,
control(FileOperation::Close, 2),
),
FileStatus::RequestConflict,
false,
);
expect_control(
exchange(&mut service, &mut provider, owner, start),
FileStatus::RequestConflict,
false,
);
for _ in 0..3 {
assert_eq!(
exchange(&mut service, &mut provider, owner, close),
lost_reply
);
}
assert_eq!(provider.bytes, b"abc");
assert_eq!(
(
provider.opens,
provider.writes,
provider.closes,
provider.aborts
),
(1, 1, 1, 0)
);
assert!(!service.expire(&mut provider));
expect_control(
exchange(&mut service, &mut provider, owner, close),
FileStatus::InvalidTransfer,
false,
);
expect_control(
exchange(&mut service, &mut provider, other_ingress, start),
FileStatus::Ok,
true,
);
expect_control(
exchange(&mut service, &mut provider, owner, close),
FileStatus::InvalidTransfer,
false,
);
assert_eq!(provider.closes, 1);
}
}
#[test]
fn read_and_abort_replay_are_bound_to_the_full_peer() {
let mut service = FileService::new(TARGET);
let mut provider = Provider {
bytes: b"snapshot".to_vec(),
..Provider::default()
};
let start = open(FileOperation::OpenRead, &[]);
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::Ok,
true,
);
let read = control(FileOperation::Read, 0);
expect_control(
exchange(&mut service, &mut provider, USB, read),
FileStatus::InvalidTransfer,
false,
);
let first = exchange(&mut service, &mut provider, UART, read);
assert!(first.refresh_lease);
let FileResponse::Data(response) = first.response else {
panic!("expected data")
};
assert_eq!(&response.data[..8], b"snapshot");
expect_control(
exchange(&mut service, &mut provider, USB, read),
FileStatus::InvalidTransfer,
false,
);
expect_control(
exchange(&mut service, &mut provider, UART, data(0, b"changed")),
FileStatus::InvalidState,
false,
);
expect_control(
exchange(
&mut service,
&mut provider,
UART,
control(FileOperation::Read, 8),
),
FileStatus::InvalidPosition,
false,
);
assert_eq!(exchange(&mut service, &mut provider, UART, read), first);
assert_eq!(provider.reads, 1);
let abort = control(FileOperation::Abort, 8);
expect_control(
exchange(&mut service, &mut provider, USB, abort),
FileStatus::InvalidTransfer,
false,
);
let aborted = exchange(&mut service, &mut provider, UART, abort);
expect_control(aborted, FileStatus::Ok, true);
expect_control(
exchange(&mut service, &mut provider, USB, abort),
FileStatus::InvalidTransfer,
false,
);
assert_eq!(exchange(&mut service, &mut provider, UART, abort), aborted);
assert!(!service.expire(&mut provider));
assert!(!service.expire(&mut provider));
expect_control(
exchange(&mut service, &mut provider, UART, abort),
FileStatus::InvalidTransfer,
false,
);
expect_control(
exchange(&mut service, &mut provider, USB, start),
FileStatus::Ok,
true,
);
assert_eq!(provider.aborts, 1);
}
#[test]
fn rejected_activity_cannot_retain_open_or_completed_lease() {
const LEASE: u64 = 10;
for completed in [false, true] {
let mut service = FileService::new(TARGET);
let mut provider = Provider::default();
let start = open(FileOperation::OpenWrite, &[]);
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::Ok,
true,
);
if completed {
expect_control(
exchange(
&mut service,
&mut provider,
UART,
control(FileOperation::Close, 0),
),
FileStatus::Ok,
true,
);
}
let mut lease_until = LEASE;
for now in 1..LEASE {
for peer in [UART, USB] {
for request in [
control(FileOperation::WriteAck, 0),
alter(start, |r| {
r.content = Content::try_new(b"bad path").unwrap()
}),
alter(start, |r| r.transfer_id = 0),
alter(control(FileOperation::Read, 0), |r| r.file_size = 1),
alter(control(FileOperation::Abort, 0), |r| r.checksum_crc32c = 1),
alter(control(FileOperation::Close, 0), |r| {
r.content = Content::try_new(b"bad").unwrap()
}),
data(1, b"x"),
] {
let result = exchange(&mut service, &mut provider, peer, request);
if result.refresh_lease {
lease_until = now + LEASE;
}
assert_eq!(
lease_until, LEASE,
"rejection retained the lease: {result:?}"
);
}
}
for request in [
start,
control(FileOperation::Close, 0),
control(FileOperation::Abort, 0),
] {
assert!(!exchange(&mut service, &mut provider, USB, request).refresh_lease);
}
}
assert_eq!(provider.opens, 1);
assert_eq!(provider.closes, usize::from(completed));
assert_eq!(provider.aborts, 0);
// A clock-owning caller expires the entire slot at the unchanged deadline.
assert_eq!(service.expire(&mut provider), !completed);
assert!(!service.expire(&mut provider));
assert_eq!(provider.aborts, usize::from(!completed));
expect_control(
exchange(
&mut service,
&mut provider,
UART,
control(FileOperation::Close, 0),
),
FileStatus::InvalidTransfer,
false,
);
expect_control(
exchange(&mut service, &mut provider, USB, start),
FileStatus::Ok,
true,
);
}
}
#[test]
fn invalid_owner_requests_do_not_mutate_a_live_upload_or_its_retry() {
let mut service = FileService::new(TARGET);
let mut provider = Provider::default();
let start = open(FileOperation::OpenWrite, b"abc");
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::Ok,
true,
);
for (request, status) in [
(control(FileOperation::Read, 0), FileStatus::InvalidState),
(
control(FileOperation::Close, 0),
FileStatus::InvalidPosition,
),
(data(1, b"bc"), FileStatus::InvalidPosition),
(data(0, b"abc!"), FileStatus::RequestConflict),
(
alter(start, |r| r.checksum_crc32c ^= 1),
FileStatus::RequestConflict,
),
(
alter(control(FileOperation::Close, 0), |r| r.transfer_id += 1),
FileStatus::InvalidTransfer,
),
] {
expect_control(
exchange(&mut service, &mut provider, UART, request),
status,
false,
);
}
assert_eq!(
(provider.writes, provider.closes, provider.aborts),
(0, 0, 0)
);
let accepted = exchange(&mut service, &mut provider, UART, data(0, b"abc"));
expect_control(accepted, FileStatus::Ok, true);
expect_control(
exchange(&mut service, &mut provider, UART, data(0, b"abd")),
FileStatus::RequestConflict,
false,
);
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::RequestConflict,
false,
);
assert_eq!(
exchange(&mut service, &mut provider, UART, data(0, b"abc")),
accepted
);
expect_control(
exchange(
&mut service,
&mut provider,
UART,
control(FileOperation::Close, 3),
),
FileStatus::Ok,
true,
);
assert_eq!(provider.bytes, b"abc");
assert_eq!(
(provider.writes, provider.closes, provider.aborts),
(1, 1, 0)
);
}
#[test]
fn terminal_failures_refresh_and_replay_only_for_the_accepted_peer() {
for stage in 0..4 {
let mut service = FileService::new(TARGET);
let mut provider = Provider {
bytes: b"abc".to_vec(),
fail_read: stage == 0,
fail_write: stage == 1,
fail_close: stage == 2,
..Provider::default()
};
let operation = if stage == 0 {
FileOperation::OpenRead
} else {
FileOperation::OpenWrite
};
let start = open(operation, b"abc");
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::Ok,
true,
);
if stage >= 2 {
let bytes = if stage == 3 { b"abd" } else { b"abc" };
expect_control(
exchange(&mut service, &mut provider, UART, data(0, bytes)),
FileStatus::Ok,
true,
);
}
let (request, status) = match stage {
0 => (control(FileOperation::Read, 0), FileStatus::InvalidPosition),
1 => (data(0, b"abc"), FileStatus::InvalidTransfer),
2 => (control(FileOperation::Close, 3), FileStatus::StorageFailure),
_ => (
control(FileOperation::Close, 3),
FileStatus::ChecksumMismatch,
),
};
let lost_reply = exchange(&mut service, &mut provider, UART, request);
expect_control(lost_reply, status, true);
expect_control(
exchange(&mut service, &mut provider, USB, request),
FileStatus::InvalidTransfer,
false,
);
let conflict = match request {
FileRequest::Control(_) => alter(request, |r| r.position += 1),
FileRequest::Data(_) => data(0, b"abd"),
};
expect_control(
exchange(&mut service, &mut provider, UART, conflict),
FileStatus::RequestConflict,
false,
);
assert_eq!(
exchange(&mut service, &mut provider, UART, request),
lost_reply
);
assert_eq!(provider.reads, usize::from(stage == 0));
assert_eq!(provider.writes, usize::from(stage != 0));
assert_eq!(provider.closes, usize::from(stage == 2));
assert_eq!(provider.aborts, 1);
}
}
#[test]
fn failed_open_does_not_claim_or_renew_the_slot_or_evict_a_completed_reply() {
let mut service = FileService::new(TARGET);
let mut provider = Provider {
fail_open: true,
..Provider::default()
};
let start = open(FileOperation::OpenWrite, &[]);
expect_control(
exchange(&mut service, &mut provider, UART, start),
FileStatus::NotFound,
false,
);
provider.fail_open = false;
expect_control(
exchange(&mut service, &mut provider, USB, start),
FileStatus::Ok,
true,
);
let close = control(FileOperation::Close, 0);
let completed = exchange(&mut service, &mut provider, USB, close);
expect_control(completed, FileStatus::Ok, true);
provider.fail_open = true;
expect_control(
exchange(
&mut service,
&mut provider,
USB,
alter(start, |r| r.transfer_id += 1),
),
FileStatus::NotFound,
false,
);
assert_eq!(exchange(&mut service, &mut provider, USB, close), completed);
assert_eq!(provider.closes, 1);
}