diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e5f31a..52ed3b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.86.0 - run: cargo check --release cross-check: diff --git a/CHANGELOG.md b/CHANGELOG.md index bc40490..ac03890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). # [unreleased] -- Bumped `spacepackets` to v0.15 +- Bumped `spacepackets` to v0.16 - Bumped `defmt` to v1 +## Added + +- Acknowledged mode support for both source and destination handler. +- `FaultInfo` structure which is passed to user fault callbacks. + # [v0.2.0] 2024-11-26 - Bumped `thiserror` to v2 diff --git a/Cargo.toml b/Cargo.toml index 2d03fa7..9436268 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "cfdp-rs" version = "0.2.0" edition = "2024" -rust-version = "1.85.0" +rust-version = "1.86.0" authors = ["Robin Mueller "] description = "High level CCSDS File Delivery Protocol components" homepage = "https://egit.irs.uni-stuttgart.de/rust/cfdp" @@ -22,6 +22,7 @@ derive-new = ">=0.6, <=0.7" hashbrown = { version = ">=0.14, <=0.15", optional = true } spacepackets = { git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git", version = "0.16", default-features = false } thiserror = { version = "2", default-features = false } +heapless = "0.9" serde = { version = "1", optional = true } defmt = { version = "1", optional = true } @@ -36,7 +37,7 @@ alloc = [ "hashbrown", "spacepackets/alloc" ] -serde = ["dep:serde", "spacepackets/serde", "hashbrown/serde"] +serde = ["dep:serde", "spacepackets/serde", "hashbrown/serde", "heapless/serde"] defmt = ["dep:defmt", "spacepackets/defmt"] [dev-dependencies] diff --git a/README.md b/README.md index 8b6693d..690aa3f 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,10 @@ The underlying base packet library used to generate the packets to be sent is th `cfdp-rs` currently supports following high-level features: - Unacknowledged (class 1) file transfers for both source and destination side. -- Acknowledged (class 2) file transfers for the source side. +- Acknowledged (class 2) file transfers for both source side and destination side. The following features have not been implemented yet. PRs or notifications for demand are welcome! -- Acknowledged (class 2) file transfers for the destination side. - Suspending transfers - Inactivity handling - Start and end of transmission and reception opportunity handling diff --git a/examples/python-interop/README.md b/examples/python-interop/README.md index a57e85c..9abe2c8 100644 --- a/examples/python-interop/README.md +++ b/examples/python-interop/README.md @@ -12,13 +12,13 @@ You can run both applications with `-h` to get more information about the availa ## Running the Python App It is recommended to run the Python App in a dedicated virtual environment. For example, on a -Unix system you can use `python3 -m venv venv` and then `source venv/bin/activate` to create +Unix system you can use `uv venv` and then `source .venv/bin/activate` to create and activate a virtual environment. After that, you can install the required dependencies using ```sh -pip install -r requirements.txt +uv pip install -r requirements.txt ``` and then run the application using `./main.py` or `python3 main.py`. diff --git a/examples/python-interop/main.py b/examples/python-interop/main.py index 752b96a..83d2db1 100755 --- a/examples/python-interop/main.py +++ b/examples/python-interop/main.py @@ -16,11 +16,11 @@ from typing import Any, Dict, List, Tuple, Optional from multiprocessing import Queue from queue import Empty -from cfdppy.handler import DestHandler, RemoteEntityCfgTable, SourceHandler +from cfdppy.handler import DestHandler, RemoteEntityConfigTable, SourceHandler from cfdppy.exceptions import InvalidDestinationId, SourceFileDoesNotExist from cfdppy import ( CfdpUserBase, - LocalEntityCfg, + LocalEntityConfig, PacketDestination, PutRequest, TransactionId, @@ -31,8 +31,8 @@ from cfdppy.mib import ( CheckTimerProvider, DefaultFaultHandlerBase, EntityType, - IndicationCfg, - RemoteEntityCfg, + IndicationConfig, + RemoteEntityConfig, ) from cfdppy.user import ( FileSegmentRecvdParams, @@ -58,7 +58,7 @@ from spacepackets.util import ByteFieldU16, UnsignedByteField PYTHON_ENTITY_ID = ByteFieldU16(1) RUST_ENTITY_ID = ByteFieldU16(2) # Enable all indications for both local and remote entity. -INDICATION_CFG = IndicationCfg() +INDICATION_CFG = IndicationConfig() BASE_STR_SRC = "PY SRC" BASE_STR_DEST = "PY DEST" @@ -79,7 +79,7 @@ DEST_ENTITY_QUEUE = Queue() # be sent by the UDP server. TM_QUEUE = Queue() -REMOTE_CFG_OF_PY_ENTITY = RemoteEntityCfg( +REMOTE_CFG_OF_PY_ENTITY = RemoteEntityConfig( entity_id=PYTHON_ENTITY_ID, max_packet_len=MAX_PACKET_LEN, max_file_segment_len=FILE_SEGMENT_SIZE, @@ -585,7 +585,7 @@ def main(): logging.basicConfig(level=logging_level) - remote_cfg_table = RemoteEntityCfgTable() + remote_cfg_table = RemoteEntityConfigTable() remote_cfg_table.add_config(REMOTE_CFG_OF_REMOTE_ENTITY) src_fault_handler = CfdpFaultHandler(BASE_STR_SRC) @@ -594,7 +594,7 @@ def main(): src_user = CfdpUser(BASE_STR_SRC, PUT_REQ_QUEUE) check_timer_provider = CustomCheckTimerProvider() source_handler = SourceHandler( - cfg=LocalEntityCfg(PYTHON_ENTITY_ID, INDICATION_CFG, src_fault_handler), + cfg=LocalEntityConfig(PYTHON_ENTITY_ID, INDICATION_CFG, src_fault_handler), seq_num_provider=src_seq_count_provider, remote_cfg_table=remote_cfg_table, user=src_user, @@ -614,7 +614,7 @@ def main(): dest_fault_handler = CfdpFaultHandler(BASE_STR_DEST) dest_user = CfdpUser(BASE_STR_DEST, PUT_REQ_QUEUE) dest_handler = DestHandler( - cfg=LocalEntityCfg(PYTHON_ENTITY_ID, INDICATION_CFG, dest_fault_handler), + cfg=LocalEntityConfig(PYTHON_ENTITY_ID, INDICATION_CFG, dest_fault_handler), user=dest_user, remote_cfg_table=remote_cfg_table, check_timer_provider=check_timer_provider, diff --git a/examples/python-interop/main.rs b/examples/python-interop/main.rs index ef21d7b..8b4a0e1 100644 --- a/examples/python-interop/main.rs +++ b/examples/python-interop/main.rs @@ -12,10 +12,11 @@ use std::{ }; use cfdp::{ - EntityType, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, PduProvider, - RemoteEntityConfig, StdTimerCreator, TransactionId, UserFaultHookProvider, + EntityType, FaultInfo, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, PduProvider, + RemoteEntityConfig, StdTimerCreator, TransactionId, UserFaultHook, dest::DestinationHandler, filestore::NativeFilestore, + lost_segments::LostSegmentsList, request::{PutRequestOwned, StaticPutRequestCacher}, source::SourceHandler, user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams}, @@ -62,43 +63,21 @@ pub struct Cli { #[derive(Default)] pub struct ExampleFaultHandler {} -impl UserFaultHookProvider for ExampleFaultHandler { - fn notice_of_suspension_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - panic!( - "unexpected suspension of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); +impl UserFaultHook for ExampleFaultHandler { + fn notice_of_suspension_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected suspension, {:?}", fault_info); } - fn notice_of_cancellation_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - panic!( - "unexpected cancellation of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn notice_of_cancellation_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected cancellation, {:?}", fault_info); } - fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { - panic!( - "unexpected abandonment of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn abandoned_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected abandonment, {:?}", fault_info); } - fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { - panic!( - "ignoring unexpected error in transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn ignore_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected ignore, {:?}", fault_info); } } @@ -261,7 +240,7 @@ impl UdpServer { while let Ok(tm) = receiver.try_recv() { debug!("Sending PDU: {:?}", tm); pdu_printout(&tm); - let result = self.socket.send_to(tm.pdu(), self.remote_addr()); + let result = self.socket.send_to(tm.raw_pdu(), self.remote_addr()); if let Err(e) = result { warn!("Sending TM with UDP socket failed: {e}") } @@ -284,7 +263,7 @@ fn pdu_printout(pdu: &PduOwnedWithInfo) { spacepackets::cfdp::pdu::FileDirectiveType::AckPdu => (), spacepackets::cfdp::pdu::FileDirectiveType::MetadataPdu => { let meta_pdu = - MetadataPduReader::new(pdu.pdu()).expect("creating metadata pdu failed"); + MetadataPduReader::new(pdu.raw_pdu()).expect("creating metadata pdu failed"); debug!("Metadata PDU: {:?}", meta_pdu) } spacepackets::cfdp::pdu::FileDirectiveType::NakPdu => (), @@ -292,7 +271,8 @@ fn pdu_printout(pdu: &PduOwnedWithInfo) { spacepackets::cfdp::pdu::FileDirectiveType::KeepAlivePdu => (), }, spacepackets::cfdp::PduType::FileData => { - let fd_pdu = FileDataPdu::from_bytes(pdu.pdu()).expect("creating file data pdu failed"); + let fd_pdu = + FileDataPdu::from_bytes(pdu.raw_pdu()).expect("creating file data pdu failed"); debug!("File data PDU: {:?}", fd_pdu); } } @@ -367,6 +347,7 @@ fn main() { NativeFilestore::default(), remote_cfg_python, StdTimerCreator::default(), + LostSegmentsList::default(), ); let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving); diff --git a/src/dest.rs b/src/dest.rs index b36f648..58a6967 100644 --- a/src/dest.rs +++ b/src/dest.rs @@ -9,7 +9,7 @@ //! //! A destination entity might still generate packets which need to be sent back to the source //! entity of the file transfer. However, this handler allows freedom of communication like the -//! source entity by using a user-provided [PduSendProvider] to send all generated PDUs. +//! source entity by using a user-provided [PduSender] instance to send all generated PDUs. //! //! The transaction will be finished when following conditions are met: //! @@ -24,31 +24,41 @@ //! //! 3. Finished PDU has been sent back to the remote side. //! -//! ### Acknowledged mode (*not implemented yet*) +//! ### Acknowledged mode //! //! 3. An EOF ACK PDU has been sent back to the remote side. //! 4. A Finished PDU has been sent back to the remote side. //! 5. A Finished PDU ACK was received. -use crate::{DummyPduProvider, GenericSendError, PduProvider, user::TransactionFinishedParams}; -use core::str::{Utf8Error, from_utf8, from_utf8_unchecked}; +use crate::{ + DummyPduProvider, FaultInfo, GenericSendError, IndicationConfig, PduProvider, + PositiveAckParams, + lost_segments::{LostSegmentError, LostSegmentStore}, + user::TransactionFinishedParams, +}; +use core::{ + cell::{Cell, RefCell}, + str::{Utf8Error, from_utf8, from_utf8_unchecked}, +}; use super::{ - CountdownProvider, EntityType, LocalEntityConfig, PacketTarget, PduSendProvider, - RemoteEntityConfig, RemoteEntityConfigProvider, State, TimerContext, TimerCreatorProvider, - TransactionId, UserFaultHookProvider, + Countdown, EntityType, LocalEntityConfig, PacketTarget, PduSender, RemoteConfigStore, + RemoteEntityConfig, State, TimerContext, TimerCreator, TransactionId, UserFaultHook, filestore::{FilestoreError, VirtualFilestore}, user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams}, }; use smallvec::SmallVec; use spacepackets::{ cfdp::{ - ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode, + ChecksumType, ConditionCode, FaultHandlerCode, LargeFileFlag, PduType, TransactionStatus, + TransmissionMode, pdu::{ CfdpPdu, CommonPduConfig, FileDirectiveType, PduError, PduHeader, + ack::AckPdu, eof::EofPdu, file_data::FileDataPdu, finished::{DeliveryCode, FileStatus, FinishedPduCreator}, metadata::{MetadataGenericParams, MetadataPduReader}, + nak::{NakPduCreator, NakPduCreatorWithReservedSeqReqsBuf}, }, tlv::{EntityIdTlv, GenericTlv, ReadableTlv, TlvType, msg_to_user::MsgToUserTlv}, }, @@ -56,7 +66,7 @@ use spacepackets::{ }; #[derive(Debug)] -struct FileProperties { +struct FileNames { src_file_name: [u8; u8::MAX as usize], src_file_name_len: usize, dest_file_name: [u8; u8::MAX as usize], @@ -65,6 +75,39 @@ struct FileProperties { dest_file_path_len: usize, } +#[derive(Debug, Default, Clone, Copy)] +pub struct AnomalyTracker { + invalid_ack_directive_code: u8, + lost_segment_errors: u8, +} + +impl AnomalyTracker { + #[inline] + pub fn invalid_ack_directive_code(&mut self) -> u8 { + self.invalid_ack_directive_code + } + + #[inline] + pub fn lost_segment_errors(&mut self) -> u8 { + self.lost_segment_errors + } + + #[inline] + pub fn increment_lost_segment_errors(&mut self) { + self.lost_segment_errors = self.lost_segment_errors.wrapping_add(1); + } + + #[inline] + pub fn increment_invalid_ack_directive_code(&mut self) { + self.invalid_ack_directive_code = self.invalid_ack_directive_code.wrapping_add(1); + } + + #[inline] + pub fn reset(&mut self) { + *self = Default::default(); + } +} + #[derive(Debug, PartialEq, Eq, Copy, Clone)] enum CompletionDisposition { Completed = 0, @@ -76,74 +119,93 @@ enum CompletionDisposition { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum TransactionStep { - Idle = 0, - TransactionStart = 1, - ReceivingFileDataPdus = 2, - ReceivingFileDataPdusWithCheckLimitHandling = 3, - SendingAckPdu = 4, - TransferCompletion = 5, - SendingFinishedPdu = 6, + Idle, + TransactionStart, + /// Special state which is only used for acknowledged mode. The CFDP entity is still waiting + /// for a missing metadata PDU to be re-sent. Until then, all arriving file data PDUs will only + /// update the internal lost segment tracker. When the EOF PDU arrives, the state will be left. + /// Please note that deferred lost segment handling might also be active when this state is set. + WaitingForMetadata, + ReceivingFileDataPdus, + /// This is the check timer step as specified in chapter 4.6.3.3 b) of the standard. + /// The destination handler will still check for file data PDUs which might lead to a full + /// file transfer completion. + ReceivingFileDataPdusWithCheckLimitHandling, + WaitingForMissingData, + //SendingAckPdu, + TransferCompletion, + WaitingForFinishedAck, } -// This contains transfer state parameters for destination transaction. #[derive(Debug)] -struct TransferState { - transaction_id: Option, - metadata_params: MetadataGenericParams, - progress: u64, - // TODO: Can we delete this for good? - // file_size_eof: u64, - metadata_only: bool, - condition_code: ConditionCode, - delivery_code: DeliveryCode, - fault_location_finished: Option, +struct FinishedParams { + condition_code: Cell, + delivery_code: Cell, + fault_location_finished: Cell>, file_status: FileStatus, - completion_disposition: CompletionDisposition, - checksum: u32, - current_check_count: u32, - current_check_timer: Option, } -impl Default for TransferState { +impl Default for FinishedParams { fn default() -> Self { Self { - transaction_id: None, - metadata_params: Default::default(), - progress: Default::default(), - // file_size_eof: Default::default(), - metadata_only: false, - condition_code: ConditionCode::NoError, - delivery_code: DeliveryCode::Incomplete, - fault_location_finished: None, + condition_code: Cell::new(ConditionCode::NoError), + delivery_code: Cell::new(DeliveryCode::Incomplete), + fault_location_finished: Cell::new(None), file_status: FileStatus::Unreported, - completion_disposition: CompletionDisposition::Completed, - checksum: 0, - current_check_count: 0, - current_check_timer: None, } } } +impl FinishedParams { + pub fn reset(&mut self) { + self.condition_code.set(ConditionCode::NoError); + self.delivery_code.set(DeliveryCode::Incomplete); + self.file_status = FileStatus::Unreported; + } +} + +#[derive(Debug, Copy, Clone)] +pub struct AcknowledgedModeParams { + last_start_offset: u64, + last_end_offset: u64, + metadata_missing: bool, + nak_activity_counter: u32, + deferred_procedure_active: bool, +} + // This contains parameters for destination transaction. #[derive(Debug)] -struct TransactionParams { - tstate: TransferState, +struct TransactionParams { pdu_conf: CommonPduConfig, - file_properties: FileProperties, - cksum_buf: [u8; 1024], + file_names: FileNames, msgs_to_user_size: usize, // TODO: Should we make this configurable? msgs_to_user_buf: [u8; 1024], remote_cfg: Option, + transaction_id: Option, + metadata_params: MetadataGenericParams, + file_size: u64, + progress: u64, + acked_params: Option, + deferred_procedure_timer: Option, + finished_params: FinishedParams, + positive_ack_params: Option, + ack_timer: Option, + metadata_only: bool, + completion_disposition: Cell, + checksum: u32, + anomaly_tracker: AnomalyTracker, + current_check_count: u32, + current_check_timer: Option, } -impl TransactionParams { +impl TransactionParams { fn transmission_mode(&self) -> TransmissionMode { self.pdu_conf.trans_mode } } -impl Default for FileProperties { +impl Default for FileNames { fn default() -> Self { Self { src_file_name: [0; u8::MAX as usize], @@ -156,39 +218,52 @@ impl Default for FileProperties { } } -impl TransactionParams { +impl TransactionParams { fn file_size(&self) -> u64 { - self.tstate.metadata_params.file_size + self.metadata_params.file_size } fn metadata_params(&self) -> &MetadataGenericParams { - &self.tstate.metadata_params + &self.metadata_params } } -impl Default for TransactionParams { +impl Default for TransactionParams { fn default() -> Self { Self { pdu_conf: Default::default(), - cksum_buf: [0; 1024], msgs_to_user_size: 0, + file_size: 0, msgs_to_user_buf: [0; 1024], - tstate: Default::default(), - file_properties: Default::default(), + file_names: Default::default(), remote_cfg: None, + transaction_id: None, + metadata_params: Default::default(), + progress: Default::default(), + deferred_procedure_timer: None, + acked_params: None, + metadata_only: false, + positive_ack_params: None, + ack_timer: None, + finished_params: FinishedParams::default(), + completion_disposition: Cell::new(CompletionDisposition::Completed), + checksum: 0, + current_check_count: 0, + anomaly_tracker: AnomalyTracker::default(), + current_check_timer: None, } } } -impl TransactionParams { +impl TransactionParams { fn reset(&mut self) { - self.tstate.condition_code = ConditionCode::NoError; - self.tstate.delivery_code = DeliveryCode::Incomplete; - self.tstate.file_status = FileStatus::Unreported; + self.finished_params.reset(); + self.anomaly_tracker.reset(); } } #[derive(Debug, thiserror::Error)] +#[non_exhaustive] pub enum DestError { /// File directive expected, but none specified #[error("expected file directive")] @@ -198,10 +273,17 @@ pub enum DestError { pdu_type: PduType, directive_type: Option, }, - #[error("can not process file data PDUs in current state")] - WrongStateForFileData, - #[error("can not process EOF PDUs in current state")] - WrongStateForEof, + #[error("first packet must be metadata PDU for unacknowledged transfers")] + FirstPacketNotMetadata, + #[error( + "can not process PDU with type {pdu_type:?} and directive field {file_directive_type:?} in \ + current transaction step {step:?}" + )] + WrongStepForPdu { + step: TransactionStep, + pdu_type: PduType, + file_directive_type: Option, + }, // Received new metadata PDU while being already being busy with a file transfer. #[error("busy with transfer")] RecvdMetadataButIsBusy, @@ -209,23 +291,27 @@ pub enum DestError { EmptySrcFileField, #[error("empty dest file field")] EmptyDestFileField, - #[error("packets to be sent are still left")] - PacketToSendLeft, #[error("pdu error {0}")] Pdu(#[from] PduError), #[error("io error {0}")] #[cfg(feature = "std")] Io(#[from] std::io::Error), - #[error("file store error {0}")] + #[error("file store error: {0}")] Filestore(#[from] FilestoreError), + #[error("unexpected large file size which is not supported for current transfer")] + UnexpectedLargeFileSize, + #[error("lost segment error: {0}")] + LostSegmentError(#[from] LostSegmentError), #[error("path conversion error {0}")] PathConversion(#[from] Utf8Error), #[error("error building dest path from source file name and dest folder")] PathConcat, #[error("no remote entity configuration found for {0:?}")] - NoRemoteCfgFound(UnsignedByteField), + NoRemoteConfigFound(UnsignedByteField), #[error("issue sending PDU: {0}")] SendError(#[from] GenericSendError), + #[error("invalid remote entity configuration: {0:?}")] + InvalidRemoteConfig(RemoteEntityConfig), #[error("cfdp feature not implemented")] NotImplemented, } @@ -239,7 +325,7 @@ pub enum DestError { /// handler and driving the state machine, which might generate new packets to be sent to the /// remote entity. Please note that the destination handler can also only process Metadata, EOF and /// Prompt PDUs in addition to ACK PDUs where the acknowledged PDU is the Finished PDU. -/// All generated packets are sent using the user provided [PduSendProvider]. +/// All generated packets are sent using the user provided [PduSender]. /// /// The handler requires the [alloc] feature but will allocated all required memory on construction /// time. This means that the handler is still suitable for embedded systems where run-time @@ -255,22 +341,24 @@ pub enum DestError { /// in different concurrent contexts. For example, you can dynamically create new handlers and /// run them inside a thread pool, or move the newly created handler to a new thread.""" pub struct DestinationHandler< - PduSender: PduSendProvider, - UserFaultHook: UserFaultHookProvider, - Vfs: VirtualFilestore, - RemoteCfgTable: RemoteEntityConfigProvider, - CheckTimerCreator: TimerCreatorProvider, - CheckTimerProvider: CountdownProvider, + PduSenderInstance: PduSender, + UserFaultHookInstance: UserFaultHook, + VirtualFileStoreInstance: VirtualFilestore, + RemoteConfigStoreInstance: RemoteConfigStore, + TimerCreatorInstance: TimerCreator, + CountdownInstance: Countdown, + LostSegmentTracker: LostSegmentStore, > { - local_cfg: LocalEntityConfig, - step: TransactionStep, + local_cfg: LocalEntityConfig, + step: core::cell::Cell, state: State, - tparams: TransactionParams, - packet_buf: alloc::vec::Vec, - pub pdu_sender: PduSender, - pub vfs: Vfs, - pub remote_cfg_table: RemoteCfgTable, - pub check_timer_creator: CheckTimerCreator, + transaction_params: TransactionParams, + pdu_and_cksum_buffer: RefCell>, + pub pdu_sender: PduSenderInstance, + pub vfs: VirtualFileStoreInstance, + pub remote_cfg_table: RemoteConfigStoreInstance, + pub check_timer_creator: TimerCreatorInstance, + lost_segment_tracker: LostSegmentTracker, } #[cfg(feature = "std")] @@ -278,57 +366,93 @@ pub type StdDestinationHandler = DestinationHandler< PduSender, UserFaultHook, crate::filestore::NativeFilestore, - crate::StdRemoteEntityConfigProvider, + crate::RemoteConfigStoreStd, crate::StdTimerCreator, crate::StdCountdown, + crate::lost_segments::LostSegmentsList, >; +#[cfg(feature = "std")] +impl + StdDestinationHandler +{ + #[cfg(feature = "std")] + pub fn new_std( + local_cfg: LocalEntityConfig, + pdu_and_cksum_buf_size: usize, + pdu_sender: PduSenderInstance, + ) -> Self { + Self::new( + local_cfg, + pdu_and_cksum_buf_size, + pdu_sender, + crate::filestore::NativeFilestore::default(), + crate::RemoteConfigStoreStd::default(), + crate::StdTimerCreator::default(), + crate::lost_segments::LostSegmentsList::default(), + ) + } +} + impl< - PduSender: PduSendProvider, - UserFaultHook: UserFaultHookProvider, - Vfs: VirtualFilestore, - RemoteCfgTable: RemoteEntityConfigProvider, - TimerCreator: TimerCreatorProvider, - Countdown: CountdownProvider, -> DestinationHandler + PduSenderInstance: PduSender, + UserFaultHookInstance: UserFaultHook, + VirtualFilestoreInstance: VirtualFilestore, + RemoteConfigStoreInstance: RemoteConfigStore, + TimerCreatorInstance: TimerCreator, + CountdownInstance: Countdown, + LostSegmentTracker: LostSegmentStore, +> + DestinationHandler< + PduSenderInstance, + UserFaultHookInstance, + VirtualFilestoreInstance, + RemoteConfigStoreInstance, + TimerCreatorInstance, + CountdownInstance, + LostSegmentTracker, + > { /// Constructs a new destination handler. /// /// # Arguments /// /// * `local_cfg` - The local CFDP entity configuration. - /// * `max_packet_len` - The maximum expected generated packet size in bytes. Each time a - /// packet is sent, it will be buffered inside an internal buffer. The length of this buffer - /// will be determined by this parameter. This parameter can either be a known upper bound, - /// or it can specifically be determined by the largest packet size parameter of all remote - /// entity configurations in the passed `remote_cfg_table`. - /// * `pdu_sender` - [PduSendProvider] used to send generated PDU packets. + /// + /// * `pdu_and_cksum_buf_size` - The handler requires a buffer to generate PDUs and perform + /// checksum calculations. This parameter can either be a known upper bound for the packet + /// size, for example 2048 or 4096 bytes. + /// It can also specifically be determined by the largest packet size parameter of all + /// remote entity configurations in the passed `remote_cfg_table`. + /// * `pdu_sender` - [PduSender] used to send generated PDU packets. /// * `vfs` - [VirtualFilestore] implementation used by the handler, which decouples the CFDP /// implementation from the underlying filestore/filesystem. This allows to use this handler /// for embedded systems where a standard runtime might not be available. - /// * `remote_cfg_table` - The [RemoteEntityConfigProvider] used to look up remote + /// * `remote_cfg_table` - The [RemoteConfigStore] used to look up remote /// entities and target specific configuration for file copy operations. - /// * `check_timer_creator` - [TimerCreatorProvider] used by the CFDP handler to generate + /// * `check_timer_creator` - [TimerCreator] used by the CFDP handler to generate /// timers required by various tasks. This allows to use this handler for embedded systems /// where the standard time APIs might not be available. pub fn new( - local_cfg: LocalEntityConfig, - max_packet_len: usize, - pdu_sender: PduSender, - vfs: Vfs, - remote_cfg_table: RemoteCfgTable, - timer_creator: TimerCreator, + local_cfg: LocalEntityConfig, + pdu_and_cksum_buf_size: usize, + pdu_sender: PduSenderInstance, + vfs: VirtualFilestoreInstance, + remote_cfg_table: RemoteConfigStoreInstance, + timer_creator: TimerCreatorInstance, + lost_segment_tracker: LostSegmentTracker, ) -> Self { Self { local_cfg, - step: TransactionStep::Idle, + step: Cell::new(TransactionStep::Idle), state: State::Idle, - tparams: Default::default(), - packet_buf: alloc::vec![0; max_packet_len], + transaction_params: Default::default(), + pdu_and_cksum_buffer: core::cell::RefCell::new(alloc::vec![0; pdu_and_cksum_buf_size]), pdu_sender, vfs, remote_cfg_table, check_timer_creator: timer_creator, + lost_segment_tracker, } } @@ -352,20 +476,22 @@ impl< cfdp_user: &mut impl CfdpUser, packet_to_insert: Option<&impl PduProvider>, ) -> Result { + let mut sent_packets = 0; if let Some(packet) = packet_to_insert { - self.insert_packet(cfdp_user, packet)?; + sent_packets += self.insert_packet(cfdp_user, packet)?; } match self.state { State::Idle => { // TODO: In acknowledged mode, add timer handling. - Ok(0) } - State::Busy => self.fsm_busy(cfdp_user), + State::Busy => { + sent_packets += self.fsm_busy(cfdp_user)?; + } State::Suspended => { // There is now way to suspend the handler currently anyway. - Ok(0) } } + Ok(sent_packets) } /// This function models the Cancel.request CFDP primitive and is the recommended way @@ -387,7 +513,7 @@ impl< EntityIdTlv::new(self.local_cfg.id), ); - self.step = TransactionStep::TransferCompletion; + self.set_step(TransactionStep::TransferCompletion); return true; } } @@ -396,22 +522,58 @@ impl< /// Returns [None] if the state machine is IDLE, and the transmission mode of the current /// request otherwise. + #[inline] pub fn transmission_mode(&self) -> Option { if self.state == State::Idle { return None; } - Some(self.tparams.transmission_mode()) + Some(self.transaction_params.transmission_mode()) } + #[inline] pub fn transaction_id(&self) -> Option { - self.tstate().transaction_id + self.transaction_params.transaction_id + } + + /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. + #[inline] + pub fn step(&self) -> TransactionStep { + self.step.get() + } + + /// Get the step, which denotes whether the CFDP handler is active, and which CFDP class + /// is used if it is active. + #[inline] + pub fn state(&self) -> State { + self.state + } + + #[inline] + pub fn local_cfg(&self) -> &LocalEntityConfig { + &self.local_cfg + } + + #[inline] + pub fn local_cfg_mut(&mut self) -> &mut LocalEntityConfig { + &mut self.local_cfg + } + + /// This function is public to allow completely resetting the handler, but it is explicitely + /// discouraged to do this. CFDP has mechanism to detect issues and errors on itself. + /// Resetting the handler might interfere with these mechanisms and lead to unexpected + /// behaviour. + pub fn reset(&mut self) { + self.set_step(TransactionStep::Idle); + self.state = State::Idle; + self.transaction_params.reset(); } fn insert_packet( &mut self, cfdp_user: &mut impl CfdpUser, packet_to_insert: &impl PduProvider, - ) -> Result<(), DestError> { + ) -> Result { + let mut sent_packets = 0; if packet_to_insert.packet_target()? != PacketTarget::DestEntity { // Unwrap is okay here, a PacketInfo for a file data PDU should always have the // destination as the target. @@ -425,14 +587,18 @@ impl< if packet_to_insert.file_directive_type().is_none() { return Err(DestError::DirectiveFieldEmpty); } - self.handle_file_directive( + sent_packets += self.handle_file_directive( cfdp_user, packet_to_insert.file_directive_type().unwrap(), - packet_to_insert.pdu(), - ) + packet_to_insert.raw_pdu(), + )?; + } + PduType::FileData => { + let fd_pdu = FileDataPdu::from_bytes(packet_to_insert.raw_pdu())?; + sent_packets += self.handle_file_data(cfdp_user, fd_pdu)?; } - PduType::FileData => self.handle_file_data(cfdp_user, packet_to_insert.pdu()), } + Ok(sent_packets) } fn handle_file_directive( @@ -440,9 +606,13 @@ impl< cfdp_user: &mut impl CfdpUser, pdu_directive: FileDirectiveType, raw_packet: &[u8], - ) -> Result<(), DestError> { + ) -> Result { + let mut sent_packets = 0; match pdu_directive { - FileDirectiveType::EofPdu => self.handle_eof_pdu(cfdp_user, raw_packet)?, + FileDirectiveType::EofPdu => { + let eof_pdu = EofPdu::from_bytes(raw_packet)?; + sent_packets += self.handle_eof_pdu(cfdp_user, eof_pdu)? + } FileDirectiveType::FinishedPdu | FileDirectiveType::NakPdu | FileDirectiveType::KeepAlivePdu => { @@ -452,299 +622,964 @@ impl< }); } FileDirectiveType::AckPdu => { - return Err(DestError::NotImplemented); + let ack_pdu = AckPdu::from_bytes(raw_packet)?; + self.handle_ack_pdu(ack_pdu)?; + } + FileDirectiveType::MetadataPdu => { + let metadata_pdu = MetadataPduReader::from_bytes(raw_packet)?; + self.handle_metadata_pdu(metadata_pdu)? } - FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet)?, FileDirectiveType::PromptPdu => self.handle_prompt_pdu(raw_packet)?, }; + Ok(sent_packets) + } + + fn handle_ack_pdu(&mut self, ack_pdu: AckPdu) -> Result<(), DestError> { + if ack_pdu.directive_code_of_acked_pdu() != FileDirectiveType::FinishedPdu { + self.transaction_params + .anomaly_tracker + .increment_invalid_ack_directive_code(); + } + // We are done. + self.reset(); Ok(()) } - fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { - if self.state != State::Idle { - return Err(DestError::RecvdMetadataButIsBusy); + fn first_packet_handling(&mut self, pdu_config: &CommonPduConfig) -> Result<(), DestError> { + self.transaction_params.reset(); + let remote_cfg = self.remote_cfg_table.get(pdu_config.source_id().value()); + if remote_cfg.is_none() { + return Err(DestError::NoRemoteConfigFound(pdu_config.source_id())); } - let metadata_pdu = MetadataPduReader::from_bytes(raw_packet)?; - self.tparams.reset(); - self.tparams.tstate.metadata_params = *metadata_pdu.metadata_params(); + self.transaction_params.remote_cfg = Some(*remote_cfg.unwrap()); + self.transaction_params.transaction_id = Some(TransactionId::new( + pdu_config.source_id(), + pdu_config.transaction_seq_num, + )); + self.transaction_params.pdu_conf = *pdu_config; + self.transaction_params.pdu_conf.direction = spacepackets::cfdp::Direction::TowardsSender; + self.state = State::Busy; + Ok(()) + } + + fn handle_metadata_pdu(&mut self, metadata_pdu: MetadataPduReader) -> Result<(), DestError> { + let first_packet = self.step() == TransactionStep::Idle; + if first_packet { + self.first_packet_handling(metadata_pdu.pdu_header().common_pdu_conf())?; + } + match self.transmission_mode() { + Some(transmission_mode) => { + if !first_packet && transmission_mode == TransmissionMode::Unacknowledged { + return Err(DestError::RecvdMetadataButIsBusy); + } + match self.transaction_params.acked_params.as_mut() { + Some(acked_params) => { + if acked_params.metadata_missing { + if let Some(timer) = + self.transaction_params.deferred_procedure_timer.as_mut() + { + acked_params.nak_activity_counter = 0; + timer.reset(); + } + acked_params.metadata_missing = false; + } else { + // Quietly ignore. + return Ok(()); + } + } + None => { + self.transaction_params.acked_params = Some(AcknowledgedModeParams { + last_start_offset: 0, + last_end_offset: 0, + metadata_missing: false, + nak_activity_counter: 0, + deferred_procedure_active: false, + }); + } + } + } + None => { + return Err(DestError::RecvdMetadataButIsBusy); + } + } + + self.transaction_params.metadata_params = *metadata_pdu.metadata_params(); let remote_cfg = self.remote_cfg_table.get(metadata_pdu.source_id().value()); if remote_cfg.is_none() { - return Err(DestError::NoRemoteCfgFound(metadata_pdu.dest_id())); + return Err(DestError::NoRemoteConfigFound(metadata_pdu.dest_id())); } - self.tparams.remote_cfg = Some(*remote_cfg.unwrap()); - // TODO: Support for metadata only PDUs. let src_name = metadata_pdu.src_file_name(); let dest_name = metadata_pdu.dest_file_name(); if src_name.is_empty() && dest_name.is_empty() { - self.tparams.tstate.metadata_only = true; + self.transaction_params.metadata_only = true; } - if !self.tparams.tstate.metadata_only && src_name.is_empty() { + if !self.transaction_params.metadata_only && src_name.is_empty() { return Err(DestError::EmptySrcFileField); } - if !self.tparams.tstate.metadata_only && dest_name.is_empty() { + if !self.transaction_params.metadata_only && dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } - if !self.tparams.tstate.metadata_only { - self.tparams.file_properties.src_file_name[..src_name.len_value()] + if !self.transaction_params.metadata_only { + self.transaction_params.file_names.src_file_name[..src_name.len_value()] .copy_from_slice(src_name.value()); - self.tparams.file_properties.src_file_name_len = src_name.len_value(); + self.transaction_params.file_names.src_file_name_len = src_name.len_value(); if dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } - self.tparams.file_properties.dest_file_name[..dest_name.len_value()] + self.transaction_params.file_names.dest_file_name[..dest_name.len_value()] .copy_from_slice(dest_name.value()); - self.tparams.file_properties.dest_file_name_len = dest_name.len_value(); - self.tparams.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); - self.tparams.msgs_to_user_size = 0; + self.transaction_params.file_names.dest_file_name_len = dest_name.len_value(); + self.transaction_params.msgs_to_user_size = 0; } if !metadata_pdu.options().is_empty() { for option_tlv in metadata_pdu.options_iter().unwrap() { if option_tlv.is_standard_tlv() && option_tlv.tlv_type().unwrap() == TlvType::MsgToUser { - self.tparams + self.transaction_params .msgs_to_user_buf .copy_from_slice(option_tlv.raw_data().unwrap()); - self.tparams.msgs_to_user_size += option_tlv.len_full(); + self.transaction_params.msgs_to_user_size += option_tlv.len_full(); } } } - self.state = State::Busy; - self.step = TransactionStep::TransactionStart; + self.set_step(TransactionStep::TransactionStart); Ok(()) } + fn reset_nak_activity_parameters_if_active(&mut self) { + if let Some(acked_params) = self.transaction_params.acked_params.as_mut() { + if acked_params.metadata_missing { + if let Some(timer) = self.transaction_params.deferred_procedure_timer.as_mut() { + acked_params.nak_activity_counter = 0; + timer.reset(); + } + } + } + } + + fn handle_file_data_without_previous_metadata( + &mut self, + fd_pdu: &FileDataPdu, + ) -> Result { + let mut packets_sent = 0; + if fd_pdu.transmission_mode() == TransmissionMode::Unacknowledged { + // In acknowledged mode, we need to wait for the metadata PDU first. + return Err(DestError::FirstPacketNotMetadata); + } + self.first_packet_handling(fd_pdu.pdu_header().common_pdu_conf())?; + self.transaction_params.progress = fd_pdu.offset() + fd_pdu.file_data().len() as u64; + self.transaction_params.acked_params = Some(AcknowledgedModeParams { + last_start_offset: 0, + last_end_offset: 0, + nak_activity_counter: 0, + metadata_missing: true, + deferred_procedure_active: false, + }); + if fd_pdu.file_data().len() as u64 > 0 { + self.lost_segment_tracker + .add_lost_segment((0, self.transaction_params.progress))?; + let ack_params = self.transaction_params.acked_params.as_mut().unwrap(); + ack_params.last_start_offset = self.transaction_params.progress; + ack_params.last_end_offset = self.transaction_params.progress; + } + if self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .immediate_nak_mode + { + let mut num_seg_reqs = 1; + if fd_pdu.file_data().len() as u64 > 0 { + num_seg_reqs += 1; + } + let pdu_header = PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + let mut nak_pdu = NakPduCreatorWithReservedSeqReqsBuf::new( + self.pdu_and_cksum_buffer.get_mut(), + pdu_header, + num_seg_reqs, + ) + .map_err(PduError::from)?; + let buf_mut = nak_pdu.segment_request_buffer_mut(); + let mut current_offset = 0; + let increment = if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large { + 8 + } else { + 4 + }; + buf_mut[0..current_offset].fill(0); + current_offset += increment; + buf_mut[current_offset..current_offset + increment].fill(0); + current_offset += increment; + if fd_pdu.file_data().len() as u64 > 0 { + buf_mut[0..current_offset].fill(0); + current_offset += increment; + if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large { + buf_mut[current_offset..current_offset + increment] + .copy_from_slice(&self.transaction_params.progress.to_be_bytes()); + } else { + buf_mut[current_offset..current_offset + increment] + .copy_from_slice(&(self.transaction_params.progress as u32).to_be_bytes()); + } + } + let written_size = nak_pdu + .finish(0, self.transaction_params.progress) + .map_err(PduError::from)?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::NakPdu, + &self.pdu_and_cksum_buffer.borrow()[0..written_size], + )?; + packets_sent += 1; + } + Ok(packets_sent) + } + fn handle_file_data( &mut self, user: &mut impl CfdpUser, - raw_packet: &[u8], - ) -> Result<(), DestError> { - if self.state == State::Idle - || (self.step != TransactionStep::ReceivingFileDataPdus - && self.step != TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling) - { - return Err(DestError::WrongStateForFileData); - } - let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; - if self.local_cfg.indication_cfg.file_segment_recv { - user.file_segment_recvd_indication(&FileSegmentRecvdParams { - id: self.tstate().transaction_id.unwrap(), - offset: fd_pdu.offset(), - length: fd_pdu.file_data().len(), - segment_metadata: fd_pdu.segment_metadata(), - }); + fd_pdu: FileDataPdu, + ) -> Result { + let mut sent_packets = 0; + let mut handle_indication = |id: TransactionId, indication_config: &IndicationConfig| { + if indication_config.file_segment_recv { + user.file_segment_recvd_indication(&FileSegmentRecvdParams { + id, + offset: fd_pdu.offset(), + length: fd_pdu.file_data().len(), + segment_metadata: fd_pdu.segment_metadata(), + }); + } + }; + let step = self.step.get(); + if self.state == State::Idle { + if step == TransactionStep::Idle { + sent_packets += self.handle_file_data_without_previous_metadata(&fd_pdu)?; + self.set_step(TransactionStep::WaitingForMetadata); + handle_indication( + self.transaction_id().unwrap(), + &self.local_cfg.indication_cfg, + ); + return Ok(sent_packets); + } + if step != TransactionStep::ReceivingFileDataPdus + && step != TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling + { + return Err(DestError::WrongStepForPdu { + pdu_type: PduType::FileData, + file_directive_type: None, + step, + }); + } } + handle_indication( + self.transaction_id().unwrap(), + &self.local_cfg.indication_cfg, + ); if let Err(e) = self.vfs.write_data( // Safety: It was already verified that the path is valid during the transaction start. unsafe { from_utf8_unchecked( //from_utf8( - &self.tparams.file_properties.dest_path_buf - [0..self.tparams.file_properties.dest_file_path_len], + &self.transaction_params.file_names.dest_path_buf + [0..self.transaction_params.file_names.dest_file_path_len], ) }, fd_pdu.offset(), fd_pdu.file_data(), ) { - self.declare_fault(ConditionCode::FilestoreRejection); + if self.declare_fault(ConditionCode::FilestoreRejection) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); + } return Err(e.into()); } - self.tstate_mut().progress += fd_pdu.file_data().len() as u64; - Ok(()) + self.transaction_params.progress = core::cmp::max( + self.transaction_params.progress, + fd_pdu.offset() + fd_pdu.file_data().len() as u64, + ); + if self.transmission_mode().unwrap() == TransmissionMode::Acknowledged { + sent_packets += self.lost_segment_handling(&fd_pdu)?; + } + Ok(sent_packets) + } + + fn lost_segment_handling(&mut self, fd_pdu: &FileDataPdu) -> Result { + // Lost segment detection: 4.6.4.3.1 a) and b) are covered by this code. c) is covered + //by dedicated code which is run when the EOF PDU is handled. + if self.transaction_params.acked_params.is_none() { + return Ok(0); + } + + let mut sent_packets = 0; + let acked_params = self.transaction_params.acked_params.as_mut().unwrap(); + if fd_pdu.offset() > acked_params.last_end_offset { + let lost_segment = (acked_params.last_end_offset, fd_pdu.offset()); + self.lost_segment_tracker.add_lost_segment(lost_segment)?; + if self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .immediate_nak_mode + { + let pdu_header = + PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + if self.transaction_params.pdu_conf.file_flag == LargeFileFlag::Normal + && lost_segment.0 > u32::MAX as u64 + || lost_segment.1 > u32::MAX as u64 + { + return Err(DestError::UnexpectedLargeFileSize); + } + let seg32 = [(lost_segment.0 as u32, lost_segment.1 as u32)]; + let seg64 = [lost_segment]; + let nak_pdu = if self.transaction_params.pdu_conf.file_flag == LargeFileFlag::Normal + { + NakPduCreator::new_normal_file_size( + pdu_header, + 0, + self.transaction_params.progress as u32, + &seg32, + ) + .unwrap() + } else { + NakPduCreator::new_large_file_size( + pdu_header, + 0, + self.transaction_params.progress, + &seg64, + ) + .unwrap() + }; + let written_len = nak_pdu.write_to_bytes(self.pdu_and_cksum_buffer.get_mut())?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::NakPdu, + &self.pdu_and_cksum_buffer.borrow()[0..written_len], + )?; + sent_packets += 1; + } + } + if fd_pdu.offset() >= acked_params.last_end_offset { + acked_params.last_start_offset = fd_pdu.offset(); + acked_params.last_end_offset = fd_pdu.offset() + fd_pdu.file_data().len() as u64; + } + if fd_pdu.offset() + fd_pdu.file_data().len() as u64 <= acked_params.last_start_offset { + // Might be a re-requested FD PDU. + let removed = self.lost_segment_tracker.remove_lost_segment(( + fd_pdu.offset(), + fd_pdu.offset() + fd_pdu.file_data().len() as u64, + ))?; + // Reception of missing segments resets the NAK activity parameters. + // See CFDP 4.6.4.7. + if removed && acked_params.deferred_procedure_active { + self.reset_nak_activity_parameters_if_active(); + } + } + Ok(sent_packets) } fn handle_eof_pdu( &mut self, cfdp_user: &mut impl CfdpUser, - raw_packet: &[u8], - ) -> Result<(), DestError> { - if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { - return Err(DestError::WrongStateForEof); + eof_pdu: EofPdu, + ) -> Result { + let sent_packets = 0; + let first_packet = self.step() == TransactionStep::Idle; + if first_packet { + self.first_packet_handling(eof_pdu.pdu_header().common_pdu_conf())?; } - let eof_pdu = EofPdu::from_bytes(raw_packet)?; if self.local_cfg.indication_cfg.eof_recv { // Unwrap is okay here, application logic ensures that transaction ID is valid here. - cfdp_user.eof_recvd_indication(self.tparams.tstate.transaction_id.as_ref().unwrap()); + cfdp_user + .eof_recvd_indication(self.transaction_params.transaction_id.as_ref().unwrap()); } - let regular_transfer_finish = if eof_pdu.condition_code() == ConditionCode::NoError { - self.handle_no_error_eof_pdu(&eof_pdu)? - } else { - // This is an EOF (Cancel), perform Cancel Response Procedures according to chapter - // 4.6.6 of the standard. - self.trigger_notice_of_completion_cancelled( - eof_pdu.condition_code(), - EntityIdTlv::new(self.tparams.remote_cfg.unwrap().entity_id), - ); - self.tparams.tstate.progress = eof_pdu.file_size(); - if eof_pdu.file_size() > 0 { - self.tparams.tstate.delivery_code = DeliveryCode::Incomplete; - } else { - self.tparams.tstate.delivery_code = DeliveryCode::Complete; + if first_packet { + if self.transmission_mode().unwrap() == TransmissionMode::Unacknowledged { + return Err(DestError::WrongStepForPdu { + pdu_type: PduType::FileDirective, + file_directive_type: Some(FileDirectiveType::EofPdu), + step: self.step(), + }); } - // TODO: The cancel EOF also supplies a checksum and a progress number. We could cross - // check that checksum, but how would we deal with a checksum failure? The standard - // does not specify anything for this case.. It could be part of the status report - // issued to the user though. - true - }; - if regular_transfer_finish { - self.file_transfer_complete_transition(); + return self.handle_eof_without_previous_metadata_in_acked_mode(&eof_pdu); } + if eof_pdu.condition_code() == ConditionCode::NoError { + if !self.handle_eof_no_error(&eof_pdu)? { + return Ok(sent_packets); + } + } else { + self.handle_eof_cancel(&eof_pdu); + }; + let mut sent_packets = 0; + match self.transaction_params.transmission_mode() { + TransmissionMode::Acknowledged => { + self.acknowledge_eof_pdu(&eof_pdu)?; + sent_packets += 1; + match self.transaction_params.acked_params.as_ref() { + Some(acked_params) => { + if acked_params.metadata_missing || !self.lost_segment_tracker.is_empty() { + self.start_deferred_lost_segment_handling(); + } else { + self.set_step(TransactionStep::TransferCompletion); + } + } + // This can happen when the EOF is the first packet to arrive. + None => { + self.start_deferred_lost_segment_handling(); + } + } + } + TransmissionMode::Unacknowledged => { + self.set_step(TransactionStep::TransferCompletion); + } + } + Ok(sent_packets) + } + + fn handle_eof_without_previous_metadata_in_acked_mode( + &mut self, + eof_pdu: &EofPdu, + ) -> Result { + let mut sent_packets = 0; + self.transaction_params.file_size = eof_pdu.file_size(); + self.transaction_params.checksum = eof_pdu.file_checksum(); + self.transaction_params.acked_params = Some(AcknowledgedModeParams { + last_start_offset: 0, + last_end_offset: 0, + nak_activity_counter: 0, + metadata_missing: true, + deferred_procedure_active: false, + }); + if self.transaction_params.file_size > 0 { + self.lost_segment_tracker.reset(); + // Add the whole file to the lost segments map for now. + self.lost_segment_tracker + .add_lost_segment((0, eof_pdu.file_size()))?; + } + if eof_pdu.condition_code() != ConditionCode::NoError { + self.handle_eof_cancel(eof_pdu); + } + self.acknowledge_eof_pdu(eof_pdu)?; + sent_packets += 1; + if self.transaction_params.completion_disposition.get() == CompletionDisposition::Cancelled + { + self.set_step(TransactionStep::TransferCompletion); + return Ok(sent_packets); + } + self.set_step(TransactionStep::WaitingForMetadata); + self.start_deferred_lost_segment_handling(); + Ok(sent_packets) + } + + fn handle_eof_cancel(&mut self, eof_pdu: &EofPdu) { + // This is an EOF (Cancel), perform Cancel Response Procedures according to chapter + // 4.6.6 of the standard. Set remote ID as fault location. + self.trigger_notice_of_completion_cancelled( + eof_pdu.condition_code(), + EntityIdTlv::new(self.transaction_params.remote_cfg.unwrap().entity_id), + ); + // Store this as progress for the checksum calculation as well. + self.transaction_params.progress = eof_pdu.file_size(); + if let Some(ack_params) = &self.transaction_params.acked_params { + if ack_params.metadata_missing { + return; + } + } + if self.transaction_params.progress == 0 { + // Empty file, no file data PDU. + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); + return; + } + if self.checksum_verify(self.transaction_params.progress, eof_pdu.file_checksum()) { + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); + return; + } + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Incomplete); + } + + fn acknowledge_eof_pdu(&mut self, eof_pdu: &EofPdu) -> Result<(), DestError> { + let pdu_header = PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + let ack_pdu = AckPdu::new_for_eof_pdu( + pdu_header, + eof_pdu.condition_code(), + TransactionStatus::Active, + ); + let written_len = ack_pdu.write_to_bytes(self.pdu_and_cksum_buffer.get_mut())?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::AckPdu, + &self.pdu_and_cksum_buffer.borrow()[0..written_len], + )?; Ok(()) } - fn trigger_notice_of_completion_cancelled( + fn start_deferred_lost_segment_handling(&mut self) { + match &mut self.transaction_params.acked_params { + Some(params) => { + params.last_start_offset = self.transaction_params.file_size; + params.last_end_offset = self.transaction_params.file_size; + params.deferred_procedure_active = true; + params.nak_activity_counter = 0; + } + None => { + self.transaction_params.acked_params = Some(AcknowledgedModeParams { + last_start_offset: self.transaction_params.file_size, + last_end_offset: self.transaction_params.file_size, + metadata_missing: false, + nak_activity_counter: 0, + deferred_procedure_active: true, + }) + } + } + } + + fn check_for_deferred_lost_segment_completion(&mut self, metadata_missing: bool) -> bool { + if self.lost_segment_tracker.is_empty() && !metadata_missing { + // We are done and have received everything. + match self.checksum_verify( + self.transaction_params.progress, + self.transaction_params.checksum, + ) { + true => { + self.transaction_params + .finished_params + .condition_code + .set(ConditionCode::NoError); + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); + } + false => { + self.transaction_params + .finished_params + .condition_code + .set(ConditionCode::FileChecksumFailure); + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Incomplete); + } + } + self.transaction_params + .acked_params + .as_mut() + .unwrap() + .deferred_procedure_active = false; + self.set_step(TransactionStep::TransferCompletion); + return true; + } + false + } + + fn deferred_lost_segment_handling(&mut self) -> Result { + assert!( + self.transaction_params.acked_params.is_some(), + "acknowledged parameters unexpectedly None" + ); + let acked_params = self.transaction_params.acked_params.unwrap(); + if self.check_for_deferred_lost_segment_completion(acked_params.metadata_missing) { + return Ok(0); + } + let mut sent_packets = 0; + let first_nak_issuance = self.transaction_params.deferred_procedure_timer.is_none(); + if first_nak_issuance { + self.transaction_params.deferred_procedure_timer = Some( + self.check_timer_creator + .create_countdown(TimerContext::NakActivity { + expiry_time: self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .nak_timer_interval, + }), + ); + } else if !self + .transaction_params + .deferred_procedure_timer + .as_ref() + .unwrap() + .has_expired() + { + return Ok(sent_packets); + } + if !first_nak_issuance + && acked_params.nak_activity_counter + 1 + == self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .nak_timer_expiration_limit + { + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Incomplete); + if self.declare_fault(ConditionCode::NakLimitReached) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); + } + return Ok(sent_packets); + } + if !first_nak_issuance { + self.transaction_params + .acked_params + .as_mut() + .unwrap() + .nak_activity_counter += 1; + self.transaction_params + .deferred_procedure_timer + .as_mut() + .unwrap() + .reset(); + } + let pdu_header = PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + let max_segment_reqs = NakPduCreatorWithReservedSeqReqsBuf::calculate_max_segment_requests( + self.transaction_params + .remote_cfg + .as_ref() + .unwrap() + .max_packet_len, + &pdu_header, + ) + .map_err(|_| DestError::InvalidRemoteConfig(self.transaction_params.remote_cfg.unwrap()))?; + let mut segments_to_send = self.lost_segment_tracker.number_of_segments(); + if acked_params.metadata_missing { + segments_to_send += 1; + } + if segments_to_send <= max_segment_reqs { + // Should never fail because we calculcated the allowed maximum number of segment + // requests for the buffer. + let mut nak_pdu_creator = NakPduCreatorWithReservedSeqReqsBuf::new( + self.pdu_and_cksum_buffer.get_mut(), + pdu_header, + segments_to_send, + ) + .unwrap(); + // All error conditions were checked so this should never fail. + // + // 1. Number of segments was calculated. + // 2. Buffer size was calculated based on PDU header and maximum packet size. + // 3. Large file flag is based on PDU header, so there should never be a missmatch. + self.lost_segment_tracker + .write_to_nak_segment_list(&mut nak_pdu_creator, acked_params.metadata_missing) + .expect("unexpected lost segment write error"); + let written_len = nak_pdu_creator + .finish(0, self.transaction_params.file_size) + .map_err(PduError::from)?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::NakPdu, + &self.pdu_and_cksum_buffer.borrow()[0..written_len], + )?; + sent_packets += 1; + } else { + sent_packets += self.write_multi_packet_nak_sequence( + segments_to_send, + max_segment_reqs, + acked_params.metadata_missing, + )?; + } + + Ok(sent_packets) + } + + #[cold] + fn write_multi_packet_nak_sequence( &mut self, + mut segments_to_send: usize, + max_segments: usize, + first_segment_metadata: bool, + ) -> Result { + let mut sent_packets = 0; + let pdu_header = PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + let mut nak_pdu_creator = NakPduCreatorWithReservedSeqReqsBuf::new( + self.pdu_and_cksum_buffer.get_mut(), + pdu_header, + max_segments, + ) + .unwrap(); + + let mut segment_index = 0; + let mut buf_index = 0; + let mut current_start_of_scope = 0; + let mut current_end_of_scope = 0; + let mut seg_buf = nak_pdu_creator.segment_request_buffer_mut(); + + // First segment re-requests metadata PDU. + if first_segment_metadata { + if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large { + seg_buf[0..16].fill(0); + buf_index = 16; + } else { + seg_buf[0..8].fill(0); + buf_index = 8; + } + segment_index = 1; // account for the header gap entry + } + + for (idx, (start, end)) in self.lost_segment_tracker.iter().enumerate() { + // flush full PDU *before* writing the new entry + if segment_index == max_segments { + let written_len = nak_pdu_creator + .finish(current_start_of_scope, current_end_of_scope) + .map_err(PduError::from)?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::NakPdu, + &self.pdu_and_cksum_buffer.borrow()[..written_len], + )?; + sent_packets += 1; + segments_to_send = segments_to_send.saturating_sub(max_segments); + + // new PDU with the same capacity + nak_pdu_creator = NakPduCreatorWithReservedSeqReqsBuf::new( + self.pdu_and_cksum_buffer.get_mut(), + pdu_header, + core::cmp::min(segments_to_send, max_segments), + ) + .unwrap(); + seg_buf = nak_pdu_creator.segment_request_buffer_mut(); + segment_index = 0; + current_start_of_scope = current_end_of_scope; + buf_index = 0; + } + + // write entry + if pdu_header.common_pdu_conf().file_flag == LargeFileFlag::Large { + seg_buf[buf_index..buf_index + 8].copy_from_slice(&start.to_be_bytes()); + buf_index += 8; + seg_buf[buf_index..buf_index + 8].copy_from_slice(&end.to_be_bytes()); + buf_index += 8; + } else { + seg_buf[buf_index..buf_index + 4].copy_from_slice(&(start as u32).to_be_bytes()); + buf_index += 4; + seg_buf[buf_index..buf_index + 4].copy_from_slice(&(end as u32).to_be_bytes()); + buf_index += 4; + } + if idx == max_segments { + // Clamp the end of scope to the progress for the last NAK in the sequence. + current_end_of_scope = self.transaction_params.progress; + } else { + current_end_of_scope = end; + } + segment_index += 1; + } + + // send trailing PDU if anything was written + if segment_index > 0 { + let written_len = nak_pdu_creator + .finish(current_start_of_scope, current_end_of_scope) + .map_err(PduError::from)?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::NakPdu, + &self.pdu_and_cksum_buffer.borrow()[..written_len], + )?; + sent_packets += 1; + } + Ok(sent_packets) + } + + fn trigger_notice_of_completion_cancelled( + &self, cond_code: ConditionCode, fault_location: EntityIdTlv, ) { - self.tparams.tstate.completion_disposition = CompletionDisposition::Cancelled; - self.tparams.tstate.condition_code = cond_code; - self.tparams.tstate.fault_location_finished = Some(fault_location); + self.transaction_params + .completion_disposition + .set(CompletionDisposition::Cancelled); + self.transaction_params + .finished_params + .condition_code + .set(cond_code); + self.transaction_params + .finished_params + .fault_location_finished + .set(Some(fault_location)); // For anything larger than 0, we'd have to do a checksum check to verify whether // the delivery is really complete, and we need the EOF checksum for that.. - if self.tparams.tstate.progress == 0 { - self.tparams.tstate.delivery_code = DeliveryCode::Complete; + if self.transaction_params.progress == 0 { + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); } } /// Returns whether the transfer can be completed regularly. - fn handle_no_error_eof_pdu(&mut self, eof_pdu: &EofPdu) -> Result { + fn handle_eof_no_error(&mut self, eof_pdu: &EofPdu) -> Result { // CFDP 4.6.1.2.9: Declare file size error if progress exceeds file size - if self.tparams.tstate.progress > eof_pdu.file_size() - && self.declare_fault(ConditionCode::FileSizeError) != FaultHandlerCode::IgnoreError - { - return Ok(false); - } else if (self.tparams.tstate.progress < eof_pdu.file_size()) - && self.tparams.transmission_mode() == TransmissionMode::Acknowledged + if self.transaction_params.progress > eof_pdu.file_size() { + match self.declare_fault(ConditionCode::FileSizeError) { + FaultHandlerCode::IgnoreError => (), + FaultHandlerCode::AbandonTransaction => { + self.abandon_transaction(); + return Ok(false); + } + FaultHandlerCode::NoticeOfCancellation | FaultHandlerCode::NoticeOfSuspension => { + return Ok(false); + } + } + } else if (self.transaction_params.progress < eof_pdu.file_size()) + && self.transaction_params.transmission_mode() == TransmissionMode::Acknowledged { // CFDP 4.6.4.3.1: The end offset of the last received file segment and the file // size as stated in the EOF PDU is not the same, so we need to add that segment to // the lost segments for the deferred lost segment detection procedure. - // TODO: Proper lost segment handling. - // self._params.acked_params.lost_seg_tracker.add_lost_segment( - // (self._params.fp.progress, self._params.fp.file_size_eof) - // ) + self.lost_segment_tracker + .add_lost_segment((self.transaction_params.progress, eof_pdu.file_size()))?; } - self.tparams.tstate.checksum = eof_pdu.file_checksum(); - if self.tparams.transmission_mode() == TransmissionMode::Unacknowledged - && !self.checksum_verify(self.tparams.tstate.checksum) + self.transaction_params.file_size = eof_pdu.file_size(); + self.transaction_params.checksum = eof_pdu.file_checksum(); + if self.transaction_params.transmission_mode() == TransmissionMode::Unacknowledged + && !self.checksum_verify( + self.transaction_params.progress, + self.transaction_params.checksum, + ) { - if self.declare_fault(ConditionCode::FileChecksumFailure) - != FaultHandlerCode::IgnoreError - { - return Ok(false); - } self.start_check_limit_handling(); return Ok(false); } + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); + self.transaction_params + .finished_params + .condition_code + .set(ConditionCode::NoError); Ok(true) } - fn file_transfer_complete_transition(&mut self) { - if self.tparams.transmission_mode() == TransmissionMode::Unacknowledged { - self.step = TransactionStep::TransferCompletion; - } else { - // TODO: Prepare ACK PDU somehow. - self.step = TransactionStep::SendingAckPdu; - } - } - - fn checksum_verify(&mut self, checksum: u32) -> bool { - let mut file_delivery_complete = false; - if self.tparams.metadata_params().checksum_type == ChecksumType::NullChecksum - || self.tparams.tstate.metadata_only + fn checksum_verify(&mut self, verify_len: u64, checksum: u32) -> bool { + if self.transaction_params.metadata_params().checksum_type == ChecksumType::NullChecksum + || self.transaction_params.metadata_only { - file_delivery_complete = true; - } else { - match self.vfs.checksum_verify( - checksum, - // Safety: It was already verified that the path is valid during the transaction start. - unsafe { - from_utf8_unchecked( - &self.tparams.file_properties.dest_path_buf - [0..self.tparams.file_properties.dest_file_path_len], - ) - }, - self.tparams.metadata_params().checksum_type, - self.tparams.tstate.progress, - &mut self.tparams.cksum_buf, - ) { - Ok(checksum_success) => { - file_delivery_complete = checksum_success; - if !checksum_success { - self.tparams.tstate.delivery_code = DeliveryCode::Incomplete; - self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure; - } + return true; + } + match self.vfs.checksum_verify( + checksum, + // Safety: It was already verified that the path is valid during the transaction start. + unsafe { + from_utf8_unchecked( + &self.transaction_params.file_names.dest_path_buf + [0..self.transaction_params.file_names.dest_file_path_len], + ) + }, + self.transaction_params.metadata_params().checksum_type, + verify_len, + self.pdu_and_cksum_buffer.get_mut(), + ) { + Ok(false) => { + if self.declare_fault(ConditionCode::FileChecksumFailure) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); } - Err(e) => match e { - FilestoreError::ChecksumTypeNotImplemented(_) => { - self.declare_fault(ConditionCode::UnsupportedChecksumType); - // For this case, the applicable algorithm shall be the the null checksum, - // which is always succesful. - file_delivery_complete = true; + false + } + Ok(true) => true, + Err(e) => match e { + FilestoreError::ChecksumTypeNotImplemented(_) => { + if self.declare_fault(ConditionCode::UnsupportedChecksumType) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); } - _ => { - self.declare_fault(ConditionCode::FilestoreRejection); - // Treat this equivalent to a failed checksum procedure. + // For this case, the applicable algorithm shall be the the null checksum, + // which is always succesful. + true + } + _ => { + if self.declare_fault(ConditionCode::FilestoreRejection) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); } - }, - }; + // Treat this equivalent to a failed checksum procedure. + false + } + }, } - if file_delivery_complete { - self.tparams.tstate.delivery_code = DeliveryCode::Complete; - self.tparams.tstate.condition_code = ConditionCode::NoError; - } - file_delivery_complete } fn start_check_limit_handling(&mut self) { - self.step = TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling; - self.tparams.tstate.current_check_timer = Some(self.check_timer_creator.create_countdown( - TimerContext::CheckLimit { - local_id: self.local_cfg.id, - remote_id: self.tparams.remote_cfg.unwrap().entity_id, - entity_type: EntityType::Receiving, - }, - )); - self.tparams.tstate.current_check_count = 0; + self.set_step(TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling); + self.transaction_params.current_check_timer = Some( + self.check_timer_creator + .create_countdown(TimerContext::CheckLimit { + local_id: self.local_cfg.id, + remote_id: self.transaction_params.remote_cfg.unwrap().entity_id, + entity_type: EntityType::Receiving, + }), + ); + self.transaction_params.current_check_count = 0; } - fn check_limit_handling(&mut self) { - if self.tparams.tstate.current_check_timer.is_none() { - return; + fn check_limit_handling(&mut self) -> Result<(), DestError> { + if self.transaction_params.current_check_timer.is_none() { + return Ok(()); } - let check_timer = self.tparams.tstate.current_check_timer.as_ref().unwrap(); + let check_timer = self + .transaction_params + .current_check_timer + .as_ref() + .unwrap(); if check_timer.has_expired() { - if self.checksum_verify(self.tparams.tstate.checksum) { - self.file_transfer_complete_transition(); - return; + if self.checksum_verify( + self.transaction_params.progress, + self.transaction_params.checksum, + ) { + self.transaction_params + .finished_params + .condition_code + .set(ConditionCode::NoError); + self.transaction_params + .finished_params + .delivery_code + .set(DeliveryCode::Complete); + self.set_step(TransactionStep::TransferCompletion); + return Ok(()); } - if self.tparams.tstate.current_check_count + 1 - >= self.tparams.remote_cfg.unwrap().check_limit + if self.transaction_params.current_check_count + 1 + >= self.transaction_params.remote_cfg.unwrap().check_limit { - self.declare_fault(ConditionCode::CheckLimitReached); + if self.declare_fault(ConditionCode::CheckLimitReached) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); + } } else { - self.tparams.tstate.current_check_count += 1; - self.tparams - .tstate + self.transaction_params.current_check_count += 1; + self.transaction_params .current_check_timer .as_mut() .unwrap() .reset(); } } + Ok(()) } - pub fn handle_prompt_pdu(&mut self, _raw_packet: &[u8]) -> Result<(), DestError> { + fn handle_prompt_pdu(&mut self, _raw_packet: &[u8]) -> Result<(), DestError> { Err(DestError::NotImplemented) } fn fsm_busy(&mut self, cfdp_user: &mut impl CfdpUser) -> Result { let mut sent_packets = 0; - if self.step == TransactionStep::TransactionStart { + if self.step() == TransactionStep::TransactionStart { let result = self.transaction_start(cfdp_user); if let Err(e) = result { // If we can not even start the transaction properly, reset the handler. @@ -754,54 +1589,54 @@ impl< return Err(e); } } - if self.step == TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling { - self.check_limit_handling(); + if self.step() == TransactionStep::WaitingForMetadata + || self.step() == TransactionStep::ReceivingFileDataPdus + { + if let Some(ack_params) = &mut self.transaction_params.acked_params { + if ack_params.deferred_procedure_active { + sent_packets += self.deferred_lost_segment_handling()?; + } + } } - if self.step == TransactionStep::TransferCompletion { + if self.step() == TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling { + self.check_limit_handling()?; + } + if self.step() == TransactionStep::TransferCompletion { sent_packets += self.transfer_completion(cfdp_user)?; } - if self.step == TransactionStep::SendingAckPdu { - return Err(DestError::NotImplemented); - } - if self.step == TransactionStep::SendingFinishedPdu { - self.reset(); + if self.step() == TransactionStep::WaitingForFinishedAck { + sent_packets += self.handle_positive_ack_procedures()?; + // Little hack because of the state machine handling order to ensure the transfer + // is completed in one go. + if self.step() == TransactionStep::TransferCompletion { + sent_packets += self.transfer_completion(cfdp_user)?; + } } Ok(sent_packets) } - /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. - pub fn step(&self) -> TransactionStep { - self.step - } - - /// Get the step, which denotes whether the CFDP handler is active, and which CFDP class - /// is used if it is active. - pub fn state(&self) -> State { - self.state - } - fn transaction_start(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { + let id = self.transaction_id().unwrap(); let dest_name = from_utf8( - &self.tparams.file_properties.dest_file_name - [..self.tparams.file_properties.dest_file_name_len], + &self.transaction_params.file_names.dest_file_name + [..self.transaction_params.file_names.dest_file_name_len], )?; - self.tparams.file_properties.dest_path_buf[0..dest_name.len()] + self.transaction_params.file_names.dest_path_buf[0..dest_name.len()] .copy_from_slice(dest_name.as_bytes()); - self.tparams.file_properties.dest_file_path_len = dest_name.len(); - let source_id = self.tparams.pdu_conf.source_id(); - let id = TransactionId::new(source_id, self.tparams.pdu_conf.transaction_seq_num); + self.transaction_params.file_names.dest_file_path_len = dest_name.len(); + let source_id = self.transaction_params.pdu_conf.source_id(); let src_name = from_utf8( - &self.tparams.file_properties.src_file_name - [0..self.tparams.file_properties.src_file_name_len], + &self.transaction_params.file_names.src_file_name + [0..self.transaction_params.file_names.src_file_name_len], )?; let mut msgs_to_user = SmallVec::<[MsgToUserTlv<'_>; 16]>::new(); let mut num_msgs_to_user = 0; - if self.tparams.msgs_to_user_size > 0 { + if self.transaction_params.msgs_to_user_size > 0 { let mut index = 0; - while index < self.tparams.msgs_to_user_size { + while index < self.transaction_params.msgs_to_user_size { // This should never panic as the validity of the options was checked beforehand. let msgs_to_user_tlv = - MsgToUserTlv::from_bytes(&self.tparams.msgs_to_user_buf[index..]) + MsgToUserTlv::from_bytes(&self.transaction_params.msgs_to_user_buf[index..]) .expect("message to user creation failed unexpectedly"); msgs_to_user.push(msgs_to_user_tlv); index += msgs_to_user_tlv.len_full(); @@ -811,12 +1646,11 @@ impl< let metadata_recvd_params = MetadataReceivedParams { id, source_id, - file_size: self.tparams.file_size(), + file_size: self.transaction_params.file_size(), src_file_name: src_name, dest_file_name: dest_name, msgs_to_user: &msgs_to_user[..num_msgs_to_user], }; - self.tparams.tstate.transaction_id = Some(id); cfdp_user.metadata_recvd_indication(&metadata_recvd_params); if self.vfs.exists(dest_name)? && self.vfs.is_dir(dest_name)? { @@ -830,105 +1664,200 @@ impl< return Err(DestError::PathConcat); } let source_name = source_file_name.unwrap(); - self.tparams.file_properties.dest_path_buf[dest_name.len()] = b'/'; - self.tparams.file_properties.dest_path_buf + self.transaction_params.file_names.dest_path_buf[dest_name.len()] = b'/'; + self.transaction_params.file_names.dest_path_buf [dest_name.len() + 1..dest_name.len() + 1 + source_name.len()] .copy_from_slice(source_name.as_bytes()); - self.tparams.file_properties.dest_file_path_len += 1 + source_name.len(); + self.transaction_params.file_names.dest_file_path_len += 1 + source_name.len(); } let dest_path_str = from_utf8( - &self.tparams.file_properties.dest_path_buf - [0..self.tparams.file_properties.dest_file_path_len], + &self.transaction_params.file_names.dest_path_buf + [0..self.transaction_params.file_names.dest_file_path_len], )?; if self.vfs.exists(dest_path_str)? { self.vfs.truncate_file(dest_path_str)?; } else { self.vfs.create_file(dest_path_str)?; } - self.tparams.tstate.file_status = FileStatus::Retained; - self.step = TransactionStep::ReceivingFileDataPdus; + self.transaction_params.finished_params.file_status = FileStatus::Retained; + drop(msgs_to_user); + self.set_step(TransactionStep::ReceivingFileDataPdus); Ok(()) } fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result { let mut sent_packets = 0; self.notice_of_completion(cfdp_user)?; - if self.tparams.transmission_mode() == TransmissionMode::Acknowledged - || self.tparams.metadata_params().closure_requested + if self.transaction_params.transmission_mode() == TransmissionMode::Acknowledged + || self.transaction_params.metadata_params().closure_requested { sent_packets += self.send_finished_pdu()?; + self.start_positive_ack_procedure(); + if self.transaction_params.transmission_mode() == TransmissionMode::Acknowledged { + self.set_step(TransactionStep::WaitingForFinishedAck); + return Ok(sent_packets); + } } - self.step = TransactionStep::SendingFinishedPdu; + self.reset(); Ok(sent_packets) } + fn start_positive_ack_procedure(&mut self) { + match &mut self.transaction_params.positive_ack_params { + Some(current) => { + current.ack_counter = 0; + } + None => { + self.transaction_params.positive_ack_params = Some(PositiveAckParams { + ack_counter: 0, + positive_ack_of_cancellation: false, + }) + } + } + self.transaction_params.ack_timer = Some( + self.check_timer_creator + .create_countdown(TimerContext::PositiveAck { + expiry_time: self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .positive_ack_timer_interval, + }), + ); + } + + fn handle_positive_ack_procedures(&mut self) -> Result { + // Do we have positive-ack params? + if self.transaction_params.positive_ack_params.is_none() { + return Ok(0); + } + let params = self.transaction_params.positive_ack_params.unwrap(); + + // Has the timer expired? + if !self + .transaction_params + .ack_timer + .as_mut() + .unwrap() + .has_expired() + { + return Ok(0); + } + + let expiration_limit = self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .positive_ack_timer_expiration_limit; + // If bumping the counter would exceed the limit, fault & maybe abandon + if params.ack_counter + 1 >= expiration_limit { + if self.declare_fault(ConditionCode::PositiveAckLimitReached) + == FaultHandlerCode::AbandonTransaction + { + self.abandon_transaction(); + return Ok(0); + } + self.transaction_params + .positive_ack_params + .as_mut() + .unwrap() + .positive_ack_of_cancellation = true; + return Ok(0); + } + + let params_mut = self + .transaction_params + .positive_ack_params + .as_mut() + .unwrap(); + params_mut.ack_counter += 1; + self.transaction_params.ack_timer.as_mut().unwrap().reset(); + self.send_finished_pdu() + } + fn notice_of_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { - if self.tstate().completion_disposition == CompletionDisposition::Completed { + if self.transaction_params.completion_disposition.get() == CompletionDisposition::Completed + { // TODO: Execute any filestore requests } else if self - .tparams + .transaction_params .remote_cfg .as_ref() .unwrap() .disposition_on_cancellation - && self.tstate().delivery_code == DeliveryCode::Incomplete + && self.transaction_params.finished_params.delivery_code.get() + == DeliveryCode::Incomplete { // Safety: We already verified that the path is valid during the transaction start. let dest_path = unsafe { from_utf8_unchecked( - &self.tparams.file_properties.dest_path_buf - [0..self.tparams.file_properties.dest_file_path_len], + &self.transaction_params.file_names.dest_path_buf + [0..self.transaction_params.file_names.dest_file_path_len], ) }; if self.vfs.exists(dest_path)? && self.vfs.is_file(dest_path)? { self.vfs.remove_file(dest_path)?; } - self.tstate_mut().file_status = FileStatus::DiscardDeliberately; + self.transaction_params.finished_params.file_status = FileStatus::DiscardDeliberately; } - let tstate = self.tstate(); + let tstate = &self.transaction_params; let transaction_finished_params = TransactionFinishedParams { id: tstate.transaction_id.unwrap(), - condition_code: tstate.condition_code, - delivery_code: tstate.delivery_code, - file_status: tstate.file_status, + condition_code: tstate.finished_params.condition_code.get(), + delivery_code: tstate.finished_params.delivery_code.get(), + file_status: tstate.finished_params.file_status, }; cfdp_user.transaction_finished_indication(&transaction_finished_params); Ok(()) } - fn declare_fault(&mut self, condition_code: ConditionCode) -> FaultHandlerCode { + // When the fault handler code [FaultHandlerCode::AbandonTransaction] is returned, the caller + // must call [Self::abandon_transaction] as soon as it is possible. + fn declare_fault(&self, condition_code: ConditionCode) -> FaultHandlerCode { // Cache those, because they might be reset when abandoning the transaction. - let transaction_id = self.tstate().transaction_id.unwrap(); - let progress = self.tstate().progress; - let fh_code = self + let transaction_id = self.transaction_id().unwrap(); + let progress = self.transaction_params.progress; + let mut fh_code = self .local_cfg .fault_handler .get_fault_handler(condition_code); + // CFDP 4.11.2.3.2: Any fault declared in the course of transferring the Finished (cancel) + // PDU must result in abadonment of the transaction. + if let Some(positive_ack) = &self.transaction_params.positive_ack_params { + if positive_ack.positive_ack_of_cancellation { + fh_code = FaultHandlerCode::AbandonTransaction; + } + } match fh_code { FaultHandlerCode::NoticeOfCancellation => { self.notice_of_cancellation(condition_code, EntityIdTlv::new(self.local_cfg().id)); } FaultHandlerCode::NoticeOfSuspension => self.notice_of_suspension(), FaultHandlerCode::IgnoreError => (), - FaultHandlerCode::AbandonTransaction => self.abandon_transaction(), + FaultHandlerCode::AbandonTransaction => (), } - self.local_cfg - .fault_handler - .report_fault(transaction_id, condition_code, progress) + self.local_cfg.fault_handler.report_fault( + fh_code, + FaultInfo::new(transaction_id, condition_code, progress), + ) } - fn notice_of_cancellation( - &mut self, - condition_code: ConditionCode, - fault_location: EntityIdTlv, - ) { - self.step = TransactionStep::TransferCompletion; - self.tstate_mut().condition_code = condition_code; - self.tstate_mut().fault_location_finished = Some(fault_location); - self.tstate_mut().completion_disposition = CompletionDisposition::Cancelled; + fn notice_of_cancellation(&self, condition_code: ConditionCode, fault_location: EntityIdTlv) { + self.set_step_internal(TransactionStep::TransferCompletion); + let tstate = &self.transaction_params; + tstate.finished_params.condition_code.set(condition_code); + tstate + .completion_disposition + .set(CompletionDisposition::Cancelled); + tstate + .finished_params + .fault_location_finished + .set(Some(fault_location)); } - fn notice_of_suspension(&mut self) { + fn notice_of_suspension(&self) { // TODO: Implement suspension handling. } @@ -936,55 +1865,45 @@ impl< self.reset(); } - /// This function is public to allow completely resetting the handler, but it is explicitely - /// discouraged to do this. CFDP has mechanism to detect issues and errors on itself. - /// Resetting the handler might interfere with these mechanisms and lead to unexpected - /// behaviour. - pub fn reset(&mut self) { - self.step = TransactionStep::Idle; - self.state = State::Idle; - // self.packets_to_send_ctx.packet_available = false; - self.tparams.reset(); + #[inline] + fn set_step_internal(&self, step: TransactionStep) { + self.step.set(step); + } + + #[inline] + fn set_step(&mut self, step: TransactionStep) { + self.set_step_internal(step); } fn send_finished_pdu(&mut self) -> Result { - let tstate = self.tstate(); + let tstate = &self.transaction_params; - let pdu_header = PduHeader::new_no_file_data(self.tparams.pdu_conf, 0); - let finished_pdu = if tstate.condition_code == ConditionCode::NoError - || tstate.condition_code == ConditionCode::UnsupportedChecksumType + let pdu_header = PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0); + let finished_pdu = if tstate.finished_params.condition_code.get() == ConditionCode::NoError + || tstate.finished_params.condition_code.get() == ConditionCode::UnsupportedChecksumType { - FinishedPduCreator::new_default(pdu_header, tstate.delivery_code, tstate.file_status) - } else { - FinishedPduCreator::new_generic( + FinishedPduCreator::new_no_error( pdu_header, - tstate.condition_code, - tstate.delivery_code, - tstate.file_status, + tstate.finished_params.delivery_code.get(), + tstate.finished_params.file_status, + ) + } else { + FinishedPduCreator::new( + pdu_header, + tstate.finished_params.condition_code.get(), + tstate.finished_params.delivery_code.get(), + tstate.finished_params.file_status, &[], - tstate.fault_location_finished, + tstate.finished_params.fault_location_finished.get(), ) }; - finished_pdu.write_to_bytes(&mut self.packet_buf)?; - self.pdu_sender.send_pdu( - finished_pdu.pdu_type(), - finished_pdu.file_directive_type(), - &self.packet_buf[0..finished_pdu.len_written()], + finished_pdu.write_to_bytes(self.pdu_and_cksum_buffer.get_mut())?; + self.pdu_sender.send_file_directive_pdu( + FileDirectiveType::FinishedPdu, + &self.pdu_and_cksum_buffer.borrow()[0..finished_pdu.len_written()], )?; Ok(1) } - - pub fn local_cfg(&self) -> &LocalEntityConfig { - &self.local_cfg - } - - fn tstate(&self) -> &TransferState { - &self.tparams.tstate - } - - fn tstate_mut(&mut self) -> &mut TransferState { - &mut self.tparams.tstate - } } #[cfg(test)] @@ -1003,14 +1922,18 @@ mod tests { cfdp::{ ChecksumType, TransmissionMode, lv::Lv, - pdu::{WritablePduPacket, finished::FinishedPduReader, metadata::MetadataPduCreator}, + pdu::{ + WritablePduPacket, finished::FinishedPduReader, metadata::MetadataPduCreator, + nak::NakPduReader, + }, }, - util::{UbfU16, UnsignedByteFieldU8}, + util::UnsignedByteFieldU8, }; use crate::{ - CRC_32, FaultHandler, IndicationConfig, PduRawWithInfo, StdRemoteEntityConfigProvider, + CRC_32, FaultHandler, IndicationConfig, PduRawWithInfo, RemoteConfigStoreStd, filestore::NativeFilestore, + lost_segments::LostSegmentsList, tests::{ LOCAL_ID, REMOTE_ID, SentPdu, TestCfdpSender, TestCfdpUser, TestCheckTimer, TestCheckTimerCreator, TestFaultHandler, TimerExpiryControl, basic_remote_cfg_table, @@ -1019,13 +1942,20 @@ mod tests { use super::*; + #[derive(Debug, Clone, Copy)] + pub struct TransferInfo { + id: TransactionId, + header: PduHeader, + } + type TestDestHandler = DestinationHandler< TestCfdpSender, TestFaultHandler, NativeFilestore, - StdRemoteEntityConfigProvider, + RemoteConfigStoreStd, TestCheckTimerCreator, TestCheckTimer, + LostSegmentsList, >; struct DestHandlerTestbench { @@ -1036,21 +1966,34 @@ mod tests { check_dest_file: bool, check_handler_idle_at_drop: bool, closure_requested: bool, - pdu_header: PduHeader, + pdu_conf: CommonPduConfig, expected_full_data: Vec, expected_file_size: u64, buf: [u8; 512], } impl DestHandlerTestbench { - fn new_with_fixed_paths(fault_handler: TestFaultHandler, closure_requested: bool) -> Self { + fn new_with_fixed_paths( + fault_handler: TestFaultHandler, + + transmission_mode: TransmissionMode, + closure_requested: bool, + ) -> Self { let (src_path, dest_path) = init_full_filepaths_textfile(); assert!(!Path::exists(&dest_path)); - Self::new(fault_handler, closure_requested, true, src_path, dest_path) + Self::new( + fault_handler, + transmission_mode, + closure_requested, + true, + src_path, + dest_path, + ) } fn new( fault_handler: TestFaultHandler, + transmission_mode: TransmissionMode, closure_requested: bool, check_dest_file: bool, src_path: PathBuf, @@ -1059,7 +2002,7 @@ mod tests { let expiry_control = TimerExpiryControl::default(); let test_sender = TestCfdpSender::default(); let dest_handler = default_dest_handler(fault_handler, test_sender, &expiry_control); - let handler = Self { + let mut handler = Self { expiry_control, handler: dest_handler, src_path, @@ -1068,14 +2011,29 @@ mod tests { check_dest_file, check_handler_idle_at_drop: true, expected_file_size: 0, - pdu_header: create_pdu_header(UbfU16::new(0)), + pdu_conf: CommonPduConfig::new_with_byte_fields( + LOCAL_ID, + REMOTE_ID, + UnsignedByteFieldU8::new(0), + ) + .unwrap(), expected_full_data: Vec::new(), buf: [0; 512], }; + handler.pdu_conf.trans_mode = transmission_mode; handler.state_check(State::Idle, TransactionStep::Idle); handler } + pub fn fault_handler(&self) -> &FaultHandler { + &self.handler.local_cfg.fault_handler + } + + #[inline] + fn set_large_file_flag(&mut self, large_file: LargeFileFlag) { + self.pdu_conf.file_flag = large_file; + } + fn dest_path(&self) -> &PathBuf { &self.dest_path } @@ -1093,6 +2051,21 @@ mod tests { &mut self.handler.local_cfg.indication_cfg } + fn remote_cfg_mut(&mut self) -> &mut RemoteEntityConfig { + self.handler + .remote_cfg_table + .get_mut(LOCAL_ID.value()) + .unwrap() + } + + fn pdu_queue_empty(&mut self) -> bool { + self.handler.pdu_sender.queue_empty() + } + + fn pdu_queue_len(&mut self) -> usize { + self.handler.pdu_sender.queue_len() + } + fn get_next_pdu(&mut self) -> Option { self.handler.pdu_sender.retrieve_next_pdu() } @@ -1102,9 +2075,15 @@ mod tests { } fn set_check_timer_expired(&mut self) { - self.expiry_control - .check_limit - .store(true, core::sync::atomic::Ordering::Release); + self.expiry_control.set_check_limit_expired(); + } + + fn set_nak_activity_timer_expired(&mut self) { + self.expiry_control.set_nak_activity_expired(); + } + + fn set_positive_ack_expired(&mut self) { + self.expiry_control.set_positive_ack_expired(); } fn test_user_from_cached_paths(&self, expected_file_size: u64) -> TestCfdpUser { @@ -1120,12 +2099,13 @@ mod tests { &mut self, user: &mut TestCfdpUser, file_size: u64, - ) -> Result { + ) -> Result { self.expected_file_size = file_size; assert_eq!(user.transaction_indication_call_count, 0); assert_eq!(user.metadata_recv_queue.len(), 0); + let pdu_header = PduHeader::new_for_file_directive(self.pdu_conf, 0); let metadata_pdu = create_metadata_pdu( - &self.pdu_header, + &pdu_header, self.src_path.as_path(), self.dest_path.as_path(), file_size, @@ -1136,7 +2116,7 @@ mod tests { assert_eq!(user.metadata_recv_queue.len(), 1); assert_eq!( self.handler.transmission_mode().unwrap(), - TransmissionMode::Unacknowledged + self.pdu_conf.trans_mode ); assert_eq!(user.transaction_indication_call_count, 0); assert_eq!(user.metadata_recv_queue.len(), 1); @@ -1153,7 +2133,10 @@ mod tests { assert_eq!(metadata_recvd.id, self.handler.transaction_id().unwrap()); assert_eq!(metadata_recvd.file_size, file_size); assert!(metadata_recvd.msgs_to_user.is_empty()); - Ok(self.handler.transaction_id().unwrap()) + Ok(TransferInfo { + id: self.handler.transaction_id().unwrap(), + header: pdu_header, + }) } fn generic_file_data_insert( @@ -1162,8 +2145,9 @@ mod tests { offset: u64, file_data_chunk: &[u8], ) -> Result { + let pdu_header = PduHeader::new_for_file_data_default(self.pdu_conf, 0); let filedata_pdu = - FileDataPdu::new_no_seg_metadata(self.pdu_header, offset, file_data_chunk); + FileDataPdu::new_no_seg_metadata(pdu_header, offset, file_data_chunk); filedata_pdu .write_to_bytes(&mut self.buf) .expect("writing file data PDU failed"); @@ -1171,11 +2155,9 @@ mod tests { let result = self.handler.state_machine(user, Some(&packet_info)); if self.indication_cfg().file_segment_recv { assert!(!user.file_seg_recvd_queue.is_empty()); - assert_eq!(user.file_seg_recvd_queue.back().unwrap().offset, offset); - assert_eq!( - user.file_seg_recvd_queue.back().unwrap().length, - file_data_chunk.len() - ); + let file_seg = user.file_seg_recvd_queue.pop_front().unwrap(); + assert_eq!(file_seg.offset, offset); + assert_eq!(file_seg.length, file_data_chunk.len()); } result } @@ -1187,7 +2169,8 @@ mod tests { ) -> Result { self.expected_full_data = expected_full_data; assert_eq!(user.finished_indic_queue.len(), 0); - let eof_pdu = create_no_error_eof(&self.expected_full_data, &self.pdu_header); + let pdu_header = PduHeader::new_for_file_directive(self.pdu_conf, 0); + let eof_pdu = create_no_error_eof(&self.expected_full_data, &pdu_header); let packet_info = create_packet_info(&eof_pdu, &mut self.buf); self.check_handler_idle_at_drop = true; self.check_dest_file = true; @@ -1210,6 +2193,85 @@ mod tests { assert_eq!(finished_indication.condition_code, ConditionCode::NoError); } + fn check_completion_indication_failure( + &mut self, + user: &mut TestCfdpUser, + cond_code: ConditionCode, + file_status: FileStatus, + delivery_code: DeliveryCode, + ) { + assert_eq!(user.finished_indic_queue.len(), 1); + let finished_indication = user.finished_indic_queue.pop_front().unwrap(); + assert_eq!( + finished_indication.id, + self.handler.transaction_id().unwrap() + ); + assert_eq!(finished_indication.file_status, file_status); + assert_eq!(finished_indication.delivery_code, delivery_code); + assert_eq!(finished_indication.condition_code, cond_code); + } + + fn check_eof_ack_pdu(&mut self, cond_code: ConditionCode) { + assert!(!self.pdu_queue_empty()); + let pdu = self.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::AckPdu); + let ack_pdu = AckPdu::from_bytes(&pdu.raw_pdu).unwrap(); + assert_eq!(ack_pdu.condition_code(), cond_code); + assert_eq!(ack_pdu.transaction_status(), TransactionStatus::Active); + assert_eq!( + ack_pdu.directive_code_of_acked_pdu(), + FileDirectiveType::EofPdu + ); + } + + fn check_finished_pdu_success(&mut self) { + let pdu = self.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!( + pdu.file_directive_type.unwrap(), + FileDirectiveType::FinishedPdu + ); + let finished_pdu = FinishedPduReader::from_bytes(&pdu.raw_pdu).unwrap(); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!(finished_pdu.condition_code(), ConditionCode::NoError); + assert!(finished_pdu.fault_location().is_none()); + } + + fn check_finished_pdu_failure( + &mut self, + cond_code: ConditionCode, + file_status: FileStatus, + delivery_code: DeliveryCode, + ) { + let pdu = self.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!( + pdu.file_directive_type.unwrap(), + FileDirectiveType::FinishedPdu + ); + let finished_pdu = FinishedPduReader::from_bytes(&pdu.raw_pdu).unwrap(); + assert_eq!(finished_pdu.delivery_code(), delivery_code); + assert_eq!(finished_pdu.file_status(), file_status); + assert_eq!(finished_pdu.condition_code(), cond_code); + } + + fn acknowledge_finished_pdu( + &mut self, + user: &mut impl CfdpUser, + transfer_info: &TransferInfo, + ) { + let ack_pdu = AckPdu::new_for_finished_pdu( + transfer_info.header, + ConditionCode::NoError, + TransactionStatus::Active, + ); + self.handler + .state_machine(user, Some(&create_packet_info(&ack_pdu, &mut self.buf))) + .expect("handling ack PDU failed"); + } + fn state_check(&self, state: State, step: TransactionStep) { assert_eq!(self.handler.state(), state); assert_eq!(self.handler.step(), step); @@ -1219,8 +2281,18 @@ mod tests { // Specifying some checks in the drop method avoids some boilerplate. impl Drop for DestHandlerTestbench { fn drop(&mut self) { - assert!(self.all_fault_queues_empty()); - assert!(self.handler.pdu_sender.queue_empty()); + if !self.all_fault_queues_empty() { + let fh_queues = self.handler.local_cfg.user_fault_hook().borrow(); + println!( + "fault queues not empty. cancellation {}, suspension {}, ignored {}, abandon {}", + fh_queues.notice_of_cancellation_queue.len(), + fh_queues.notice_of_suspension_queue.len(), + fh_queues.ignored_queue.len(), + fh_queues.abandoned_queue.len() + ); + } + assert!(self.all_fault_queues_empty(), "fault queues not empty, "); + assert!(self.pdu_queue_empty()); if self.check_handler_idle_at_drop { self.state_check(State::Idle, TransactionStep::Idle); } @@ -1261,16 +2333,10 @@ mod tests { NativeFilestore::default(), basic_remote_cfg_table(LOCAL_ID, 1024, true), TestCheckTimerCreator::new(expiry_control), + LostSegmentsList::default(), ) } - fn create_pdu_header(seq_num: impl Into) -> PduHeader { - let mut pdu_conf = - CommonPduConfig::new_with_byte_fields(LOCAL_ID, REMOTE_ID, seq_num).unwrap(); - pdu_conf.trans_mode = TransmissionMode::Unacknowledged; - PduHeader::new_no_file_data(pdu_conf, 0) - } - fn create_metadata_pdu<'filename>( pdu_header: &PduHeader, src_name: &'filename Path, @@ -1349,7 +2415,11 @@ mod tests { #[test] fn test_empty_file_transfer_not_acked_no_closure() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); let mut test_user = tb.test_user_from_cached_paths(0); tb.generic_transfer_init(&mut test_user, 0) .expect("transfer init failed"); @@ -1359,6 +2429,59 @@ mod tests { tb.check_completion_indication_success(&mut test_user); } + #[test] + fn test_empty_file_transfer_invalid_remote_id() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); + let mut test_user = tb.test_user_from_cached_paths(0); + let mut conf = tb.pdu_conf; + // Just swap them.. + conf.set_source_and_dest_id(REMOTE_ID, LOCAL_ID).unwrap(); + let pdu_header = PduHeader::new_for_file_directive(conf, 0); + let metadata_pdu = create_metadata_pdu( + &pdu_header, + tb.src_path.as_path(), + tb.dest_path.as_path(), + 0, + false, + ); + let packet_info = create_packet_info(&metadata_pdu, &mut tb.buf); + if let Err(DestError::NoRemoteConfigFound(id)) = + tb.handler.state_machine(&mut test_user, Some(&packet_info)) + { + assert_eq!(id, REMOTE_ID.into()); + } else { + panic!("expected no remote config found error"); + } + tb.check_dest_file = false; + } + + #[test] + fn test_empty_file_transfer_acked() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_eof_no_error(&mut user, Vec::new()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + #[test] fn test_small_file_transfer_not_acked() { let file_data_str = "Hello World!"; @@ -1366,16 +2489,61 @@ mod tests { let file_size = file_data.len() as u64; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); - let mut test_user = tb.test_user_from_cached_paths(file_size); - tb.generic_transfer_init(&mut test_user, file_size) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(file_size); + let _transfer_info = tb + .generic_transfer_init(&mut user, file_size) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.generic_file_data_insert(&mut test_user, 0, file_data) - .expect("file data insertion failed"); - tb.generic_eof_no_error(&mut test_user, file_data.to_vec()) - .expect("EOF no error insertion failed"); - tb.check_completion_indication_success(&mut test_user); + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, file_data) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 0 + ); + tb.check_completion_indication_success(&mut user); + } + + #[test] + fn test_small_file_transfer_acked() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, file_data) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 2 + ); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); } #[test] @@ -1387,7 +2555,11 @@ mod tests { let segment_len = 256; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); let mut test_user = tb.test_user_from_cached_paths(file_size); tb.generic_transfer_init(&mut test_user, file_size) .expect("transfer init failed"); @@ -1405,6 +2577,38 @@ mod tests { tb.check_completion_indication_success(&mut test_user); } + #[test] + fn test_segmented_file_transfer_acked() { + let mut rng = rand::rng(); + let mut random_data = [0u8; 512]; + rng.fill(&mut random_data); + let file_size = random_data.len() as u64; + let segment_len = 256; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_file_data_insert(&mut user, 0, &random_data[0..segment_len]) + .expect("file data insertion failed"); + tb.generic_file_data_insert(&mut user, segment_len as u64, &random_data[segment_len..]) + .expect("file data insertion failed"); + tb.generic_eof_no_error(&mut user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + #[test] fn test_check_limit_handling_transfer_success() { let mut rng = rand::rng(); @@ -1414,9 +2618,13 @@ mod tests { let segment_len = 256; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); let mut test_user = tb.test_user_from_cached_paths(file_size); - let transaction_id = tb + let transfer_info = tb .generic_transfer_init(&mut test_user, file_size) .expect("transfer init failed"); @@ -1425,30 +2633,37 @@ mod tests { .expect("file data insertion 0 failed"); tb.generic_eof_no_error(&mut test_user, random_data.to_vec()) .expect("EOF no error insertion failed"); + + // Checksum failure. + let mut fault_handler = tb.handler.local_cfg.fault_handler.user_hook.borrow_mut(); + assert_eq!(fault_handler.ignored_queue.len(), 1); + let cancelled = fault_handler.ignored_queue.pop_front().unwrap(); + assert_eq!(cancelled.transaction_id(), transfer_info.id); + assert_eq!( + cancelled.condition_code(), + ConditionCode::FileChecksumFailure + ); + assert_eq!(cancelled.progress(), segment_len as u64); + + drop(fault_handler); tb.state_check( State::Busy, TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, ); + tb.set_check_timer_expired(); + tb.generic_file_data_insert( &mut test_user, segment_len as u64, &random_data[segment_len..], ) .expect("file data insertion 1 failed"); - tb.set_check_timer_expired(); tb.handler .state_machine_no_packet(&mut test_user) .expect("fsm failure"); - let mut fault_handler = tb.handler.local_cfg.fault_handler.user_hook.borrow_mut(); - - assert_eq!(fault_handler.ignored_queue.len(), 1); - let cancelled = fault_handler.ignored_queue.pop_front().unwrap(); - assert_eq!(cancelled.0, transaction_id); - assert_eq!(cancelled.1, ConditionCode::FileChecksumFailure); - assert_eq!(cancelled.2, segment_len as u64); - drop(fault_handler); tb.check_completion_indication_success(&mut test_user); + assert!(test_user.indication_queues_empty()); } #[test] @@ -1457,12 +2672,27 @@ mod tests { let mut random_data = [0u8; 512]; rng.fill(&mut random_data); let file_size = random_data.len() as u64; - let segment_len = 256; + let segment_len: usize = 256; + let check_checksum_failure = + |testbench: &mut DestHandlerTestbench, transaction_id: TransactionId| { + let mut fault_hook = testbench.fault_handler().user_hook.borrow_mut(); + assert!(fault_hook.notice_of_suspension_queue.is_empty()); + let ignored_queue = &mut fault_hook.ignored_queue; + assert_eq!(ignored_queue.len(), 1); + let ignored = ignored_queue.pop_front().unwrap(); + assert_eq!(ignored.transaction_id(), transaction_id); + assert_eq!(ignored.condition_code(), ConditionCode::FileChecksumFailure); + assert_eq!(ignored.progress(), segment_len as u64); + }; let fault_handler = TestFaultHandler::default(); - let mut testbench = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut testbench = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); let mut test_user = testbench.test_user_from_cached_paths(file_size); - let transaction_id = testbench + let transfer_info = testbench .generic_transfer_init(&mut test_user, file_size) .expect("transfer init failed"); @@ -1473,10 +2703,13 @@ mod tests { testbench .generic_eof_no_error(&mut test_user, random_data.to_vec()) .expect("EOF no error insertion failed"); + check_checksum_failure(&mut testbench, transfer_info.id); + testbench.state_check( State::Busy, TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, ); + testbench.set_check_timer_expired(); testbench .handler @@ -1486,30 +2719,36 @@ mod tests { State::Busy, TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, ); + check_checksum_failure(&mut testbench, transfer_info.id); + testbench.set_check_timer_expired(); testbench .handler .state_machine_no_packet(&mut test_user) .expect("fsm error"); + check_checksum_failure(&mut testbench, transfer_info.id); + testbench.state_check(State::Idle, TransactionStep::Idle); - let mut fault_hook = testbench.handler.local_cfg.user_fault_hook().borrow_mut(); - assert!(fault_hook.notice_of_suspension_queue.is_empty()); - let ignored_queue = &mut fault_hook.ignored_queue; - assert_eq!(ignored_queue.len(), 1); - let cancelled = ignored_queue.pop_front().unwrap(); - assert_eq!(cancelled.0, transaction_id); - assert_eq!(cancelled.1, ConditionCode::FileChecksumFailure); - assert_eq!(cancelled.2, segment_len as u64); + { + let mut fault_hook = testbench.fault_handler().user_hook.borrow_mut(); + let cancelled_queue = &mut fault_hook.notice_of_cancellation_queue; + assert_eq!(cancelled_queue.len(), 1); + let cancelled = cancelled_queue.pop_front().unwrap(); + assert_eq!(cancelled.transaction_id(), transfer_info.id); + assert_eq!(cancelled.condition_code(), ConditionCode::CheckLimitReached); + assert_eq!(cancelled.progress(), segment_len as u64); + } - let cancelled_queue = &mut fault_hook.notice_of_cancellation_queue; - assert_eq!(cancelled_queue.len(), 1); - let cancelled = cancelled_queue.pop_front().unwrap(); - assert_eq!(cancelled.0, transaction_id); - assert_eq!(cancelled.1, ConditionCode::CheckLimitReached); - assert_eq!(cancelled.2, segment_len as u64); - - drop(fault_hook); + assert_eq!(test_user.finished_indic_queue.len(), 1); + let finished_indication = test_user.finished_indic_queue.pop_front().unwrap(); + assert_eq!(finished_indication.id, transfer_info.id); + assert_eq!( + finished_indication.condition_code, + ConditionCode::CheckLimitReached + ); + assert_eq!(finished_indication.delivery_code, DeliveryCode::Incomplete); + assert_eq!(finished_indication.file_status, FileStatus::Retained); assert!(testbench.handler.pdu_sender.queue_empty()); @@ -1520,6 +2759,7 @@ mod tests { assert_eq!(read_content.len(), segment_len); assert_eq!(read_content, &random_data[0..segment_len]); assert!(fs::remove_file(testbench.dest_path().as_path()).is_ok()); + assert!(test_user.indication_queues_empty()); } fn check_finished_pdu_success(sent_pdu: &SentPdu) { @@ -1539,7 +2779,11 @@ mod tests { #[test] fn test_file_transfer_with_closure() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + true, + ); let mut test_user = tb.test_user_from_cached_paths(0); tb.generic_transfer_init(&mut test_user, 0) .expect("transfer init failed"); @@ -1555,16 +2799,21 @@ mod tests { let sent_pdu = tb.handler.pdu_sender.retrieve_next_pdu().unwrap(); check_finished_pdu_success(&sent_pdu); tb.check_completion_indication_success(&mut test_user); + assert!(test_user.indication_queues_empty()); } #[test] fn test_finished_pdu_insertion_rejected() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); tb.check_dest_file = false; let mut user = tb.test_user_from_cached_paths(0); - let finished_pdu = FinishedPduCreator::new_default( - PduHeader::new_no_file_data(CommonPduConfig::default(), 0), + let finished_pdu = FinishedPduCreator::new_no_error( + PduHeader::new_for_file_directive(CommonPduConfig::default(), 0), DeliveryCode::Complete, FileStatus::Retained, ); @@ -1586,14 +2835,19 @@ mod tests { #[test] fn test_metadata_insertion_twice_fails() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + true, + ); let mut user = tb.test_user_from_cached_paths(0); tb.generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.check_handler_idle_at_drop = false; tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let pdu_header = PduHeader::new_for_file_directive(tb.pdu_conf, 0); let metadata_pdu = create_metadata_pdu( - &tb.pdu_header, + &pdu_header, tb.src_path.as_path(), tb.dest_path.as_path(), 0, @@ -1615,7 +2869,11 @@ mod tests { let file_data = file_data_str.as_bytes(); let file_size = file_data.len() as u64; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + true, + ); let mut user = tb.test_user_from_cached_paths(file_size); tb.generic_transfer_init(&mut user, file_size) .expect("transfer init failed"); @@ -1641,9 +2899,12 @@ mod tests { let ignored_queue = &mut fault_hook.ignored_queue; assert_eq!(ignored_queue.len(), 1); let cancelled = ignored_queue.pop_front().unwrap(); - assert_eq!(cancelled.0, transaction_id); - assert_eq!(cancelled.1, ConditionCode::FileChecksumFailure); - assert_eq!(cancelled.2, file_size); + assert_eq!(cancelled.transaction_id(), transaction_id); + assert_eq!( + cancelled.condition_code(), + ConditionCode::FileChecksumFailure + ); + assert_eq!(cancelled.progress(), file_size); drop(fault_hook); tb.state_check( @@ -1669,9 +2930,9 @@ mod tests { let cancelled_queue = &mut fault_hook.notice_of_cancellation_queue; assert_eq!(cancelled_queue.len(), 1); let cancelled = cancelled_queue.pop_front().unwrap(); - assert_eq!(cancelled.0, transaction_id); - assert_eq!(cancelled.1, ConditionCode::CheckLimitReached); - assert_eq!(cancelled.2, file_size); + assert_eq!(cancelled.transaction_id(), transaction_id); + assert_eq!(cancelled.condition_code(), ConditionCode::CheckLimitReached); + assert_eq!(cancelled.progress(), file_size); drop(fault_hook); @@ -1687,6 +2948,19 @@ mod tests { finished_pdu.condition_code(), ConditionCode::CheckLimitReached ); + + let mut fault_hook = tb.handler.local_cfg.user_fault_hook().borrow_mut(); + let ignored_queue = &mut fault_hook.ignored_queue; + assert_eq!(ignored_queue.len(), 2); + let mut ignored = ignored_queue.pop_front().unwrap(); + assert_eq!(ignored.transaction_id(), transaction_id); + assert_eq!(ignored.condition_code(), ConditionCode::FileChecksumFailure); + assert_eq!(ignored.progress(), file_size); + ignored = ignored_queue.pop_front().unwrap(); + assert_eq!(ignored.transaction_id(), transaction_id); + assert_eq!(ignored.condition_code(), ConditionCode::FileChecksumFailure); + assert_eq!(ignored.progress(), file_size); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); assert!(finished_pdu.fault_location().is_some()); assert_eq!( @@ -1695,6 +2969,11 @@ mod tests { ); assert_eq!(finished_pdu.fs_responses_raw(), &[]); assert!(tb.handler.pdu_sender.queue_empty()); + user.verify_finished_indication_retained( + DeliveryCode::Incomplete, + ConditionCode::CheckLimitReached, + transaction_id, + ); tb.expected_full_data = faulty_file_data.to_vec(); } @@ -1706,6 +2985,7 @@ mod tests { let mut dest_path_buf = dest_path.keep(); let mut tb = DestHandlerTestbench::new( fault_handler, + TransmissionMode::Unacknowledged, false, false, src_path.clone(), @@ -1713,25 +2993,31 @@ mod tests { ); dest_path_buf.push(src_path.file_name().unwrap()); tb.dest_path = dest_path_buf; - let mut test_user = tb.test_user_from_cached_paths(0); - tb.generic_transfer_init(&mut test_user, 0) + let mut user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.generic_eof_no_error(&mut test_user, Vec::new()) + tb.generic_eof_no_error(&mut user, Vec::new()) .expect("EOF no error insertion failed"); - tb.check_completion_indication_success(&mut test_user); + tb.check_completion_indication_success(&mut user); } #[test] fn test_tranfer_cancellation_empty_file_with_eof_pdu() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); - let mut test_user = tb.test_user_from_cached_paths(0); - tb.generic_transfer_init(&mut test_user, 0) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let pdu_header = PduHeader::new_for_file_directive(tb.pdu_conf, 0); let cancel_eof = EofPdu::new( - tb.pdu_header, + pdu_header, ConditionCode::CancelRequestReceived, 0, 0, @@ -1740,33 +3026,47 @@ mod tests { let packets = tb .handler .state_machine( - &mut test_user, + &mut user, Some(&PduRawWithInfo::new(&cancel_eof.to_vec().unwrap()).unwrap()), ) .expect("state machine call with EOF insertion failed"); assert_eq!(packets, 0); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); } - fn generic_tranfer_cancellation_partial_file_with_eof_pdu(with_closure: bool) { + fn generic_tranfer_cancellation_partial_file_with_eof_pdu( + with_closure: bool, + insert_packet: bool, + ) { let file_data_str = "Hello World!"; let file_data = file_data_str.as_bytes(); let file_size = 5; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, with_closure); - let mut test_user = tb.test_user_from_cached_paths(file_size); - tb.generic_transfer_init(&mut test_user, file_size) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + with_closure, + ); + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) - .expect("file data insertion failed"); - // Checksum is currently ignored on remote side.. we still supply it, according to the - // standard. + if insert_packet { + tb.generic_file_data_insert(&mut user, 0, &file_data[0..5]) + .expect("file data insertion failed"); + } let mut digest = CRC_32.digest(); digest.update(&file_data[0..5]); let checksum = digest.finalize(); + let pdu_header = PduHeader::new_for_file_directive(tb.pdu_conf, 0); let cancel_eof = EofPdu::new( - tb.pdu_header, + pdu_header, ConditionCode::CancelRequestReceived, checksum, 5, @@ -1775,7 +3075,7 @@ mod tests { let packets = tb .handler .state_machine( - &mut test_user, + &mut user, Some(&PduRawWithInfo::new(&cancel_eof.to_vec().unwrap()).unwrap()), ) .expect("state machine call with EOF insertion failed"); @@ -1792,7 +3092,30 @@ mod tests { finished_pdu.condition_code(), ConditionCode::CancelRequestReceived ); - assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); + if insert_packet { + // Checksum success, so data is complete. + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); + } else { + // Checksum failure, so data is incomplete. + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); + tb.check_dest_file = false; + let mut fault_hook = tb.handler.local_cfg.user_fault_hook().borrow_mut(); + let ignored_queue = &mut fault_hook.ignored_queue; + let ignored = ignored_queue.pop_front().unwrap(); + assert_eq!(ignored.transaction_id(), transfer_info.id); + assert_eq!(ignored.condition_code(), ConditionCode::FileChecksumFailure); + assert_eq!(ignored.progress(), 5); + user.verify_finished_indication_retained( + DeliveryCode::Incomplete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); + } assert_eq!(finished_pdu.file_status(), FileStatus::Retained); assert_eq!( finished_pdu @@ -1801,6 +3124,11 @@ mod tests { EntityIdTlv::new(LOCAL_ID.into()) ); } else { + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); assert_eq!(packets, 0); } tb.expected_file_size = file_size; @@ -1808,44 +3136,63 @@ mod tests { } #[test] - fn test_tranfer_cancellation_partial_file_with_eof_pdu_no_closure() { - generic_tranfer_cancellation_partial_file_with_eof_pdu(false); + fn test_tranfer_cancellation_partial_file_with_eof_pdu_no_closure_complete() { + generic_tranfer_cancellation_partial_file_with_eof_pdu(false, true); } + #[test] - fn test_tranfer_cancellation_partial_file_with_eof_pdu_with_closure() { - generic_tranfer_cancellation_partial_file_with_eof_pdu(true); + fn test_tranfer_cancellation_partial_file_with_eof_pdu_with_closure_complete() { + generic_tranfer_cancellation_partial_file_with_eof_pdu(true, true); + } + + #[test] + fn test_tranfer_cancellation_partial_file_with_eof_pdu_with_closure_incomplete() { + generic_tranfer_cancellation_partial_file_with_eof_pdu(true, false); } #[test] fn test_tranfer_cancellation_empty_file_with_cancel_api() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); - let mut test_user = tb.test_user_from_cached_paths(0); - let transaction_id = tb - .generic_transfer_init(&mut test_user, 0) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.handler.cancel_request(&transaction_id); + tb.handler.cancel_request(&transfer_info.id); let packets = tb .handler - .state_machine_no_packet(&mut test_user) + .state_machine_no_packet(&mut user) .expect("state machine call with EOF insertion failed"); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); assert_eq!(packets, 0); } #[test] fn test_tranfer_cancellation_empty_file_with_cancel_api_and_closure() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); - let mut test_user = tb.test_user_from_cached_paths(0); - let transaction_id = tb - .generic_transfer_init(&mut test_user, 0) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + true, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.handler.cancel_request(&transaction_id); + tb.handler.cancel_request(&transfer_info.id); let packets = tb .handler - .state_machine_no_packet(&mut test_user) + .state_machine_no_packet(&mut user) .expect("state machine call with EOF insertion failed"); assert_eq!(packets, 1); let next_pdu = tb.get_next_pdu().unwrap(); @@ -1867,6 +3214,11 @@ mod tests { finished_pdu.fault_location(), Some(EntityIdTlv::new(REMOTE_ID.into())) ); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); } #[test] @@ -1876,19 +3228,23 @@ mod tests { let file_size = 5; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); - let mut test_user = tb.test_user_from_cached_paths(file_size); - let transaction_id = tb - .generic_transfer_init(&mut test_user, file_size) + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + true, + ); + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) + tb.generic_file_data_insert(&mut user, 0, &file_data[0..5]) .expect("file data insertion failed"); - tb.handler.cancel_request(&transaction_id); + tb.handler.cancel_request(&transfer_info.id); let packets = tb .handler - .state_machine_no_packet(&mut test_user) + .state_machine_no_packet(&mut user) .expect("state machine call with EOF insertion failed"); assert_eq!(packets, 1); let next_pdu = tb.get_next_pdu().unwrap(); @@ -1911,31 +3267,45 @@ mod tests { ); tb.expected_file_size = file_size; tb.expected_full_data = file_data[0..file_size as usize].to_vec(); + user.verify_finished_indication_retained( + DeliveryCode::Incomplete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); } // Only incomplete received files will be removed. #[test] fn test_tranfer_cancellation_file_disposition_not_done_for_empty_file() { let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); let remote_cfg_mut = tb .handler .remote_cfg_table .get_mut(LOCAL_ID.value()) .unwrap(); remote_cfg_mut.disposition_on_cancellation = true; - let mut test_user = tb.test_user_from_cached_paths(0); - let transaction_id = tb - .generic_transfer_init(&mut test_user, 0) + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.handler.cancel_request(&transaction_id); + tb.handler.cancel_request(&transfer_info.id); let packets = tb .handler - .state_machine_no_packet(&mut test_user) + .state_machine_no_packet(&mut user) .expect("state machine call with EOF insertion failed"); assert_eq!(packets, 0); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + ); } #[test] @@ -1945,7 +3315,11 @@ mod tests { let file_size = file_data.len() as u64; let fault_handler = TestFaultHandler::default(); - let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Unacknowledged, + false, + ); tb.check_dest_file = false; let remote_cfg_mut = tb .handler @@ -1953,21 +3327,759 @@ mod tests { .get_mut(LOCAL_ID.value()) .unwrap(); remote_cfg_mut.disposition_on_cancellation = true; - let mut test_user = tb.test_user_from_cached_paths(file_size); - let transaction_id = tb - .generic_transfer_init(&mut test_user, file_size) + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) .expect("transfer init failed"); tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); - tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) + tb.generic_file_data_insert(&mut user, 0, &file_data[0..5]) .expect("file data insertion failed"); - tb.handler.cancel_request(&transaction_id); + tb.handler.cancel_request(&transfer_info.id); let packets = tb .handler - .state_machine_no_packet(&mut test_user) + .state_machine_no_packet(&mut user) .expect("state machine call with EOF insertion failed"); assert_eq!(packets, 0); // File was disposed. assert!(!Path::exists(tb.dest_path())); + assert_eq!(user.finished_indic_queue.len(), 1); + user.verify_finished_indication( + DeliveryCode::Incomplete, + ConditionCode::CancelRequestReceived, + transfer_info.id, + FileStatus::DiscardDeliberately, + ); + } + + fn generic_test_immediate_nak_request(file_flag: LargeFileFlag) { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + tb.set_large_file_flag(file_flag); + tb.remote_cfg_mut().immediate_nak_mode = true; + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + // This should immediately trigger a NAK request for file segment 0..4 + assert_eq!( + tb.generic_file_data_insert(&mut user, 4, &file_data[4..]) + .expect("file data insertion failed"), + 1 + ); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.pdu_header().common_pdu_conf().file_flag, file_flag); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 1); + assert_eq!(seg_reqs[0], (0, 4)); + // We simulate the reply by re-inserting the missing file segment. + tb.generic_file_data_insert(&mut user, 0, &file_data[0..4]) + .expect("file data insertion failed"); + + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_immediate_nak_request() { + generic_test_immediate_nak_request(LargeFileFlag::Normal); + } + + #[test] + fn test_immediate_nak_request_large_file() { + generic_test_immediate_nak_request(LargeFileFlag::Large); + } + + fn generic_test_deferred_nak_request(file_flag: LargeFileFlag) { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + tb.set_large_file_flag(file_flag); + // Disable this, we only want to check the deferred procedure. + tb.remote_cfg_mut().immediate_nak_mode = false; + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + assert_eq!( + tb.generic_file_data_insert(&mut user, 4, &file_data[4..]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 2 + ); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 1); + assert_eq!(seg_reqs[0], (0, 4)); + + // We simulate the reply by re-inserting the missing file segment. + tb.generic_file_data_insert(&mut user, 0, &file_data[0..4]) + .expect("file data insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_deferred_nak_request() { + generic_test_deferred_nak_request(LargeFileFlag::Normal); + } + + #[test] + fn test_deferred_nak_request_large() { + generic_test_deferred_nak_request(LargeFileFlag::Large); + } + + #[test] + fn test_file_data_before_metadata() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + tb.remote_cfg_mut().immediate_nak_mode = true; + let mut user = tb.test_user_from_cached_paths(file_size); + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, file_data) + .expect("file data insertion failed"), + 1 + ); + tb.state_check(State::Busy, TransactionStep::WaitingForMetadata); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 2); + // Metadata is re-requested. + assert_eq!(seg_reqs[0], (0, 0)); + // File data is re-requested. The destination handler can not do anything with the file + // data as long as no metadata has been received. Buffering of FD PDUs is not supported. + assert_eq!(seg_reqs[1], (0, 12)); + + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + // Re-insert missing file data. + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, file_data) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 2 + ); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_eof_before_metadata() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + tb.remote_cfg_mut().immediate_nak_mode = true; + let mut user = tb.test_user_from_cached_paths(file_size); + // We expect the ACK for the EOF and a NAK packet re-requesting the metadata and file data. + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("file data insertion failed"), + 2 + ); + tb.state_check(State::Busy, TransactionStep::WaitingForMetadata); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 2); + // Metadata is re-requested. + assert_eq!(seg_reqs[0], (0, 0)); + // File data is re-requested. The destination handler can not do anything with the file + // data as long as no metadata has been received. Buffering of FD PDUs is not supported. + assert_eq!(seg_reqs[1], (0, 12)); + + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + // Re-insert missing file data. + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, file_data) + .expect("file data insertion failed"), + 1 + ); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_nak_limit_reached() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + // Disable this, we only want to check the deferred procedure. + tb.remote_cfg_mut().immediate_nak_mode = false; + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + assert_eq!( + tb.generic_file_data_insert(&mut user, 4, &file_data[4..]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 2 + ); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 1); + assert_eq!(seg_reqs[0], (0, 4)); + + // Let the NAK timer expire + tb.set_nak_activity_timer_expired(); + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 1); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 1); + assert_eq!(seg_reqs[0], (0, 4)); + + // Let the NAK timer expire again. + tb.set_nak_activity_timer_expired(); + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 1); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_completion_indication_failure( + &mut user, + ConditionCode::NakLimitReached, + FileStatus::Retained, + DeliveryCode::Incomplete, + ); + tb.check_finished_pdu_failure( + ConditionCode::NakLimitReached, + FileStatus::Retained, + DeliveryCode::Incomplete, + ); + + { + let mut fault_hook = tb.fault_handler().user_hook.borrow_mut(); + assert_eq!(fault_hook.notice_of_cancellation_queue.len(), 1); + let cancellation = fault_hook.notice_of_cancellation_queue.pop_front().unwrap(); + assert_eq!(cancellation.transaction_id(), transfer_info.id); + assert_eq!( + cancellation.condition_code(), + ConditionCode::NakLimitReached + ); + assert_eq!(cancellation.progress(), file_size); + } + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + tb.check_dest_file = false; + } + + #[test] + fn test_positive_ack_procedure() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = tb + .generic_transfer_init(&mut user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_eof_no_error(&mut user, Vec::new()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + + tb.set_positive_ack_expired(); + // This should cause the PDU to be sent again. + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + fn generic_positive_ack_test( + tb: &mut DestHandlerTestbench, + user: &mut TestCfdpUser, + ) -> TransferInfo { + let transfer_info = tb + .generic_transfer_init(user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_eof_no_error(user, Vec::new()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(user); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + tb.check_finished_pdu_success(); + + // This should cause the PDU to be sent again. + tb.set_positive_ack_expired(); + assert_eq!(tb.handler.state_machine_no_packet(user).unwrap(), 1); + tb.check_finished_pdu_success(); + + // Positive ACK limit reached. + tb.set_positive_ack_expired(); + assert_eq!(tb.handler.state_machine_no_packet(user).unwrap(), 1); + + tb.check_finished_pdu_failure( + ConditionCode::PositiveAckLimitReached, + FileStatus::Retained, + DeliveryCode::Complete, + ); + tb.check_completion_indication_failure( + user, + ConditionCode::PositiveAckLimitReached, + FileStatus::Retained, + DeliveryCode::Complete, + ); + { + let mut fault_handler = tb.fault_handler().user_hook.borrow_mut(); + assert!(!fault_handler.cancellation_queue_empty()); + let cancellation = fault_handler + .notice_of_cancellation_queue + .pop_front() + .unwrap(); + assert_eq!(cancellation.transaction_id(), transfer_info.id); + assert_eq!( + cancellation.condition_code(), + ConditionCode::PositiveAckLimitReached + ); + assert_eq!(cancellation.progress(), 0); + } + transfer_info + } + + #[test] + fn test_positive_ack_limit_reached() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = generic_positive_ack_test(&mut tb, &mut user); + // Chances are that this one won't work either leading to transfer abandonment, but we + // acknowledge it here + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_positive_ack_limit_reached_with_subsequent_abandonment() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + let mut user = tb.test_user_from_cached_paths(0); + let transfer_info = generic_positive_ack_test(&mut tb, &mut user); + + // This should cause the PDU to be sent again. + tb.set_positive_ack_expired(); + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 1); + tb.check_finished_pdu_failure( + ConditionCode::PositiveAckLimitReached, + FileStatus::Retained, + DeliveryCode::Complete, + ); + + // Postive ACK limit reached which leads to abandonment. + tb.set_positive_ack_expired(); + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 0); + { + let mut fault_handler = tb.fault_handler().user_hook.borrow_mut(); + assert!(!fault_handler.abandoned_queue_empty()); + let cancellation = fault_handler.abandoned_queue.pop_front().unwrap(); + assert_eq!(cancellation.transaction_id(), transfer_info.id); + assert_eq!( + cancellation.condition_code(), + ConditionCode::PositiveAckLimitReached + ); + assert_eq!(cancellation.progress(), 0); + } + } + + #[test] + fn test_multi_segment_nak() { + let file_data_str = "Hello Wooorld!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + // Disable this, we only want to check the deferred procedure. + tb.remote_cfg_mut().immediate_nak_mode = false; + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + assert_eq!( + tb.generic_file_data_insert(&mut user, 2, &file_data[2..4]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 6, &file_data[6..8]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 10, &file_data[10..]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 2 + ); + assert_eq!(tb.pdu_queue_len(), 2); + tb.check_eof_ack_pdu(ConditionCode::NoError); + assert_eq!(tb.pdu_queue_len(), 1); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs.len(), 3); + assert_eq!(seg_reqs[0], (0, 2)); + assert_eq!(seg_reqs[1], (4, 6)); + assert_eq!(seg_reqs[2], (8, 10)); + + // We simulate the reply by re-inserting the missing file segment. + tb.generic_file_data_insert(&mut user, 0, &file_data[0..2]) + .expect("file data insertion failed"); + tb.generic_file_data_insert(&mut user, 4, &file_data[4..6]) + .expect("file data insertion failed"); + tb.generic_file_data_insert(&mut user, 8, &file_data[8..10]) + .expect("file data insertion failed"); + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_multi_packet_nak_sequence_large_file_flag() { + let file_data = [0_u8; 64]; + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + // Disable this, we only want to check the deferred procedure. + tb.remote_cfg_mut().immediate_nak_mode = false; + let max_packet_len = 80; + tb.remote_cfg_mut().max_packet_len = max_packet_len; + tb.set_large_file_flag(LargeFileFlag::Large); + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + assert_eq!( + NakPduCreatorWithReservedSeqReqsBuf::calculate_max_segment_requests( + max_packet_len, + &PduHeader::new_for_file_directive(tb.pdu_conf, 0), + ) + .unwrap(), + 3 + ); + let missing_segs_0: &[(u64, u64)] = &[(0, 2), (4, 6), (8, 10)]; + let missing_segs_1: &[(u64, u64)] = &[(12, 14)]; + assert_eq!( + tb.generic_file_data_insert(&mut user, 2, &file_data[2..4]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 6, &file_data[6..8]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 10, &file_data[10..12]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 14, &file_data[14..file_size as usize]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 3 + ); + assert_eq!(tb.pdu_queue_len(), 3); + tb.check_eof_ack_pdu(ConditionCode::NoError); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), 10); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs, missing_segs_0); + + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 10); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs, missing_segs_1); + + for missing_seg in missing_segs_0 { + assert_eq!( + tb.generic_file_data_insert( + &mut user, + missing_seg.0, + &file_data[missing_seg.0 as usize..missing_seg.1 as usize] + ) + .expect("file data insertion failed"), + 0 + ); + } + for missing_seg in missing_segs_1 { + assert_eq!( + tb.generic_file_data_insert( + &mut user, + missing_seg.0, + &file_data[missing_seg.0 as usize..missing_seg.1 as usize] + ) + .expect("file data insertion failed"), + 1 + ); + } + + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); + } + + #[test] + fn test_multi_packet_nak_sequence_normal_file_flag() { + let file_data = [0_u8; 64]; + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); + + let mut tb = DestHandlerTestbench::new_with_fixed_paths( + fault_handler, + TransmissionMode::Acknowledged, + false, + ); + // Disable this, we only want to check the deferred procedure. + tb.remote_cfg_mut().immediate_nak_mode = false; + let max_packet_len = 50; + tb.remote_cfg_mut().max_packet_len = max_packet_len; + let mut user = tb.test_user_from_cached_paths(file_size); + let transfer_info = tb + .generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + assert_eq!( + NakPduCreatorWithReservedSeqReqsBuf::calculate_max_segment_requests( + max_packet_len, + &PduHeader::new_for_file_directive(tb.pdu_conf, 0), + ) + .unwrap(), + 4 + ); + let missing_segs_0: &[(u64, u64)] = &[(0, 2), (4, 6), (8, 10), (12, 14)]; + let missing_segs_1: &[(u64, u64)] = &[(16, 18)]; + assert_eq!( + tb.generic_file_data_insert(&mut user, 2, &file_data[2..4]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 6, &file_data[6..8]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 10, &file_data[10..12]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 14, &file_data[14..16]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_file_data_insert(&mut user, 18, &file_data[18..file_size as usize]) + .expect("file data insertion failed"), + 0 + ); + assert_eq!( + tb.generic_eof_no_error(&mut user, file_data.to_vec()) + .expect("EOF no error insertion failed"), + 3 + ); + assert_eq!(tb.pdu_queue_len(), 3); + tb.check_eof_ack_pdu(ConditionCode::NoError); + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 0); + assert_eq!(nak_pdu.end_of_scope(), 14); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs, missing_segs_0); + + let pdu = tb.get_next_pdu().unwrap(); + assert_eq!(pdu.pdu_type, PduType::FileDirective); + assert_eq!(pdu.file_directive_type.unwrap(), FileDirectiveType::NakPdu); + let nak_pdu = NakPduReader::new(&pdu.raw_pdu).unwrap(); + assert_eq!(nak_pdu.start_of_scope(), 14); + assert_eq!(nak_pdu.end_of_scope(), file_size); + let seg_reqs: Vec<(u64, u64)> = nak_pdu.get_segment_requests_iterator().unwrap().collect(); + assert_eq!(seg_reqs, missing_segs_1); + + for missing_seg in missing_segs_0 { + assert_eq!( + tb.generic_file_data_insert( + &mut user, + missing_seg.0, + &file_data[missing_seg.0 as usize..missing_seg.1 as usize] + ) + .expect("file data insertion failed"), + 0 + ); + } + for missing_seg in missing_segs_1 { + assert_eq!( + tb.generic_file_data_insert( + &mut user, + missing_seg.0, + &file_data[missing_seg.0 as usize..missing_seg.1 as usize] + ) + .expect("file data insertion failed"), + 1 + ); + } + + tb.check_completion_indication_success(&mut user); + assert_eq!(tb.pdu_queue_len(), 1); + tb.check_finished_pdu_success(); + tb.acknowledge_finished_pdu(&mut user, &transfer_info); } } diff --git a/src/lib.rs b/src/lib.rs index adef6df..e899094 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,10 @@ //! `cfdp-rs` currently supports following high-level features: //! //! - Unacknowledged (class 1) file transfers for both source and destination side. -//! - Acknowledged (class 2) file transfers for the source side. +//! - Acknowledged (class 2) file transfers for both source side and destination side. //! //! The following features have not been implemented yet. PRs or notifications for demand are welcome! //! -//! - Acknowledged (class 2) file transfers for the destination side. //! - Suspending transfers //! - Inactivity handling //! - Start and end of transmission and reception opportunity handling @@ -72,7 +71,7 @@ //! //! # Notes on the user hooks and scheduling //! -//! Both examples feature implementations of the [UserFaultHookProvider] and the [user::CfdpUser] +//! Both examples feature implementations of the [UserFaultHook] and the [user::CfdpUser] //! trait which simply print some information to the console to monitor the progress of a file //! copy operation. These implementations could be adapted for other handler integrations. For //! example, they could signal a GUI application to display some information for the user. @@ -93,13 +92,14 @@ extern crate std; #[cfg(feature = "alloc")] pub mod dest; pub mod filestore; +pub mod lost_segments; pub mod request; #[cfg(feature = "alloc")] pub mod source; pub mod time; pub mod user; -use crate::time::CountdownProvider; +use crate::time::Countdown; use core::{cell::RefCell, fmt::Debug, hash::Hash}; use crc::{CRC_32_ISCSI, CRC_32_ISO_HDLC, Crc}; @@ -180,8 +180,8 @@ pub enum TimerContext { /// The timer will be used to perform the Positive Acknowledgement Procedures as specified in /// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer /// interval of the remote entity configuration. -pub trait TimerCreatorProvider { - type Countdown: CountdownProvider; +pub trait TimerCreator { + type Countdown: Countdown; fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown; } @@ -256,12 +256,12 @@ pub struct RemoteEntityConfig { pub crc_on_transmission_by_default: bool, pub default_transmission_mode: TransmissionMode, pub default_crc_type: ChecksumType, - pub positive_ack_timer_interval_seconds: f32, + pub positive_ack_timer_interval: Duration, pub positive_ack_timer_expiration_limit: u32, pub check_limit: u32, pub disposition_on_cancellation: bool, pub immediate_nak_mode: bool, - pub nak_timer_interval_seconds: f32, + pub nak_timer_interval: Duration, pub nak_timer_expiration_limit: u32, } @@ -283,61 +283,74 @@ impl RemoteEntityConfig { default_transmission_mode, default_crc_type, check_limit: 2, - positive_ack_timer_interval_seconds: 10.0, + positive_ack_timer_interval: Duration::from_secs(10), positive_ack_timer_expiration_limit: 2, disposition_on_cancellation: false, immediate_nak_mode: true, - nak_timer_interval_seconds: 10.0, + nak_timer_interval: Duration::from_secs(10), nak_timer_expiration_limit: 2, } } } -pub trait RemoteEntityConfigProvider { +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum RemoteConfigStoreError { + #[error("store is full")] + Full, +} + +pub trait RemoteConfigStore { /// Retrieve the remote entity configuration for the given remote ID. fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig>; + fn get_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig>; + /// Add a new remote configuration. Return [true] if the configuration was /// inserted successfully, and [false] if a configuration already exists. - fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool; - /// Remote a configuration. Returns [true] if the configuration was removed successfully, - /// and [false] if no configuration exists for the given remote ID. - fn remove_config(&mut self, remote_id: u64) -> bool; + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> Result; } /// This is a thin wrapper around a [hashbrown::HashMap] to store remote entity configurations. -/// It implements the full [RemoteEntityConfigProvider] trait. +/// It implements the full [RemoteEntityConfig] trait. #[cfg(feature = "alloc")] #[derive(Default, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct StdRemoteEntityConfigProvider(pub hashbrown::HashMap); +pub struct RemoteConfigStoreStd(pub hashbrown::HashMap); #[cfg(feature = "std")] -impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider { +impl RemoteConfigStore for RemoteConfigStoreStd { fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { self.0.get(&remote_id) } + fn get_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> { self.0.get_mut(&remote_id) } - fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool { - self.0.insert(cfg.entity_id.value(), *cfg).is_some() + + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> Result { + Ok(self.0.insert(cfg.entity_id.value(), *cfg).is_some()) } - fn remove_config(&mut self, remote_id: u64) -> bool { +} + +#[cfg(feature = "std")] +impl RemoteConfigStoreStd { + pub fn remove_config(&mut self, remote_id: u64) -> bool { self.0.remove(&remote_id).is_some() } } /// This is a thin wrapper around a [alloc::vec::Vec] to store remote entity configurations. -/// It implements the full [RemoteEntityConfigProvider] trait. +/// It implements the full [RemoteEntityConfig] trait. #[cfg(feature = "alloc")] #[derive(Default, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct VecRemoteEntityConfigProvider(pub alloc::vec::Vec); +pub struct RemoteConfigList(pub alloc::vec::Vec); #[cfg(feature = "alloc")] -impl RemoteEntityConfigProvider for VecRemoteEntityConfigProvider { +impl RemoteConfigStore for RemoteConfigList { fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { self.0 .iter() @@ -350,12 +363,19 @@ impl RemoteEntityConfigProvider for VecRemoteEntityConfigProvider { .find(|cfg| cfg.entity_id.value() == remote_id) } - fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool { + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> Result { + for other_cfg in self.0.iter() { + if cfg.entity_id.value() == other_cfg.entity_id.value() { + return Ok(false); + } + } self.0.push(*cfg); - true + Ok(true) } +} - fn remove_config(&mut self, remote_id: u64) -> bool { +impl RemoteConfigList { + pub fn remove_config(&mut self, remote_id: u64) -> bool { for (idx, cfg) in self.0.iter().enumerate() { if cfg.entity_id.value() == remote_id { self.0.remove(idx); @@ -366,10 +386,55 @@ impl RemoteEntityConfigProvider for VecRemoteEntityConfigProvider { } } -/// A remote entity configurations also implements the [RemoteEntityConfigProvider], but the -/// [RemoteEntityConfigProvider::add_config] and [RemoteEntityConfigProvider::remove_config] -/// are no-ops and always returns [false]. -impl RemoteEntityConfigProvider for RemoteEntityConfig { +/// This is a thin wrapper around a [alloc::vec::Vec] to store remote entity configurations. +/// It implements the full [RemoteEntityConfig] trait. +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RemoteConfigListHeapless(pub heapless::vec::Vec); + +impl RemoteConfigStore for RemoteConfigListHeapless { + fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { + self.0 + .iter() + .find(|&cfg| cfg.entity_id.value() == remote_id) + } + + fn get_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> { + self.0 + .iter_mut() + .find(|cfg| cfg.entity_id.value() == remote_id) + } + + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> Result { + if self.0.is_full() { + return Err(RemoteConfigStoreError::Full); + } + for other_cfg in self.0.iter() { + if cfg.entity_id.value() == other_cfg.entity_id.value() { + return Ok(false); + } + } + self.0.push(*cfg).unwrap(); + Ok(true) + } +} + +impl RemoteConfigListHeapless { + pub fn remove_config(&mut self, remote_id: u64) -> bool { + for (idx, cfg) in self.0.iter().enumerate() { + if cfg.entity_id.value() == remote_id { + self.0.remove(idx); + return true; + } + } + false + } +} + +/// A remote entity configurations also implements the [RemoteConfigStore], but the +/// [RemoteConfigStore::add_config] always returns [RemoteConfigStoreError::Full]. +impl RemoteConfigStore for RemoteEntityConfig { fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { if remote_id == self.entity_id.value() { return Some(self); @@ -384,76 +449,44 @@ impl RemoteEntityConfigProvider for RemoteEntityConfig { None } - fn add_config(&mut self, _cfg: &RemoteEntityConfig) -> bool { - false - } - - fn remove_config(&mut self, _remote_id: u64) -> bool { - false + fn add_config(&mut self, _cfg: &RemoteEntityConfig) -> Result { + Err(RemoteConfigStoreError::Full) } } /// This trait introduces some callbacks which will be called when a particular CFDP fault /// handler is called. /// -/// It is passed into the CFDP handlers as part of the [UserFaultHookProvider] and the local entity +/// It is passed into the CFDP handlers as part of the [UserFaultHook] and the local entity /// configuration and provides a way to specify custom user error handlers. This allows to /// implement some CFDP features like fault handler logging, which would not be possible /// generically otherwise. /// /// For each error reported by the [FaultHandler], the appropriate fault handler callback /// will be called depending on the [FaultHandlerCode]. -pub trait UserFaultHookProvider { - fn notice_of_suspension_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ); +pub trait UserFaultHook { + fn notice_of_suspension_cb(&mut self, fault_info: FaultInfo); - fn notice_of_cancellation_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ); + fn notice_of_cancellation_cb(&mut self, fault_info: FaultInfo); - fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); + fn abandoned_cb(&mut self, fault_info: FaultInfo); - fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); + fn ignore_cb(&mut self, fault_info: FaultInfo); } -/// Dummy fault hook which implements [UserFaultHookProvider] but only provides empty +/// Dummy fault hook which implements [UserFaultHook] but only provides empty /// implementations. #[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] pub struct DummyFaultHook {} -impl UserFaultHookProvider for DummyFaultHook { - fn notice_of_suspension_cb( - &mut self, - _transaction_id: TransactionId, - _cond: ConditionCode, - _progress: u64, - ) { - } +impl UserFaultHook for DummyFaultHook { + fn notice_of_suspension_cb(&mut self, _fault_info: FaultInfo) {} - fn notice_of_cancellation_cb( - &mut self, - _transaction_id: TransactionId, - _cond: ConditionCode, - _progress: u64, - ) { - } + fn notice_of_cancellation_cb(&mut self, _fault_info: FaultInfo) {} - fn abandoned_cb( - &mut self, - _transaction_id: TransactionId, - _cond: ConditionCode, - _progress: u64, - ) { - } + fn abandoned_cb(&mut self, _fault_info: FaultInfo) {} - fn ignore_cb(&mut self, _transaction_id: TransactionId, _cond: ConditionCode, _progress: u64) {} + fn ignore_cb(&mut self, _fault_info: FaultInfo) {} } /// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP @@ -462,7 +495,7 @@ impl UserFaultHookProvider for DummyFaultHook { /// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler /// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used /// to select the error handling inside the CFDP handler itself in addition to dispatching to a -/// user-provided callback function provided by the [UserFaultHookProvider]. +/// user-provided callback function provided by the [UserFaultHook]. /// /// Some note on the provided default settings: /// @@ -476,14 +509,51 @@ impl UserFaultHookProvider for DummyFaultHook { /// These defaults can be overriden by using the [Self::set_fault_handler] method. /// Please note that in any case, fault handler overrides can be specified by the sending CFDP /// entity. -pub struct FaultHandler { +pub struct FaultHandler { handler_array: [FaultHandlerCode; 10], // Could also change the user fault handler trait to have non mutable methods, but that limits // flexbility on the user side.. pub user_hook: RefCell, } -impl FaultHandler { +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct FaultInfo { + transaction_id: TransactionId, + condition_code: ConditionCode, + progress: u64, +} + +impl FaultInfo { + pub const fn new( + transaction_id: TransactionId, + condition_code: ConditionCode, + progress: u64, + ) -> Self { + Self { + transaction_id, + condition_code, + progress, + } + } + + #[inline] + pub const fn transaction_id(&self) -> TransactionId { + self.transaction_id + } + + #[inline] + pub const fn condition_code(&self) -> ConditionCode { + self.condition_code + } + + #[inline] + pub const fn progress(&self) -> u64 { + self.progress + } +} + +impl FaultHandler { fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option { Some(match conditon_code { ConditionCode::PositiveAckLimitReached => 0, @@ -533,33 +603,23 @@ impl FaultHandler { self.handler_array[array_idx.unwrap()] } - pub fn report_fault( - &self, - transaction_id: TransactionId, - condition: ConditionCode, - progress: u64, - ) -> FaultHandlerCode { - let array_idx = Self::condition_code_to_array_index(condition); - if array_idx.is_none() { - return FaultHandlerCode::IgnoreError; - } - let fh_code = self.handler_array[array_idx.unwrap()]; + pub fn report_fault(&self, code: FaultHandlerCode, fault_info: FaultInfo) -> FaultHandlerCode { let mut handler_mut = self.user_hook.borrow_mut(); - match fh_code { + match code { FaultHandlerCode::NoticeOfCancellation => { - handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress); + handler_mut.notice_of_cancellation_cb(fault_info); } FaultHandlerCode::NoticeOfSuspension => { - handler_mut.notice_of_suspension_cb(transaction_id, condition, progress); + handler_mut.notice_of_suspension_cb(fault_info); } FaultHandlerCode::IgnoreError => { - handler_mut.ignore_cb(transaction_id, condition, progress); + handler_mut.ignore_cb(fault_info); } FaultHandlerCode::AbandonTransaction => { - handler_mut.abandoned_cb(transaction_id, condition, progress); + handler_mut.abandoned_cb(fault_info); } } - fh_code + code } } @@ -589,17 +649,17 @@ impl Default for IndicationConfig { } /// Each CFDP entity handler has a [LocalEntityConfig]uration. -pub struct LocalEntityConfig { +pub struct LocalEntityConfig { pub id: UnsignedByteField, pub indication_cfg: IndicationConfig, - pub fault_handler: FaultHandler, + pub fault_handler: FaultHandler, } -impl LocalEntityConfig { +impl LocalEntityConfig { pub fn new( id: UnsignedByteField, indication_cfg: IndicationConfig, - hook: UserFaultHook, + hook: UserFaultHookInstance, ) -> Self { Self { id, @@ -609,12 +669,12 @@ impl LocalEntityConfig { } } -impl LocalEntityConfig { - pub fn user_fault_hook_mut(&mut self) -> &mut RefCell { +impl LocalEntityConfig { + pub fn user_fault_hook_mut(&mut self) -> &mut RefCell { &mut self.fault_handler.user_hook } - pub fn user_fault_hook(&self) -> &RefCell { + pub fn user_fault_hook(&self) -> &RefCell { &self.fault_handler.user_hook } } @@ -631,13 +691,21 @@ pub enum GenericSendError { Other, } -pub trait PduSendProvider { +pub trait PduSender { fn send_pdu( &self, pdu_type: PduType, file_directive_type: Option, raw_pdu: &[u8], ) -> Result<(), GenericSendError>; + + fn send_file_directive_pdu( + &self, + file_directive_type: FileDirectiveType, + raw_pdu: &[u8], + ) -> Result<(), GenericSendError> { + self.send_pdu(PduType::FileDirective, Some(file_directive_type), raw_pdu) + } } #[cfg(feature = "std")] @@ -646,7 +714,7 @@ pub mod std_mod { use super::*; - impl PduSendProvider for mpsc::Sender { + impl PduSender for mpsc::Sender { fn send_pdu( &self, pdu_type: PduType, @@ -663,7 +731,7 @@ pub mod std_mod { } } - /// Simple implementation of the [CountdownProvider] trait assuming a standard runtime. + /// Simple implementation of the [Countdown] trait assuming a standard runtime. #[derive(Debug)] pub struct StdCountdown { expiry_time: Duration, @@ -683,7 +751,7 @@ pub mod std_mod { } } - impl CountdownProvider for StdCountdown { + impl Countdown for StdCountdown { fn has_expired(&self) -> bool { if self.start_time.elapsed() > self.expiry_time { return true; @@ -714,7 +782,7 @@ pub mod std_mod { } } - impl TimerCreatorProvider for StdTimerCreator { + impl TimerCreator for StdTimerCreator { type Countdown = StdCountdown; fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown { @@ -800,7 +868,7 @@ pub enum PacketTarget { pub trait PduProvider { fn pdu_type(&self) -> PduType; fn file_directive_type(&self) -> Option; - fn pdu(&self) -> &[u8]; + fn raw_pdu(&self) -> &[u8]; fn packet_target(&self) -> Result; } @@ -815,7 +883,7 @@ impl PduProvider for DummyPduProvider { None } - fn pdu(&self) -> &[u8] { + fn raw_pdu(&self) -> &[u8] { &[] } @@ -927,7 +995,7 @@ impl PduProvider for PduRawWithInfo<'_> { self.file_directive_type } - fn pdu(&self) -> &[u8] { + fn raw_pdu(&self) -> &[u8] { self.raw_packet } @@ -989,7 +1057,7 @@ pub mod alloc_mod { self.file_directive_type } - fn pdu(&self) -> &[u8] { + fn raw_pdu(&self) -> &[u8] { &self.pdu } @@ -999,6 +1067,12 @@ pub mod alloc_mod { } } +#[derive(Debug, Clone, Copy)] +struct PositiveAckParams { + ack_counter: u32, + positive_ack_of_cancellation: bool, +} + #[cfg(test)] pub(crate) mod tests { use core::{ @@ -1016,6 +1090,7 @@ pub(crate) mod tests { CommonPduConfig, FileDirectiveType, PduHeader, eof::EofPdu, file_data::FileDataPdu, + finished::{DeliveryCode, FileStatus}, metadata::{MetadataGenericParams, MetadataPduCreator}, }, }, @@ -1035,6 +1110,7 @@ pub(crate) mod tests { pub(crate) struct TimerExpiryControl { pub(crate) check_limit: Arc, pub(crate) positive_ack: Arc, + pub(crate) nak_activity: Arc, } impl TimerExpiryControl { @@ -1043,11 +1119,15 @@ pub(crate) mod tests { .store(true, core::sync::atomic::Ordering::Release); } - #[allow(dead_code)] pub fn set_positive_ack_expired(&mut self) { self.positive_ack .store(true, core::sync::atomic::Ordering::Release); } + + pub fn set_nak_activity_expired(&mut self) { + self.nak_activity + .store(true, core::sync::atomic::Ordering::Release); + } } #[derive(Debug)] @@ -1057,7 +1137,7 @@ pub(crate) mod tests { expiry_control: TimerExpiryControl, } - impl CountdownProvider for TestCheckTimer { + impl Countdown for TestCheckTimer { fn has_expired(&self) -> bool { match self.context { TimerContext::CheckLimit { @@ -1072,7 +1152,10 @@ pub(crate) mod tests { .expiry_control .positive_ack .load(core::sync::atomic::Ordering::Acquire), - TimerContext::NakActivity { expiry_time: _ } => todo!(), + TimerContext::NakActivity { expiry_time: _ } => self + .expiry_control + .nak_activity + .load(core::sync::atomic::Ordering::Acquire), } } fn reset(&mut self) { @@ -1085,7 +1168,10 @@ pub(crate) mod tests { .expiry_control .check_limit .store(false, core::sync::atomic::Ordering::Release), - TimerContext::NakActivity { expiry_time: _ } => todo!(), + TimerContext::NakActivity { expiry_time: _ } => self + .expiry_control + .nak_activity + .store(false, core::sync::atomic::Ordering::Release), TimerContext::PositiveAck { expiry_time: _ } => self .expiry_control .positive_ack @@ -1117,7 +1203,7 @@ pub(crate) mod tests { } } - impl TimerCreatorProvider for TestCheckTimerCreator { + impl TimerCreator for TestCheckTimerCreator { type Countdown = TestCheckTimer; fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown { @@ -1128,13 +1214,14 @@ pub(crate) mod tests { TimerContext::PositiveAck { expiry_time: _ } => { TestCheckTimer::new(timer_context, &self.expiry_control) } - _ => { - panic!("invalid check timer creator, can only be used for check limit handling") + TimerContext::NakActivity { expiry_time: _ } => { + TestCheckTimer::new(timer_context, &self.expiry_control) } } } } + #[derive(Debug)] pub struct FileSegmentRecvdParamsNoSegMetadata { #[allow(dead_code)] pub id: TransactionId, @@ -1142,8 +1229,9 @@ pub(crate) mod tests { pub length: usize, } - #[derive(Default)] + #[derive(Default, Debug)] pub struct TestCfdpUser { + pub check_queues_empty_on_drop: bool, pub next_expected_seq_num: u64, pub expected_full_src_name: String, pub expected_full_dest_name: String, @@ -1164,6 +1252,7 @@ pub(crate) mod tests { expected_file_size: u64, ) -> Self { Self { + check_queues_empty_on_drop: true, next_expected_seq_num, expected_full_src_name, expected_full_dest_name, @@ -1181,6 +1270,36 @@ pub(crate) mod tests { assert_eq!(id.source_id, LOCAL_ID.into()); assert_eq!(id.seq_num().value(), self.next_expected_seq_num); } + + pub fn indication_queues_empty(&self) -> bool { + self.finished_indic_queue.is_empty() + && self.metadata_recv_queue.is_empty() + && self.file_seg_recvd_queue.is_empty() + } + + pub fn verify_finished_indication_retained( + &mut self, + delivery_code: DeliveryCode, + cond_code: ConditionCode, + id: TransactionId, + ) { + self.verify_finished_indication(delivery_code, cond_code, id, FileStatus::Retained); + } + + pub fn verify_finished_indication( + &mut self, + delivery_code: DeliveryCode, + cond_code: ConditionCode, + id: TransactionId, + file_status: FileStatus, + ) { + assert_eq!(self.finished_indic_queue.len(), 1); + let finished_indication = self.finished_indic_queue.pop_front().unwrap(); + assert_eq!(finished_indication.id, id); + assert_eq!(finished_indication.condition_code, cond_code); + assert_eq!(finished_indication.delivery_code, delivery_code); + assert_eq!(finished_indication.file_status, file_status); + } } impl CfdpUser for TestCfdpUser { @@ -1270,48 +1389,43 @@ pub(crate) mod tests { } } - #[derive(Default, Debug)] - pub(crate) struct TestFaultHandler { - pub notice_of_suspension_queue: VecDeque<(TransactionId, ConditionCode, u64)>, - pub notice_of_cancellation_queue: VecDeque<(TransactionId, ConditionCode, u64)>, - pub abandoned_queue: VecDeque<(TransactionId, ConditionCode, u64)>, - pub ignored_queue: VecDeque<(TransactionId, ConditionCode, u64)>, + impl Drop for TestCfdpUser { + fn drop(&mut self) { + if self.check_queues_empty_on_drop { + assert!( + self.indication_queues_empty(), + "indication queues not empty on drop: finished: {}, metadata: {}, file seg: {}", + self.finished_indic_queue.len(), + self.metadata_recv_queue.len(), + self.file_seg_recvd_queue.len() + ); + } + } } - impl UserFaultHookProvider for TestFaultHandler { - fn notice_of_suspension_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - self.notice_of_suspension_queue - .push_back((transaction_id, cond, progress)) + #[derive(Default, Debug)] + pub(crate) struct TestFaultHandler { + pub notice_of_suspension_queue: VecDeque, + pub notice_of_cancellation_queue: VecDeque, + pub abandoned_queue: VecDeque, + pub ignored_queue: VecDeque, + } + + impl UserFaultHook for TestFaultHandler { + fn notice_of_suspension_cb(&mut self, fault_info: FaultInfo) { + self.notice_of_suspension_queue.push_back(fault_info) } - fn notice_of_cancellation_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - self.notice_of_cancellation_queue - .push_back((transaction_id, cond, progress)) + fn notice_of_cancellation_cb(&mut self, fault_info: FaultInfo) { + self.notice_of_cancellation_queue.push_back(fault_info) } - fn abandoned_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - self.abandoned_queue - .push_back((transaction_id, cond, progress)) + fn abandoned_cb(&mut self, fault_info: FaultInfo) { + self.abandoned_queue.push_back(fault_info) } - fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { - self.ignored_queue - .push_back((transaction_id, cond, progress)) + fn ignore_cb(&mut self, fault_info: FaultInfo) { + self.ignored_queue.push_back(fault_info) } } @@ -1347,7 +1461,7 @@ pub(crate) mod tests { pub packet_queue: RefCell>, } - impl PduSendProvider for TestCfdpSender { + impl PduSender for TestCfdpSender { fn send_pdu( &self, pdu_type: PduType, @@ -1370,6 +1484,10 @@ pub(crate) mod tests { } impl TestCfdpSender { + pub fn queue_len(&self) -> usize { + self.packet_queue.borrow_mut().len() + } + pub fn retrieve_next_pdu(&self) -> Option { self.packet_queue.borrow_mut().pop_front() } @@ -1383,8 +1501,8 @@ pub(crate) mod tests { dest_id: impl Into, max_packet_len: usize, crc_on_transmission_by_default: bool, - ) -> StdRemoteEntityConfigProvider { - let mut table = StdRemoteEntityConfigProvider::default(); + ) -> RemoteConfigStoreStd { + let mut table = RemoteConfigStoreStd::default(); let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( dest_id.into(), max_packet_len, @@ -1393,13 +1511,13 @@ pub(crate) mod tests { TransmissionMode::Unacknowledged, ChecksumType::Crc32, ); - table.add_config(&remote_entity_cfg); + table.add_config(&remote_entity_cfg).unwrap(); table } fn generic_pdu_header() -> PduHeader { let pdu_conf = CommonPduConfig::default(); - PduHeader::new_no_file_data(pdu_conf, 0) + PduHeader::new_for_file_directive(pdu_conf, 0) } #[test] @@ -1534,9 +1652,10 @@ pub(crate) mod tests { TransmissionMode::Unacknowledged, ChecksumType::Crc32, ); - assert!(!remote_entity_cfg.add_config(&dummy)); - // Removal is no-op. - assert!(!remote_entity_cfg.remove_config(REMOTE_ID.value())); + assert_eq!( + remote_entity_cfg.add_config(&dummy).unwrap_err(), + RemoteConfigStoreError::Full + ); let remote_entity_retrieved = remote_entity_cfg.get(REMOTE_ID.value()).unwrap(); assert_eq!(remote_entity_retrieved.entity_id, REMOTE_ID.into()); // Does not exist. @@ -1554,9 +1673,9 @@ pub(crate) mod tests { TransmissionMode::Unacknowledged, ChecksumType::Crc32, ); - let mut remote_cfg_provider = StdRemoteEntityConfigProvider::default(); + let mut remote_cfg_provider = RemoteConfigStoreStd::default(); assert!(remote_cfg_provider.0.is_empty()); - remote_cfg_provider.add_config(&remote_entity_cfg); + remote_cfg_provider.add_config(&remote_entity_cfg).unwrap(); assert_eq!(remote_cfg_provider.0.len(), 1); let remote_entity_cfg_2 = RemoteEntityConfig::new_with_default_values( LOCAL_ID.into(), @@ -1568,7 +1687,9 @@ pub(crate) mod tests { ); let cfg_0 = remote_cfg_provider.get(REMOTE_ID.value()).unwrap(); assert_eq!(cfg_0.entity_id, REMOTE_ID.into()); - remote_cfg_provider.add_config(&remote_entity_cfg_2); + remote_cfg_provider + .add_config(&remote_entity_cfg_2) + .unwrap(); assert_eq!(remote_cfg_provider.0.len(), 2); let cfg_1 = remote_cfg_provider.get(LOCAL_ID.value()).unwrap(); assert_eq!(cfg_1.entity_id, LOCAL_ID.into()); @@ -1582,7 +1703,7 @@ pub(crate) mod tests { #[test] fn test_remote_cfg_provider_vector() { - let mut remote_cfg_provider = VecRemoteEntityConfigProvider::default(); + let mut remote_cfg_provider = RemoteConfigList::default(); let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( REMOTE_ID.into(), 1024, @@ -1592,7 +1713,7 @@ pub(crate) mod tests { ChecksumType::Crc32, ); assert!(remote_cfg_provider.0.is_empty()); - remote_cfg_provider.add_config(&remote_entity_cfg); + remote_cfg_provider.add_config(&remote_entity_cfg).unwrap(); assert_eq!(remote_cfg_provider.0.len(), 1); let remote_entity_cfg_2 = RemoteEntityConfig::new_with_default_values( LOCAL_ID.into(), @@ -1604,7 +1725,11 @@ pub(crate) mod tests { ); let cfg_0 = remote_cfg_provider.get(REMOTE_ID.value()).unwrap(); assert_eq!(cfg_0.entity_id, REMOTE_ID.into()); - remote_cfg_provider.add_config(&remote_entity_cfg_2); + assert!( + remote_cfg_provider + .add_config(&remote_entity_cfg_2) + .unwrap() + ); assert_eq!(remote_cfg_provider.0.len(), 2); let cfg_1 = remote_cfg_provider.get(LOCAL_ID.value()).unwrap(); assert_eq!(cfg_1.entity_id, LOCAL_ID.into()); @@ -1623,10 +1748,18 @@ pub(crate) mod tests { UnsignedByteFieldU8::new(0).into(), UnsignedByteFieldU8::new(0).into(), ); - user_hook_dummy.notice_of_cancellation_cb(transaction_id, ConditionCode::NoError, 0); - user_hook_dummy.notice_of_suspension_cb(transaction_id, ConditionCode::NoError, 0); - user_hook_dummy.abandoned_cb(transaction_id, ConditionCode::NoError, 0); - user_hook_dummy.ignore_cb(transaction_id, ConditionCode::NoError, 0); + user_hook_dummy.notice_of_cancellation_cb(FaultInfo::new( + transaction_id, + ConditionCode::NoError, + 0, + )); + user_hook_dummy.notice_of_suspension_cb(FaultInfo::new( + transaction_id, + ConditionCode::NoError, + 0, + )); + user_hook_dummy.abandoned_cb(FaultInfo::new(transaction_id, ConditionCode::NoError, 0)); + user_hook_dummy.ignore_cb(FaultInfo::new(transaction_id, ConditionCode::NoError, 0)); } #[test] @@ -1634,7 +1767,7 @@ pub(crate) mod tests { let dummy_pdu_provider = DummyPduProvider(()); assert_eq!(dummy_pdu_provider.pdu_type(), PduType::FileData); assert!(dummy_pdu_provider.file_directive_type().is_none()); - assert_eq!(dummy_pdu_provider.pdu(), &[]); + assert_eq!(dummy_pdu_provider.raw_pdu(), &[]); assert_eq!( dummy_pdu_provider.packet_target(), Ok(PacketTarget::SourceEntity) diff --git a/src/lost_segments.rs b/src/lost_segments.rs new file mode 100644 index 0000000..3fa9475 --- /dev/null +++ b/src/lost_segments.rs @@ -0,0 +1,1361 @@ +//! # Lost Segment Store Module +//! +//! The core abstraction provided by this module in the [LostSegmentStore]. +//! +//! The two concrete implementations provided are: +//! +//! * [LostSegmentsList]: A hash set based implementation which can grow dynamically andcan +//! optionally be bounded. Suitable for systems where dynamic allocation is allowed. +//! * [LostSegmentsListHeapless]: A fixed-size list based implementation where the size +//! of the lost segment list is statically known at compile-time. Suitable for resource +//! constrained devices where dyanamic allocation is not allowed or possible. + +use spacepackets::cfdp::{LargeFileFlag, pdu::nak::NakPduCreatorWithReservedSeqReqsBuf}; + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum LostSegmentError { + #[error("store is full")] + StoreFull, + #[error("segment is empty")] + EmptySegment, + #[error("segment start {0} is larger than segment end {1}")] + StartLargerThanEnd(u64, u64), + #[error("large file segments are not supported")] + LargeFileSegmentNotSupported, + #[error("invalid segment boundary detected for lost segment ({0}, {1})")] + InvalidSegmentBoundary(u64, u64), +} + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] +pub enum LostSegmentWriteError { + #[error("number of segments mismatch: expected {expected}, actual {actual}")] + NumberOfSegmentsMismatch { expected: usize, actual: usize }, + #[error("buffer size not equal to required size")] + BufferSizeMissmatch { expected: usize, actual: usize }, + #[error("large file segment not compatible to normal file size")] + LargeSegmentForNormalFileSize, +} + +/// Generic trait to model a lost segment store. +/// +/// The destination handler can use this store to keep track of lost segments and re-requesting +/// them. This abstraction allow using different data structures as a backend. +pub trait LostSegmentStore { + // Iteration + type Iter<'a>: Iterator + 'a + where + Self: 'a; + + /// Iterate over all lost segments stored. + fn iter(&self) -> Self::Iter<'_>; + + /// Current number of lost segments stored. + fn number_of_segments(&self) -> usize; + + /// Implementations may explicitely omit support for large file segments to save memory if + /// large file sizes are not used. + fn supports_large_file_size(&self) -> bool; + fn capacity(&self) -> Option; + fn reset(&mut self); + + /// Checks whether a segment is already in the store. + /// + /// Implementors should be check whether the provided segment is a subset of an existing + /// segments. + fn segment_in_store(&self, segment: (u64, u64)) -> bool; + + /// Add a new lost segment. + /// + /// For efficiency reason, the implementors must not check whether the new segments already + /// exists in the store which is provided by the [Self::segment_in_store] method. + /// + /// Therefore, the caller must ensure that no duplicate segments are added. + fn add_lost_segment(&mut self, lost_seg: (u64, u64)) -> Result<(), LostSegmentError>; + + /// Remove a lost segment. + /// + /// Implementors should also be able to remove lost segments which are a subset of an existing + /// section but can return an error if an segment to remove only partially overlaps an existing + /// segment. + /// + /// Returns whether a segment was removed. + fn remove_lost_segment( + &mut self, + segment_to_remove: (u64, u64), + ) -> Result; + + /// The lost segment store may additionally have the capability to coalesce overlapping or + /// adjacent segments. + fn coalesce_lost_segments(&mut self) -> Result<(), LostSegmentError>; + + #[inline] + fn is_empty(&self) -> bool { + self.number_of_segments() == 0 + } + + /// Write the segments to the raw byte format of the NAK PDU segment requests as specified by + /// the CFDP standard 5.2.6.1 (NAK PDU). + fn write_segments_to_bytes( + &self, + buf: &mut [u8], + file_flag: LargeFileFlag, + ) -> Result { + let len_per_segment = if file_flag == LargeFileFlag::Large { + 16 + } else { + 8 + }; + let written_len = self.number_of_segments() * len_per_segment; + if written_len != buf.len() { + return Err(LostSegmentWriteError::BufferSizeMissmatch { + expected: written_len, + actual: buf.len(), + }); + } + let mut current_index = 0; + for segment in self.iter() { + match file_flag { + LargeFileFlag::Normal => { + if segment.0 > u32::MAX as u64 || segment.1 > u32::MAX as u64 { + return Err(LostSegmentWriteError::LargeSegmentForNormalFileSize); + } + buf[current_index..current_index + 4] + .copy_from_slice(&(segment.0 as u32).to_be_bytes()); + current_index += 4; + buf[current_index..current_index + 4] + .copy_from_slice(&(segment.1 as u32).to_be_bytes()); + current_index += 4; + } + LargeFileFlag::Large => { + buf[current_index..current_index + 8] + .copy_from_slice(&(segment.0).to_be_bytes()); + current_index += 8; + buf[current_index..current_index + 8] + .copy_from_slice(&(segment.1).to_be_bytes()); + current_index += 8; + } + } + } + Ok(current_index) + } + + /// Write the segments to the raw byte buffer of the supplied + /// [NAK builder][NakPduCreatorWithReservedSeqReqsBuf]. + /// + /// Please note that this function only works if all the segment requests fit into the NAK + /// builder buffer. In any other case, you should write a custom iteration and serialization + /// sequence which spreads the lost segments across multiple packets. + fn write_to_nak_segment_list( + &self, + nak_builder: &mut NakPduCreatorWithReservedSeqReqsBuf, + first_segment_request_for_metadata: bool, + ) -> Result { + let file_flag = nak_builder.pdu_header().common_pdu_conf().file_flag; + let mut relevant_size = self.number_of_segments(); + if first_segment_request_for_metadata { + relevant_size += 1; + } + if nak_builder.num_segment_reqs() != relevant_size { + return Err(LostSegmentWriteError::NumberOfSegmentsMismatch { + expected: self.number_of_segments(), + actual: nak_builder.num_segment_reqs(), + }); + } + let mut buf = nak_builder.segment_request_buffer_mut(); + let mut written_len = 0; + if first_segment_request_for_metadata { + match file_flag { + LargeFileFlag::Normal => { + buf[0..8].fill(0); + buf = &mut buf[8..]; + written_len += 8; + } + LargeFileFlag::Large => { + buf[0..16].fill(0); + buf = &mut buf[16..]; + written_len += 16; + } + } + } + written_len += self.write_segments_to_bytes(buf, file_flag)?; + Ok(written_len) + } +} + +/// Implementation based on a [alloc::vec::Vec] which can grow dynamically. +/// +/// Optionally, a maximum capacity can be specified at creation time. This container allocates at +/// run-time! +#[cfg(feature = "alloc")] +#[derive(Debug, Default)] +pub struct LostSegmentsList { + list: alloc::vec::Vec<(u64, u64)>, + opt_capacity: Option, +} + +#[cfg(feature = "alloc")] +impl LostSegmentsList { + pub fn new(opt_capacity: Option) -> Self { + Self { + list: alloc::vec::Vec::new(), + opt_capacity, + } + } +} + +#[cfg(feature = "alloc")] +impl LostSegmentStore for LostSegmentsList { + type Iter<'a> + = core::iter::Cloned> + where + Self: 'a; + + fn iter(&self) -> Self::Iter<'_> { + self.list.iter().cloned() + } + + #[inline] + fn number_of_segments(&self) -> usize { + self.list.len() + } + + #[inline] + fn supports_large_file_size(&self) -> bool { + true + } + + #[inline] + fn capacity(&self) -> Option { + self.opt_capacity + } + + #[inline] + fn reset(&mut self) { + self.list.clear(); + } + + fn segment_in_store(&self, segment: (u64, u64)) -> bool { + for (seg_start, seg_end) in &self.list { + if segment.0 >= *seg_start && segment.1 <= *seg_end { + return true; + } + } + false + } + + #[inline] + fn add_lost_segment(&mut self, lost_seg: (u64, u64)) -> Result<(), LostSegmentError> { + if lost_seg.1 == lost_seg.0 { + return Err(LostSegmentError::EmptySegment); + } + if lost_seg.0 > lost_seg.1 { + return Err(LostSegmentError::StartLargerThanEnd(lost_seg.0, lost_seg.1)); + } + if let Some(capacity) = self.opt_capacity { + if self.list.len() == capacity { + return Err(LostSegmentError::StoreFull); + } + } + self.list.push((lost_seg.0, lost_seg.1)); + Ok(()) + } + + fn coalesce_lost_segments(&mut self) -> Result<(), LostSegmentError> { + // Remove empty/invalid ranges + self.list.retain(|&(s, e)| e > s); + if self.list.len() <= 1 { + return Ok(()); + } + + // Sort by start, then end (no extra allocs) + self.list + .as_mut_slice() + .sort_unstable_by_key(|&(start, _)| start); + + // In-place merge; merges overlapping or adjacent [s, e) where next.s <= prev.e + let mut w = 0usize; + for i in 0..self.list.len() { + if w == 0 { + self.list[w] = self.list[i]; + w = 1; + continue; + } + + let (prev_s, mut prev_e) = self.list[w - 1]; + let (s, e) = self.list[i]; + + if s <= prev_e { + // Extend previous + if e > prev_e { + prev_e = e; + self.list[w - 1] = (prev_s, prev_e); + } + } else { + // Start new merged interval + self.list[w] = (s, e); + w += 1; + } + } + + // Truncate to merged length + self.list.truncate(w); + Ok(()) + } + + #[inline] + fn remove_lost_segment( + &mut self, + segment_to_remove: (u64, u64), + ) -> Result { + if segment_to_remove.1 == segment_to_remove.0 { + return Err(LostSegmentError::EmptySegment); + } + if segment_to_remove.0 > segment_to_remove.1 { + return Err(LostSegmentError::StartLargerThanEnd( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Binary search for the first candidate. + let idx = match self + .list + .binary_search_by_key(&segment_to_remove.0, |&(s, _)| s) + { + Ok(idx) => idx, + Err(insertion) => insertion.saturating_sub(1), + }; + + // --- single linear scan ------------------------------------------------- + let mut i = idx; + let list_len = self.list.len(); + while i < self.list.len() && self.list[i].0 <= segment_to_remove.1 { + let seg = &mut self.list[i]; + + // no overlap + if seg.1 < segment_to_remove.0 { + i += 1; + continue; + } + + // exact match → remove whole segment + if seg == &segment_to_remove { + self.list.remove(i); + // keep `i` unchanged: we swapped the tail element forward + return Ok(true); + } + + // partial overlap → forbidden + if (segment_to_remove.0 < seg.0 && segment_to_remove.1 > seg.0) + || (segment_to_remove.1 > seg.1 && segment_to_remove.0 < seg.1) + { + return Err(LostSegmentError::InvalidSegmentBoundary( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Removal of subset. + + let mut changed = false; + + // Removal touches right edge only → shorten from the right + if segment_to_remove.1 == seg.1 { + seg.1 = segment_to_remove.0; + changed = true; + } + // Removal touches left edge only → shorten from the left + if segment_to_remove.0 == seg.0 { + seg.0 = segment_to_remove.1; + changed = true; + } + // Removal is strictly inside → split into two parts + if segment_to_remove.0 > seg.0 && segment_to_remove.1 < seg.1 { + if list_len == self.opt_capacity.unwrap_or(usize::MAX) { + return Err(LostSegmentError::StoreFull); + } + // Right remainder. + let end_of_right_remainder = seg.1; + // Left remainder. + seg.1 = segment_to_remove.0; + self.list + .insert(i + 1, (segment_to_remove.1, end_of_right_remainder)); + changed = true; + } + + // when both sides remain we truncated the current segment already + if changed { + return Ok(true); + } + + i += 1; + } + Ok(false) + } +} + +/// Implementation based on a [heapless::Vec] with a statically known container size. +#[derive(Default, Debug)] +pub struct LostSegmentsListHeapless { + list: heapless::vec::Vec<(T, T), N>, +} + +/// Type definition for segment list which only supports normal file sizes. This can be used +/// to save memory required for the lost segment list. +pub type LostSegmentsListNormalFilesHeapless = LostSegmentsListHeapless; + +impl LostSegmentsListHeapless { + pub fn new() -> Self { + Self { + list: heapless::Vec::new(), + } + } + + #[inline] + fn num_lost_segments(&self) -> usize { + self.list.len() + } + + #[inline] + fn capacity(&self) -> Option { + Some(N) + } + + #[inline] + fn reset(&mut self) { + self.list.clear(); + } +} + +impl LostSegmentsListHeapless { + fn coalesce_lost_segments(&mut self) -> Result<(), LostSegmentError> { + // Remove empty/invalid ranges + self.list.retain(|&(s, e)| e > s); + if self.list.len() <= 1 { + return Ok(()); + } + + // Sort by start, then end (no extra allocs) + self.list + .as_mut_slice() + .sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + + // In-place merge; merges overlapping or adjacent [s, e) where next.s <= prev.e + let mut w = 0usize; + for i in 0..self.list.len() { + if w == 0 { + self.list[w] = self.list[i]; + w = 1; + continue; + } + + let (prev_s, mut prev_e) = self.list[w - 1]; + let (s, e) = self.list[i]; + + if s <= prev_e { + // Extend previous + if e > prev_e { + prev_e = e; + self.list[w - 1] = (prev_s, prev_e); + } + } else { + // Start new merged interval + self.list[w] = (s, e); + w += 1; + } + } + + // Truncate to merged length + self.list.truncate(w); + Ok(()) + } +} + +impl LostSegmentStore for LostSegmentsListHeapless { + type Iter<'a> + = core::iter::Cloned> + where + Self: 'a; + + fn iter(&self) -> Self::Iter<'_> { + self.list.iter().cloned() + } + + fn add_lost_segment(&mut self, lost_seg: (u64, u64)) -> Result<(), LostSegmentError> { + if lost_seg.1 == lost_seg.0 { + return Err(LostSegmentError::EmptySegment); + } + if lost_seg.0 > lost_seg.1 { + return Err(LostSegmentError::StartLargerThanEnd(lost_seg.0, lost_seg.1)); + } + if self.list.is_full() { + return Err(LostSegmentError::StoreFull); + } + self.list.push(lost_seg).ok(); + Ok(()) + } + + fn remove_lost_segment( + &mut self, + segment_to_remove: (u64, u64), + ) -> Result { + if segment_to_remove.1 == segment_to_remove.0 { + return Err(LostSegmentError::EmptySegment); + } + if segment_to_remove.0 > segment_to_remove.1 { + return Err(LostSegmentError::StartLargerThanEnd( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Binary search for the first candidate. + let idx = match self + .list + .binary_search_by_key(&segment_to_remove.0, |&(s, _)| s) + { + Ok(idx) => idx, + Err(insertion) => insertion.saturating_sub(1), + }; + + // --- single linear scan ------------------------------------------------- + let mut i = idx; + let list_len = self.list.len(); + while i < self.list.len() && self.list[i].0 <= segment_to_remove.1 { + let seg = &mut self.list[i]; + + // no overlap + if seg.1 < segment_to_remove.0 { + i += 1; + continue; + } + + // exact match → remove whole segment + if seg == &segment_to_remove { + self.list.remove(i); + return Ok(true); + } + + // partial overlap → forbidden + if (segment_to_remove.0 < seg.0 && segment_to_remove.1 > seg.0) + || (segment_to_remove.1 > seg.1 && segment_to_remove.0 < seg.1) + { + return Err(LostSegmentError::InvalidSegmentBoundary( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Removal of subset. + + let mut changed = false; + + // Removal touches right edge only → shorten from the right + if segment_to_remove.1 == seg.1 { + seg.1 = segment_to_remove.0; + changed = true; + } + // Removal touches left edge only → shorten from the left + if segment_to_remove.0 == seg.0 { + seg.0 = segment_to_remove.1; + changed = true; + } + // Removal is strictly inside → split into two parts + if segment_to_remove.0 > seg.0 && segment_to_remove.1 < seg.1 { + if list_len == N { + return Err(LostSegmentError::StoreFull); + } + // Right remainder. + let end_of_right_remainder = seg.1; + // Left remainder. + seg.1 = segment_to_remove.0; + self.list + .insert(i + 1, (segment_to_remove.1, end_of_right_remainder)) + .unwrap(); + changed = true; + } + + // when both sides remain we truncated the current segment already + if changed { + return Ok(true); + } + + i += 1; + } + Ok(false) + } + + fn coalesce_lost_segments(&mut self) -> Result<(), LostSegmentError> { + self.coalesce_lost_segments() + } + + fn number_of_segments(&self) -> usize { + self.num_lost_segments() + } + + fn supports_large_file_size(&self) -> bool { + true + } + + fn capacity(&self) -> Option { + self.capacity() + } + + fn reset(&mut self) { + self.reset(); + } + + fn segment_in_store(&self, segment: (u64, u64)) -> bool { + for (seg_start, seg_end) in &self.list { + if segment.0 >= *seg_start && segment.1 <= *seg_end { + return true; + } + } + false + } +} + +impl LostSegmentStore for LostSegmentsListHeapless { + type Iter<'a> + = core::iter::Map, fn(&(u32, u32)) -> (u64, u64)> + where + Self: 'a; + + fn iter(&self) -> Self::Iter<'_> { + self.list.iter().map(|pair| (pair.0 as u64, pair.1 as u64)) + } + + fn add_lost_segment(&mut self, lost_seg: (u64, u64)) -> Result<(), LostSegmentError> { + if lost_seg.1 == lost_seg.0 { + return Err(LostSegmentError::EmptySegment); + } + if lost_seg.0 > lost_seg.1 { + return Err(LostSegmentError::StartLargerThanEnd(lost_seg.0, lost_seg.1)); + } + if lost_seg.1 > u32::MAX as u64 || lost_seg.0 > u32::MAX as u64 { + return Err(LostSegmentError::LargeFileSegmentNotSupported); + } + if self.list.is_full() { + return Err(LostSegmentError::StoreFull); + } + self.list.push((lost_seg.0 as u32, lost_seg.1 as u32)).ok(); + Ok(()) + } + + fn remove_lost_segment( + &mut self, + segment_to_remove: (u64, u64), + ) -> Result { + if segment_to_remove.0 > u32::MAX as u64 || segment_to_remove.1 > u32::MAX as u64 { + return Err(LostSegmentError::LargeFileSegmentNotSupported); + } + if segment_to_remove.1 == segment_to_remove.0 { + return Err(LostSegmentError::EmptySegment); + } + if segment_to_remove.0 > segment_to_remove.1 { + return Err(LostSegmentError::StartLargerThanEnd( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Binary search for the first candidate. + let idx = match self + .list + .binary_search_by_key(&segment_to_remove.0, |&(s, _)| s as u64) + { + Ok(idx) => idx, + Err(insertion) => insertion.saturating_sub(1), + }; + + // --- single linear scan ------------------------------------------------- + let mut i = idx; + let list_len = self.list.len(); + while i < self.list.len() && self.list[i].0 as u64 <= segment_to_remove.1 { + let seg = &mut self.list[i]; + + // no overlap + if (seg.1 as u64) < segment_to_remove.0 { + i += 1; + continue; + } + + // exact match → remove whole segment + if seg.0 as u64 == segment_to_remove.0 && seg.1 as u64 == segment_to_remove.1 { + self.list.remove(i); + return Ok(true); + } + + // partial overlap → forbidden + if (segment_to_remove.0 < seg.0 as u64 && segment_to_remove.1 > seg.0 as u64) + || (segment_to_remove.1 > seg.1 as u64 && segment_to_remove.0 < seg.1 as u64) + { + return Err(LostSegmentError::InvalidSegmentBoundary( + segment_to_remove.0, + segment_to_remove.1, + )); + } + + // Removal of subset. + + let mut changed = false; + + // Removal touches right edge only → shorten from the right + if segment_to_remove.1 == seg.1 as u64 { + seg.1 = segment_to_remove.0 as u32; + changed = true; + } + // Removal touches left edge only → shorten from the left + if segment_to_remove.0 == seg.0 as u64 { + seg.0 = segment_to_remove.1 as u32; + changed = true; + } + // Removal is strictly inside → split into two parts + if segment_to_remove.0 > seg.0 as u64 && segment_to_remove.1 < seg.1 as u64 { + if list_len == N { + return Err(LostSegmentError::StoreFull); + } + // Right remainder. + let end_of_right_remainder = seg.1; + // Left remainder. + seg.1 = segment_to_remove.0 as u32; + self.list + .insert(i + 1, (segment_to_remove.1 as u32, end_of_right_remainder)) + .unwrap(); + changed = true; + } + + // when both sides remain we truncated the current segment already + if changed { + return Ok(true); + } + + i += 1; + } + Ok(false) + } + + fn coalesce_lost_segments(&mut self) -> Result<(), LostSegmentError> { + self.coalesce_lost_segments() + } + + #[inline] + fn number_of_segments(&self) -> usize { + self.num_lost_segments() + } + + #[inline] + fn supports_large_file_size(&self) -> bool { + false + } + + #[inline] + fn capacity(&self) -> Option { + self.capacity() + } + + #[inline] + fn reset(&mut self) { + self.reset(); + } + + fn segment_in_store(&self, segment: (u64, u64)) -> bool { + for (seg_start, seg_end) in &self.list { + if segment.0 >= *seg_start as u64 && segment.1 <= *seg_end as u64 { + return true; + } + } + false + } +} + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use super::*; + + fn generic_basic_state_test( + store: &impl LostSegmentStore, + supports_large_file_size: bool, + capacity: Option, + ) { + assert_eq!(store.supports_large_file_size(), supports_large_file_size); + assert_eq!(store.number_of_segments(), 0); + assert!(store.is_empty()); + assert_eq!(store.capacity(), capacity); + assert_eq!(store.iter().count(), 0); + } + + fn generic_error_tests(store: &mut impl LostSegmentStore) { + matches!( + store.add_lost_segment((0, 0)).unwrap_err(), + LostSegmentError::EmptySegment + ); + matches!( + store.add_lost_segment((10, 0)).unwrap_err(), + LostSegmentError::StartLargerThanEnd(10, 0) + ); + matches!( + store.remove_lost_segment((0, 0)).unwrap_err(), + LostSegmentError::EmptySegment + ); + matches!( + store.remove_lost_segment((10, 0)).unwrap_err(), + LostSegmentError::StartLargerThanEnd(10, 0) + ); + } + + fn generic_add_segments_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + for segment in store.iter() { + assert_eq!(segment, (0, 20)); + } + store.add_lost_segment((20, 40)).unwrap(); + let mut segments: Vec<(u64, u64)> = store.iter().collect(); + segments.sort_unstable(); + assert_eq!(segments.len(), 2); + assert_eq!(segments[0], (0, 20)); + assert_eq!(segments[1], (20, 40)); + } + + fn generic_reset_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + store.reset(); + assert_eq!(store.number_of_segments(), 0); + assert!(store.is_empty()); + assert_eq!(store.iter().count(), 0); + } + + fn generic_removal_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + store.add_lost_segment((20, 40)).unwrap(); + assert_eq!(store.number_of_segments(), 2); + assert!(store.remove_lost_segment((0, 20)).unwrap()); + assert_eq!(store.number_of_segments(), 1); + assert!(!store.remove_lost_segment((0, 20)).unwrap()); + assert_eq!(store.number_of_segments(), 1); + assert!(store.remove_lost_segment((20, 40)).unwrap()); + assert_eq!(store.number_of_segments(), 0); + } + + fn generic_partial_removal_test_right_aligned(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + store.remove_lost_segment((0, 10)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + for list in store.iter() { + assert_eq!(list, (10, 20)); + } + store.remove_lost_segment((10, 20)).unwrap(); + assert!(store.is_empty()); + } + + fn generic_partial_removal_test_left_aligned(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + store.remove_lost_segment((10, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + for list in store.iter() { + assert_eq!(list, (0, 10)); + } + store.remove_lost_segment((0, 10)).unwrap(); + assert!(store.is_empty()); + } + + fn generic_partial_removal_test_fully_contained(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + store.remove_lost_segment((5, 15)).unwrap(); + assert_eq!(store.number_of_segments(), 2); + let seg_list = store.iter().collect::>(); + assert!(seg_list.contains(&(0, 5))); + assert!(seg_list.contains(&(15, 20))); + store.remove_lost_segment((0, 5)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + store.remove_lost_segment((15, 20)).unwrap(); + assert!(store.is_empty()); + } + + fn generic_partial_removal_fails_test_0(store: &mut impl LostSegmentStore) { + store.add_lost_segment((10, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + assert_eq!( + store.remove_lost_segment((5, 15)).unwrap_err(), + LostSegmentError::InvalidSegmentBoundary(5, 15) + ); + } + + fn generic_partial_removal_fails_test_1(store: &mut impl LostSegmentStore) { + store.add_lost_segment((10, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + assert_eq!( + store.remove_lost_segment((15, 25)).unwrap_err(), + LostSegmentError::InvalidSegmentBoundary(15, 25) + ); + } + + fn generic_partial_removal_fails_test_2(store: &mut impl LostSegmentStore) { + store.add_lost_segment((10, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + assert_eq!( + store.remove_lost_segment((10, 25)).unwrap_err(), + LostSegmentError::InvalidSegmentBoundary(10, 25) + ); + } + + fn generic_partial_removal_fails_test_3(store: &mut impl LostSegmentStore) { + store.add_lost_segment((10, 20)).unwrap(); + assert_eq!(store.number_of_segments(), 1); + assert_eq!( + store.remove_lost_segment((5, 20)).unwrap_err(), + LostSegmentError::InvalidSegmentBoundary(5, 20) + ); + } + + fn generic_coalescing_simple_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + store.add_lost_segment((20, 40)).unwrap(); + store.add_lost_segment((40, 60)).unwrap(); + store.coalesce_lost_segments().unwrap(); + for segment in store.iter() { + assert_eq!(segment, (0, 60)); + } + assert_eq!(store.number_of_segments(), 1); + } + + fn generic_coalescing_simple_with_gaps_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + store.add_lost_segment((20, 40)).unwrap(); + store.add_lost_segment((40, 60)).unwrap(); + + store.add_lost_segment((80, 100)).unwrap(); + store.add_lost_segment((110, 120)).unwrap(); + store.add_lost_segment((120, 130)).unwrap(); + store.coalesce_lost_segments().unwrap(); + let mut segments: Vec<(u64, u64)> = store.iter().collect(); + segments.sort_unstable(); + assert_eq!(segments.len(), 3); + assert_eq!(segments[0], (0, 60)); + assert_eq!(segments[1], (80, 100)); + assert_eq!(segments[2], (110, 130)); + } + + fn generic_coalescing_overlapping_simple_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + store.add_lost_segment((10, 30)).unwrap(); + store.coalesce_lost_segments().unwrap(); + for segment in store.iter() { + assert_eq!(segment, (0, 30)); + } + assert_eq!(store.number_of_segments(), 1); + } + + fn generic_coalescing_overlapping_adjacent_test(store: &mut impl LostSegmentStore) { + store.add_lost_segment((0, 20)).unwrap(); + store.add_lost_segment((10, 30)).unwrap(); + store.add_lost_segment((20, 40)).unwrap(); + store.coalesce_lost_segments().unwrap(); + for segment in store.iter() { + assert_eq!(segment, (0, 40)); + } + assert_eq!(store.number_of_segments(), 1); + } + + fn generic_useless_coalescing_test(store: &mut impl LostSegmentStore) { + // Is okay, does nothing. + assert!(store.coalesce_lost_segments().is_ok()); + assert_eq!(store.number_of_segments(), 0); + assert!(store.is_empty()); + store.add_lost_segment((10, 20)).unwrap(); + // Is okay, does nothing. + assert!(store.coalesce_lost_segments().is_ok()); + for segment in store.iter() { + assert_eq!(segment, (10, 20)); + } + } + + #[test] + fn test_basic_map_state_list() { + let store = LostSegmentsList::default(); + generic_basic_state_test(&store, true, None); + } + + #[test] + fn test_basic_errors_list() { + let mut store = LostSegmentsList::default(); + generic_error_tests(&mut store); + } + + #[test] + fn test_add_segments_list() { + let mut store = LostSegmentsList::default(); + generic_add_segments_test(&mut store); + } + + #[test] + fn test_reset_list() { + let mut store = LostSegmentsList::default(); + generic_reset_test(&mut store); + } + + #[test] + fn test_removal_map() { + let mut store = LostSegmentsList::default(); + generic_removal_test(&mut store); + } + + #[test] + fn test_partial_removal_list_0() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_test_right_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_1() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_test_left_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_2() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_test_fully_contained(&mut store); + } + + #[test] + fn test_partial_removal_list_fails_0() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_fails_test_0(&mut store); + } + + #[test] + fn test_partial_removal_list_fails_1() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_fails_test_1(&mut store); + } + + #[test] + fn test_partial_removal_list_fails_2() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_fails_test_2(&mut store); + } + + #[test] + fn test_partial_removal_list_fails_3() { + let mut store = LostSegmentsList::default(); + generic_partial_removal_fails_test_3(&mut store); + } + + #[test] + fn test_cap_limit_list() { + let mut store = LostSegmentsList::new(Some(4)); + for i in 0..4 { + store.add_lost_segment((i * 20, (i + 1) * 20)).unwrap(); + } + matches!( + store.add_lost_segment((80, 100)), + Err(LostSegmentError::StoreFull) + ); + } + + #[test] + fn test_basic_list_state_list() { + let store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_basic_state_test(&store, false, Some(12)); + let store = LostSegmentsListNormalFilesHeapless::<12>::new(); + generic_basic_state_test(&store, false, Some(12)); + } + #[test] + fn test_basic_errors_list_heapless() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_error_tests(&mut store); + } + + #[test] + fn test_add_segments_list_heapless() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_add_segments_test(&mut store); + } + + #[test] + fn test_reset_list_heapless() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_reset_test(&mut store); + } + + #[test] + fn test_removal_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_removal_test(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_0() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_test_right_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_1() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_test_left_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_2() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_test_fully_contained(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_fails_0() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_fails_test_0(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_fails_1() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_fails_test_1(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_fails_2() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_fails_test_2(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_fails_3() { + let mut store = LostSegmentsListNormalFilesHeapless::<12>::default(); + generic_partial_removal_fails_test_3(&mut store); + } + + fn generic_cap_limit_list_by_removal_test(store: &mut impl LostSegmentStore) { + for i in 0..4 { + store.add_lost_segment((i * 20, (i + 1) * 20)).unwrap(); + } + // This splits the segments, and the insert attempt should fail. + matches!( + store.remove_lost_segment((85, 95)), + Err(LostSegmentError::StoreFull) + ); + } + + fn generic_cap_limit_list_by_addition_test(store: &mut impl LostSegmentStore) { + for i in 0..4 { + store.add_lost_segment((i * 20, (i + 1) * 20)).unwrap(); + } + // This splits the segments, and the insert attempt should fail. + matches!( + store.add_lost_segment((100, 120)), + Err(LostSegmentError::StoreFull) + ); + } + + #[test] + fn test_cap_limit_list_by_removal() { + let mut store = LostSegmentsList::new(Some(4)); + generic_cap_limit_list_by_removal_test(&mut store); + } + + #[test] + fn test_cap_limit_list_heapless_by_removal() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_cap_limit_list_by_removal_test(&mut store); + } + + #[test] + fn test_cap_limit_list_heapless() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_cap_limit_list_by_addition_test(&mut store); + } + + #[test] + fn test_large_file_size_unsupported() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + matches!( + store.add_lost_segment((0, u32::MAX as u64 + 1)), + Err(LostSegmentError::LargeFileSegmentNotSupported) + ); + } + + #[test] + fn test_large_file_size_unsupported_2() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + matches!( + store.remove_lost_segment((0, u32::MAX as u64 + 1)), + Err(LostSegmentError::LargeFileSegmentNotSupported) + ); + } + + #[test] + fn test_basic_list_state_list_large() { + let store = LostSegmentsListHeapless::<12, u64>::default(); + generic_basic_state_test(&store, true, Some(12)); + } + #[test] + fn test_basic_errors_list_large() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_error_tests(&mut store); + } + + #[test] + fn test_add_segments_list_large() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_add_segments_test(&mut store); + } + + #[test] + fn test_reset_list_large() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_reset_test(&mut store); + } + + #[test] + fn test_removal_list_large() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_removal_test(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_large_0() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_test_right_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_large_1() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_test_left_aligned(&mut store); + } + + #[test] + fn test_partial_removal_list_heapless_large_2() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_test_fully_contained(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_large_fails_0() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_fails_test_0(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_large_fails_1() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_fails_test_1(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_large_fails_2() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_fails_test_2(&mut store); + } + + #[test] + fn test_partial_removal_heapless_list_large_fails_3() { + let mut store = LostSegmentsListHeapless::<12, u64>::default(); + generic_partial_removal_fails_test_3(&mut store); + } + + #[test] + fn test_cap_limit_list_large() { + let mut store = LostSegmentsListHeapless::<4, u64>::default(); + generic_cap_limit_list_by_removal_test(&mut store); + } + + #[test] + fn test_coalescing_simple_in_map() { + let mut store = LostSegmentsList::default(); + generic_coalescing_simple_test(&mut store); + } + + #[test] + fn test_useless_coalescing_map() { + let mut store = LostSegmentsList::default(); + generic_useless_coalescing_test(&mut store); + } + + #[test] + fn test_useless_coalescing_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_useless_coalescing_test(&mut store); + } + + #[test] + fn test_coalescing_simple_in_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_coalescing_simple_test(&mut store); + } + + #[test] + fn test_coalescing_simple_in_list_large() { + let mut store = LostSegmentsListHeapless::<4, u64>::default(); + generic_coalescing_simple_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_simple_in_map() { + let mut store = LostSegmentsList::default(); + generic_coalescing_overlapping_simple_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_simple_in_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_coalescing_overlapping_simple_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_simple_in_list_large() { + let mut store = LostSegmentsListHeapless::<4, u64>::default(); + generic_coalescing_overlapping_simple_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_adjacent_in_map() { + let mut store = LostSegmentsList::default(); + generic_coalescing_overlapping_adjacent_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_adjacent_in_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<4>::default(); + generic_coalescing_overlapping_adjacent_test(&mut store); + } + + #[test] + fn test_coalescing_overlapping_adjacent_in_list_large() { + let mut store = LostSegmentsListHeapless::<4, u64>::default(); + generic_coalescing_overlapping_adjacent_test(&mut store); + } + + #[test] + fn test_coalescing_simple_with_gaps_in_map() { + let mut store = LostSegmentsList::default(); + generic_coalescing_simple_with_gaps_test(&mut store); + } + + #[test] + fn test_coalescing_simple_with_gaps_in_list() { + let mut store = LostSegmentsListNormalFilesHeapless::<8>::default(); + generic_coalescing_simple_with_gaps_test(&mut store); + } + + #[test] + fn test_coalescing_simple_with_gaps_in_list_large() { + let mut store = LostSegmentsListHeapless::<8, u64>::default(); + generic_coalescing_simple_with_gaps_test(&mut store); + } +} diff --git a/src/source.rs b/src/source.rs index 3eb46a7..2bcdb6a 100644 --- a/src/source.rs +++ b/src/source.rs @@ -4,7 +4,7 @@ //! [ReadablePutRequest] into all packet data units (PDUs) which need to be sent to a remote //! CFDP entity to perform a File Copy operation to a remote entity. //! -//! The source entity allows freedom communication by using a user-provided [PduSendProvider] +//! The source entity allows freedom communication by using a user-provided [PduSender] instance //! to send all generated PDUs. It should be noted that for regular file transfers, each //! [SourceHandler::state_machine] call will map to one generated file data PDU. This allows //! flow control for the user of the state machine. @@ -40,14 +40,13 @@ use core::{ cell::{Cell, RefCell}, ops::ControlFlow, str::Utf8Error, - time::Duration, }; use spacepackets::{ ByteConversionError, cfdp::{ - ConditionCode, Direction, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl, - TransactionStatus, TransmissionMode, + ConditionCode, Direction, FaultHandlerCode, LargeFileFlag, PduType, SegmentMetadataFlag, + SegmentationControl, TransactionStatus, TransmissionMode, lv::Lv, pdu::{ CfdpPdu, CommonPduConfig, FileDirectiveType, PduError, PduHeader, WritablePduPacket, @@ -68,13 +67,13 @@ use spacepackets::{ use spacepackets::seq_count::SequenceCounter; use crate::{ - DummyPduProvider, EntityType, GenericSendError, PduProvider, TimerCreatorProvider, - time::CountdownProvider, + DummyPduProvider, EntityType, FaultInfo, GenericSendError, PduProvider, PositiveAckParams, + TimerCreator, time::Countdown, }; use super::{ - LocalEntityConfig, PacketTarget, PduSendProvider, RemoteEntityConfig, - RemoteEntityConfigProvider, State, TransactionId, UserFaultHookProvider, + LocalEntityConfig, PacketTarget, PduSender, RemoteConfigStore, RemoteEntityConfig, State, + TransactionId, UserFaultHook, filestore::{FilestoreError, VirtualFilestore}, request::{ReadablePutRequest, StaticPutRequestCacher}, user::{CfdpUser, TransactionFinishedParams}, @@ -94,11 +93,10 @@ pub enum TransactionStep { SendingEof = 6, WaitingForEofAck = 7, WaitingForFinished = 8, - //SendingAckOfFinished = 9, NoticeOfCompletion = 10, } -#[derive(Default, Copy, Clone)] +#[derive(Default, Debug, Copy, Clone)] pub struct FileParams { pub progress: u64, pub segment_len: u64, @@ -146,16 +144,6 @@ pub struct FinishedParams { file_status: FileStatus, } -#[derive(Debug, derive_new::new)] -pub struct TransferState { - transaction_id: Cell, - remote_cfg: RefCell, - transmission_mode: Cell, - closure_requested: Cell, - cond_code_eof: Cell>, - finished_params: Cell>, -} - #[derive(Debug, thiserror::Error)] pub enum SourceError { #[error("can not process packet type {pdu_type:?} with directive type {directive_type:?}")] @@ -204,12 +192,6 @@ pub enum PutRequestError { FilestoreError(#[from] FilestoreError), } -#[derive(Debug)] -struct PositiveAckParams { - ack_timer: Countdown, - ack_counter: u32, -} - #[derive(Debug, Default, Clone, Copy)] pub struct AnomalyTracker { invalid_ack_directive_code: u8, @@ -222,12 +204,55 @@ pub enum FsmContext { ResetWhenPossible, } +#[derive(Debug)] +pub struct TransactionParams { + transaction_id: Option, + remote_cfg: Option, + transmission_mode: Option, + closure_requested: bool, + cond_code_eof: Cell>, + finished_params: Option, + // File specific transfer fields + file_params: FileParams, + // PDU configuration is cached so it can be re-used for all PDUs generated for file transfers. + pdu_conf: CommonPduConfig, + check_timer: Option, + positive_ack_params: Cell>, + ack_timer: RefCell>, +} + +impl Default for TransactionParams { + fn default() -> Self { + Self { + transaction_id: Default::default(), + remote_cfg: Default::default(), + transmission_mode: Default::default(), + closure_requested: Default::default(), + cond_code_eof: Default::default(), + finished_params: Default::default(), + file_params: Default::default(), + pdu_conf: Default::default(), + check_timer: Default::default(), + positive_ack_params: Default::default(), + ack_timer: Default::default(), + } + } +} + +impl TransactionParams { + #[inline] + fn reset(&mut self) { + self.transaction_id = None; + self.transmission_mode = None; + } +} + /// This is the primary CFDP source handler. It models the CFDP source entity, which is /// primarily responsible for handling put requests to send files to another CFDP destination /// entity. /// /// As such, it contains a state machine to perform all operations necessary to perform a -/// source-to-destination file transfer. This class uses the user provides [PduSendProvider] to +/// source-to-destination file transfer. This class uses the user provides [PduSender] to /// send the CFDP PDU packets generated by the state machine. /// /// The following core functions are the primary interface: @@ -250,50 +275,43 @@ pub enum FsmContext { /// is required, it is recommended to create a new handler and run all active handlers inside a /// thread pool, or move the newly created handler to a new thread. pub struct SourceHandler< - PduSender: PduSendProvider, - UserFaultHook: UserFaultHookProvider, + PduSenderInstance: PduSender, + UserFaultHookInstance: UserFaultHook, Vfs: VirtualFilestore, - RemoteCfgTable: RemoteEntityConfigProvider, - TimerCreator: TimerCreatorProvider, - Countdown: CountdownProvider, + RemoteConfigStoreInstance: RemoteConfigStore, + TimerCreatorInstance: TimerCreator, + CountdownInstance: Countdown, SequenceCounterInstance: SequenceCounter, > { - local_cfg: LocalEntityConfig, - pdu_sender: PduSender, + local_cfg: LocalEntityConfig, + pdu_sender: PduSenderInstance, pdu_and_cksum_buffer: RefCell>, put_request_cacher: StaticPutRequestCacher, - remote_cfg_table: RemoteCfgTable, + remote_cfg_table: RemoteConfigStoreInstance, vfs: Vfs, state_helper: StateHelper, - // Transfer related state information - transfer_state: Option, - // File specific transfer fields - file_params: FileParams, - // PDU configuration is cached so it can be re-used for all PDUs generated for file transfers. - pdu_conf: CommonPduConfig, - check_timer: RefCell>, - positive_ack_params: RefCell>>, - timer_creator: TimerCreator, + transaction_params: TransactionParams, + timer_creator: TimerCreatorInstance, seq_count_provider: SequenceCounterInstance, anomalies: AnomalyTracker, } impl< - PduSender: PduSendProvider, - UserFaultHook: UserFaultHookProvider, + PduSenderInstance: PduSender, + UserFaultHookInstance: UserFaultHook, Vfs: VirtualFilestore, - RemoteCfgTable: RemoteEntityConfigProvider, - TimerCreator: TimerCreatorProvider, - Countdown: CountdownProvider, + RemoteConfigStoreInstance: RemoteConfigStore, + TimerCreatorInstance: TimerCreator, + CountdownInstance: Countdown, SequenceCounterInstance: SequenceCounter, > SourceHandler< - PduSender, - UserFaultHook, + PduSenderInstance, + UserFaultHookInstance, Vfs, - RemoteCfgTable, - TimerCreator, - Countdown, + RemoteConfigStoreInstance, + TimerCreatorInstance, + CountdownInstance, SequenceCounterInstance, > { @@ -302,7 +320,7 @@ impl< /// # Arguments /// /// * `cfg` - The local entity configuration for this source handler. - /// * `pdu_sender` - [PduSendProvider] provider used to send CFDP PDUs generated by the handler. + /// * `pdu_sender` - [PduSender] used to send CFDP PDUs generated by the handler. /// * `vfs` - [VirtualFilestore] implementation used by the handler, which decouples the CFDP /// implementation from the underlying filestore/filesystem. This allows to use this handler /// for embedded systems where a standard runtime might not be available. @@ -312,22 +330,22 @@ impl< /// checksum calculations. The user can specify the size of this buffer, so this should be /// set to the maximum expected PDU size or a conservative upper bound for this size, for /// example 2048 or 4096 bytes. - /// * `remote_cfg_table` - The [RemoteEntityConfigProvider] used to look up remote + /// * `remote_cfg_table` - The [RemoteEntityConfig] used to look up remote /// entities and target specific configuration for file copy operations. - /// * `timer_creator` - [TimerCreatorProvider] used by the CFDP handler to generate + /// * `timer_creator` - [TimerCreator] used by the CFDP handler to generate /// timers required by various tasks. This allows to use this handler for embedded systems /// where the standard time APIs might not be available. /// * `seq_count_provider` - The [SequenceCounter] used to generate the [TransactionId] /// which contains an incrementing counter. #[allow(clippy::too_many_arguments)] pub fn new( - cfg: LocalEntityConfig, - pdu_sender: PduSender, + cfg: LocalEntityConfig, + pdu_sender: PduSenderInstance, vfs: Vfs, put_request_cacher: StaticPutRequestCacher, pdu_and_cksum_buf_size: usize, - remote_cfg_table: RemoteCfgTable, - timer_creator: TimerCreator, + remote_cfg_table: RemoteConfigStoreInstance, + timer_creator: TimerCreatorInstance, seq_count_provider: SequenceCounterInstance, ) -> Self { Self { @@ -338,12 +356,8 @@ impl< vfs, put_request_cacher, state_helper: Default::default(), - transfer_state: Default::default(), - file_params: Default::default(), - pdu_conf: Default::default(), + transaction_params: Default::default(), anomalies: Default::default(), - check_timer: RefCell::new(None), - positive_ack_params: RefCell::new(None), timer_creator, seq_count_provider, } @@ -392,15 +406,13 @@ impl< #[inline] pub fn transaction_id(&self) -> Option { - self.transfer_state.as_ref().map(|v| v.transaction_id.get()) + self.transaction_params.transaction_id } /// Returns the [TransmissionMode] for the active file operation. #[inline] pub fn transmission_mode(&self) -> Option { - self.transfer_state - .as_ref() - .map(|v| v.transmission_mode.get()) + self.transaction_params.transmission_mode } /// Get the [TransactionStep], which denotes the exact step of a pending CFDP transaction when @@ -416,7 +428,7 @@ impl< } #[inline] - pub fn local_cfg(&self) -> &LocalEntityConfig { + pub fn local_cfg(&self) -> &LocalEntityConfig { &self.local_cfg } @@ -495,28 +507,29 @@ impl< }; // Set PDU configuration fields which are important for generating PDUs. - self.pdu_conf + self.transaction_params + .pdu_conf .set_source_and_dest_id( create_id(&self.local_cfg.id), create_id(&self.put_request_cacher.static_fields.destination_id), ) .unwrap(); // Set up other PDU configuration fields. - self.pdu_conf.direction = Direction::TowardsReceiver; - self.pdu_conf.crc_flag = remote_cfg.crc_on_transmission_by_default.into(); - self.pdu_conf.transaction_seq_num = *transaction_id.seq_num(); - self.pdu_conf.trans_mode = transmission_mode; - self.file_params.segment_len = self.calculate_max_file_seg_len(remote_cfg); + self.transaction_params.pdu_conf.direction = Direction::TowardsReceiver; + self.transaction_params.pdu_conf.crc_flag = + remote_cfg.crc_on_transmission_by_default.into(); + self.transaction_params.pdu_conf.transaction_seq_num = *transaction_id.seq_num(); + self.transaction_params.pdu_conf.trans_mode = transmission_mode; + self.transaction_params.file_params.segment_len = + self.calculate_max_file_seg_len(remote_cfg); + + self.transaction_params.transaction_id = Some(transaction_id); + self.transaction_params.remote_cfg = Some(*remote_cfg); + self.transaction_params.transmission_mode = Some(transmission_mode); + self.transaction_params.closure_requested = closure_requested; + self.transaction_params.cond_code_eof.set(None); + self.transaction_params.finished_params = None; - // Set up the transfer context structure. - self.transfer_state = Some(TransferState { - transaction_id: Cell::new(transaction_id), - remote_cfg: RefCell::new(*remote_cfg), - transmission_mode: Cell::new(transmission_mode), - closure_requested: Cell::new(closure_requested), - cond_code_eof: Cell::new(None), - finished_params: Cell::new(None), - }); self.state_helper.state.set(super::State::Busy); Ok(()) } @@ -551,16 +564,16 @@ impl< .expect("PDU directive type unexpectedly not set") { FileDirectiveType::FinishedPdu => { - let finished_pdu = FinishedPduReader::new(packet_to_insert.pdu())?; + let finished_pdu = FinishedPduReader::new(packet_to_insert.raw_pdu())?; self.handle_finished_pdu(&finished_pdu)? } FileDirectiveType::NakPdu => { - let nak_pdu = NakPduReader::new(packet_to_insert.pdu())?; + let nak_pdu = NakPduReader::new(packet_to_insert.raw_pdu())?; sent_packets += self.handle_nak_pdu(&nak_pdu)?; } FileDirectiveType::KeepAlivePdu => self.handle_keep_alive_pdu(), FileDirectiveType::AckPdu => { - let ack_pdu = AckPdu::from_bytes(packet_to_insert.pdu())?; + let ack_pdu = AckPdu::from_bytes(packet_to_insert.raw_pdu())?; self.handle_ack_pdu(&ack_pdu)? } FileDirectiveType::EofPdu @@ -610,9 +623,7 @@ impl< /// behaviour. pub fn reset(&mut self) { self.state_helper = Default::default(); - self.transfer_state = None; - self.file_params = Default::default(); - *self.check_timer.borrow_mut() = None; + self.transaction_params.reset(); } #[inline] @@ -676,36 +687,54 @@ impl< user: &mut impl CfdpUser, ) -> Result { let mut sent_packets = 0; - let mut positive_ack_limit_reached = false; - if let Some(positive_ack_params) = self.positive_ack_params.borrow_mut().as_mut() { - if positive_ack_params.ack_timer.has_expired() { + let current_params = self.transaction_params.positive_ack_params.get(); + if let Some(mut positive_ack_params) = current_params { + if self + .transaction_params + .ack_timer + .borrow_mut() + .as_ref() + .unwrap() + .has_expired() + { let ack_timer_exp_limit = self - .transfer_state + .transaction_params + .remote_cfg .as_ref() .unwrap() - .remote_cfg - .borrow() .positive_ack_timer_expiration_limit; if positive_ack_params.ack_counter + 1 >= ack_timer_exp_limit { - positive_ack_limit_reached = true; + let (fault_packets_sent, ctx) = + self.declare_fault(user, ConditionCode::PositiveAckLimitReached)?; + sent_packets += fault_packets_sent; + if ctx == FsmContext::ResetWhenPossible { + self.reset(); + } else { + positive_ack_params.ack_counter = 0; + positive_ack_params.positive_ack_of_cancellation = true; + } } else { - positive_ack_params.ack_timer.reset(); + self.transaction_params + .ack_timer + .borrow_mut() + .as_mut() + .unwrap() + .reset(); positive_ack_params.ack_counter += 1; self.prepare_and_send_eof_pdu( user, - self.file_params.checksum_completed_file.unwrap(), + self.transaction_params + .file_params + .checksum_completed_file + .unwrap(), )?; sent_packets += 1; } } - } - if positive_ack_limit_reached { - let (fault_packets_sent, ctx) = - self.declare_fault(user, ConditionCode::PositiveAckLimitReached)?; - if ctx == FsmContext::ResetWhenPossible { - self.reset(); - } - sent_packets += fault_packets_sent; + + self.transaction_params + .positive_ack_params + .set(Some(positive_ack_params)); } Ok(sent_packets) } @@ -720,14 +749,18 @@ impl< sent_packets += 1; continue; } else { - if (segment_req.1 < segment_req.0) || (segment_req.0 > self.file_params.progress) { + if (segment_req.1 < segment_req.0) + || (segment_req.0 > self.transaction_params.file_params.progress) + { return Err(SourceError::InvalidNakPdu); } let mut missing_chunk_len = segment_req.1 - segment_req.0; let current_offset = segment_req.0; while missing_chunk_len > 0 { - let chunk_size = - core::cmp::min(missing_chunk_len, self.file_params.segment_len); + let chunk_size = core::cmp::min( + missing_chunk_len, + self.transaction_params.file_params.segment_len, + ); self.prepare_and_send_file_data_pdu(current_offset, chunk_size)?; sent_packets += 1; missing_chunk_len -= missing_chunk_len; @@ -744,7 +777,12 @@ impl< // If we reach this state, countdown definitely is set. #[allow(clippy::collapsible_if)] if self.transmission_mode().unwrap() == TransmissionMode::Unacknowledged - && self.check_timer.borrow().as_ref().unwrap().has_expired() + && self + .transaction_params + .check_timer + .as_ref() + .unwrap() + .has_expired() { let (sent_packets, ctx) = self.declare_fault(user, ConditionCode::CheckLimitReached)?; if ctx == FsmContext::ResetWhenPossible { @@ -758,21 +796,31 @@ impl< fn eof_fsm(&mut self, user: &mut impl CfdpUser) -> Result<(), SourceError> { let checksum = self.vfs.calculate_checksum( self.put_request_cacher.source_file().unwrap(), - self.tstate_ref().remote_cfg.borrow().default_crc_type, - self.file_params.file_size, + self.transaction_params + .remote_cfg + .as_ref() + .unwrap() + .default_crc_type, + self.transaction_params.file_params.file_size, &mut self.pdu_and_cksum_buffer.borrow_mut(), )?; - self.file_params.checksum_completed_file = Some(checksum); + self.transaction_params.file_params.checksum_completed_file = Some(checksum); self.prepare_and_send_eof_pdu(user, checksum)?; if self.transmission_mode().unwrap() == TransmissionMode::Unacknowledged { - if self.tstate_ref().closure_requested.get() { - *self.check_timer.borrow_mut() = Some(self.timer_creator.create_countdown( - crate::TimerContext::CheckLimit { - local_id: self.local_cfg.id, - remote_id: self.tstate_ref().remote_cfg.borrow().entity_id, - entity_type: EntityType::Sending, - }, - )); + if self.transaction_params.closure_requested { + self.transaction_params.check_timer = Some( + self.timer_creator + .create_countdown(crate::TimerContext::CheckLimit { + local_id: self.local_cfg.id, + remote_id: self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .entity_id, + entity_type: EntityType::Sending, + }), + ); self.set_step(TransactionStep::WaitingForFinished); } else { self.set_step(TransactionStep::NoticeOfCompletion); @@ -785,19 +833,33 @@ impl< fn start_positive_ack_procedure(&self) { self.set_step_internal(TransactionStep::WaitingForEofAck); - *self.positive_ack_params.borrow_mut() = Some(PositiveAckParams { - ack_timer: self - .timer_creator + match self.transaction_params.positive_ack_params.get() { + Some(mut current) => { + current.ack_counter = 0; + self.transaction_params + .positive_ack_params + .set(Some(current)); + } + None => self + .transaction_params + .positive_ack_params + .set(Some(PositiveAckParams { + ack_counter: 0, + positive_ack_of_cancellation: false, + })), + } + + *self.transaction_params.ack_timer.borrow_mut() = Some( + self.timer_creator .create_countdown(crate::TimerContext::PositiveAck { - expiry_time: Duration::from_secs( - self.tstate_ref() - .remote_cfg - .borrow() - .positive_ack_timer_interval_seconds as u64, - ), + expiry_time: self + .transaction_params + .remote_cfg + .as_ref() + .unwrap() + .positive_ack_timer_interval, }), - ack_counter: 0, - }) + ); } fn handle_transaction_start( @@ -805,7 +867,7 @@ impl< cfdp_user: &mut impl CfdpUser, ) -> Result<(), SourceError> { if !self.put_request_cacher.has_source_file() { - self.file_params.metadata_only = true; + self.transaction_params.file_params.metadata_only = true; } else { let source_file = self .put_request_cacher @@ -820,14 +882,14 @@ impl< self.put_request_cacher .dest_file() .map_err(SourceError::DestFileNotValidUtf8)?; - self.file_params.file_size = self.vfs.file_size(source_file)?; - if self.file_params.file_size > u32::MAX as u64 { - self.pdu_conf.file_flag = LargeFileFlag::Large + self.transaction_params.file_params.file_size = self.vfs.file_size(source_file)?; + if self.transaction_params.file_params.file_size > u32::MAX as u64 { + self.transaction_params.pdu_conf.file_flag = LargeFileFlag::Large } else { - if self.file_params.file_size == 0 { - self.file_params.empty_file = true; + if self.transaction_params.file_params.file_size == 0 { + self.transaction_params.file_params.empty_file = true; } - self.pdu_conf.file_flag = LargeFileFlag::Normal + self.transaction_params.pdu_conf.file_flag = LargeFileFlag::Normal } } cfdp_user.transaction_indication(&self.transaction_id().unwrap()); @@ -840,25 +902,29 @@ impl< transaction_status: TransactionStatus, ) -> Result<(), SourceError> { let ack_pdu = AckPdu::new( - PduHeader::new_no_file_data(self.pdu_conf, 0), + PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0), FileDirectiveType::FinishedPdu, condition_code, transaction_status, - )?; + ) + .map_err(PduError::from)?; self.pdu_send_helper(&ack_pdu)?; Ok(()) } fn prepare_and_send_metadata_pdu(&mut self) -> Result<(), SourceError> { - let tstate = self.tstate_ref(); let metadata_params = MetadataGenericParams::new( - tstate.closure_requested.get(), - tstate.remote_cfg.borrow().default_crc_type, - self.file_params.file_size, + self.transaction_params.closure_requested, + self.transaction_params + .remote_cfg + .as_ref() + .unwrap() + .default_crc_type, + self.transaction_params.file_params.file_size, ); - if self.file_params.metadata_only { + if self.transaction_params.file_params.metadata_only { let metadata_pdu = MetadataPduCreator::new( - PduHeader::new_no_file_data(self.pdu_conf, 0), + PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0), metadata_params, Lv::new_empty(), Lv::new_empty(), @@ -867,7 +933,7 @@ impl< return self.pdu_send_helper(&metadata_pdu); } let metadata_pdu = MetadataPduCreator::new( - PduHeader::new_no_file_data(self.pdu_conf, 0), + PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0), metadata_params, Lv::new_from_str(self.put_request_cacher.source_file().unwrap()).unwrap(), Lv::new_from_str(self.put_request_cacher.dest_file().unwrap()).unwrap(), @@ -877,24 +943,25 @@ impl< } fn file_data_fsm(&mut self) -> Result, SourceError> { - //if self.transmission_mode().unwrap() == super::TransmissionMode::Acknowledged { - // TODO: Handle re-transmission - //} - if !self.file_params.metadata_only - && self.file_params.progress < self.file_params.file_size + if !self.transaction_params.file_params.metadata_only + && self.transaction_params.file_params.progress + < self.transaction_params.file_params.file_size && self.send_progressing_file_data_pdu()? { return Ok(ControlFlow::Break(1)); } - if self.file_params.empty_file || self.file_params.progress >= self.file_params.file_size { + if self.transaction_params.file_params.empty_file + || self.transaction_params.file_params.progress + >= self.transaction_params.file_params.file_size + { // EOF is still expected. self.set_step(TransactionStep::SendingEof); - self.tstate_ref() + self.transaction_params .cond_code_eof .set(Some(ConditionCode::NoError)); - } else if self.file_params.metadata_only { + } else if self.transaction_params.file_params.metadata_only { // Special case: Metadata Only, no EOF required. - if self.tstate_ref().closure_requested.get() { + if self.transaction_params.closure_requested { self.set_step(TransactionStep::WaitingForFinished); } else { self.set_step(TransactionStep::NoticeOfCompletion); @@ -906,7 +973,7 @@ impl< fn notice_of_completion(&mut self, cfdp_user: &mut impl CfdpUser) { if self.local_cfg.indication_cfg.transaction_finished { // The first case happens for unacknowledged file copy operation with no closure. - let finished_params = match self.tstate_ref().finished_params.get() { + let finished_params = match self.transaction_params.finished_params { Some(finished_params) => TransactionFinishedParams { id: self.transaction_id().unwrap(), condition_code: finished_params.condition_code, @@ -926,7 +993,7 @@ impl< fn calculate_max_file_seg_len(&self, remote_cfg: &RemoteEntityConfig) -> u64 { let mut derived_max_seg_len = calculate_max_file_seg_len_for_max_packet_len_and_pdu_header( - &PduHeader::new_no_file_data(self.pdu_conf, 0), + &PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0), remote_cfg.max_packet_len, None, ); @@ -941,14 +1008,19 @@ impl< fn send_progressing_file_data_pdu(&mut self) -> Result { // Should never be called, but use defensive programming here. - if self.file_params.progress >= self.file_params.file_size { + if self.transaction_params.file_params.progress + >= self.transaction_params.file_params.file_size + { return Ok(false); } - let read_len = self - .file_params - .segment_len - .min(self.file_params.file_size - self.file_params.progress); - self.prepare_and_send_file_data_pdu(self.file_params.progress, read_len)?; + let read_len = self.transaction_params.file_params.segment_len.min( + self.transaction_params.file_params.file_size + - self.transaction_params.file_params.progress, + ); + self.prepare_and_send_file_data_pdu( + self.transaction_params.file_params.progress, + read_len, + )?; Ok(true) } @@ -959,7 +1031,7 @@ impl< ) -> Result<(), SourceError> { let pdu_creator = FileDataPduCreatorWithReservedDatafield::new_no_seg_metadata( PduHeader::new_for_file_data( - self.pdu_conf, + self.transaction_params.pdu_conf, 0, SegmentMetadataFlag::NotPresent, SegmentationControl::NoRecordBoundaryPreservation, @@ -981,7 +1053,7 @@ impl< None, &self.pdu_and_cksum_buffer.borrow()[0..written_len], )?; - self.file_params.progress += size; + self.transaction_params.file_params.progress += size; Ok(()) } @@ -991,13 +1063,13 @@ impl< checksum: u32, ) -> Result<(), SourceError> { let eof_pdu = EofPdu::new( - PduHeader::new_no_file_data(self.pdu_conf, 0), - self.tstate_ref() + PduHeader::new_for_file_directive(self.transaction_params.pdu_conf, 0), + self.transaction_params .cond_code_eof .get() .unwrap_or(ConditionCode::NoError), checksum, - self.file_params.progress, + self.transaction_params.file_params.progress, None, ); self.pdu_send_helper(&eof_pdu)?; @@ -1029,15 +1101,14 @@ impl< directive_type: Some(FileDirectiveType::FinishedPdu), }); } - let tstate_ref = self.tstate_ref(); // Unwrapping should be fine here, the transfer state is valid when we are not in IDLE // mode. - tstate_ref.finished_params.set(Some(FinishedParams { + self.transaction_params.finished_params = Some(FinishedParams { condition_code: finished_pdu.condition_code(), delivery_code: finished_pdu.delivery_code(), file_status: finished_pdu.file_status(), - })); - if tstate_ref.transmission_mode.get() == TransmissionMode::Acknowledged { + }); + if let Some(TransmissionMode::Acknowledged) = self.transmission_mode() { self.prepare_and_send_ack_pdu( finished_pdu.condition_code(), TransactionStatus::Active, @@ -1074,12 +1145,9 @@ impl< condition_code: ConditionCode, ) -> Result { let mut sent_packets = 0; - match self.notice_of_cancellation_internal(user, condition_code, &mut sent_packets)? { - ControlFlow::Continue(ctx) | ControlFlow::Break(ctx) => { - if ctx == FsmContext::ResetWhenPossible { - self.reset(); - } - } + let ctx = self.notice_of_cancellation_internal(user, condition_code, &mut sent_packets)?; + if ctx == FsmContext::ResetWhenPossible { + self.reset(); } Ok(sent_packets) } @@ -1089,39 +1157,30 @@ impl< user: &mut impl CfdpUser, condition_code: ConditionCode, sent_packets: &mut u32, - ) -> Result, SourceError> { - let transaction_id = self.transaction_id().unwrap(); - // CFDP standard 4.11.2.2.3: Any fault declared in the course of transferring - // the EOF (cancel) PDU must result in abandonment of the transaction. - if let Some(cond_code_eof) = self.tstate_ref().cond_code_eof.get() { - if cond_code_eof != ConditionCode::NoError { - // Still call the abandonment callback to ensure the fault is logged. - self.local_cfg - .fault_handler - .user_hook - .borrow_mut() - .abandoned_cb(transaction_id, cond_code_eof, self.file_params.progress); - return Ok(ControlFlow::Break(FsmContext::ResetWhenPossible)); - } - } - - self.tstate_ref().cond_code_eof.set(Some(condition_code)); + ) -> Result { + self.transaction_params + .cond_code_eof + .set(Some(condition_code)); // As specified in 4.11.2.2, prepare an EOF PDU to be sent to the remote entity. Supply // the checksum for the file copy progress sent so far. let checksum = self.vfs.calculate_checksum( self.put_request_cacher.source_file().unwrap(), - self.tstate_ref().remote_cfg.borrow().default_crc_type, - self.file_params.progress, + self.transaction_params + .remote_cfg + .as_ref() + .unwrap() + .default_crc_type, + self.transaction_params.file_params.progress, &mut self.pdu_and_cksum_buffer.borrow_mut(), )?; self.prepare_and_send_eof_pdu(user, checksum)?; *sent_packets += 1; if self.transmission_mode().unwrap() == TransmissionMode::Unacknowledged { // We are done. - Ok(ControlFlow::Continue(FsmContext::ResetWhenPossible)) + Ok(FsmContext::ResetWhenPossible) } else { self.start_positive_ack_procedure(); - Ok(ControlFlow::Continue(FsmContext::default())) + Ok(FsmContext::default()) } } @@ -1144,42 +1203,38 @@ impl< cond: ConditionCode, ) -> Result<(u32, FsmContext), SourceError> { let mut sent_packets = 0; - let fh = self.local_cfg.fault_handler.get_fault_handler(cond); + let mut fh = self.local_cfg.fault_handler.get_fault_handler(cond); + // CFDP standard 4.11.2.2.3: Any fault declared in the course of transferring + // the EOF (cancel) PDU must result in abandonment of the transaction. + if let Some(positive_ack_params) = self.transaction_params.positive_ack_params.get() { + if positive_ack_params.positive_ack_of_cancellation { + fh = FaultHandlerCode::AbandonTransaction; + } + } let mut ctx = FsmContext::default(); match fh { - spacepackets::cfdp::FaultHandlerCode::NoticeOfCancellation => { - match self.notice_of_cancellation_internal(user, cond, &mut sent_packets)? { - ControlFlow::Continue(ctx_cancellation) => { - ctx = ctx_cancellation; - } - ControlFlow::Break(ctx_cancellation) => { - return Ok((sent_packets, ctx_cancellation)); - } - } + FaultHandlerCode::NoticeOfCancellation => { + ctx = self.notice_of_cancellation_internal(user, cond, &mut sent_packets)?; } - spacepackets::cfdp::FaultHandlerCode::NoticeOfSuspension => { + FaultHandlerCode::NoticeOfSuspension => { self.notice_of_suspension_internal(); } - spacepackets::cfdp::FaultHandlerCode::IgnoreError => (), - spacepackets::cfdp::FaultHandlerCode::AbandonTransaction => { - return Ok((sent_packets, FsmContext::ResetWhenPossible)); + FaultHandlerCode::IgnoreError => (), + FaultHandlerCode::AbandonTransaction => { + ctx = FsmContext::ResetWhenPossible; } } self.local_cfg.fault_handler.report_fault( - self.transaction_id().unwrap(), - cond, - self.file_params.progress, + fh, + FaultInfo::new( + self.transaction_id().unwrap(), + cond, + self.transaction_params.file_params.progress, + ), ); Ok((sent_packets, ctx)) } - // Internal helper function. - fn tstate_ref(&self) -> &TransferState { - self.transfer_state - .as_ref() - .expect("transfer state should be set in busy state") - } - fn handle_keep_alive_pdu(&mut self) {} } @@ -1203,7 +1258,7 @@ mod tests { use super::*; use crate::{ - CRC_32, FaultHandler, IndicationConfig, PduRawWithInfo, StdRemoteEntityConfigProvider, + CRC_32, FaultHandler, IndicationConfig, PduRawWithInfo, RemoteConfigStoreStd, filestore::NativeFilestore, request::PutRequestOwned, source::TransactionStep, @@ -1229,7 +1284,7 @@ mod tests { TestCfdpSender, TestFaultHandler, NativeFilestore, - StdRemoteEntityConfigProvider, + RemoteConfigStoreStd, TestCheckTimerCreator, TestCheckTimer, SequenceCounterSimple, @@ -1387,7 +1442,7 @@ mod tests { transfer_info: &TransferInfo, seg_reqs: &[(u32, u32)], ) { - let nak_pdu = NakPduCreator::new( + let nak_pdu = NakPduCreator::new_normal_file_size( transfer_info.pdu_header, 0, transfer_info.file_size as u32, @@ -1651,7 +1706,7 @@ mod tests { // Finish handling: Simulate completion from the destination side by insert finished PDU. fn finish_handling(&mut self, user: &mut TestCfdpUser, transfer_info: &TransferInfo) { - let finished_pdu = FinishedPduCreator::new_default( + let finished_pdu = FinishedPduCreator::new_no_error( transfer_info.pdu_header, DeliveryCode::Complete, FileStatus::Retained, @@ -1693,18 +1748,21 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transaction_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transfer_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); tb.common_eof_pdu_check( - &mut cfdp_user, - transaction_info.closure_requested, + &mut user, + transfer_info.closure_requested, EofParams::new_success(file_size, CRC_32.digest().finalize()), 1, - ) + ); + user.verify_finished_indication( + DeliveryCode::Complete, + ConditionCode::NoError, + transfer_info.id, + FileStatus::Unreported, + ); } #[test] @@ -1719,53 +1777,55 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transaction_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transaction_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); tb.common_eof_pdu_check( - &mut cfdp_user, + &mut user, transaction_info.closure_requested, EofParams::new_success(file_size, CRC_32.digest().finalize()), 1, ); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transaction_info); - tb.finish_handling(&mut cfdp_user, &transaction_info); + tb.acknowledge_eof_pdu(&mut user, &transaction_info); + tb.finish_handling(&mut user, &transaction_info); tb.common_finished_pdu_ack_check(); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::NoError, + transaction_info.id, + ); } #[test] fn test_tiny_file_transfer_not_acked_no_closure() { - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut tb = SourceHandlerTestbench::new(TransmissionMode::Unacknowledged, false, 512); - tb.common_tiny_file_transfer(&mut cfdp_user, false); + tb.common_tiny_file_transfer(&mut user, false); } #[test] fn test_tiny_file_transfer_acked() { - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut tb = SourceHandlerTestbench::new(TransmissionMode::Acknowledged, false, 512); - let (_data, transfer_info) = tb.common_tiny_file_transfer(&mut cfdp_user, false); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + let (_data, transfer_info) = tb.common_tiny_file_transfer(&mut user, false); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); } #[test] fn test_tiny_file_transfer_not_acked_with_closure() { let mut tb = SourceHandlerTestbench::new(TransmissionMode::Unacknowledged, false, 512); - let mut cfdp_user = TestCfdpUser::default(); - let (_data, transfer_info) = tb.common_tiny_file_transfer(&mut cfdp_user, true); - tb.finish_handling(&mut cfdp_user, &transfer_info) + let mut user = TestCfdpUser::default(); + let (_data, transfer_info) = tb.common_tiny_file_transfer(&mut user, true); + tb.finish_handling(&mut user, &transfer_info) } #[test] fn test_two_segment_file_transfer_not_acked_no_closure() { let mut tb = SourceHandlerTestbench::new(TransmissionMode::Unacknowledged, false, 128); - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut file = OpenOptions::new() .write(true) .open(&tb.srcfile) @@ -1775,14 +1835,14 @@ mod tests { file.write_all(&rand_data) .expect("writing file content failed"); drop(file); - let (_, fd_pdus) = tb.generic_file_transfer(&mut cfdp_user, false, rand_data.to_vec()); + let (_, fd_pdus) = tb.generic_file_transfer(&mut user, false, rand_data.to_vec()); assert_eq!(fd_pdus, 2); } #[test] fn test_two_segment_file_transfer_not_acked_with_closure() { let mut tb = SourceHandlerTestbench::new(TransmissionMode::Unacknowledged, false, 128); - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut file = OpenOptions::new() .write(true) .open(&tb.srcfile) @@ -1793,14 +1853,14 @@ mod tests { .expect("writing file content failed"); drop(file); let (transfer_info, fd_pdus) = - tb.generic_file_transfer(&mut cfdp_user, true, rand_data.to_vec()); + tb.generic_file_transfer(&mut user, true, rand_data.to_vec()); assert_eq!(fd_pdus, 2); - tb.finish_handling(&mut cfdp_user, &transfer_info) + tb.finish_handling(&mut user, &transfer_info) } #[test] fn test_two_segment_file_transfer_acked() { - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut tb = SourceHandlerTestbench::new(TransmissionMode::Acknowledged, false, 128); let mut file = OpenOptions::new() .write(true) @@ -1812,10 +1872,10 @@ mod tests { .expect("writing file content failed"); drop(file); let (transfer_info, fd_pdus) = - tb.generic_file_transfer(&mut cfdp_user, true, rand_data.to_vec()); + tb.generic_file_transfer(&mut user, true, rand_data.to_vec()); assert_eq!(fd_pdus, 2); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); } @@ -1831,19 +1891,21 @@ mod tests { Some(true), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transaction_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transaction_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); tb.common_eof_pdu_check( - &mut cfdp_user, + &mut user, transaction_info.closure_requested, EofParams::new_success(file_size, CRC_32.digest().finalize()), 1, ); - tb.finish_handling(&mut cfdp_user, &transaction_info) + tb.finish_handling(&mut user, &transaction_info); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::NoError, + transaction_info.id, + ); } #[test] @@ -1905,15 +1967,12 @@ mod tests { Some(true), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transaction_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transaction_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); let expected_id = tb.handler.transaction_id().unwrap(); tb.common_eof_pdu_check( - &mut cfdp_user, + &mut user, transaction_info.closure_requested, EofParams::new_success(file_size, CRC_32.digest().finalize()), 1, @@ -1924,10 +1983,7 @@ mod tests { // cancellation -> leads to an EOF PDU with the appropriate error code. tb.expiry_control.set_check_limit_expired(); - assert_eq!( - tb.handler.state_machine_no_packet(&mut cfdp_user).unwrap(), - 1 - ); + assert_eq!(tb.handler.state_machine_no_packet(&mut user).unwrap(), 1); assert!(!tb.pdu_queue_empty()); let next_pdu = tb.get_next_sent_pdu().unwrap(); let eof_pdu = EofPdu::from_bytes(&next_pdu.raw_pdu).expect("invalid EOF PDU format"); @@ -1941,9 +1997,13 @@ mod tests { let fh_ref_mut = fault_handler.get_mut(); assert!(!fh_ref_mut.cancellation_queue_empty()); assert_eq!(fh_ref_mut.notice_of_cancellation_queue.len(), 1); - let (id, cond_code, progress) = fh_ref_mut.notice_of_cancellation_queue.pop_back().unwrap(); - assert_eq!(id, expected_id); - assert_eq!(cond_code, ConditionCode::CheckLimitReached); + let FaultInfo { + transaction_id, + condition_code, + progress, + } = fh_ref_mut.notice_of_cancellation_queue.pop_back().unwrap(); + assert_eq!(transaction_id, expected_id); + assert_eq!(condition_code, ConditionCode::CheckLimitReached); assert_eq!(progress, 0); fh_ref_mut.all_queues_empty(); } @@ -1960,9 +2020,9 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, filesize); - assert_eq!(cfdp_user.transaction_indication_call_count, 0); - assert_eq!(cfdp_user.eof_sent_call_count, 0); + let mut user = tb.create_user(0, filesize); + assert_eq!(user.transaction_indication_call_count, 0); + assert_eq!(user.eof_sent_call_count, 0); tb.put_request(&put_request) .expect("put_request call failed"); @@ -1971,7 +2031,7 @@ mod tests { assert!(tb.get_next_sent_pdu().is_none()); let id = tb.handler.transaction_id().unwrap(); tb.handler - .cancel_request(&mut cfdp_user, &id) + .cancel_request(&mut user, &id) .expect("transaction cancellation failed"); assert_eq!(tb.handler.state(), State::Idle); assert_eq!(tb.handler.step(), TransactionStep::Idle); @@ -2014,12 +2074,9 @@ mod tests { ) .expect("creating put request failed"); let file_size = rand_data.len() as u64; - let mut cfdp_user = tb.create_user(0, file_size); - let transaction_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transaction_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); let mut chunks = rand_data.chunks( calculate_max_file_seg_len_for_max_packet_len_and_pdu_header( &transaction_info.pdu_header, @@ -2038,7 +2095,7 @@ mod tests { let expected_id = tb.handler.transaction_id().unwrap(); assert!( tb.handler - .cancel_request(&mut cfdp_user, &expected_id) + .cancel_request(&mut user, &expected_id) .expect("cancellation failed") ); assert_eq!(tb.handler.state(), State::Idle); @@ -2077,18 +2134,10 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transfer_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 1, - ); + let mut user = tb.create_user(0, file_size); + let transfer_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 1); assert!(tb.pdu_queue_empty()); @@ -2096,19 +2145,19 @@ mod tests { tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 2, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 2); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::NoError, + transfer_info.id, + ); } #[test] @@ -2124,18 +2173,10 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transfer_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 1, - ); + let mut user = tb.create_user(0, file_size); + let transfer_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 1); assert!(tb.pdu_queue_empty()); @@ -2143,34 +2184,29 @@ mod tests { tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 2, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 2); // Enforce a postive ack timer expiry -> leads to a re-send of the EOF PDU. tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); eof_params.condition_code = ConditionCode::PositiveAckLimitReached; - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 3, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 3); // This boilerplate handling is still expected. In a real-life use-case I would expect // this to fail as well, leading to a transaction abandonment. This is tested separately. - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::NoError, + transfer_info.id, + ); } #[test] @@ -2186,18 +2222,10 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transfer_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 1, - ); + let mut user = tb.create_user(0, file_size); + let transfer_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 1); assert!(tb.pdu_queue_empty()); @@ -2205,37 +2233,31 @@ mod tests { tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 2, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 2); // Enforce a postive ack timer expiry -> positive ACK limit reached -> Cancel EOF sent. tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); eof_params.condition_code = ConditionCode::PositiveAckLimitReached; - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 3, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 3); // Cancellation fault should have been triggered. let fault_handler = tb.test_fault_handler_mut(); let fh_ref_mut = fault_handler.get_mut(); assert!(!fh_ref_mut.cancellation_queue_empty()); assert_eq!(fh_ref_mut.notice_of_cancellation_queue.len(), 1); - let (id, cond_code, progress) = fh_ref_mut.notice_of_cancellation_queue.pop_back().unwrap(); - assert_eq!(id, transfer_info.id); - assert_eq!(cond_code, ConditionCode::PositiveAckLimitReached); + let FaultInfo { + transaction_id, + condition_code, + progress, + } = fh_ref_mut.notice_of_cancellation_queue.pop_back().unwrap(); + assert_eq!(transaction_id, transfer_info.id); + assert_eq!(condition_code, ConditionCode::PositiveAckLimitReached); assert_eq!(progress, file_size); fh_ref_mut.all_queues_empty(); @@ -2243,22 +2265,17 @@ mod tests { tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 1); - tb.common_eof_pdu_check( - &mut cfdp_user, - transfer_info.closure_requested, - eof_params, - 4, - ); + tb.common_eof_pdu_check(&mut user, transfer_info.closure_requested, eof_params, 4); // Enforce a postive ack timer expiry -> positive ACK limit reached -> Transaction // abandoned tb.expiry_control.set_positive_ack_expired(); let sent_packets = tb .handler - .state_machine_no_packet(&mut cfdp_user) + .state_machine_no_packet(&mut user) .expect("source handler FSM failure"); assert_eq!(sent_packets, 0); // Abandonment fault should have been triggered. @@ -2266,9 +2283,13 @@ mod tests { let fh_ref_mut = fault_handler.get_mut(); assert!(!fh_ref_mut.abandoned_queue_empty()); assert_eq!(fh_ref_mut.abandoned_queue.len(), 1); - let (id, cond_code, progress) = fh_ref_mut.abandoned_queue.pop_back().unwrap(); - assert_eq!(id, transfer_info.id); - assert_eq!(cond_code, ConditionCode::PositiveAckLimitReached); + let FaultInfo { + transaction_id, + condition_code, + progress, + } = fh_ref_mut.abandoned_queue.pop_back().unwrap(); + assert_eq!(transaction_id, transfer_info.id); + assert_eq!(condition_code, ConditionCode::PositiveAckLimitReached); assert_eq!(progress, file_size); fh_ref_mut.all_queues_empty(); } @@ -2276,21 +2297,21 @@ mod tests { #[test] fn test_nak_for_whole_file() { let mut tb = SourceHandlerTestbench::new(TransmissionMode::Acknowledged, false, 512); - let mut cfdp_user = TestCfdpUser::default(); - let (data, transfer_info) = tb.common_tiny_file_transfer(&mut cfdp_user, true); + let mut user = TestCfdpUser::default(); + let (data, transfer_info) = tb.common_tiny_file_transfer(&mut user, true); let seg_reqs = &[(0, transfer_info.file_size as u32)]; - tb.nak_for_file_segments(&mut cfdp_user, &transfer_info, seg_reqs); + tb.nak_for_file_segments(&mut user, &transfer_info, seg_reqs); tb.check_next_file_pdu(0, data.as_bytes()); tb.all_fault_queues_empty(); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); } #[test] fn test_nak_for_file_segment() { - let mut cfdp_user = TestCfdpUser::default(); + let mut user = TestCfdpUser::default(); let mut tb = SourceHandlerTestbench::new(TransmissionMode::Acknowledged, false, 128); let mut file = OpenOptions::new() .write(true) @@ -2302,14 +2323,14 @@ mod tests { .expect("writing file content failed"); drop(file); let (transfer_info, fd_pdus) = - tb.generic_file_transfer(&mut cfdp_user, false, rand_data.to_vec()); + tb.generic_file_transfer(&mut user, false, rand_data.to_vec()); assert_eq!(fd_pdus, 2); - tb.nak_for_file_segments(&mut cfdp_user, &transfer_info, &[(0, 90)]); + tb.nak_for_file_segments(&mut user, &transfer_info, &[(0, 90)]); tb.check_next_file_pdu(0, &rand_data[0..90]); tb.all_fault_queues_empty(); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); } @@ -2325,21 +2346,18 @@ mod tests { Some(false), ) .expect("creating put request failed"); - let mut cfdp_user = tb.create_user(0, file_size); - let transfer_info = tb.common_file_transfer_init_with_metadata_check( - &mut cfdp_user, - put_request, - file_size, - ); + let mut user = tb.create_user(0, file_size); + let transfer_info = + tb.common_file_transfer_init_with_metadata_check(&mut user, put_request, file_size); tb.common_eof_pdu_check( - &mut cfdp_user, + &mut user, transfer_info.closure_requested, EofParams::new_success(file_size, CRC_32.digest().finalize()), 1, ); // NAK to cause re-transmission of metadata PDU. - let nak_pdu = NakPduCreator::new( + let nak_pdu = NakPduCreator::new_normal_file_size( transfer_info.pdu_header, 0, transfer_info.file_size as u32, @@ -2350,7 +2368,7 @@ mod tests { let packet_info = PduRawWithInfo::new(&nak_pdu_vec).unwrap(); let sent_packets = tb .handler - .state_machine(&mut cfdp_user, Some(&packet_info)) + .state_machine(&mut user, Some(&packet_info)) .unwrap(); assert_eq!(sent_packets, 1); let next_pdu = tb.get_next_sent_pdu().unwrap(); @@ -2358,8 +2376,13 @@ mod tests { tb.metadata_check(&next_pdu, file_size); tb.all_fault_queues_empty(); - tb.acknowledge_eof_pdu(&mut cfdp_user, &transfer_info); - tb.finish_handling(&mut cfdp_user, &transfer_info); + tb.acknowledge_eof_pdu(&mut user, &transfer_info); + tb.finish_handling(&mut user, &transfer_info); tb.common_finished_pdu_ack_check(); + user.verify_finished_indication_retained( + DeliveryCode::Complete, + ConditionCode::NoError, + transfer_info.id, + ); } } diff --git a/src/time.rs b/src/time.rs index 23d69d2..8a8f433 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,7 +1,7 @@ use core::fmt::Debug; /// Generic abstraction for a check/countdown timer. Should also be cheap to copy and clone. -pub trait CountdownProvider: Debug { +pub trait Countdown: Debug { fn has_expired(&self) -> bool; fn reset(&mut self); } diff --git a/tests/end-to-end.rs b/tests/end-to-end.rs index 2f7ba87..1d3afaa 100644 --- a/tests/end-to-end.rs +++ b/tests/end-to-end.rs @@ -12,10 +12,11 @@ use std::{ }; use cfdp::{ - EntityType, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, RemoteEntityConfig, - StdTimerCreator, TransactionId, UserFaultHookProvider, + EntityType, FaultInfo, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, + RemoteEntityConfig, StdTimerCreator, TransactionId, UserFaultHook, dest::DestinationHandler, filestore::NativeFilestore, + lost_segments::LostSegmentsList, request::{PutRequestOwned, StaticPutRequestCacher}, source::SourceHandler, user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams}, @@ -33,43 +34,21 @@ const FILE_DATA: &str = "Hello World!"; #[derive(Default)] pub struct ExampleFaultHandler {} -impl UserFaultHookProvider for ExampleFaultHandler { - fn notice_of_suspension_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - panic!( - "unexpected suspension of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); +impl UserFaultHook for ExampleFaultHandler { + fn notice_of_suspension_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected suspension, {:?}", fault_info); } - fn notice_of_cancellation_cb( - &mut self, - transaction_id: TransactionId, - cond: ConditionCode, - progress: u64, - ) { - panic!( - "unexpected cancellation of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn notice_of_cancellation_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected cancellation, {:?}", fault_info); } - fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { - panic!( - "unexpected abandonment of transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn abandoned_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected abandonment, {:?}", fault_info); } - fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { - panic!( - "ignoring unexpected error in transaction {:?}, condition code {:?}, progress {}", - transaction_id, cond, progress - ); + fn ignore_cb(&mut self, fault_info: FaultInfo) { + panic!("unexpected ignore, {:?}", fault_info); } } @@ -159,7 +138,7 @@ impl CfdpUser for ExampleCfdpUser { } } -fn end_to_end_test(with_closure: bool) { +fn end_to_end_test(transmission_mode: TransmissionMode, with_closure: bool) { // Simplified event handling using atomic signals. let stop_signal_source = Arc::new(AtomicBool::new(false)); let stop_signal_dest = stop_signal_source.clone(); @@ -194,7 +173,7 @@ fn end_to_end_test(with_closure: bool) { 1024, with_closure, false, - spacepackets::cfdp::TransmissionMode::Unacknowledged, + transmission_mode, ChecksumType::Crc32, ); let seq_count_provider = AtomicU16::default(); @@ -220,7 +199,7 @@ fn end_to_end_test(with_closure: bool) { 1024, true, false, - spacepackets::cfdp::TransmissionMode::Unacknowledged, + transmission_mode, ChecksumType::Crc32, ); let mut dest_handler = DestinationHandler::new( @@ -230,6 +209,7 @@ fn end_to_end_test(with_closure: bool) { NativeFilestore::default(), remote_cfg_of_source, StdTimerCreator::default(), + LostSegmentsList::default(), ); let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving, completion_signal_dest); @@ -237,7 +217,7 @@ fn end_to_end_test(with_closure: bool) { REMOTE_ID.into(), srcfile.to_str().expect("invaid path string"), destfile.to_str().expect("invaid path string"), - Some(TransmissionMode::Unacknowledged), + Some(transmission_mode), Some(with_closure), ) .expect("put request creation failed"); @@ -346,11 +326,16 @@ fn end_to_end_test(with_closure: bool) { } #[test] -fn end_to_end_test_no_closure() { - end_to_end_test(false); +fn end_to_end_unacknowledged_no_closure() { + end_to_end_test(TransmissionMode::Unacknowledged, false); } #[test] -fn end_to_end_test_with_closure() { - end_to_end_test(true); +fn end_to_end_unacknowledged_with_closure() { + end_to_end_test(TransmissionMode::Unacknowledged, true); +} + +#[test] +fn end_to_end_acknowledged() { + end_to_end_test(TransmissionMode::Acknowledged, true); }