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:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user