12 Commits

Author SHA1 Message Date
muellerr c6df24b947 readme update 2025-08-13 18:18:52 +02:00
muellerr 732fac1d1c Merge pull request 'Dependency updates and doc improvements' (#7) from dep-update-docs-improvements into main
Reviewed-on: #7
2025-08-13 18:17:17 +02:00
muellerr 226a7494a0 Dependency updates and Doc improvements 2025-08-13 18:12:33 +02:00
muellerr e401d5f1ac Merge pull request 'prepare v0.2.0' (#6) from prep-v0.2.0 into main
Reviewed-on: #6
2024-11-26 10:57:25 +01:00
muellerr 1b7d128a10 prepare v0.2.0 2024-11-26 10:45:30 +01:00
muellerr fae27bdf93 Merge pull request 'test fixes' (#5) from smaller-fixes-for-tests into main
Reviewed-on: #5
2024-11-08 17:14:22 +01:00
muellerr d5e2162f52 test fixes 2024-11-08 17:13:01 +01:00
muellerr e3fff18cd2 Merge pull request 'bump dependencies' (#4) from bump-dependencies into main
Reviewed-on: #4
2024-11-08 17:01:59 +01:00
muellerr 5c579209f0 bump dependencies 2024-11-08 17:01:17 +01:00
muellerr 1103203e64 Merge pull request 'prepare first release' (#2) from prepare-v0.1.0 into main
Rust/cfdp/pipeline/head This commit looks good
Reviewed-on: #2
2024-09-11 10:41:11 +02:00
muellerr de8be20f36 prepare first release
Rust/cfdp/pipeline/head This commit looks good
2024-09-11 10:23:33 +02:00
muellerr b87ccde07d init commit
Rust/cfdp/pipeline/head This commit looks good
2024-09-10 18:52:37 +02:00
13 changed files with 515 additions and 258 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.75.0
- uses: dtolnay/rust-toolchain@1.81.0
- run: cargo check --release
cross-check:
+14
View File
@@ -7,3 +7,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
- Bumped `spacepackets` to v0.15
- Bumped `defmt` to v1
# [v0.2.0] 2024-11-26
- Bumped `thiserror` to v2
- Bumped `spacepackets` to v0.13
- The source and destination handlers can now be used without the `std` feature and only require
the `alloc` feature.
# [v0.1.0] 2024-09-11
Initial release
+13 -25
View File
@@ -1,8 +1,8 @@
[package]
name = "cfdp-rs"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
rust-version = "1.75.0"
rust-version = "1.81.0"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
description = "High level CCSDS File Delivery Protocol components"
homepage = "https://egit.irs.uni-stuttgart.de/rust/cfdp"
@@ -18,44 +18,32 @@ name = "cfdp"
[dependencies]
crc = "3"
smallvec = "1"
derive-new = "0.6"
[dependencies.thiserror]
version = "1"
optional = true
[dependencies.hashbrown]
version = "0.14"
optional = true
[dependencies.serde]
version = "1"
optional = true
[dependencies.spacepackets]
version = "0.12"
default-features = false
git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets"
branch = "main"
derive-new = ">=0.6, <=0.7"
hashbrown = { version = ">=0.14, <=0.15", optional = true }
spacepackets = { version = "0.15", default-features = false }
thiserror = { version = "2", default-features = false }
serde = { version = "1", optional = true }
defmt = { version = "1", optional = true }
[features]
default = ["std"]
std = [
"alloc",
"thiserror",
"thiserror/std",
"spacepackets/std"
]
alloc = [
"hashbrown",
"spacepackets/alloc"
]
serde = ["dep:serde", "spacepackets/serde"]
serde = ["dep:serde", "spacepackets/serde", "hashbrown/serde"]
defmt = ["dep:defmt", "spacepackets/defmt"]
[dev-dependencies]
tempfile = "3"
rand = "0.8"
rand = "0.9"
log = "0.4"
fern = "0.6"
fern = "0.7"
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
+9 -5
View File
@@ -13,7 +13,7 @@ The underlying base packet library used to generate the packets to be sent is th
# Features
`cfdp-rs` currently supports following features:
`cfdp-rs` currently supports following high-level features:
- Unacknowledged (class 1) file transfers for both source and destination side.
@@ -27,10 +27,14 @@ The following features have not been implemented yet. PRs or notifications for d
## Rust features
`cfdp-rs` supports various runtime environments and is also suitable for `no_std` environments.
It is recommended to activate the `alloc` feature at the very least to allow using the primary
components provided by this crate. These components will only allocate memory at initialization
time and thus are still viable for systems where run-time allocation is prohibited.
The goal of this library is to be flexible enough to support the use-cases of both on-board
software and of ground software. It has support to make integration on `std` systems as simple
as possible, but also has sufficient abstraction to allow for integration on`no_std` environments
and can be used on these systems as well as long as the `alloc` feature is activated.
Please note that even though the `alloc` feature is required for the core handlers, these
components will only allocate memory at initialization time and thus are still viable for systems
where run-time allocation is prohibited.
### Default features
+1 -1
View File
@@ -20,7 +20,7 @@ def generate_cov_report(open_report: bool, format: str):
out_path = "./target/debug/lcov.info"
os.system(
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
f"-o {out_path}"
f"--ignore \"examples/*\" -o {out_path}"
)
if format == "lcov":
os.system(
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
cargo +nightly doc --all-features --open
+37
View File
@@ -0,0 +1,37 @@
Python Interoperability Example for cfdp-rs
=======
This example application showcases the interoperability of the CFDP handlers written in Rust
with a Python implementation which uses [cfdp-py](https://github.com/us-irs/cfdp-py) library.
Both the Rust and the Python app exchange packet data units via a UDP interface and launch
both a destination and source handler. As such, they are both able to send and receive files.
Both applications can be started with the command line argument `-f` to initiate a file transfer.
You can run both applications with `-h` to get more information about the available options.
## 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
and activate a virtual environment.
After that, you can install the required dependencies using
```sh
pip install -r requirements.txt
```
and then run the application using `./main.py` or `python3 main.py`.
It is recommended to run `./main.py -h` first to get an overview of some possible options.
Running the Python App with `./main.py -f` will cause the Python App to start a file copy operation
with fixed temporary paths.
## Running the Rust App
You can run the Rust application using `cargo`, for example `cargo run --example python-interop`.
It is recommended to run `cargo run --example python-interop -- -h` to get an overview of some
possible launch options.
Running the Rust App with `cargo run --example python-interop -- -f` will cause the Rust app to
start a file copy operation with fixed temporary paths.
+263 -58
View File
@@ -55,7 +55,6 @@ use spacepackets::{
},
util::{UnsignedByteField, UnsignedEnum},
};
use thiserror::Error;
#[derive(Debug)]
struct FileProperties {
@@ -76,6 +75,7 @@ enum CompletionDisposition {
/// This enumeration models the different transaction steps of the destination entity handler.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TransactionStep {
Idle = 0,
TransactionStart = 1,
@@ -92,7 +92,8 @@ struct TransferState<Countdown: CountdownProvider> {
transaction_id: Option<TransactionId>,
metadata_params: MetadataGenericParams,
progress: u64,
file_size_eof: u64,
// TODO: Can we delete this for good?
// file_size_eof: u64,
metadata_only: bool,
condition_code: ConditionCode,
delivery_code: DeliveryCode,
@@ -110,7 +111,7 @@ impl<CheckTimer: CountdownProvider> Default for TransferState<CheckTimer> {
transaction_id: None,
metadata_params: Default::default(),
progress: Default::default(),
file_size_eof: Default::default(),
// file_size_eof: Default::default(),
metadata_only: false,
condition_code: ConditionCode::NoError,
delivery_code: DeliveryCode::Incomplete,
@@ -188,7 +189,7 @@ impl<CheckTimer: CountdownProvider> TransactionParams<CheckTimer> {
}
}
#[derive(Debug, Error)]
#[derive(Debug, thiserror::Error)]
pub enum DestError {
/// File directive expected, but none specified
#[error("expected file directive")]
@@ -214,6 +215,7 @@ pub enum DestError {
#[error("pdu error {0}")]
Pdu(#[from] PduError),
#[error("io error {0}")]
#[cfg(feature = "std")]
Io(#[from] std::io::Error),
#[error("file store error {0}")]
Filestore(#[from] FilestoreError),
@@ -297,19 +299,19 @@ impl<
///
/// * `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`.
/// 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.
/// * `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.
/// 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
/// entities and target specific configuration for file copy operations.
/// entities and target specific configuration for file copy operations.
/// * `check_timer_creator` - [TimerCreatorProvider] 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.
/// 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<UserFaultHook>,
max_packet_len: usize,
@@ -367,8 +369,30 @@ impl<
}
}
pub fn cancel_request(&mut self, transaction_id: &TransactionId) {
// TODO: Implement.
/// This function models the Cancel.request CFDP primitive and is the recommended way
/// to cancel a transaction. It will cause a Notice Of Cancellation at this entity.
/// Please note that the state machine might still be active because a cancelled transfer
/// might still require some packets to be sent to the remote sender entity.
///
/// If no unexpected errors occur, this function returns whether the current transfer
/// was cancelled. It returns [false] if the state machine is currently idle or if there
/// is a transaction ID missmatch.
pub fn cancel_request(&mut self, transaction_id: &TransactionId) -> bool {
if self.state() == super::State::Idle {
return false;
}
if let Some(active_id) = self.transaction_id() {
if active_id == *transaction_id {
self.trigger_notice_of_completion_cancelled(
ConditionCode::CancelRequestReceived,
EntityIdTlv::new(self.local_cfg.id),
);
self.step = TransactionStep::TransferCompletion;
return true;
}
}
false
}
/// Returns [None] if the state machine is IDLE, and the transmission mode of the current
@@ -549,11 +573,16 @@ impl<
} 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());
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();
self.tparams.tstate.delivery_code = DeliveryCode::Incomplete;
self.tparams.tstate.fault_location_finished =
Some(EntityIdTlv::new(self.tparams.remote_cfg.unwrap().entity_id));
if eof_pdu.file_size() > 0 {
self.tparams.tstate.delivery_code = DeliveryCode::Incomplete;
} else {
self.tparams.tstate.delivery_code = DeliveryCode::Complete;
}
// 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
@@ -566,9 +595,19 @@ impl<
Ok(())
}
fn trigger_notice_of_completion_cancelled(&mut self, cond_code: ConditionCode) {
fn trigger_notice_of_completion_cancelled(
&mut 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);
// 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;
}
}
/// Returns whether the transfer can be completed regularly.
@@ -819,10 +858,8 @@ impl<
|| self.tparams.metadata_params().closure_requested
{
sent_packets += self.send_finished_pdu()?;
self.step = TransactionStep::SendingFinishedPdu;
} else {
self.reset();
}
self.step = TransactionStep::SendingFinishedPdu;
Ok(sent_packets)
}
@@ -837,6 +874,7 @@ impl<
.disposition_on_cancellation
&& self.tstate().delivery_code == 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
@@ -846,7 +884,6 @@ impl<
if self.vfs.exists(dest_path)? && self.vfs.is_file(dest_path)? {
self.vfs.remove_file(dest_path)?;
}
// Safety: We already verified that the path is valid during the transaction start.
self.tstate_mut().file_status = FileStatus::DiscardDeliberately;
}
let tstate = self.tstate();
@@ -970,14 +1007,14 @@ mod tests {
pdu::{finished::FinishedPduReader, metadata::MetadataPduCreator, WritablePduPacket},
ChecksumType, TransmissionMode,
},
util::{UbfU16, UnsignedByteFieldU16},
util::{UbfU16, UnsignedByteFieldU8},
};
use crate::{
filestore::NativeFilestore,
tests::{
basic_remote_cfg_table, SentPdu, TestCfdpSender, TestCfdpUser, TestFaultHandler,
LOCAL_ID,
LOCAL_ID, REMOTE_ID,
},
CountdownProvider, FaultHandler, IndicationConfig, PduRawWithInfo,
StdRemoteEntityConfigProvider, TimerCreatorProvider, CRC_32,
@@ -985,8 +1022,6 @@ mod tests {
use super::*;
const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2);
#[derive(Debug)]
struct TestCheckTimer {
counter: Cell<u32>,
@@ -1054,23 +1089,24 @@ mod tests {
dest_path: PathBuf,
check_dest_file: bool,
check_handler_idle_at_drop: bool,
expected_file_size: u64,
closure_requested: bool,
pdu_header: PduHeader,
expected_full_data: Vec<u8>,
expected_file_size: u64,
buf: [u8; 512],
}
impl DestHandlerTestbench {
fn new(fault_handler: TestFaultHandler, closure_requested: bool) -> Self {
fn new_with_fixed_paths(fault_handler: TestFaultHandler, closure_requested: bool) -> Self {
let (src_path, dest_path) = init_full_filepaths_textfile();
assert!(!Path::exists(&dest_path));
Self::new_with_custom_paths(fault_handler, closure_requested, src_path, dest_path)
Self::new(fault_handler, closure_requested, true, src_path, dest_path)
}
fn new_with_custom_paths(
fn new(
fault_handler: TestFaultHandler,
closure_requested: bool,
check_dest_file: bool,
src_path: PathBuf,
dest_path: PathBuf,
) -> Self {
@@ -1084,7 +1120,7 @@ mod tests {
src_path,
closure_requested,
dest_path,
check_dest_file: false,
check_dest_file,
check_handler_idle_at_drop: true,
expected_file_size: 0,
pdu_header: create_pdu_header(UbfU16::new(0)),
@@ -1112,6 +1148,10 @@ mod tests {
&mut self.handler.local_cfg.indication_cfg
}
fn get_next_pdu(&mut self) -> Option<SentPdu> {
self.handler.pdu_sender.retrieve_next_pdu()
}
fn indication_cfg(&mut self) -> &IndicationConfig {
&self.handler.local_cfg.indication_cfg
}
@@ -1345,10 +1385,21 @@ mod tests {
assert_eq!(dest_handler.step(), TransactionStep::Idle);
}
#[test]
fn test_cancelling_idle_fsm() {
let fault_handler = TestFaultHandler::default();
let test_sender = TestCfdpSender::default();
let mut dest_handler = default_dest_handler(fault_handler, test_sender, Arc::default());
assert!(!dest_handler.cancel_request(&TransactionId::new(
UnsignedByteFieldU8::new(0).into(),
UnsignedByteFieldU8::new(0).into()
)));
}
#[test]
fn test_empty_file_transfer_not_acked_no_closure() {
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, false);
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)
.expect("transfer init failed");
@@ -1365,7 +1416,7 @@ mod tests {
let file_size = file_data.len() as u64;
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, false);
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)
.expect("transfer init failed");
@@ -1379,14 +1430,14 @@ mod tests {
#[test]
fn test_segmented_file_transfer_not_acked() {
let mut rng = rand::thread_rng();
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(fault_handler, false);
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)
.expect("transfer init failed");
@@ -1406,14 +1457,14 @@ mod tests {
#[test]
fn test_check_limit_handling_transfer_success() {
let mut rng = rand::thread_rng();
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(fault_handler, false);
let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false);
let mut test_user = tb.test_user_from_cached_paths(file_size);
let transaction_id = tb
.generic_transfer_init(&mut test_user, file_size)
@@ -1452,14 +1503,14 @@ mod tests {
#[test]
fn test_check_limit_handling_limit_reached() {
let mut rng = rand::thread_rng();
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 testbench = DestHandlerTestbench::new(fault_handler, false);
let mut testbench = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false);
let mut test_user = testbench.test_user_from_cached_paths(file_size);
let transaction_id = testbench
.generic_transfer_init(&mut test_user, file_size)
@@ -1538,7 +1589,7 @@ mod tests {
#[test]
fn test_file_transfer_with_closure() {
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, true);
let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true);
let mut test_user = tb.test_user_from_cached_paths(0);
tb.generic_transfer_init(&mut test_user, 0)
.expect("transfer init failed");
@@ -1556,15 +1607,11 @@ mod tests {
tb.check_completion_indication_success(&mut test_user);
}
#[test]
fn test_file_transfer_with_closure_check_limit_reached() {
// TODO: Implement test.
}
#[test]
fn test_finished_pdu_insertion_rejected() {
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, false);
let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, 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),
@@ -1589,7 +1636,7 @@ mod tests {
#[test]
fn test_metadata_insertion_twice_fails() {
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, true);
let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true);
let mut user = tb.test_user_from_cached_paths(0);
tb.generic_transfer_init(&mut user, 0)
.expect("transfer init failed");
@@ -1618,7 +1665,7 @@ 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(fault_handler, true);
let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true);
let mut user = tb.test_user_from_cached_paths(file_size);
tb.generic_transfer_init(&mut user, file_size)
.expect("transfer init failed");
@@ -1706,10 +1753,11 @@ mod tests {
let fault_handler = TestFaultHandler::default();
let src_path = tempfile::TempPath::from_path("/tmp/test.txt").to_path_buf();
let dest_path = tempfile::TempDir::new().unwrap();
let mut dest_path_buf = dest_path.into_path();
let mut tb = DestHandlerTestbench::new_with_custom_paths(
let mut dest_path_buf = dest_path.keep();
let mut tb = DestHandlerTestbench::new(
fault_handler,
false,
false,
src_path.clone(),
dest_path_buf.clone(),
);
@@ -1727,7 +1775,7 @@ mod tests {
#[test]
fn test_tranfer_cancellation_empty_file_with_eof_pdu() {
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, false);
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)
.expect("transfer init failed");
@@ -1749,13 +1797,13 @@ mod tests {
assert_eq!(packets, 0);
}
fn generic_tranfer_cancellation_partial_file_with_eof_pdu_no_closure(with_closure: bool) {
fn generic_tranfer_cancellation_partial_file_with_eof_pdu(with_closure: bool) {
let file_data_str = "Hello World!";
let file_data = file_data_str.as_bytes();
let file_size = file_data.len() as u64;
let file_size = 5;
let fault_handler = TestFaultHandler::default();
let mut tb = DestHandlerTestbench::new(fault_handler, with_closure);
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)
.expect("transfer init failed");
@@ -1805,14 +1853,171 @@ mod tests {
} else {
assert_eq!(packets, 0);
}
tb.expected_file_size = file_size;
tb.expected_full_data = file_data[0..file_size as usize].to_vec();
}
#[test]
fn test_tranfer_cancellation_partial_file_with_eof_pdu_no_closure() {
generic_tranfer_cancellation_partial_file_with_eof_pdu_no_closure(false);
generic_tranfer_cancellation_partial_file_with_eof_pdu(false);
}
#[test]
fn test_tranfer_cancellation_partial_file_with_eof_pdu_with_closure() {
generic_tranfer_cancellation_partial_file_with_eof_pdu_no_closure(true);
generic_tranfer_cancellation_partial_file_with_eof_pdu(true);
}
#[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)
.expect("transfer init failed");
tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus);
tb.handler.cancel_request(&transaction_id);
let packets = tb
.handler
.state_machine_no_packet(&mut test_user)
.expect("state machine call with EOF insertion failed");
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)
.expect("transfer init failed");
tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus);
tb.handler.cancel_request(&transaction_id);
let packets = tb
.handler
.state_machine_no_packet(&mut test_user)
.expect("state machine call with EOF insertion failed");
assert_eq!(packets, 1);
let next_pdu = tb.get_next_pdu().unwrap();
assert_eq!(next_pdu.pdu_type, PduType::FileDirective);
assert_eq!(
next_pdu.file_directive_type.unwrap(),
FileDirectiveType::FinishedPdu
);
let finished_pdu =
FinishedPduReader::new(&next_pdu.raw_pdu).expect("finished pdu read failed");
assert_eq!(
finished_pdu.condition_code(),
ConditionCode::CancelRequestReceived
);
assert_eq!(finished_pdu.file_status(), FileStatus::Retained);
// Empty file, so still complete.
assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete);
assert_eq!(
finished_pdu.fault_location(),
Some(EntityIdTlv::new(REMOTE_ID.into()))
);
}
#[test]
fn test_tranfer_cancellation_partial_file_with_cancel_api_and_closure() {
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, 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)
.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");
tb.handler.cancel_request(&transaction_id);
let packets = tb
.handler
.state_machine_no_packet(&mut test_user)
.expect("state machine call with EOF insertion failed");
assert_eq!(packets, 1);
let next_pdu = tb.get_next_pdu().unwrap();
assert_eq!(next_pdu.pdu_type, PduType::FileDirective);
assert_eq!(
next_pdu.file_directive_type.unwrap(),
FileDirectiveType::FinishedPdu
);
let finished_pdu =
FinishedPduReader::new(&next_pdu.raw_pdu).expect("finished pdu read failed");
assert_eq!(
finished_pdu.condition_code(),
ConditionCode::CancelRequestReceived
);
assert_eq!(finished_pdu.file_status(), FileStatus::Retained);
assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete);
assert_eq!(
finished_pdu.fault_location(),
Some(EntityIdTlv::new(REMOTE_ID.into()))
);
tb.expected_file_size = file_size;
tb.expected_full_data = file_data[0..file_size as usize].to_vec();
}
// 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 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)
.expect("transfer init failed");
tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus);
tb.handler.cancel_request(&transaction_id);
let packets = tb
.handler
.state_machine_no_packet(&mut test_user)
.expect("state machine call with EOF insertion failed");
assert_eq!(packets, 0);
}
#[test]
fn test_tranfer_cancellation_file_disposition_not_done_for_incomplete_file() {
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, false);
tb.check_dest_file = 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(file_size);
let transaction_id = tb
.generic_transfer_init(&mut test_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");
tb.handler.cancel_request(&transaction_id);
let packets = tb
.handler
.state_machine_no_packet(&mut test_user)
.expect("state machine call with EOF insertion failed");
assert_eq!(packets, 0);
// File was disposed.
assert!(!Path::exists(tb.dest_path()));
}
}
+33 -95
View File
@@ -1,101 +1,37 @@
use alloc::string::{String, ToString};
use core::fmt::Display;
use spacepackets::cfdp::ChecksumType;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
use std::path::Path;
#[cfg(feature = "std")]
pub use std_mod::*;
#[derive(Debug, Clone)]
#[derive(Debug, thiserror::Error)]
#[cfg_attr(all(feature = "defmt", not(feature = "std")), derive(defmt::Format))]
#[non_exhaustive]
pub enum FilestoreError {
#[error("file does not exist")]
FileDoesNotExist,
#[error("file already exists")]
FileAlreadyExists,
#[error("directory does not exist")]
DirDoesNotExist,
#[error("permission error")]
Permission,
#[error("is not a file")]
IsNotFile,
#[error("is not a directory")]
IsNotDirectory,
ByteConversion(ByteConversionError),
Io {
raw_errno: Option<i32>,
string: String,
},
#[error("byte conversion: {0}")]
ByteConversion(#[from] ByteConversionError),
#[error("IO error: {0})")]
#[cfg(feature = "std")]
Io(#[from] std::io::Error),
#[error("checksum type not implemented: {0:?}")]
ChecksumTypeNotImplemented(ChecksumType),
#[error("utf8 error")]
Utf8Error,
#[error("other error")]
Other,
}
impl From<ByteConversionError> for FilestoreError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversion(value)
}
}
impl Display for FilestoreError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FilestoreError::FileDoesNotExist => {
write!(f, "file does not exist")
}
FilestoreError::FileAlreadyExists => {
write!(f, "file already exists")
}
FilestoreError::DirDoesNotExist => {
write!(f, "directory does not exist")
}
FilestoreError::Permission => {
write!(f, "permission error")
}
FilestoreError::IsNotFile => {
write!(f, "is not a file")
}
FilestoreError::IsNotDirectory => {
write!(f, "is not a directory")
}
FilestoreError::ByteConversion(e) => {
write!(f, "filestore error: {e}")
}
FilestoreError::Io { raw_errno, string } => {
write!(
f,
"filestore generic IO error with raw errno {:?}: {}",
raw_errno, string
)
}
FilestoreError::ChecksumTypeNotImplemented(checksum_type) => {
write!(f, "checksum {:?} not implemented", checksum_type)
}
FilestoreError::Utf8Error => {
write!(f, "utf8 error")
}
FilestoreError::Other => {
write!(f, "some filestore error occured")
}
}
}
}
impl Error for FilestoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
FilestoreError::ByteConversion(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for FilestoreError {
fn from(value: std::io::Error) -> Self {
Self::Io {
raw_errno: value.raw_os_error(),
string: value.to_string(),
}
}
}
pub trait VirtualFilestore {
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError>;
@@ -120,14 +56,7 @@ pub trait VirtualFilestore {
fn filename_from_full_path(path: &str) -> Option<&str>
where
Self: Sized,
{
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
Self: Sized;
fn is_file(&self, path: &str) -> Result<bool, FilestoreError>;
@@ -192,6 +121,7 @@ pub mod std_mod {
use std::{
fs::{self, File, OpenOptions},
io::{BufReader, Read, Seek, SeekFrom, Write},
path::Path,
};
#[derive(Default)]
@@ -239,10 +169,7 @@ pub mod std_mod {
}
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError> {
fs::create_dir(dir_path).map_err(|e| FilestoreError::Io {
raw_errno: e.raw_os_error(),
string: e.to_string(),
})?;
fs::create_dir(dir_path)?;
Ok(())
}
@@ -356,6 +283,17 @@ pub mod std_mod {
_ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)),
}
}
fn filename_from_full_path(path: &str) -> Option<&str>
where
Self: Sized,
{
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
}
impl NativeFilestore {
@@ -391,7 +329,7 @@ pub mod std_mod {
#[cfg(test)]
mod tests {
use std::{fs, path::Path, println};
use std::{fs, path::Path, println, string::ToString};
use super::*;
use alloc::format;
@@ -704,7 +642,7 @@ mod tests {
}
assert_eq!(
error.to_string(),
format!("filestore error: {}", byte_conv_error)
format!("byte conversion: {}", byte_conv_error)
);
} else {
panic!("unexpected error");
@@ -827,7 +765,7 @@ mod tests {
if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error {
assert_eq!(
error.to_string(),
format!("checksum {:?} not implemented", cksum_type)
format!("checksum type not implemented: {:?}", cksum_type)
);
} else {
panic!("unexpected error");
+105 -27
View File
@@ -1,20 +1,87 @@
//! This module contains the implementation of the CCSDS File Delivery Protocol (CFDP) high level
//! abstractions as specified in CCSDS 727.0-B-5.
//! # `cfdp` - High-level support for implementing CFDP
//!
//! This is a high-level support library for implementing the CCSDS File Delivery Protocol
//! according to the CCSDS 727.0-B-5 standard.
//!
//! # Features
//!
//! The crate currently supports following features:
//!
//! - Unacknowledged (class 1) file transfers for both source and destination side.
//!
//! The following features have not been implemented yet. PRs or notifications for demand are welcome!
//!
//! - Acknowledged (class 2) file transfers for both source and destination side.
//! - Suspending transfers
//! - Inactivity handling
//! - Start and end of transmission and reception opportunity handling
//! - Keep Alive and Prompt PDU handling
//!
//! # Overview
//!
//! The basic idea of CFDP is to convert files of any size into a stream of packets called packet
//! data units (PDU). CFPD has an unacknowledged and acknowledged mode, with the option to request
//! a transaction closure for the unacknowledged mode. Using the unacknowledged mode with no
//! transaction closure is applicable for simplex communication paths, while the unacknowledged
//! mode with closure is the easiest way to get a confirmation of a successful file transfer,
//! including a CRC check on the remote side to verify file int egrity. The acknowledged mode is
//! including a CRC check on the remote side to verify file integrity. The acknowledged mode is
//! the most complex mode which includes multiple mechanism to ensure succesfull packet transaction
//! even for unreliable connections, including lost segment detection. As such, it can be compared
//! to a specialized TCP for file transfers with remote systems.
//!
//! The core of these high-level components are the [crate::dest::DestinationHandler] and the
//! [crate::source::SourceHandler] component. These model the CFDP destination and source entity
//! The goal of this library is to be flexible enough to support the use-cases of both on-board
//! software and of ground software. It has support to make integration on [std] systems as simple
//! as possible, but also has sufficient abstraction to allow for integration on `no_std`
//! environments and can be used on these systems as well as long as the [alloc] feature is used
//! as well.
//!
//! Please note even though the [alloc] feature is required for the core handlers, these components
//! will only allocate memory at initialization time and thus are still viable for systems where
//! run-time allocation is prohibited.
//!
//! The core of this library are the [crate::dest::DestinationHandler] and the
//! [crate::source::SourceHandler] components which model the CFDP destination and source entity
//! respectively. You can find high-level and API documentation for both handlers in the respective
//! [crate::dest] and [crate::source] module.
//!
//! # Examples
//!
//! This library currently features two example application which showcase how the provided
//! components could be used to provide CFDP services.
//!
//! The [end-to-end test](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/tests/end-to-end.rs)
//! is an integration tests which spawns a CFDP source entity and a CFDP destination entity,
//! moves them to separate threads and then performs a small file copy operation.
//! You can run the integration test for a transfer with no closure and with printout to the
//! standard console by running:
//!
//! ```sh
//! cargo test end_to_end_test_no_closure -- --nocapture
//! ```
//!
//! or with closure:
//!
//! ```sh
//! cargo test end_to_end_test_with_closure -- --nocapture
//! ```
//!
//! The [Python Interoperability](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/examples/python-interop)
//! example showcases the interoperability of the CFDP handlers written in Rust with a Python
//! implementation. The dedicated example documentation shows how to run this example.
//!
//! # Notes on the user hooks and scheduling
//!
//! Both examples feature implementations of the [UserFaultHookProvider] 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.
//!
//! Even though both examples move the newly spawned handlers to dedicated threads, this is not
//! the only way they could be scheduled. For example, to support an arbitrary (or bounded)
//! amount of file copy operations on either source or destination side, those handlers could be
//! moved into a [std::collections::HashMap] structure which is then scheduled inside a thread, or
//! you could schedule a fixed amount of handlers inside a
//! [threadpool](https://docs.rs/threadpool/latest/threadpool/).
#![no_std]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "alloc")]
@@ -22,12 +89,11 @@ extern crate alloc;
#[cfg(any(feature = "std", test))]
extern crate std;
#[cfg(feature = "std")]
pub mod dest;
#[cfg(feature = "alloc")]
pub mod dest;
pub mod filestore;
pub mod request;
#[cfg(feature = "std")]
#[cfg(feature = "alloc")]
pub mod source;
pub mod time;
pub mod user;
@@ -35,8 +101,6 @@ pub mod user;
use crate::time::CountdownProvider;
use core::{cell::RefCell, fmt::Debug, hash::Hash};
use crc::{Crc, CRC_32_ISCSI, CRC_32_ISO_HDLC};
#[cfg(feature = "std")]
use hashbrown::HashMap;
#[cfg(feature = "alloc")]
pub use alloc_mod::*;
@@ -54,11 +118,16 @@ use spacepackets::{
pub use std_mod::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum EntityType {
Sending,
Receiving,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TimerContext {
CheckLimit {
local_id: UnsignedByteField,
@@ -118,7 +187,7 @@ pub trait TimerCreatorProvider {
/// This structure models the remote entity configuration information as specified in chapter 8.3
/// of the CFDP standard.
///
/// Some of the fields which were not considered necessary for the Rust implementation
/// were omitted. Some other fields which are not contained inside the standard but are considered
/// necessary for the Rust implementation are included.
@@ -144,16 +213,16 @@ pub trait TimerCreatorProvider {
///
/// * `entity_id` - The ID of the remote entity.
/// * `max_packet_len` - This determines of all PDUs generated for that remote entity in addition
/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs.
/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs.
/// * `max_file_segment_len` The maximum file segment length which determines the maximum size
/// of file data PDUs in addition to the `max_packet_len` attribute. If this field is set
/// to None, the maximum file segment length will be derived from the maximum packet length.
/// If this has some value which is smaller than the segment value derived from
/// `max_packet_len`, this value will be picked.
/// * `closure_requested_by_default` - If the closure requested field is not supplied as part of
/// the Put Request, it will be determined from this field in the remote configuration.
/// the Put Request, it will be determined from this field in the remote configuration.
/// * `crc_on_transmission_by_default` - If the CRC option is not supplied as part of the Put
/// Request, it will be determined from this field in the remote configuration.
/// Request, it will be determined from this field in the remote configuration.
/// * `default_transmission_mode` - If the transmission mode is not supplied as part of the
/// Put Request, it will be determined from this field in the remote configuration.
/// * `disposition_on_cancellation` - Determines whether an incomplete received file is discard on
@@ -165,17 +234,19 @@ pub trait TimerCreatorProvider {
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to
/// 2, so the check limit timer may expire twice.
/// * `positive_ack_timer_interval_seconds`- See the notes on the Positive Acknowledgment
/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to
/// 10 seconds.
/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to
/// 10 seconds.
/// * `positive_ack_timer_expiration_limit` - See the notes on the Positive Acknowledgment
/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice.
/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice.
/// * `immediate_nak_mode` - Specifies whether a NAK sequence should be issued immediately when a
/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True.
/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True.
/// * `nak_timer_interval_seconds` - See the notes on the Deferred Lost Segment Procedure inside
/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds.
/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds.
/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside
/// the class documentation. Defaults to 2, so the timer may expire two times.
/// the class documentation. Defaults to 2, so the timer may expire two times.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RemoteEntityConfig {
pub entity_id: UnsignedByteField,
pub max_packet_len: usize,
@@ -233,11 +304,12 @@ pub trait RemoteEntityConfigProvider {
fn remove_config(&mut self, remote_id: u64) -> bool;
}
/// This is a thin wrapper around a [HashMap] to store remote entity configurations.
/// This is a thin wrapper around a [hashbrown::HashMap] to store remote entity configurations.
/// It implements the full [RemoteEntityConfigProvider] trait.
#[cfg(feature = "std")]
#[derive(Default)]
pub struct StdRemoteEntityConfigProvider(pub HashMap<u64, RemoteEntityConfig>);
#[cfg(feature = "alloc")]
#[derive(Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StdRemoteEntityConfigProvider(pub hashbrown::HashMap<u64, RemoteEntityConfig>);
#[cfg(feature = "std")]
impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
@@ -258,7 +330,9 @@ impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
/// This is a thin wrapper around a [alloc::vec::Vec] to store remote entity configurations.
/// It implements the full [RemoteEntityConfigProvider] trait.
#[cfg(feature = "alloc")]
#[derive(Default)]
#[derive(Default, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct VecRemoteEntityConfigProvider(pub alloc::vec::Vec<RemoteEntityConfig>);
#[cfg(feature = "alloc")]
@@ -488,6 +562,9 @@ impl<UserHandler: UserFaultHookProvider> FaultHandler<UserHandler> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IndicationConfig {
pub eof_sent: bool,
pub eof_recv: bool,
@@ -659,6 +736,7 @@ pub mod std_mod {
/// number of that transfer which is also determined by the CFDP source entity.
#[derive(Debug, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TransactionId {
source_id: UnsignedByteField,
seq_num: UnsignedByteField,
@@ -817,7 +895,7 @@ impl<'raw> PduRawWithInfo<'raw> {
});
}
if pdu_header.pdu_datafield_len() < 1 {
return Err(PduError::FormatError);
return Err(PduError::Format);
}
// Route depending on PDU type and directive type if applicable. Retrieve directive type
// from the raw stream for better performance (with sanity and directive code check).
@@ -1486,7 +1564,7 @@ pub(crate) mod tests {
#[test]
fn transaction_id_hashable_usable_as_map_key() {
let mut map = HashMap::new();
let mut map = hashbrown::HashMap::new();
let transaction_id_0 = TransactionId::new(
UnsignedByteFieldU8::new(1).into(),
UnsignedByteFieldU8::new(2).into(),
+17 -12
View File
@@ -10,6 +10,8 @@ use spacepackets::{
pub use alloc_mod::*;
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FilePathTooLarge(pub usize);
/// This trait is an abstraction for different Put Request structures which can be used
@@ -22,10 +24,10 @@ pub trait ReadablePutRequest {
fn closure_requested(&self) -> Option<bool>;
fn seg_ctrl(&self) -> Option<SegmentationControl>;
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv>>;
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv>>;
fn flow_label(&self) -> Option<Tlv>;
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv>>;
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv<'_>>>;
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv<'_>>>;
fn flow_label(&self) -> Option<Tlv<'_>>;
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv<'_>>>;
}
#[derive(Debug, PartialEq, Eq)]
@@ -99,25 +101,25 @@ impl ReadablePutRequest for PutRequest<'_, '_, '_, '_, '_, '_> {
self.seg_ctrl
}
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv>> {
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(msgs_to_user) = self.msgs_to_user {
return Some(msgs_to_user.iter().copied());
}
None
}
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv>> {
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(fh_overrides) = self.fault_handler_overrides {
return Some(fh_overrides.iter().copied());
}
None
}
fn flow_label(&self) -> Option<Tlv> {
fn flow_label(&self) -> Option<Tlv<'_>> {
self.flow_label
}
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv>> {
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(fs_requests) = self.msgs_to_user {
return Some(fs_requests.iter().copied());
}
@@ -167,6 +169,8 @@ impl<'src_file, 'dest_file> PutRequest<'src_file, 'dest_file, 'static, 'static,
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TlvWithInvalidType(pub(crate) ());
impl<'msgs_to_user> PutRequest<'static, 'static, 'msgs_to_user, 'static, 'static, 'static> {
@@ -236,6 +240,7 @@ pub mod alloc_mod {
/// Owned variant of [PutRequest] with no lifetimes which is also [Clone]able.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PutRequestOwned {
pub destination_id: UnsignedByteField,
source_file: Option<alloc::string::String>,
@@ -365,25 +370,25 @@ pub mod alloc_mod {
self.seg_ctrl
}
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv>> {
fn msgs_to_user(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(msgs_to_user) = &self.msgs_to_user {
return Some(msgs_to_user.iter().map(|tlv_owned| tlv_owned.as_tlv()));
}
None
}
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv>> {
fn fault_handler_overrides(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(fh_overrides) = &self.fault_handler_overrides {
return Some(fh_overrides.iter().map(|tlv_owned| tlv_owned.as_tlv()));
}
None
}
fn flow_label(&self) -> Option<Tlv> {
fn flow_label(&self) -> Option<Tlv<'_>> {
self.flow_label.as_ref().map(|tlv| tlv.as_tlv())
}
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv>> {
fn fs_requests(&self) -> Option<impl Iterator<Item = Tlv<'_>>> {
if let Some(requests) = &self.fs_requests {
return Some(requests.iter().map(|tlv_owned| tlv_owned.as_tlv()));
}
+17 -34
View File
@@ -76,6 +76,7 @@ use super::{
/// This enumeration models the different transaction steps of the source entity handler.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum TransactionStep {
Idle = 0,
TransactionStart = 1,
@@ -259,21 +260,21 @@ impl<
/// * `cfg` - The local entity configuration for this source handler.
/// * `pdu_sender` - [PduSendProvider] provider 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.
/// implementation from the underlying filestore/filesystem. This allows to use this handler
/// for embedded systems where a standard runtime might not be available.
/// * `put_request_cacher` - The put request cacher is used cache put requests without
/// requiring run-time allocation.
/// requiring run-time allocation.
/// * `pdu_and_cksum_buf_size` - The handler requires a buffer to generate PDUs and perform
/// 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.
/// 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
/// entities and target specific configuration for file copy operations.
/// entities and target specific configuration for file copy operations.
/// * `timer_creator` - [TimerCreatorProvider] 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.
/// 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 [SequenceCountProvider] used to generate the [TransactionId]
/// which contains an incrementing counter.
/// which contains an incrementing counter.
#[allow(clippy::too_many_arguments)]
pub fn new(
cfg: LocalEntityConfig<UserFaultHook>,
@@ -385,7 +386,7 @@ impl<
/// This function is used to pass a put request to the source handler, which is
/// also used to start a file copy operation. As such, this function models the Put.request
/// CFDP primtiive.
///
/// Please note that the source handler can also process one put request at a time.
/// The caller is responsible of creating a new source handler, one handler can only handle
/// one file copy request at a time.
@@ -504,7 +505,8 @@ impl<
}
if let Some(active_id) = self.transaction_id() {
if active_id == *transaction_id {
self.notice_of_cancellation(user, ConditionCode::CancelRequestReceived)?;
// Control flow result can be ignored here for the cancel request.
let _ = self.notice_of_cancellation(user, ConditionCode::CancelRequestReceived)?;
return Ok(true);
}
}
@@ -747,25 +749,6 @@ impl<
}
fn notice_of_completion(&mut self, cfdp_user: &mut impl CfdpUser) {
/*
def _notice_of_completion(self):
if self.cfg.indication_cfg.transaction_finished_indication_required:
assert self._params.transaction_id is not None
# This happens for unacknowledged file copy operation with no closure.
if self._params.finished_params is None:
self._params.finished_params = FinishedParams(
condition_code=ConditionCode.NO_ERROR,
delivery_code=DeliveryCode.DATA_COMPLETE,
file_status=FileStatus.FILE_STATUS_UNREPORTED,
)
indication_params = TransactionFinishedParams(
transaction_id=self._params.transaction_id,
finished_params=self._params.finished_params,
)
self.user.transaction_finished_indication(indication_params)
# Transaction finished
self.reset()
*/
let tstate = self.tstate.as_ref().unwrap();
if self.local_cfg.indication_cfg.transaction_finished {
// The first case happens for unacknowledged file copy operation with no closure.
@@ -1551,7 +1534,7 @@ mod tests {
.open(&tb.srcfile)
.expect("opening file failed");
let mut rand_data = [0u8; 140];
rand::thread_rng().fill(&mut rand_data[..]);
rand::rng().fill(&mut rand_data[..]);
file.write_all(&rand_data)
.expect("writing file content failed");
drop(file);
@@ -1570,7 +1553,7 @@ mod tests {
.open(&tb.srcfile)
.expect("opening file failed");
let mut rand_data = [0u8; 140];
rand::thread_rng().fill(&mut rand_data[..]);
rand::rng().fill(&mut rand_data[..]);
file.write_all(&rand_data)
.expect("writing file content failed");
drop(file);
@@ -1766,7 +1749,7 @@ mod tests {
.open(&tb.srcfile)
.expect("opening file failed");
let mut rand_data = [0u8; 140];
rand::thread_rng().fill(&mut rand_data[..]);
rand::rng().fill(&mut rand_data[..]);
file.write_all(&rand_data)
.expect("writing file content failed");
drop(file);
+2
View File
@@ -15,6 +15,8 @@ use spacepackets::{
use super::TransactionId;
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransactionFinishedParams {
pub id: TransactionId,
pub condition_code: ConditionCode,