From c732eda986314f1a714679a56b3595bdc62890e4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 20 Aug 2024 11:50:13 +0200 Subject: [PATCH] init commit --- .github/workflows/ci.yml | 72 ++ .gitignore | 7 + CHANGELOG.md | 9 + Cargo.toml | 60 ++ LICENSE-APACHE | 201 +++++ NOTICE | 1 + README.md | 49 ++ automation/Dockerfile | 28 + automation/Jenkinsfile | 81 ++ coverage.py | 54 ++ release-checklist.md | 25 + src/dest.rs | 1591 ++++++++++++++++++++++++++++++++++++++ src/filestore.rs | 802 +++++++++++++++++++ src/lib.rs | 1450 ++++++++++++++++++++++++++++++++++ src/request.rs | 777 +++++++++++++++++++ src/source.rs | 1329 +++++++++++++++++++++++++++++++ src/time.rs | 7 + src/user.rs | 98 +++ tests/end-to-end.rs | 352 +++++++++ 19 files changed, 6993 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 automation/Dockerfile create mode 100644 automation/Jenkinsfile create mode 100755 coverage.py create mode 100644 release-checklist.md create mode 100644 src/dest.rs create mode 100644 src/filestore.rs create mode 100644 src/lib.rs create mode 100644 src/request.rs create mode 100644 src/source.rs create mode 100644 src/time.rs create mode 100644 src/user.rs create mode 100644 tests/end-to-end.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c1fe60c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,72 @@ +name: ci +on: [push, pull_request] + +jobs: + check: + name: Check build + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo check --release + + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install nextest + uses: taiki-e/install-action@nextest + - run: cargo nextest run --all-features + - run: cargo test --doc + + msrv: + name: Check MSRV + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.75.0 + - run: cargo check --release + + cross-check: + name: Check Cross-Compilation + runs-on: ubuntu-latest + strategy: + matrix: + target: + - armv7-unknown-linux-gnueabihf + - thumbv7em-none-eabihf + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: "armv7-unknown-linux-gnueabihf, thumbv7em-none-eabihf" + - run: cargo check --release --target=${{matrix.target}} --no-default-features + + fmt: + name: Check formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo fmt --all -- --check + + docs: + name: Check Documentation Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + - run: RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo clippy -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e74cf0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Rust +/target +/Cargo.lock + +# CLion +/.idea/* +!/.idea/runConfigurations diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..68e54a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +Change Log +======= + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +# [unreleased] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a38733b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "cfdp-rs" +version = "0.1.0" +edition = "2021" +rust-version = "1.75.0" +authors = ["Robin Mueller "] +description = "High level CCSDS File Delivery Protocol components" +homepage = "https://egit.irs.uni-stuttgart.de/rust/cfdp" +repository = "https://egit.irs.uni-stuttgart.de/rust/cfdp" +license = "Apache-2.0" +keywords = ["no-std", "space", "packets", "ccsds", "ecss"] +categories = ["aerospace", "aerospace::space-protocols", "no-std", "filesystem"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +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" + +[features] +default = ["std"] +std = [ + "alloc", + "thiserror", + "spacepackets/std" +] +alloc = [ + "hashbrown", + "spacepackets/alloc" +] +serde = ["dep:serde", "spacepackets/serde"] + +[dev-dependencies] +tempfile = "3" +rand = "0.8" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--generate-link-to-definition"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..717a583 --- /dev/null +++ b/NOTICE @@ -0,0 +1 @@ +This software contains code developed at the University of Stuttgart's Institute of Space Systems. diff --git a/README.md b/README.md new file mode 100644 index 0000000..716c669 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +[![Crates.io](https://img.shields.io/crates/v/cfdp-rs)](https://crates.io/crates/cfdp-rs) +[![docs.rs](https://img.shields.io/docsrs/cfdp-rs)](https://docs.rs/cfdp-rs) +[![ci](https://github.com/us-irs/cfdp-rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/us-irs/cfdp-rs/actions/workflows/ci.yml) +[![coverage](https://shields.io/endpoint?url=https://absatsw.irs.uni-stuttgart.de/projects/cfdp/coverage-rs/latest/coverage.json)](https://absatsw.irs.uni-stuttgart.de/projects/cfdp/coverage-rs/latest/index.html) + +cfdp-rs - High level Rust crate for CFDP components +====================== + +The `cfdp-rs` Rust crate offers some high-level CCSDS File Delivery Protocol (CFDP) components to +perform file transfers according to the [CCSDS Blue Book 727.0-B-5](https://public.ccsds.org/Pubs/727x0b5.pdf). +The underlying base packet library used to generate the packets to be sent is the +[spacepackets](https://egit.irs.uni-stuttgart.de/rust/spacepackets) library. + +# 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. + +## Default features + + - [`std`](https://doc.rust-lang.org/std/): Enables functionality relying on the standard library. + - [`alloc`](https://doc.rust-lang.org/alloc/): Enables features which require allocation support. + Enabled by the `std` feature. + +## Optional Features + + - [`serde`](https://serde.rs/): Adds `serde` support for most types by adding `Serialize` and `Deserialize` `derive`s + - [`defmt`](https://defmt.ferrous-systems.com/): Add support for the `defmt` by adding the + [`defmt::Format`](https://defmt.ferrous-systems.com/format) derive on many types. + +# Examples + +You can check the [documentation](https://docs.rs/cfdp-rs) of individual modules for various usage +examples. + +# Coverage + +Coverage was generated using [`grcov`](https://github.com/mozilla/grcov). If you have not done so +already, install the `llvm-tools-preview`: + +```sh +rustup component add llvm-tools-preview +cargo install grcov --locked +``` + +After that, you can simply run `coverage.py` to test the project with coverage. You can optionally +supply the `--open` flag to open the coverage report in your webbrowser. diff --git a/automation/Dockerfile b/automation/Dockerfile new file mode 100644 index 0000000..7d53a05 --- /dev/null +++ b/automation/Dockerfile @@ -0,0 +1,28 @@ +# Run the following commands from root directory to build and run locally +# docker build -f automation/Dockerfile -t . +# docker run -it +FROM rust:latest +RUN apt-get update +RUN apt-get --yes upgrade +# tzdata is a dependency, won't install otherwise +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get --yes install rsync curl + +# set CROSS_CONTAINER_IN_CONTAINER to inform `cross` that it is executed from within a container +ENV CROSS_CONTAINER_IN_CONTAINER=true + +RUN rustup install nightly && \ + rustup target add thumbv7em-none-eabihf armv7-unknown-linux-gnueabihf && \ + rustup component add rustfmt clippy llvm-tools-preview + +# Get grcov +RUN curl -sSL https://github.com/mozilla/grcov/releases/download/v0.8.19/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar -xj --directory /usr/local/bin +# Get nextest +RUN curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin + +# SSH stuff to allow deployment to doc server +RUN adduser --uid 114 jenkins + +# Add documentation server to known hosts +RUN echo "|1|/LzCV4BuTmTb2wKnD146l9fTKgQ=|NJJtVjvWbtRt8OYqFgcYRnMQyVw= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNL8ssTonYtgiR/6RRlSIK9WU1ywOcJmxFTLcEblAwH7oifZzmYq3XRfwXrgfMpylEfMFYfCU8JRqtmi19xc21A=" >> /etc/ssh/ssh_known_hosts +RUN echo "|1|CcBvBc3EG03G+XM5rqRHs6gK/Gg=|oGeJQ+1I8NGI2THIkJsW92DpTzs= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNL8ssTonYtgiR/6RRlSIK9WU1ywOcJmxFTLcEblAwH7oifZzmYq3XRfwXrgfMpylEfMFYfCU8JRqtmi19xc21A=" >> /etc/ssh/ssh_known_hosts diff --git a/automation/Jenkinsfile b/automation/Jenkinsfile new file mode 100644 index 0000000..b0337c3 --- /dev/null +++ b/automation/Jenkinsfile @@ -0,0 +1,81 @@ +pipeline { + + agent { + dockerfile { + dir 'automation' + reuseNode true + args '--network host' + } + } + + stages { + stage('Rust Toolchain Info') { + steps { + sh 'rustc --version' + } + } + stage('Clippy') { + steps { + sh 'cargo clippy' + } + } + stage('Docs') { + steps { + sh """ + RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features + """ + } + } + stage('Rustfmt') { + steps { + sh 'cargo fmt --all --check' + } + } + stage('Test') { + steps { + sh 'cargo nextest r --all-features' + sh 'cargo test --doc' + } + } + stage('Check with all features') { + steps { + sh 'cargo check --all-features' + } + } + stage('Check with no features') { + steps { + sh 'cargo check --no-default-features' + } + } + stage('Check Cross Embedded Bare Metal') { + steps { + sh 'cargo check --target thumbv7em-none-eabihf --no-default-features' + } + } + stage('Check Cross Embedded Linux') { + steps { + sh 'cargo check --target armv7-unknown-linux-gnueabihf' + } + } + stage('Run test with Coverage') { + when { + anyOf { + branch 'main'; + branch pattern: 'cov-deployment*' + } + } + steps { + withEnv(['RUSTFLAGS=-Cinstrument-coverage', 'LLVM_PROFILE_FILE=target/coverage/%p-%m.profraw']) { + echo "Executing tests with coverage" + sh 'cargo clean' + sh 'cargo test --all-features' + sh 'grcov . -s . --binary-path ./target/debug -t html --branch --ignore-not-existing -o ./target/debug/coverage/' + sshagent(credentials: ['documentation-buildfix']) { + // Deploy to Apache webserver + sh 'rsync --mkpath -r --delete ./target/debug/coverage/ buildfix@documentation.irs.uni-stuttgart.de:/projects/cfdp/coverage-rs/latest/' + } + } + } + } + } +} diff --git a/coverage.py b/coverage.py new file mode 100755 index 0000000..cfbe006 --- /dev/null +++ b/coverage.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import os +import logging +import argparse +import webbrowser + + +_LOGGER = logging.getLogger() + + +def generate_cov_report(open_report: bool, format: str): + logging.basicConfig(level=logging.INFO) + os.environ["RUSTFLAGS"] = "-Cinstrument-coverage" + os.environ["LLVM_PROFILE_FILE"] = "target/coverage/%p-%m.profraw" + _LOGGER.info("Executing tests with coverage") + os.system("cargo test --all-features") + + out_path = "./target/debug/coverage" + if format == "lcov": + 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}" + ) + if format == "lcov": + os.system( + "genhtml -o ./target/debug/coverage/ --show-details --highlight --ignore-errors source " + "--legend ./target/debug/lcov.info" + ) + if open_report: + coverage_report_path = os.path.abspath("./target/debug/coverage/index.html") + webbrowser.open_new_tab(coverage_report_path) + _LOGGER.info("Done") + + +def main(): + parser = argparse.ArgumentParser( + description="Generate coverage report and optionally open it in a browser" + ) + parser.add_argument( + "--open", action="store_true", help="Open the coverage report in a browser" + ) + parser.add_argument( + "--format", + choices=["html", "lcov"], + default="html", + help="Choose report format (html or lcov)", + ) + args = parser.parse_args() + generate_cov_report(args.open, args.format) + + +if __name__ == "__main__": + main() diff --git a/release-checklist.md b/release-checklist.md new file mode 100644 index 0000000..b8dea5e --- /dev/null +++ b/release-checklist.md @@ -0,0 +1,25 @@ +Checklist for new releases +======= + +# Pre-Release + +1. Make sure any new modules are documented sufficiently enough and check docs with + `RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options" cargo +nightly doc --all-features --open` + or `cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docsrs" --generate-link-to-definition"]' --open` + (was problematic on more recent nightly versions). +2. Bump version specifier in `Cargo.toml`. +3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new + `unreleased` section. +4. Run `cargo test --all-features` or `cargo nextest r --all-features` together with + `cargo test --doc`. +5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`. +6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal + targets. + +# Release + +1. `cargo publish` + +# Post-Release + +1. Create a new release on `EGit` based on the release branch. diff --git a/src/dest.rs b/src/dest.rs new file mode 100644 index 0000000..fb8c6f2 --- /dev/null +++ b/src/dest.rs @@ -0,0 +1,1591 @@ +use crate::{user::TransactionFinishedParams, DummyPduProvider, GenericSendError, PduProvider}; +use core::str::{from_utf8, Utf8Error}; +use std::path::{Path, PathBuf}; + +use super::{ + filestore::{FilestoreError, NativeFilestore, VirtualFilestore}, + user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams}, + CheckTimerProviderCreator, CountdownProvider, EntityType, LocalEntityConfig, PacketTarget, + PduSendProvider, RemoteEntityConfig, RemoteEntityConfigProvider, State, StdCheckTimer, + StdCheckTimerCreator, StdRemoteEntityConfigProvider, TimerContext, TransactionId, + UserFaultHookProvider, +}; +use smallvec::SmallVec; +use spacepackets::{ + cfdp::{ + pdu::{ + eof::EofPdu, + file_data::FileDataPdu, + finished::{DeliveryCode, FileStatus, FinishedPduCreator}, + metadata::{MetadataGenericParams, MetadataPduReader}, + CfdpPdu, CommonPduConfig, FileDirectiveType, PduError, PduHeader, WritablePduPacket, + }, + tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, GenericTlv, ReadableTlv, TlvType}, + ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode, + }, + util::{UnsignedByteField, UnsignedEnum}, +}; +use thiserror::Error; + +#[derive(Debug)] +struct FileProperties { + src_file_name: [u8; u8::MAX as usize], + src_file_name_len: usize, + dest_file_name: [u8; u8::MAX as usize], + dest_file_name_len: usize, + dest_path_buf: PathBuf, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum CompletionDisposition { + Completed = 0, + Cancelled = 1, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TransactionStep { + Idle = 0, + TransactionStart = 1, + ReceivingFileDataPdus = 2, + ReceivingFileDataPdusWithCheckLimitHandling = 3, + SendingAckPdu = 4, + TransferCompletion = 5, + SendingFinishedPdu = 6, +} + +// This contains transfer state parameters for destination transaction. +#[derive(Debug)] +struct TransferState { + transaction_id: Option, + metadata_params: MetadataGenericParams, + progress: u64, + metadata_only: bool, + condition_code: ConditionCode, + delivery_code: DeliveryCode, + file_status: FileStatus, + completion_disposition: CompletionDisposition, + checksum: u32, + current_check_count: u32, + current_check_timer: Option, +} + +impl Default for TransferState { + fn default() -> Self { + Self { + transaction_id: None, + metadata_params: Default::default(), + progress: Default::default(), + metadata_only: false, + condition_code: ConditionCode::NoError, + delivery_code: DeliveryCode::Incomplete, + file_status: FileStatus::Unreported, + completion_disposition: CompletionDisposition::Completed, + checksum: 0, + current_check_count: 0, + current_check_timer: None, + } + } +} + +// This contains parameters for destination transaction. +#[derive(Debug)] +struct TransactionParams { + tstate: TransferState, + pdu_conf: CommonPduConfig, + file_properties: FileProperties, + cksum_buf: [u8; 1024], + msgs_to_user_size: usize, + // TODO: Should we make this configurable? + msgs_to_user_buf: [u8; 1024], + remote_cfg: Option, +} + +impl TransactionParams { + fn transmission_mode(&self) -> TransmissionMode { + self.pdu_conf.trans_mode + } +} + +impl Default for FileProperties { + fn default() -> Self { + Self { + src_file_name: [0; u8::MAX as usize], + src_file_name_len: Default::default(), + dest_file_name: [0; u8::MAX as usize], + dest_file_name_len: Default::default(), + dest_path_buf: Default::default(), + } + } +} + +impl TransactionParams { + fn file_size(&self) -> u64 { + self.tstate.metadata_params.file_size + } + + fn metadata_params(&self) -> &MetadataGenericParams { + &self.tstate.metadata_params + } +} + +impl Default for TransactionParams { + fn default() -> Self { + Self { + pdu_conf: Default::default(), + cksum_buf: [0; 1024], + msgs_to_user_size: 0, + msgs_to_user_buf: [0; 1024], + tstate: Default::default(), + file_properties: Default::default(), + remote_cfg: None, + } + } +} + +impl TransactionParams { + fn reset(&mut self) { + self.tstate.condition_code = ConditionCode::NoError; + self.tstate.delivery_code = DeliveryCode::Incomplete; + self.tstate.file_status = FileStatus::Unreported; + } +} + +#[derive(Debug, Error)] +pub enum DestError { + /// File directive expected, but none specified + #[error("expected file directive")] + DirectiveFieldEmpty, + #[error("can not process packet type {pdu_type:?} with directive type {directive_type:?}")] + CantProcessPacketType { + pdu_type: PduType, + directive_type: Option, + }, + #[error("can not process file data PDUs in current state")] + WrongStateForFileDataAndEof, + // Received new metadata PDU while being already being busy with a file transfer. + #[error("busy with transfer")] + RecvdMetadataButIsBusy, + #[error("empty source file field")] + 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}")] + Io(#[from] std::io::Error), + #[error("file store error {0}")] + Filestore(#[from] FilestoreError), + #[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), + #[error("issue sending PDU: {0}")] + SendError(#[from] GenericSendError), + #[error("cfdp feature not implemented")] + NotImplemented, +} + +/// This is the primary CFDP destination handler. It models the CFDP destination entity, which is +/// primarily responsible for receiving files sent from another CFDP entity. It performs the +/// reception side of File Copy Operations. +/// +/// The [DestinationHandler::state_machine] function is the primary function to drive the +/// destination handler. It can be used to insert packets into the destination +/// 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 via the [CfdpPacketSender] trait, which is implemented by the +/// user and passed as a constructor parameter. The number of generated packets is returned +/// by the state machine call. +pub struct DestinationHandler< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + CheckTimerCreator: CheckTimerProviderCreator, + CheckTimerProvider: CountdownProvider, +> { + local_cfg: LocalEntityConfig, + step: TransactionStep, + 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, +} + +#[cfg(feature = "std")] +pub type StdDestinationHandler = DestinationHandler< + PduSender, + UserFaultHook, + NativeFilestore, + StdRemoteEntityConfigProvider, + StdCheckTimerCreator, + StdCheckTimer, +>; + +impl< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + CheckTimerCreator: CheckTimerProviderCreator, + CheckTimerProvider: CountdownProvider, + > + DestinationHandler< + PduSender, + UserFaultHook, + Vfs, + RemoteCfgTable, + CheckTimerCreator, + CheckTimerProvider, + > +{ + /// Constructs a new destination handler. + /// + /// # Arguments + /// + /// * `local_cfg` - The local CFDP entity configuration, consisting of the local entity ID, + /// the indication configuration, and the fault handlers. + /// * `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_sender` - All generated packets are sent via this abstraction. + /// * `vfs` - Virtual filestore implementation to decouple 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` - A table of all expected remote entities this entity will communicate + /// with. It contains various configuration parameters required for file transfers. + /// * `check_timer_creator` - This is used by the CFDP handler to generate timers required + /// by various tasks. + pub fn new( + local_cfg: LocalEntityConfig, + max_packet_len: usize, + pdu_sender: PduSender, + vfs: Vfs, + remote_cfg_table: RemoteCfgTable, + check_timer_creator: CheckTimerCreator, + ) -> Self { + Self { + local_cfg, + step: TransactionStep::Idle, + state: State::Idle, + tparams: Default::default(), + packet_buf: alloc::vec![0; max_packet_len], + pdu_sender, + vfs, + remote_cfg_table, + check_timer_creator, + } + } + + pub fn state_machine_no_packet( + &mut self, + cfdp_user: &mut impl CfdpUser, + ) -> Result { + self.state_machine(cfdp_user, None::<&DummyPduProvider>) + } + + /// This is the core function to drive the destination handler. It is also used to insert + /// packets into the destination handler. + /// + /// The state machine should either be called if a packet with the appropriate destination ID + /// is received, or periodically in IDLE periods to perform all CFDP related tasks, for example + /// checking for timeouts or missed file segments. + /// + /// The function returns the number of sent PDU packets on success. + pub fn state_machine( + &mut self, + cfdp_user: &mut impl CfdpUser, + packet_to_insert: Option<&impl PduProvider>, + ) -> Result { + if let Some(packet) = packet_to_insert { + 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::Suspended => { + // There is now way to suspend the handler currently anyway. + Ok(0) + } + } + } + + /// Returns [None] if the state machine is IDLE, and the transmission mode of the current + /// request otherwise. + pub fn transmission_mode(&self) -> Option { + if self.state == State::Idle { + return None; + } + Some(self.tparams.transmission_mode()) + } + + pub fn transaction_id(&self) -> Option { + self.tstate().transaction_id + } + + fn insert_packet( + &mut self, + cfdp_user: &mut impl CfdpUser, + packet_to_insert: &impl PduProvider, + ) -> Result<(), DestError> { + 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. + return Err(DestError::CantProcessPacketType { + pdu_type: packet_to_insert.pdu_type(), + directive_type: packet_to_insert.file_directive_type(), + }); + } + match packet_to_insert.pdu_type() { + PduType::FileDirective => { + if packet_to_insert.file_directive_type().is_none() { + return Err(DestError::DirectiveFieldEmpty); + } + self.handle_file_directive( + cfdp_user, + packet_to_insert.file_directive_type().unwrap(), + packet_to_insert.pdu(), + ) + } + PduType::FileData => self.handle_file_data(cfdp_user, packet_to_insert.pdu()), + } + } + + fn handle_file_directive( + &mut self, + cfdp_user: &mut impl CfdpUser, + pdu_directive: FileDirectiveType, + raw_packet: &[u8], + ) -> Result<(), DestError> { + match pdu_directive { + FileDirectiveType::EofPdu => self.handle_eof_pdu(cfdp_user, raw_packet)?, + FileDirectiveType::FinishedPdu + | FileDirectiveType::NakPdu + | FileDirectiveType::KeepAlivePdu => { + return Err(DestError::CantProcessPacketType { + pdu_type: PduType::FileDirective, + directive_type: Some(pdu_directive), + }); + } + FileDirectiveType::AckPdu => { + return Err(DestError::NotImplemented); + } + FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet)?, + FileDirectiveType::PromptPdu => self.handle_prompt_pdu(raw_packet)?, + }; + Ok(()) + } + + fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + if self.state != State::Idle { + return Err(DestError::RecvdMetadataButIsBusy); + } + let metadata_pdu = MetadataPduReader::from_bytes(raw_packet)?; + self.tparams.reset(); + self.tparams.tstate.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())); + } + 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; + } + if !self.tparams.tstate.metadata_only && src_name.is_empty() { + return Err(DestError::EmptySrcFileField); + } + if !self.tparams.tstate.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()] + .copy_from_slice(src_name.value()); + self.tparams.file_properties.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()] + .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; + } + 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 + .msgs_to_user_buf + .copy_from_slice(option_tlv.raw_data().unwrap()); + self.tparams.msgs_to_user_size += option_tlv.len_full(); + } + } + } + self.state = State::Busy; + self.step = TransactionStep::TransactionStart; + Ok(()) + } + + 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::WrongStateForFileDataAndEof); + } + 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(), + }); + } + if let Err(e) = self.vfs.write_data( + self.tparams.file_properties.dest_path_buf.to_str().unwrap(), + fd_pdu.offset(), + fd_pdu.file_data(), + ) { + self.declare_fault(ConditionCode::FilestoreRejection); + return Err(e.into()); + } + self.tstate_mut().progress += fd_pdu.file_data().len() as u64; + Ok(()) + } + + 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::WrongStateForFileDataAndEof); + } + 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()); + } + let regular_transfer_finish = if eof_pdu.condition_code() == ConditionCode::NoError { + self.handle_no_error_eof_pdu(&eof_pdu)? + } else { + return Err(DestError::NotImplemented); + }; + if regular_transfer_finish { + self.file_transfer_complete_transition(); + } + Ok(()) + } + + /// Returns whether the transfer can be completed regularly. + fn handle_no_error_eof_pdu(&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 + { + // 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.tparams.tstate.checksum = eof_pdu.file_checksum(); + if self.tparams.transmission_mode() == TransmissionMode::Unacknowledged + && !self.checksum_verify(self.tparams.tstate.checksum) + { + if self.declare_fault(ConditionCode::FileChecksumFailure) + != FaultHandlerCode::IgnoreError + { + return Ok(false); + } + self.start_check_limit_handling(); + return Ok(false); + } + 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 + { + file_delivery_complete = true; + } else { + match self.vfs.checksum_verify( + self.tparams.file_properties.dest_path_buf.to_str().unwrap(), + self.tparams.metadata_params().checksum_type, + checksum, + &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; + } + } + 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; + } + _ => { + self.declare_fault(ConditionCode::FilestoreRejection); + // Treat this equivalent to a failed checksum procedure. + } + }, + }; + } + 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_check_timer_provider(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; + } + + fn check_limit_handling(&mut self) { + if self.tparams.tstate.current_check_timer.is_none() { + return; + } + let check_timer = self.tparams.tstate.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.tparams.tstate.current_check_count + 1 + >= self.tparams.remote_cfg.unwrap().check_limit + { + self.declare_fault(ConditionCode::CheckLimitReached); + } else { + self.tparams.tstate.current_check_count += 1; + self.tparams + .tstate + .current_check_timer + .as_mut() + .unwrap() + .reset(); + } + } + } + + pub 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 { + self.transaction_start(cfdp_user)?; + } + 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(); + } + 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 dest_name = from_utf8( + &self.tparams.file_properties.dest_file_name + [..self.tparams.file_properties.dest_file_name_len], + )?; + let dest_path = Path::new(dest_name); + self.tparams.file_properties.dest_path_buf = dest_path.to_path_buf(); + let source_id = self.tparams.pdu_conf.source_id(); + let id = TransactionId::new(source_id, self.tparams.pdu_conf.transaction_seq_num); + let src_name = from_utf8( + &self.tparams.file_properties.src_file_name + [0..self.tparams.file_properties.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 { + let mut index = 0; + while index < self.tparams.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..]) + .expect("message to user creation failed unexpectedly"); + msgs_to_user.push(msgs_to_user_tlv); + index += msgs_to_user_tlv.len_full(); + num_msgs_to_user += 1; + } + } + let metadata_recvd_params = MetadataReceivedParams { + id, + source_id, + file_size: self.tparams.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); + + // TODO: This is the only remaining function which uses std.. the easiest way would + // probably be to use a static pre-allocated dest path buffer to store any concatenated + // paths. + if dest_path.exists() && self.vfs.is_dir(dest_path.to_str().unwrap())? { + // Create new destination path by concatenating the last part of the source source + // name and the destination folder. For example, for a source file of /tmp/hello.txt + // and a destination name of /home/test, the resulting file name should be + // /home/test/hello.txt + let source_path = Path::new(from_utf8( + &self.tparams.file_properties.src_file_name + [..self.tparams.file_properties.src_file_name_len], + )?); + let source_name = source_path.file_name(); + if source_name.is_none() { + return Err(DestError::PathConcat); + } + let source_name = source_name.unwrap(); + self.tparams.file_properties.dest_path_buf.push(source_name); + } + let dest_path_str = self.tparams.file_properties.dest_path_buf.to_str().unwrap(); + 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; + 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 + { + sent_packets += self.send_finished_pdu()?; + self.step = TransactionStep::SendingFinishedPdu; + } else { + self.reset(); + } + Ok(sent_packets) + } + + fn notice_of_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { + if self.tstate().completion_disposition == CompletionDisposition::Completed { + // TODO: Execute any filestore requests + } else if self + .tparams + .remote_cfg + .as_ref() + .unwrap() + .disposition_on_cancellation + && self.tstate().delivery_code == DeliveryCode::Incomplete + { + self.vfs + .remove_file(self.tparams.file_properties.dest_path_buf.to_str().unwrap())?; + self.tstate_mut().file_status = FileStatus::DiscardDeliberately; + } + let tstate = self.tstate(); + 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, + }; + cfdp_user.transaction_finished_indication(&transaction_finished_params); + Ok(()) + } + + fn declare_fault(&mut 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 + .local_cfg + .fault_handler + .get_fault_handler(condition_code); + match fh_code { + FaultHandlerCode::NoticeOfCancellation => { + self.notice_of_cancellation(condition_code); + } + FaultHandlerCode::NoticeOfSuspension => self.notice_of_suspension(), + FaultHandlerCode::IgnoreError => (), + FaultHandlerCode::AbandonTransaction => self.abandon_transaction(), + } + self.local_cfg + .fault_handler + .report_fault(transaction_id, condition_code, progress) + } + + fn notice_of_cancellation(&mut self, condition_code: ConditionCode) { + self.step = TransactionStep::TransferCompletion; + self.tstate_mut().condition_code = condition_code; + self.tstate_mut().completion_disposition = CompletionDisposition::Cancelled; + } + + fn notice_of_suspension(&mut self) { + // TODO: Implement suspension handling. + } + + fn abandon_transaction(&mut self) { + self.reset(); + } + + fn reset(&mut self) { + self.step = TransactionStep::Idle; + self.state = State::Idle; + // self.packets_to_send_ctx.packet_available = false; + self.tparams.reset(); + } + + fn send_finished_pdu(&mut self) -> Result { + let tstate = self.tstate(); + + 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 + { + FinishedPduCreator::new_default(pdu_header, tstate.delivery_code, tstate.file_status) + } else { + // TODO: Are there cases where this ID is actually the source entity ID? + let entity_id = EntityIdTlv::new(self.local_cfg.id); + FinishedPduCreator::new_with_error( + pdu_header, + tstate.condition_code, + tstate.delivery_code, + tstate.file_status, + entity_id, + ) + }; + 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()], + )?; + 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)] +mod tests { + use core::{cell::Cell, sync::atomic::AtomicBool}; + #[allow(unused_imports)] + use std::println; + use std::{fs, string::String}; + + use alloc::{sync::Arc, vec::Vec}; + use rand::Rng; + use spacepackets::{ + cfdp::{ + lv::Lv, + pdu::{finished::FinishedPduReader, metadata::MetadataPduCreator, WritablePduPacket}, + ChecksumType, TransmissionMode, + }, + util::{UbfU16, UnsignedByteFieldU16}, + }; + + use crate::{ + filestore::NativeFilestore, + tests::{ + basic_remote_cfg_table, SentPdu, TestCfdpSender, TestCfdpUser, TestFaultHandler, + LOCAL_ID, + }, + CheckTimerProviderCreator, CountdownProvider, FaultHandler, IndicationConfig, PacketInfo, + StdRemoteEntityConfigProvider, CRC_32, + }; + + use super::*; + + const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + + #[derive(Debug)] + struct TestCheckTimer { + counter: Cell, + expired: Arc, + } + + impl CountdownProvider for TestCheckTimer { + fn has_expired(&self) -> bool { + self.expired.load(core::sync::atomic::Ordering::Relaxed) + } + fn reset(&mut self) { + self.counter.set(0); + } + } + + impl TestCheckTimer { + pub fn new(expired_flag: Arc) -> Self { + Self { + counter: Cell::new(0), + expired: expired_flag, + } + } + } + + struct TestCheckTimerCreator { + check_limit_expired_flag: Arc, + } + + impl TestCheckTimerCreator { + pub fn new(expired_flag: Arc) -> Self { + Self { + check_limit_expired_flag: expired_flag, + } + } + } + + impl CheckTimerProviderCreator for TestCheckTimerCreator { + type CheckTimer = TestCheckTimer; + + fn create_check_timer_provider(&self, timer_context: TimerContext) -> Self::CheckTimer { + match timer_context { + TimerContext::CheckLimit { .. } => { + TestCheckTimer::new(self.check_limit_expired_flag.clone()) + } + _ => { + panic!("invalid check timer creator, can only be used for check limit handling") + } + } + } + } + + type TestDestHandler = DestinationHandler< + TestCfdpSender, + TestFaultHandler, + NativeFilestore, + StdRemoteEntityConfigProvider, + TestCheckTimerCreator, + TestCheckTimer, + >; + + struct DestHandlerTestbench { + check_timer_expired: Arc, + handler: TestDestHandler, + src_path: PathBuf, + 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, + buf: [u8; 512], + } + + impl DestHandlerTestbench { + fn new(fault_handler: TestFaultHandler, closure_requested: bool) -> Self { + let check_timer_expired = Arc::new(AtomicBool::new(false)); + let test_sender = TestCfdpSender::default(); + let dest_handler = + default_dest_handler(fault_handler, test_sender, check_timer_expired.clone()); + let (src_path, dest_path) = init_full_filepaths_textfile(); + assert!(!Path::exists(&dest_path)); + let handler = Self { + check_timer_expired, + handler: dest_handler, + src_path, + closure_requested, + dest_path, + check_dest_file: false, + check_handler_idle_at_drop: true, + expected_file_size: 0, + pdu_header: create_pdu_header(UbfU16::new(0)), + expected_full_data: Vec::new(), + buf: [0; 512], + }; + handler.state_check(State::Idle, TransactionStep::Idle); + handler + } + + fn dest_path(&self) -> &PathBuf { + &self.dest_path + } + + fn all_fault_queues_empty(&self) -> bool { + self.handler + .local_cfg + .user_fault_hook() + .borrow() + .all_queues_empty() + } + + #[allow(dead_code)] + fn indication_cfg_mut(&mut self) -> &mut IndicationConfig { + &mut self.handler.local_cfg.indication_cfg + } + + fn indication_cfg(&mut self) -> &IndicationConfig { + &self.handler.local_cfg.indication_cfg + } + + fn set_check_timer_expired(&mut self) { + self.check_timer_expired + .store(true, core::sync::atomic::Ordering::Relaxed); + } + + fn test_user_from_cached_paths(&self, expected_file_size: u64) -> TestCfdpUser { + TestCfdpUser::new( + 0, + self.src_path.to_string_lossy().into(), + self.dest_path.to_string_lossy().into(), + expected_file_size, + ) + } + + fn generic_transfer_init( + &mut self, + user: &mut TestCfdpUser, + file_size: u64, + ) -> Result { + self.expected_file_size = file_size; + assert_eq!(user.transaction_indication_call_count, 0); + assert_eq!(user.metadata_recv_queue.len(), 0); + let metadata_pdu = create_metadata_pdu( + &self.pdu_header, + self.src_path.as_path(), + self.dest_path.as_path(), + file_size, + self.closure_requested, + ); + let packet_info = create_packet_info(&metadata_pdu, &mut self.buf); + self.handler.state_machine(user, Some(&packet_info))?; + assert_eq!(user.metadata_recv_queue.len(), 1); + assert_eq!( + self.handler.transmission_mode().unwrap(), + TransmissionMode::Unacknowledged + ); + assert_eq!(user.transaction_indication_call_count, 0); + assert_eq!(user.metadata_recv_queue.len(), 1); + let metadata_recvd = user.metadata_recv_queue.pop_front().unwrap(); + assert_eq!(metadata_recvd.source_id, LOCAL_ID.into()); + assert_eq!( + metadata_recvd.src_file_name, + String::from(self.src_path.to_str().unwrap()) + ); + assert_eq!( + metadata_recvd.dest_file_name, + String::from(self.dest_path().to_str().unwrap()) + ); + 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()) + } + + fn generic_file_data_insert( + &mut self, + user: &mut TestCfdpUser, + offset: u64, + file_data_chunk: &[u8], + ) -> Result { + let filedata_pdu = + FileDataPdu::new_no_seg_metadata(self.pdu_header, offset, file_data_chunk); + filedata_pdu + .write_to_bytes(&mut self.buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&self.buf).expect("creating packet info failed"); + 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() + ); + } + result + } + + fn generic_eof_no_error( + &mut self, + user: &mut TestCfdpUser, + expected_full_data: Vec, + ) -> 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 packet_info = create_packet_info(&eof_pdu, &mut self.buf); + self.check_handler_idle_at_drop = true; + self.check_dest_file = true; + let result = self.handler.state_machine(user, Some(&packet_info)); + if self.indication_cfg().eof_recv { + assert_eq!(user.eof_recvd_call_count, 1); + } + result + } + + fn check_completion_indication_success(&mut self, user: &mut TestCfdpUser) { + 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, FileStatus::Retained); + assert_eq!(finished_indication.delivery_code, DeliveryCode::Complete); + assert_eq!(finished_indication.condition_code, ConditionCode::NoError); + } + + fn state_check(&self, state: State, step: TransactionStep) { + assert_eq!(self.handler.state(), state); + assert_eq!(self.handler.step(), step); + } + } + + // 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.check_handler_idle_at_drop { + self.state_check(State::Idle, TransactionStep::Idle); + } + if self.check_dest_file { + assert!(Path::exists(&self.dest_path)); + let read_content = fs::read(&self.dest_path).expect("reading back string failed"); + assert_eq!(read_content.len() as u64, self.expected_file_size); + assert_eq!(read_content, self.expected_full_data); + assert!(fs::remove_file(self.dest_path.as_path()).is_ok()); + } + } + } + + fn init_full_filepaths_textfile() -> (PathBuf, PathBuf) { + ( + tempfile::TempPath::from_path("/tmp/test.txt").to_path_buf(), + tempfile::NamedTempFile::new() + .unwrap() + .into_temp_path() + .to_path_buf(), + ) + } + + fn default_dest_handler( + test_fault_handler: TestFaultHandler, + test_packet_sender: TestCfdpSender, + check_timer_expired: Arc, + ) -> TestDestHandler { + let local_entity_cfg = LocalEntityConfig { + id: REMOTE_ID.into(), + indication_cfg: IndicationConfig::default(), + fault_handler: FaultHandler::new(test_fault_handler), + }; + DestinationHandler::new( + local_entity_cfg, + 2048, + test_packet_sender, + NativeFilestore::default(), + basic_remote_cfg_table(LOCAL_ID, 1024, true), + TestCheckTimerCreator::new(check_timer_expired), + ) + } + + 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, + dest_name: &'filename Path, + file_size: u64, + closure_requested: bool, + ) -> MetadataPduCreator<'filename, 'filename, 'static> { + let checksum_type = if file_size == 0 { + ChecksumType::NullChecksum + } else { + ChecksumType::Crc32 + }; + let metadata_params = + MetadataGenericParams::new(closure_requested, checksum_type, file_size); + MetadataPduCreator::new_no_opts( + *pdu_header, + metadata_params, + Lv::new_from_str(src_name.as_os_str().to_str().unwrap()).unwrap(), + Lv::new_from_str(dest_name.as_os_str().to_str().unwrap()).unwrap(), + ) + } + + fn create_packet_info<'a>( + pdu: &'a impl WritablePduPacket, + buf: &'a mut [u8], + ) -> PacketInfo<'a> { + let written_len = pdu + .write_to_bytes(buf) + .expect("writing metadata PDU failed"); + PacketInfo::new(&buf[..written_len]).expect("generating packet info failed") + } + + fn create_no_error_eof(file_data: &[u8], pdu_header: &PduHeader) -> EofPdu { + let crc32 = if !file_data.is_empty() { + let mut digest = CRC_32.digest(); + digest.update(file_data); + digest.finalize() + } else { + 0 + }; + EofPdu::new_no_error(*pdu_header, crc32, file_data.len() as u64) + } + + #[test] + fn test_basic() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let dest_handler = default_dest_handler(fault_handler, test_sender, Arc::default()); + assert!(dest_handler.transmission_mode().is_none()); + assert!(dest_handler + .local_cfg + .fault_handler + .user_hook + .borrow() + .all_queues_empty()); + assert!(dest_handler.pdu_sender.queue_empty()); + assert_eq!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::Idle); + } + + #[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 test_user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_eof_no_error(&mut test_user, Vec::new()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut test_user); + } + + #[test] + fn test_small_file_transfer_not_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(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"); + 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); + } + + #[test] + fn test_segmented_file_transfer_not_acked() { + let mut rng = rand::thread_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 test_user = tb.test_user_from_cached_paths(file_size); + 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, &random_data[0..segment_len]) + .expect("file data insertion failed"); + tb.generic_file_data_insert( + &mut test_user, + segment_len as u64, + &random_data[segment_len..], + ) + .expect("file data insertion failed"); + tb.generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + tb.check_completion_indication_success(&mut test_user); + } + + #[test] + fn test_check_limit_handling_transfer_success() { + let mut rng = rand::thread_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 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, &random_data[0..segment_len]) + .expect("file data insertion 0 failed"); + tb.generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + tb.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + 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); + } + + #[test] + fn test_check_limit_handling_limit_reached() { + let mut rng = rand::thread_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 test_user = testbench.test_user_from_cached_paths(file_size); + let transaction_id = testbench + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + + testbench.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + testbench + .generic_file_data_insert(&mut test_user, 0, &random_data[0..segment_len]) + .expect("file data insertion 0 failed"); + testbench + .generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + testbench.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + testbench.set_check_timer_expired(); + testbench + .handler + .state_machine_no_packet(&mut test_user) + .expect("fsm error"); + testbench.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + testbench.set_check_timer_expired(); + testbench + .handler + .state_machine_no_packet(&mut test_user) + .expect("fsm error"); + 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 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!(testbench.handler.pdu_sender.queue_empty()); + + // Check that the broken file exists. + testbench.check_dest_file = false; + assert!(Path::exists(testbench.dest_path())); + let read_content = fs::read(testbench.dest_path()).expect("reading back string failed"); + 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()); + } + + fn check_finished_pdu_success(sent_pdu: &SentPdu) { + assert_eq!(sent_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + sent_pdu.file_directive_type, + Some(FileDirectiveType::FinishedPdu) + ); + let finished_pdu = FinishedPduReader::from_bytes(&sent_pdu.raw_pdu).unwrap(); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!(finished_pdu.condition_code(), ConditionCode::NoError); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete); + assert!(finished_pdu.fault_location().is_none()); + assert_eq!(finished_pdu.fs_responses_raw(), &[]); + } + + #[test] + fn test_file_transfer_with_closure() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new(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"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let sent_packets = tb + .generic_eof_no_error(&mut test_user, Vec::new()) + .expect("EOF no error insertion failed"); + assert_eq!(sent_packets, 1); + assert!(tb.all_fault_queues_empty()); + // The Finished PDU was sent, so the state machine is done. + tb.state_check(State::Idle, TransactionStep::Idle); + assert!(!tb.handler.pdu_sender.queue_empty()); + 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); + } + + #[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 user = tb.test_user_from_cached_paths(0); + let finished_pdu = FinishedPduCreator::new_default( + PduHeader::new_no_file_data(CommonPduConfig::default(), 0), + DeliveryCode::Complete, + FileStatus::Retained, + ); + let finished_pdu_raw = finished_pdu.to_vec().unwrap(); + let packet_info = PacketInfo::new(&finished_pdu_raw).unwrap(); + let error = tb.handler.state_machine(&mut user, Some(&packet_info)); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let DestError::CantProcessPacketType { + pdu_type, + directive_type, + } = error + { + assert_eq!(pdu_type, PduType::FileDirective); + assert_eq!(directive_type, Some(FileDirectiveType::FinishedPdu)); + } + } + + #[test] + fn test_metadata_insertion_twice_fails() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new(fault_handler, 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 metadata_pdu = create_metadata_pdu( + &tb.pdu_header, + tb.src_path.as_path(), + tb.dest_path.as_path(), + 0, + tb.closure_requested, + ); + let packet_info = create_packet_info(&metadata_pdu, &mut tb.buf); + let error = tb.handler.state_machine(&mut user, Some(&packet_info)); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let DestError::RecvdMetadataButIsBusy = error { + } else { + panic!("unexpected error: {:?}", error); + } + } + + #[test] + fn test_checksum_failure_not_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(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"); + let faulty_file_data = b"Hemlo World!"; + assert_eq!( + tb.generic_file_data_insert(&mut user, 0, faulty_file_data) + .expect("file data insertion failed"), + 0 + ); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let sent_packets = tb + .generic_eof_no_error(&mut user, file_data.into()) + .expect("EOF no error insertion failed"); + // FSM enters check limit algorithm here, so no finished PDU is created. + assert_eq!(sent_packets, 0); + + let transaction_id = tb.handler.transaction_id().unwrap(); + let mut fault_hook = tb.handler.local_cfg.user_fault_hook().borrow_mut(); + assert!(fault_hook.notice_of_suspension_queue.is_empty()); + + // The file checksum failure is ignored by default and check limit handling is now + // performed. + 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); + drop(fault_hook); + + tb.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + tb.set_check_timer_expired(); + tb.handler + .state_machine_no_packet(&mut user) + .expect("fsm error"); + tb.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + tb.set_check_timer_expired(); + tb.handler + .state_machine_no_packet(&mut user) + .expect("fsm error"); + tb.state_check(State::Idle, TransactionStep::Idle); + + // Transaction is cancelled because the check limit is reached. + let mut fault_hook = tb.handler.local_cfg.user_fault_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.0, transaction_id); + assert_eq!(cancelled.1, ConditionCode::CheckLimitReached); + assert_eq!(cancelled.2, file_size); + + drop(fault_hook); + + let sent_pdu = tb.handler.pdu_sender.retrieve_next_pdu().unwrap(); + assert_eq!(sent_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + sent_pdu.file_directive_type, + Some(FileDirectiveType::FinishedPdu) + ); + let finished_pdu = FinishedPduReader::from_bytes(&sent_pdu.raw_pdu).unwrap(); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!( + finished_pdu.condition_code(), + ConditionCode::CheckLimitReached + ); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); + assert!(finished_pdu.fault_location().is_some()); + assert_eq!( + *finished_pdu.fault_location().unwrap().entity_id(), + REMOTE_ID.into() + ); + assert_eq!(finished_pdu.fs_responses_raw(), &[]); + assert!(tb.handler.pdu_sender.queue_empty()); + tb.expected_full_data = faulty_file_data.to_vec(); + } +} diff --git a/src/filestore.rs b/src/filestore.rs new file mode 100644 index 0000000..d865b5e --- /dev/null +++ b/src/filestore.rs @@ -0,0 +1,802 @@ +use alloc::string::{String, ToString}; +use core::fmt::Display; +use crc::{Crc, CRC_32_CKSUM}; +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::*; + +pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); + +#[derive(Debug, Clone)] +pub enum FilestoreError { + FileDoesNotExist, + FileAlreadyExists, + DirDoesNotExist, + Permission, + IsNotFile, + IsNotDirectory, + ByteConversion(ByteConversionError), + Io { + raw_errno: Option, + string: String, + }, + ChecksumTypeNotImplemented(ChecksumType), +} + +impl From 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) + } + } + } +} + +impl Error for FilestoreError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + FilestoreError::ByteConversion(e) => Some(e), + _ => None, + } + } +} + +#[cfg(feature = "std")] +impl From 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>; + + fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError>; + + /// Truncating a file means deleting all its data so the resulting file is empty. + /// This can be more efficient than removing and re-creating a file. + fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>; + + fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError>; + fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError>; + + fn read_data( + &self, + file_path: &str, + offset: u64, + read_len: u64, + buf: &mut [u8], + ) -> Result<(), FilestoreError>; + + fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>; + + 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()) + } + + fn is_file(&self, path: &str) -> Result; + + fn is_dir(&self, path: &str) -> Result { + Ok(!self.is_file(path)?) + } + + fn exists(&self, path: &str) -> Result; + + fn file_size(&self, path: &str) -> Result; + + /// This special function is the CFDP specific abstraction to calculate the checksum of a file. + /// This allows to keep OS specific details like reading the whole file in the most efficient + /// manner inside the file system abstraction. + /// + /// The passed verification buffer argument will be used by the specific implementation as + /// a buffer to read the file into. It is recommended to use common buffer sizes like + /// 4096 or 8192 bytes. + fn calculate_checksum( + &self, + file_path: &str, + checksum_type: ChecksumType, + verification_buf: &mut [u8], + ) -> Result; + + /// This special function is the CFDP specific abstraction to verify the checksum of a file. + /// This allows to keep OS specific details like reading the whole file in the most efficient + /// manner inside the file system abstraction. + /// + /// The passed verification buffer argument will be used by the specific implementation as + /// a buffer to read the file into. It is recommended to use common buffer sizes like + /// 4096 or 8192 bytes. + fn checksum_verify( + &self, + file_path: &str, + checksum_type: ChecksumType, + expected_checksum: u32, + verification_buf: &mut [u8], + ) -> Result { + Ok( + self.calculate_checksum(file_path, checksum_type, verification_buf)? + == expected_checksum, + ) + } +} + +#[cfg(feature = "std")] +pub mod std_mod { + + use super::*; + use std::{ + fs::{self, File, OpenOptions}, + io::{BufReader, Read, Seek, SeekFrom, Write}, + }; + + #[derive(Default)] + pub struct NativeFilestore {} + + impl VirtualFilestore for NativeFilestore { + fn create_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if self.exists(file_path)? { + return Err(FilestoreError::FileAlreadyExists); + } + File::create(file_path)?; + Ok(()) + } + + fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if !self.exists(file_path)? { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file_path)? { + return Err(FilestoreError::IsNotFile); + } + fs::remove_file(file_path)?; + Ok(()) + } + + fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if !self.exists(file_path)? { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file_path)? { + return Err(FilestoreError::IsNotFile); + } + OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path)?; + Ok(()) + } + + 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(), + })?; + Ok(()) + } + + fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError> { + if !self.exists(dir_path)? { + return Err(FilestoreError::DirDoesNotExist); + } + if !self.is_dir(dir_path)? { + return Err(FilestoreError::IsNotDirectory); + } + if !all { + fs::remove_dir(dir_path)?; + return Ok(()); + } + fs::remove_dir_all(dir_path)?; + Ok(()) + } + + fn read_data( + &self, + file_name: &str, + offset: u64, + read_len: u64, + buf: &mut [u8], + ) -> Result<(), FilestoreError> { + if buf.len() < read_len as usize { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: read_len as usize, + } + .into()); + } + if !self.exists(file_name)? { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file_name)? { + return Err(FilestoreError::IsNotFile); + } + let mut file = File::open(file_name)?; + file.seek(SeekFrom::Start(offset))?; + file.read_exact(&mut buf[0..read_len as usize])?; + Ok(()) + } + + fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError> { + if !self.exists(file)? { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file)? { + return Err(FilestoreError::IsNotFile); + } + let mut file = OpenOptions::new().write(true).open(file)?; + file.seek(SeekFrom::Start(offset))?; + file.write_all(buf)?; + Ok(()) + } + + fn is_file(&self, str_path: &str) -> Result { + let path = Path::new(str_path); + if !self.exists(str_path)? { + return Err(FilestoreError::FileDoesNotExist); + } + Ok(path.is_file()) + } + + fn exists(&self, path: &str) -> Result { + let path = Path::new(path); + Ok(self.exists_internal(path)) + } + + fn file_size(&self, str_path: &str) -> Result { + let path = Path::new(str_path); + if !self.exists_internal(path) { + return Err(FilestoreError::FileDoesNotExist); + } + if !path.is_file() { + return Err(FilestoreError::IsNotFile); + } + Ok(path.metadata()?.len()) + } + + fn calculate_checksum( + &self, + file_path: &str, + checksum_type: ChecksumType, + verification_buf: &mut [u8], + ) -> Result { + match checksum_type { + ChecksumType::Modular => self.calc_modular_checksum(file_path), + ChecksumType::Crc32 => { + let mut digest = CRC_32.digest(); + let file_to_check = File::open(file_path)?; + let mut buf_reader = BufReader::new(file_to_check); + loop { + let bytes_read = buf_reader.read(verification_buf)?; + if bytes_read == 0 { + break; + } + digest.update(&verification_buf[0..bytes_read]); + } + Ok(digest.finalize()) + } + ChecksumType::NullChecksum => Ok(0), + _ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)), + } + } + } + + impl NativeFilestore { + pub fn calc_modular_checksum(&self, file_path: &str) -> Result { + let mut checksum: u32 = 0; + let file = File::open(file_path)?; + let mut buf_reader = BufReader::new(file); + let mut buffer = [0; 4]; + + loop { + let bytes_read = buf_reader.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + // Perform padding directly in the buffer + (bytes_read..4).for_each(|i| { + buffer[i] = 0; + }); + + checksum = checksum.wrapping_add(u32::from_be_bytes(buffer)); + } + Ok(checksum) + } + + fn exists_internal(&self, path: &Path) -> bool { + if !path.exists() { + return false; + } + true + } + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::Path, println}; + + use super::*; + use alloc::format; + use tempfile::tempdir; + + const EXAMPLE_DATA_CFDP: [u8; 15] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + ]; + + const NATIVE_FS: NativeFilestore = NativeFilestore {}; + + #[test] + fn test_basic_native_filestore_create() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = + NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed")); + assert!(result.is_ok()); + let path = Path::new(&file_path); + assert!(path.exists()); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()).unwrap()); + } + + #[test] + fn test_basic_native_fs_file_exists() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()).unwrap()); + } + + #[test] + fn test_basic_native_fs_dir_exists() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let dir_path = tmpdir.path().join("testdir"); + assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .create_dir(dir_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + assert!(NATIVE_FS + .is_dir(dir_path.as_path().to_str().unwrap()) + .unwrap()); + } + + #[test] + fn test_basic_native_fs_remove_file() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .expect("creating file failed"); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .remove_file(file_path.to_str().unwrap()) + .expect("removing file failed"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + } + + #[test] + fn test_basic_native_fs_write() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()).unwrap()); + println!("{}", file_path.to_str().unwrap()); + let write_data = "hello world\n"; + NATIVE_FS + .write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes()) + .expect("writing to file failed"); + let read_back = fs::read_to_string(file_path).expect("reading back data failed"); + assert_eq!(read_back, write_data); + } + + #[test] + fn test_basic_native_fs_read() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap()).unwrap()); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()).unwrap()); + println!("{}", file_path.to_str().unwrap()); + let write_data = "hello world\n"; + NATIVE_FS + .write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes()) + .expect("writing to file failed"); + let read_back = fs::read_to_string(file_path).expect("reading back data failed"); + assert_eq!(read_back, write_data); + } + + #[test] + fn test_truncate_file() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .expect("creating file failed"); + fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap(); + assert_eq!(fs::read(file_path.clone()).unwrap(), [1, 2, 3, 4]); + NATIVE_FS + .truncate_file(file_path.to_str().unwrap()) + .unwrap(); + assert_eq!(fs::read(file_path.clone()).unwrap(), []); + } + + #[test] + fn test_remove_dir() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let dir_path = tmpdir.path().join("testdir"); + assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .create_dir(dir_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + NATIVE_FS + .remove_dir(dir_path.to_str().unwrap(), false) + .unwrap(); + assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + } + + #[test] + fn test_read_file() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .expect("creating file failed"); + fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap(); + let read_buf: &mut [u8] = &mut [0; 4]; + NATIVE_FS + .read_data(file_path.to_str().unwrap(), 0, 4, read_buf) + .unwrap(); + assert_eq!([1, 2, 3, 4], read_buf); + NATIVE_FS + .write_data(file_path.to_str().unwrap(), 4, &[5, 6, 7, 8]) + .expect("writing to file failed"); + NATIVE_FS + .read_data(file_path.to_str().unwrap(), 2, 4, read_buf) + .unwrap(); + assert_eq!([3, 4, 5, 6], read_buf); + } + + #[test] + fn test_remove_which_does_not_exist() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 4, &mut [0; 4]); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::FileDoesNotExist = error { + assert_eq!(error.to_string(), "file does not exist"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_file_already_exists() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = + NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed")); + assert!(result.is_ok()); + let result = + NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed")); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::FileAlreadyExists = error { + assert_eq!(error.to_string(), "file already exists"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_remove_file_with_dir_api() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::IsNotDirectory = error { + assert_eq!(error.to_string(), "is not a directory"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_remove_dir_remove_all() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let dir_path = tmpdir.path().join("test"); + NATIVE_FS + .create_dir(dir_path.to_str().expect("getting str for file failed")) + .unwrap(); + let file_path = dir_path.as_path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + let result = NATIVE_FS.remove_dir(dir_path.to_str().unwrap(), true); + assert!(result.is_ok()); + assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()).unwrap()); + } + + #[test] + fn test_remove_dir_with_file_api() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test"); + NATIVE_FS + .create_dir(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + let result = NATIVE_FS.remove_file(file_path.to_str().unwrap()); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::IsNotFile = error { + assert_eq!(error.to_string(), "is not a file"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_remove_dir_which_does_not_exist() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test"); + let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::DirDoesNotExist = error { + assert_eq!(error.to_string(), "directory does not exist"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_remove_file_which_does_not_exist() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = NATIVE_FS.remove_file(file_path.to_str().unwrap()); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::FileDoesNotExist = error { + assert_eq!(error.to_string(), "file does not exist"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_truncate_file_which_does_not_exist() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap()); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::FileDoesNotExist = error { + assert_eq!(error.to_string(), "file does not exist"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_truncate_file_on_directory() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test"); + NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap(); + let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap()); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::IsNotFile = error { + assert_eq!(error.to_string(), "is not a file"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_byte_conversion_error_when_reading() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 2, &mut []); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::ByteConversion(byte_conv_error) = error { + if let ByteConversionError::ToSliceTooSmall { found, expected } = byte_conv_error { + assert_eq!(found, 0); + assert_eq!(expected, 2); + } else { + panic!("unexpected error"); + } + assert_eq!( + error.to_string(), + format!("filestore error: {}", byte_conv_error) + ); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_read_file_on_dir() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let dir_path = tmpdir.path().join("test"); + NATIVE_FS + .create_dir(dir_path.to_str().expect("getting str for file failed")) + .unwrap(); + let result = NATIVE_FS.read_data(dir_path.to_str().unwrap(), 0, 4, &mut [0; 4]); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::IsNotFile = error { + assert_eq!(error.to_string(), "is not a file"); + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_write_file_non_existing() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::FileDoesNotExist = error { + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_write_file_on_dir() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test"); + NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap(); + let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::IsNotFile = error { + } else { + panic!("unexpected error"); + } + } + + #[test] + fn test_filename_extraction() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + NativeFilestore::filename_from_full_path(file_path.to_str().unwrap()); + } + + #[test] + fn test_modular_checksum() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("mod-crc.bin"); + fs::write(file_path.as_path(), EXAMPLE_DATA_CFDP).expect("writing test file failed"); + // Kind of re-writing the modular checksum impl here which we are trying to test, but the + // numbers/correctness were verified manually using calculators, so this is okay. + let mut checksum: u32 = 0; + let mut buffer: [u8; 4] = [0; 4]; + for i in 0..3 { + buffer = EXAMPLE_DATA_CFDP[i * 4..(i + 1) * 4].try_into().unwrap(); + checksum = checksum.wrapping_add(u32::from_be_bytes(buffer)); + } + buffer[0..3].copy_from_slice(&EXAMPLE_DATA_CFDP[12..15]); + buffer[3] = 0; + checksum = checksum.wrapping_add(u32::from_be_bytes(buffer)); + let mut verif_buf: [u8; 32] = [0; 32]; + let result = NATIVE_FS.checksum_verify( + file_path.to_str().unwrap(), + ChecksumType::Modular, + checksum, + &mut verif_buf, + ); + assert!(result.is_ok()); + } + + #[test] + fn test_null_checksum_impl() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("mod-crc.bin"); + // The file to check does not even need to exist, and the verification buffer can be + // empty: the null checksum is always yields the same result. + let result = NATIVE_FS.checksum_verify( + file_path.to_str().unwrap(), + ChecksumType::NullChecksum, + 0, + &mut [], + ); + assert!(result.is_ok()); + assert!(result.unwrap()); + } + + #[test] + fn test_checksum_not_implemented() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("mod-crc.bin"); + // The file to check does not even need to exist, and the verification buffer can be + // empty: the null checksum is always yields the same result. + let result = NATIVE_FS.checksum_verify( + file_path.to_str().unwrap(), + ChecksumType::Crc32Proximity1, + 0, + &mut [], + ); + assert!(result.is_err()); + let error = result.unwrap_err(); + if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error { + assert_eq!( + error.to_string(), + format!("checksum {:?} not implemented", cksum_type) + ); + } else { + panic!("unexpected error"); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1b31487 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1450 @@ +//! This module contains the implementation of the CFDP high level abstractions as specified in +//! CCSDS 727.0-B-5. +#![no_std] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[cfg(feature = "alloc")] +extern crate alloc; +#[cfg(any(feature = "std", test))] +extern crate std; + +#[cfg(feature = "std")] +pub mod dest; +#[cfg(feature = "alloc")] +pub mod filestore; +pub mod request; +#[cfg(feature = "std")] +pub mod source; +pub mod time; +pub mod user; + +use crate::time::CountdownProvider; +use core::{cell::RefCell, fmt::Debug, hash::Hash}; +use crc::{Crc, CRC_32_CKSUM}; +#[cfg(feature = "std")] +use hashbrown::HashMap; + +#[cfg(feature = "alloc")] +pub use alloc_mod::*; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use spacepackets::{ + cfdp::{ + pdu::{FileDirectiveType, PduError, PduHeader}, + ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode, + }, + util::{UnsignedByteField, UnsignedEnum}, +}; +#[cfg(feature = "std")] +use std::time::Duration; +#[cfg(feature = "std")] +pub use std_mod::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntityType { + Sending, + Receiving, +} + +pub enum TimerContext { + CheckLimit { + local_id: UnsignedByteField, + remote_id: UnsignedByteField, + entity_type: EntityType, + }, + NakActivity { + expiry_time_seconds: f32, + }, + PositiveAck { + expiry_time_seconds: f32, + }, +} + +/// A generic trait which allows CFDP entities to create check timers which are required to +/// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2 +/// and 4.6.3.3. +/// +/// This trait also allows the creation of different check timers depending on context and purpose +/// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or +/// other factors. +/// +/// The countdown timer is used by 3 mechanisms of the CFDP protocol. +/// +/// ## 1. Check limit handling +/// +/// The first mechanism is the check limit handling for unacknowledged transfers as specified +/// in 4.6.3.2 and 4.6.3.3 of the CFDP standard. +/// For this mechanism, the timer has different functionality depending on whether +/// the using entity is the sending entity or the receiving entity for the unacknowledged +/// transmission mode. +/// +/// For the sending entity, this timer determines the expiry period for declaring a check limit +/// fault after sending an EOF PDU with requested closure. This allows a timeout of the transfer. +/// Also see 4.6.3.2 of the CFDP standard. +/// +/// For the receiving entity, this timer determines the expiry period for incrementing a check +/// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order +/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. +/// +/// ## 2. NAK activity limit +/// +/// The timer will be used to perform the NAK activity check as specified in 4.6.4.7 of the CFDP +/// standard. The expiration period will be provided by the NAK timer expiration limit of the +/// remote entity configuration. +/// +/// ## 3. Positive ACK procedures +/// +/// 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 CheckTimerProviderCreator { + type CheckTimer: CountdownProvider; + + fn create_check_timer_provider(&self, timer_context: TimerContext) -> Self::CheckTimer; +} + +/// 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. +/// +/// ## Notes on Positive Acknowledgment Procedures +/// +/// The `positive_ack_timer_interval_seconds` and `positive_ack_timer_expiration_limit` will +/// be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending +/// entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU). +/// Once the expected ACK response has not been received for that interval, as counter will be +/// incremented and the timer will be reset. Once the counter exceeds the +/// `positive_ack_timer_expiration_limit`, a Positive ACK Limit Reached fault will be declared. +/// +/// ## Notes on Deferred Lost Segment Procedures +/// +/// This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After +/// issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is +/// reset when missing segments or missing metadata is received. The timer will be deactivated if +/// all missing data is received. If the timer expires, a new NAK sequence will be issued and a +/// counter will be incremented, which can lead to a NAK Limit Reached fault being declared. +/// +/// ## Fields +/// +/// * `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. +/// * `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. +/// * `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. +/// * `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 +/// transaction cancellation. Defaults to False. +/// * `default_crc_type` - Default checksum type used to calculate for all file transmissions to +/// this remote entity. +/// * `check_limit` - This timer determines the expiry period for incrementing a check counter +/// after an EOF PDU is received for an incomplete file transfer. This allows out-of-order +/// 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. +/// * `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. +/// * `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. +/// * `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. +/// * `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. +#[derive(Debug, Copy, Clone)] +pub struct RemoteEntityConfig { + pub entity_id: UnsignedByteField, + pub max_packet_len: usize, + pub max_file_segment_len: Option, + pub closure_requested_by_default: bool, + 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_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_expiration_limit: u32, +} + +impl RemoteEntityConfig { + pub fn new_with_default_values( + entity_id: UnsignedByteField, + max_packet_len: usize, + closure_requested_by_default: bool, + crc_on_transmission_by_default: bool, + default_transmission_mode: TransmissionMode, + default_crc_type: ChecksumType, + ) -> Self { + Self { + entity_id, + max_file_segment_len: None, + max_packet_len, + closure_requested_by_default, + crc_on_transmission_by_default, + default_transmission_mode, + default_crc_type, + check_limit: 2, + positive_ack_timer_interval_seconds: 10.0, + positive_ack_timer_expiration_limit: 2, + disposition_on_cancellation: false, + immediate_nak_mode: true, + nak_timer_interval_seconds: 10.0, + nak_timer_expiration_limit: 2, + } + } +} + +pub trait RemoteEntityConfigProvider { + /// 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; +} + +#[cfg(feature = "std")] +#[derive(Default)] +pub struct StdRemoteEntityConfigProvider(pub HashMap); + +#[cfg(feature = "std")] +impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider { + 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 remove_config(&mut self, remote_id: u64) -> bool { + self.0.remove(&remote_id).is_some() + } +} + +#[cfg(feature = "alloc")] +#[derive(Default)] +pub struct VecRemoteEntityConfigProvider(pub alloc::vec::Vec); + +#[cfg(feature = "alloc")] +impl RemoteEntityConfigProvider for VecRemoteEntityConfigProvider { + 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) -> bool { + self.0.push(*cfg); + true + } + + 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 + } +} + +impl RemoteEntityConfigProvider for RemoteEntityConfig { + fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { + if remote_id == self.entity_id.value() { + return Some(self); + } + None + } + + fn get_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> { + if remote_id == self.entity_id.value() { + return Some(self); + } + None + } + + fn add_config(&mut self, _cfg: &RemoteEntityConfig) -> bool { + false + } + + fn remove_config(&mut self, _remote_id: u64) -> bool { + false + } +} + +/// 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 [DefaultFaultHandler] 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, + ); + + fn notice_of_cancellation_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ); + + fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); + + fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); +} + +#[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, + ) { + } + + fn notice_of_cancellation_cb( + &mut self, + _transaction_id: TransactionId, + _cond: ConditionCode, + _progress: u64, + ) { + } + + fn abandoned_cb( + &mut self, + _transaction_id: TransactionId, + _cond: ConditionCode, + _progress: u64, + ) { + } + + fn ignore_cb(&mut self, _transaction_id: TransactionId, _cond: ConditionCode, _progress: u64) {} +} + +/// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP +/// standard. +/// +/// 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 [UserFaultHandler]. +/// +/// Some note on the provided default settings: +/// +/// - Checksum failures will be ignored by default. This is because for unacknowledged transfers, +/// cancelling the transfer immediately would interfere with the check limit mechanism specified +/// in chapter 4.6.3.3. +/// - Unsupported checksum types will also be ignored by default. Even if the checksum type is +/// not supported the file transfer might still have worked properly. +/// +/// For all other faults, the default fault handling operation will be to cancel the transaction. +/// 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 { + 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 { + fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option { + Some(match conditon_code { + ConditionCode::PositiveAckLimitReached => 0, + ConditionCode::KeepAliveLimitReached => 1, + ConditionCode::InvalidTransmissionMode => 2, + ConditionCode::FilestoreRejection => 3, + ConditionCode::FileChecksumFailure => 4, + ConditionCode::FileSizeError => 5, + ConditionCode::NakLimitReached => 6, + ConditionCode::InactivityDetected => 7, + ConditionCode::CheckLimitReached => 8, + ConditionCode::UnsupportedChecksumType => 9, + _ => return None, + }) + } + + pub fn set_fault_handler( + &mut self, + condition_code: ConditionCode, + fault_handler: FaultHandlerCode, + ) { + let array_idx = Self::condition_code_to_array_index(condition_code); + if array_idx.is_none() { + return; + } + self.handler_array[array_idx.unwrap()] = fault_handler; + } + + pub fn new(user_fault_handler: UserHandler) -> Self { + let mut init_array = [FaultHandlerCode::NoticeOfCancellation; 10]; + init_array + [Self::condition_code_to_array_index(ConditionCode::FileChecksumFailure).unwrap()] = + FaultHandlerCode::IgnoreError; + init_array[Self::condition_code_to_array_index(ConditionCode::UnsupportedChecksumType) + .unwrap()] = FaultHandlerCode::IgnoreError; + Self { + handler_array: init_array, + user_hook: RefCell::new(user_fault_handler), + } + } + + pub fn get_fault_handler(&self, condition_code: ConditionCode) -> FaultHandlerCode { + let array_idx = Self::condition_code_to_array_index(condition_code); + if array_idx.is_none() { + return FaultHandlerCode::IgnoreError; + } + 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()]; + let mut handler_mut = self.user_hook.borrow_mut(); + match fh_code { + FaultHandlerCode::NoticeOfCancellation => { + handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress); + } + FaultHandlerCode::NoticeOfSuspension => { + handler_mut.notice_of_suspension_cb(transaction_id, condition, progress); + } + FaultHandlerCode::IgnoreError => { + handler_mut.ignore_cb(transaction_id, condition, progress); + } + FaultHandlerCode::AbandonTransaction => { + handler_mut.abandoned_cb(transaction_id, condition, progress); + } + } + fh_code + } +} + +pub struct IndicationConfig { + pub eof_sent: bool, + pub eof_recv: bool, + pub file_segment_recv: bool, + pub transaction_finished: bool, + pub suspended: bool, + pub resumed: bool, +} + +impl Default for IndicationConfig { + fn default() -> Self { + Self { + eof_sent: true, + eof_recv: true, + file_segment_recv: true, + transaction_finished: true, + suspended: true, + resumed: true, + } + } +} + +pub struct LocalEntityConfig { + pub id: UnsignedByteField, + pub indication_cfg: IndicationConfig, + pub fault_handler: FaultHandler, +} + +impl LocalEntityConfig { + pub fn new( + id: UnsignedByteField, + indication_cfg: IndicationConfig, + hook: UserFaultHook, + ) -> Self { + Self { + id, + indication_cfg, + fault_handler: FaultHandler::new(hook), + } + } +} + +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 { + &self.fault_handler.user_hook + } +} + +/// Generic error type for sending a PDU via a message queue. +#[cfg(feature = "std")] +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] +pub enum GenericSendError { + #[error("RX disconnected")] + RxDisconnected, + #[error("queue is full, fill count {0:?}")] + QueueFull(Option), + #[error("other send error")] + Other, +} + +#[cfg(feature = "std")] +pub trait PduSendProvider { + fn send_pdu( + &self, + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: &[u8], + ) -> Result<(), GenericSendError>; +} + +#[cfg(feature = "std")] +pub mod std_mod { + use std::sync::mpsc; + + use super::*; + + impl PduSendProvider for mpsc::Sender { + fn send_pdu( + &self, + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: &[u8], + ) -> Result<(), GenericSendError> { + self.send(PduWithInfo::new( + pdu_type, + file_directive_type, + raw_pdu.to_vec(), + )) + .map_err(|_| GenericSendError::RxDisconnected)?; + Ok(()) + } + } + + /// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime. + /// It also assumes that a second accuracy of the check timer period is sufficient. + #[derive(Debug)] + pub struct StdCheckTimer { + expiry_time_seconds: u64, + start_time: std::time::Instant, + } + + impl StdCheckTimer { + pub fn new(expiry_time_seconds: u64) -> Self { + Self { + expiry_time_seconds, + start_time: std::time::Instant::now(), + } + } + + pub fn expiry_time_seconds(&self) -> u64 { + self.expiry_time_seconds + } + } + + impl CountdownProvider for StdCheckTimer { + fn has_expired(&self) -> bool { + let elapsed_time = self.start_time.elapsed(); + if elapsed_time.as_nanos() > self.expiry_time_seconds as u128 * 1_000_000_000 { + return true; + } + false + } + + fn reset(&mut self) { + self.start_time = std::time::Instant::now(); + } + } + + pub struct StdCheckTimerCreator { + pub check_limit_timeout_secs: u64, + } + + impl StdCheckTimerCreator { + pub const fn new(check_limit_timeout_secs: u64) -> Self { + Self { + check_limit_timeout_secs, + } + } + } + + impl Default for StdCheckTimerCreator { + fn default() -> Self { + Self::new(5) + } + } + + impl CheckTimerProviderCreator for StdCheckTimerCreator { + type CheckTimer = StdCheckTimer; + + fn create_check_timer_provider(&self, timer_context: TimerContext) -> Self::CheckTimer { + match timer_context { + TimerContext::CheckLimit { + local_id: _, + remote_id: _, + entity_type: _, + } => StdCheckTimer::new(self.check_limit_timeout_secs), + TimerContext::NakActivity { + expiry_time_seconds, + } => StdCheckTimer::new(Duration::from_secs_f32(expiry_time_seconds).as_secs()), + TimerContext::PositiveAck { + expiry_time_seconds, + } => StdCheckTimer::new(Duration::from_secs_f32(expiry_time_seconds).as_secs()), + } + } + } +} + +/// The CFDP transaction ID of a CFDP transaction consists of the source entity ID and the sequence +/// 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))] +pub struct TransactionId { + source_id: UnsignedByteField, + seq_num: UnsignedByteField, +} + +impl TransactionId { + pub fn new(source_id: UnsignedByteField, seq_num: UnsignedByteField) -> Self { + Self { source_id, seq_num } + } + + pub fn source_id(&self) -> &UnsignedByteField { + &self.source_id + } + + pub fn seq_num(&self) -> &UnsignedByteField { + &self.seq_num + } +} + +impl Hash for TransactionId { + fn hash(&self, state: &mut H) { + self.source_id.value().hash(state); + self.seq_num.value().hash(state); + } +} + +impl PartialEq for TransactionId { + fn eq(&self, other: &Self) -> bool { + self.source_id.value() == other.source_id.value() + && self.seq_num.value() == other.seq_num.value() + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum State { + Idle = 0, + Busy = 1, + Suspended = 2, +} + +pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PacketTarget { + SourceEntity, + DestEntity, +} + +pub trait PduProvider { + fn pdu_type(&self) -> PduType; + fn file_directive_type(&self) -> Option; + fn pdu(&self) -> &[u8]; + fn packet_target(&self) -> Result; +} + +pub struct DummyPduProvider(()); + +impl PduProvider for DummyPduProvider { + fn pdu_type(&self) -> PduType { + PduType::FileData + } + + fn file_directive_type(&self) -> Option { + None + } + + fn pdu(&self) -> &[u8] { + &[] + } + + fn packet_target(&self) -> Result { + Ok(PacketTarget::SourceEntity) + } +} + +/// This is a helper struct which contains base information about a particular PDU packet. +/// This is also necessary information for CFDP packet routing. For example, some packet types +/// like file data PDUs can only be used by CFDP source entities. +pub struct PacketInfo<'raw_packet> { + pdu_type: PduType, + file_directive_type: Option, + packet_len: usize, + raw_packet: &'raw_packet [u8], +} + +pub fn determine_packet_target( + file_directive_type: Option, + raw_pdu: &[u8], +) -> Result { + if file_directive_type.is_none() { + return Ok(PacketTarget::DestEntity); + } + let (_, header_len) = PduHeader::from_bytes(raw_pdu)?; + let file_directive_type = file_directive_type.unwrap(); + let packet_target = + match file_directive_type { + // Section c) of 4.5.3: These PDUs should always be targeted towards the file sender a.k.a. + // the source handler + FileDirectiveType::NakPdu + | FileDirectiveType::FinishedPdu + | FileDirectiveType::KeepAlivePdu => PacketTarget::SourceEntity, + // Section b) of 4.5.3: These PDUs should always be targeted towards the file receiver a.k.a. + // the destination handler + FileDirectiveType::MetadataPdu + | FileDirectiveType::EofPdu + | FileDirectiveType::PromptPdu => PacketTarget::DestEntity, + // Section a): Recipient depends of the type of PDU that is being acknowledged. We can simply + // extract the PDU type from the raw stream. If it is an EOF PDU, this packet is passed to + // the source handler, for a Finished PDU, it is passed to the destination handler. + FileDirectiveType::AckPdu => { + let acked_directive = FileDirectiveType::try_from(raw_pdu[header_len + 1]) + .map_err(|_| PduError::InvalidDirectiveType { + found: raw_pdu[header_len], + expected: None, + })?; + if acked_directive == FileDirectiveType::EofPdu { + PacketTarget::SourceEntity + } else if acked_directive == FileDirectiveType::FinishedPdu { + PacketTarget::DestEntity + } else { + // TODO: Maybe a better error? This might be confusing.. + return Err(PduError::InvalidDirectiveType { + found: raw_pdu[header_len + 1], + expected: None, + }); + } + } + }; + Ok(packet_target) +} + +impl<'raw> PacketInfo<'raw> { + pub fn new(raw_packet: &'raw [u8]) -> Result { + let (pdu_header, header_len) = PduHeader::from_bytes(raw_packet)?; + if pdu_header.pdu_type() == PduType::FileData { + return Ok(Self { + pdu_type: pdu_header.pdu_type(), + file_directive_type: None, + packet_len: pdu_header.pdu_len(), + raw_packet, + }); + } + if pdu_header.pdu_datafield_len() < 1 { + return Err(PduError::FormatError); + } + // 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). + // The routing is based on section 4.5 of the CFDP standard which specifies the PDU forwarding + // procedure. + let directive = FileDirectiveType::try_from(raw_packet[header_len]).map_err(|_| { + PduError::InvalidDirectiveType { + found: raw_packet[header_len], + expected: None, + } + })?; + Ok(Self { + pdu_type: pdu_header.pdu_type(), + file_directive_type: Some(directive), + packet_len: pdu_header.pdu_len(), + raw_packet, + }) + } + + pub fn raw_packet(&self) -> &[u8] { + &self.raw_packet[0..self.packet_len] + } +} + +impl PduProvider for PacketInfo<'_> { + fn pdu_type(&self) -> PduType { + self.pdu_type + } + + fn file_directive_type(&self) -> Option { + self.file_directive_type + } + + fn pdu(&self) -> &[u8] { + self.raw_packet + } + + fn packet_target(&self) -> Result { + determine_packet_target(self.file_directive_type, self.raw_packet) + } +} + +#[cfg(feature = "alloc")] +pub mod alloc_mod { + use spacepackets::cfdp::{ + pdu::{FileDirectiveType, PduError}, + PduType, + }; + + use crate::{determine_packet_target, PacketTarget, PduProvider}; + + pub struct PduWithInfo { + pub pdu_type: PduType, + pub file_directive_type: Option, + pub pdu: alloc::vec::Vec, + } + + impl PduWithInfo { + pub fn new( + pdu_type: PduType, + file_directive_type: Option, + pdu: alloc::vec::Vec, + ) -> Self { + Self { + pdu_type, + file_directive_type, + pdu, + } + } + } + + impl PduProvider for PduWithInfo { + fn pdu_type(&self) -> PduType { + self.pdu_type + } + + fn file_directive_type(&self) -> Option { + self.file_directive_type + } + + fn pdu(&self) -> &[u8] { + &self.pdu + } + + fn packet_target(&self) -> Result { + determine_packet_target(self.file_directive_type, &self.pdu) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use core::cell::RefCell; + + use alloc::{collections::VecDeque, string::String, vec::Vec}; + use spacepackets::{ + cfdp::{ + lv::Lv, + pdu::{ + eof::EofPdu, + file_data::FileDataPdu, + metadata::{MetadataGenericParams, MetadataPduCreator}, + CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket, + }, + ChecksumType, ConditionCode, PduType, TransmissionMode, + }, + util::{UnsignedByteField, UnsignedByteFieldU16, UnsignedByteFieldU8, UnsignedEnum}, + }; + use user::{CfdpUser, OwnedMetadataRecvdParams, TransactionFinishedParams}; + + use crate::{PacketTarget, StdCheckTimer}; + + use super::*; + + pub const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); + pub const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + + pub struct FileSegmentRecvdParamsNoSegMetadata { + #[allow(dead_code)] + pub id: TransactionId, + pub offset: u64, + pub length: usize, + } + + #[derive(Default)] + pub struct TestCfdpUser { + pub next_expected_seq_num: u64, + pub expected_full_src_name: String, + pub expected_full_dest_name: String, + pub expected_file_size: u64, + pub transaction_indication_call_count: u32, + pub eof_sent_call_count: u32, + pub eof_recvd_call_count: u32, + pub finished_indic_queue: VecDeque, + pub metadata_recv_queue: VecDeque, + pub file_seg_recvd_queue: VecDeque, + } + + impl TestCfdpUser { + pub fn new( + next_expected_seq_num: u64, + expected_full_src_name: String, + expected_full_dest_name: String, + expected_file_size: u64, + ) -> Self { + Self { + next_expected_seq_num, + expected_full_src_name, + expected_full_dest_name, + expected_file_size, + transaction_indication_call_count: 0, + eof_recvd_call_count: 0, + eof_sent_call_count: 0, + finished_indic_queue: VecDeque::new(), + metadata_recv_queue: VecDeque::new(), + file_seg_recvd_queue: VecDeque::new(), + } + } + + pub fn generic_id_check(&self, id: &crate::TransactionId) { + assert_eq!(id.source_id, LOCAL_ID.into()); + assert_eq!(id.seq_num().value(), self.next_expected_seq_num); + } + } + + impl CfdpUser for TestCfdpUser { + fn transaction_indication(&mut self, id: &crate::TransactionId) { + self.generic_id_check(id); + self.transaction_indication_call_count += 1; + } + + fn eof_sent_indication(&mut self, id: &crate::TransactionId) { + self.generic_id_check(id); + self.eof_sent_call_count += 1; + } + + fn transaction_finished_indication( + &mut self, + finished_params: &crate::user::TransactionFinishedParams, + ) { + self.generic_id_check(&finished_params.id); + self.finished_indic_queue.push_back(*finished_params); + } + + fn metadata_recvd_indication( + &mut self, + md_recvd_params: &crate::user::MetadataReceivedParams, + ) { + self.generic_id_check(&md_recvd_params.id); + assert_eq!( + String::from(md_recvd_params.src_file_name), + self.expected_full_src_name + ); + assert_eq!( + String::from(md_recvd_params.dest_file_name), + self.expected_full_dest_name + ); + assert_eq!(md_recvd_params.msgs_to_user.len(), 0); + assert_eq!(md_recvd_params.source_id, LOCAL_ID.into()); + assert_eq!(md_recvd_params.file_size, self.expected_file_size); + self.metadata_recv_queue.push_back(md_recvd_params.into()); + } + + fn file_segment_recvd_indication( + &mut self, + segment_recvd_params: &crate::user::FileSegmentRecvdParams, + ) { + self.generic_id_check(&segment_recvd_params.id); + self.file_seg_recvd_queue + .push_back(FileSegmentRecvdParamsNoSegMetadata { + id: segment_recvd_params.id, + offset: segment_recvd_params.offset, + length: segment_recvd_params.length, + }) + } + + fn report_indication(&mut self, _id: &crate::TransactionId) {} + + fn suspended_indication( + &mut self, + _id: &crate::TransactionId, + _condition_code: ConditionCode, + ) { + panic!("unexpected suspended indication"); + } + + fn resumed_indication(&mut self, _id: &crate::TransactionId, _progresss: u64) {} + + fn fault_indication( + &mut self, + _id: &crate::TransactionId, + _condition_code: ConditionCode, + _progress: u64, + ) { + panic!("unexpected fault indication"); + } + + fn abandoned_indication( + &mut self, + _id: &crate::TransactionId, + _condition_code: ConditionCode, + _progress: u64, + ) { + panic!("unexpected abandoned indication"); + } + + fn eof_recvd_indication(&mut self, id: &crate::TransactionId) { + self.generic_id_check(id); + self.eof_recvd_call_count += 1; + } + } + + #[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 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)) + } + + 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 abandoned_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ) { + self.abandoned_queue + .push_back((transaction_id, cond, progress)) + } + + fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { + self.ignored_queue + .push_back((transaction_id, cond, progress)) + } + } + + impl TestFaultHandler { + pub(crate) fn suspension_queue_empty(&self) -> bool { + self.notice_of_suspension_queue.is_empty() + } + pub(crate) fn cancellation_queue_empty(&self) -> bool { + self.notice_of_cancellation_queue.is_empty() + } + pub(crate) fn ignored_queue_empty(&self) -> bool { + self.ignored_queue.is_empty() + } + pub(crate) fn abandoned_queue_empty(&self) -> bool { + self.abandoned_queue.is_empty() + } + pub(crate) fn all_queues_empty(&self) -> bool { + self.suspension_queue_empty() + && self.cancellation_queue_empty() + && self.ignored_queue_empty() + && self.abandoned_queue_empty() + } + } + + pub struct SentPdu { + pub pdu_type: PduType, + pub file_directive_type: Option, + pub raw_pdu: Vec, + } + + #[derive(Default)] + pub struct TestCfdpSender { + pub packet_queue: RefCell>, + } + + impl PduSendProvider for TestCfdpSender { + fn send_pdu( + &self, + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: &[u8], + ) -> Result<(), GenericSendError> { + self.packet_queue.borrow_mut().push_back(SentPdu { + pdu_type, + file_directive_type, + raw_pdu: raw_pdu.to_vec(), + }); + Ok(()) + } + } + + impl TestCfdpSender { + pub fn retrieve_next_pdu(&self) -> Option { + self.packet_queue.borrow_mut().pop_front() + } + pub fn queue_empty(&self) -> bool { + self.packet_queue.borrow_mut().is_empty() + } + } + + pub fn basic_remote_cfg_table( + dest_id: impl Into, + max_packet_len: usize, + crc_on_transmission_by_default: bool, + ) -> StdRemoteEntityConfigProvider { + let mut table = StdRemoteEntityConfigProvider::default(); + let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( + dest_id.into(), + max_packet_len, + true, + crc_on_transmission_by_default, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + table.add_config(&remote_entity_cfg); + table + } + + fn generic_pdu_header() -> PduHeader { + let pdu_conf = CommonPduConfig::default(); + PduHeader::new_no_file_data(pdu_conf, 0) + } + + #[test] + fn test_transaction_id() { + let transaction_id = TransactionId::new( + UnsignedByteFieldU16::new(1).into(), + UnsignedByteFieldU16::new(2).into(), + ); + assert_eq!(transaction_id.source_id().value(), 1); + assert_eq!(transaction_id.seq_num().value(), 2); + } + + #[test] + fn test_metadata_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let metadata_params = MetadataGenericParams::default(); + let src_file_name = "hello.txt"; + let dest_file_name = "hello-dest.txt"; + let src_lv = Lv::new_from_str(src_file_name).unwrap(); + let dest_lv = Lv::new_from_str(dest_file_name).unwrap(); + let metadata_pdu = + MetadataPduCreator::new_no_opts(pdu_header, metadata_params, src_lv, dest_lv); + metadata_pdu + .write_to_bytes(&mut buf) + .expect("writing metadata PDU failed"); + + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!(packet_info.pdu_type(), PduType::FileDirective); + assert!(packet_info.file_directive_type().is_some()); + assert_eq!( + packet_info.file_directive_type().unwrap(), + FileDirectiveType::MetadataPdu + ); + assert_eq!( + packet_info.raw_packet(), + &buf[0..metadata_pdu.len_written()] + ); + assert_eq!( + packet_info.packet_target().unwrap(), + PacketTarget::DestEntity + ); + } + + #[test] + fn test_filedata_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let file_data_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 0, &[]); + file_data_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!( + packet_info.raw_packet(), + &buf[0..file_data_pdu.len_written()] + ); + assert_eq!(packet_info.pdu_type(), PduType::FileData); + assert!(packet_info.file_directive_type().is_none()); + assert_eq!( + packet_info.packet_target().unwrap(), + PacketTarget::DestEntity + ); + } + + #[test] + fn test_eof_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let eof_pdu = EofPdu::new_no_error(pdu_header, 0, 0); + eof_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!(packet_info.pdu_type(), PduType::FileDirective); + assert!(packet_info.file_directive_type().is_some()); + assert_eq!(packet_info.raw_packet(), &buf[0..eof_pdu.len_written()]); + assert_eq!( + packet_info.file_directive_type().unwrap(), + FileDirectiveType::EofPdu + ); + } + + #[test] + fn test_std_check_timer() { + let mut std_check_timer = StdCheckTimer::new(1); + assert!(!std_check_timer.has_expired()); + assert_eq!(std_check_timer.expiry_time_seconds(), 1); + std::thread::sleep(Duration::from_millis(800)); + assert!(!std_check_timer.has_expired()); + std::thread::sleep(Duration::from_millis(205)); + assert!(std_check_timer.has_expired()); + std_check_timer.reset(); + assert!(!std_check_timer.has_expired()); + } + + #[test] + fn test_std_check_timer_creator() { + let std_check_timer_creator = StdCheckTimerCreator::new(1); + let check_timer = + std_check_timer_creator.create_check_timer_provider(TimerContext::NakActivity { + expiry_time_seconds: 1.0, + }); + assert_eq!(check_timer.expiry_time_seconds(), 1); + } + + #[test] + fn test_remote_cfg_provider_single() { + let mut remote_entity_cfg = RemoteEntityConfig::new_with_default_values( + REMOTE_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + let remote_entity_retrieved = remote_entity_cfg.get(REMOTE_ID.value()).unwrap(); + assert_eq!(remote_entity_retrieved.entity_id, REMOTE_ID.into()); + assert_eq!(remote_entity_retrieved.max_packet_len, 1024); + assert!(remote_entity_retrieved.closure_requested_by_default); + assert!(!remote_entity_retrieved.crc_on_transmission_by_default); + assert_eq!( + remote_entity_retrieved.default_crc_type, + ChecksumType::Crc32 + ); + let remote_entity_mut = remote_entity_cfg.get_mut(REMOTE_ID.value()).unwrap(); + assert_eq!(remote_entity_mut.entity_id, REMOTE_ID.into()); + let dummy = RemoteEntityConfig::new_with_default_values( + LOCAL_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + assert!(!remote_entity_cfg.add_config(&dummy)); + // Removal is no-op. + assert!(!remote_entity_cfg.remove_config(REMOTE_ID.value())); + 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. + assert!(remote_entity_cfg.get(LOCAL_ID.value()).is_none()); + assert!(remote_entity_cfg.get_mut(LOCAL_ID.value()).is_none()); + } + + #[test] + fn test_remote_cfg_provider_std() { + let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( + REMOTE_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + let mut remote_cfg_provider = StdRemoteEntityConfigProvider::default(); + assert!(remote_cfg_provider.0.is_empty()); + remote_cfg_provider.add_config(&remote_entity_cfg); + assert_eq!(remote_cfg_provider.0.len(), 1); + let remote_entity_cfg_2 = RemoteEntityConfig::new_with_default_values( + LOCAL_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + 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_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()); + assert!(remote_cfg_provider.remove_config(REMOTE_ID.value())); + assert_eq!(remote_cfg_provider.0.len(), 1); + let cfg_1_mut = remote_cfg_provider.get_mut(LOCAL_ID.value()).unwrap(); + cfg_1_mut.default_crc_type = ChecksumType::Crc32C; + assert!(!remote_cfg_provider.remove_config(REMOTE_ID.value())); + assert!(remote_cfg_provider.get_mut(REMOTE_ID.value()).is_none()); + } + + #[test] + fn test_remote_cfg_provider_vector() { + let mut remote_cfg_provider = VecRemoteEntityConfigProvider::default(); + let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( + REMOTE_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + assert!(remote_cfg_provider.0.is_empty()); + remote_cfg_provider.add_config(&remote_entity_cfg); + assert_eq!(remote_cfg_provider.0.len(), 1); + let remote_entity_cfg_2 = RemoteEntityConfig::new_with_default_values( + LOCAL_ID.into(), + 1024, + true, + false, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + 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_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()); + assert!(remote_cfg_provider.remove_config(REMOTE_ID.value())); + assert_eq!(remote_cfg_provider.0.len(), 1); + let cfg_1_mut = remote_cfg_provider.get_mut(LOCAL_ID.value()).unwrap(); + cfg_1_mut.default_crc_type = ChecksumType::Crc32C; + assert!(!remote_cfg_provider.remove_config(REMOTE_ID.value())); + assert!(remote_cfg_provider.get_mut(REMOTE_ID.value()).is_none()); + } + + #[test] + fn dummy_fault_hook_test() { + let mut user_hook_dummy = DummyFaultHook::default(); + let transaction_id = TransactionId::new( + 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); + } + + #[test] + fn dummy_pdu_provider_test() { + 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.packet_target(), + Ok(PacketTarget::SourceEntity) + ); + } + #[test] + fn test_fault_handler_checksum_error_ignored_by_default() { + let fault_handler = FaultHandler::new(TestFaultHandler::default()); + assert_eq!( + fault_handler.get_fault_handler(ConditionCode::FileChecksumFailure), + FaultHandlerCode::IgnoreError + ); + } + + #[test] + fn test_fault_handler_unsupported_checksum_ignored_by_default() { + let fault_handler = FaultHandler::new(TestFaultHandler::default()); + assert_eq!( + fault_handler.get_fault_handler(ConditionCode::UnsupportedChecksumType), + FaultHandlerCode::IgnoreError + ); + } + + #[test] + fn test_fault_handler_basic() { + let mut fault_handler = FaultHandler::new(TestFaultHandler::default()); + assert_eq!( + fault_handler.get_fault_handler(ConditionCode::FileChecksumFailure), + FaultHandlerCode::IgnoreError + ); + fault_handler.set_fault_handler( + ConditionCode::FileChecksumFailure, + FaultHandlerCode::NoticeOfCancellation, + ); + assert_eq!( + fault_handler.get_fault_handler(ConditionCode::FileChecksumFailure), + FaultHandlerCode::NoticeOfCancellation + ); + } + + #[test] + fn transaction_id_hashable_usable_as_map_key() { + let mut map = HashMap::new(); + let transaction_id_0 = TransactionId::new( + UnsignedByteFieldU8::new(1).into(), + UnsignedByteFieldU8::new(2).into(), + ); + map.insert(transaction_id_0, 5_u32); + } +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..1d51941 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,777 @@ +use spacepackets::{ + cfdp::{ + tlv::{GenericTlv, Tlv, TlvType}, + SegmentationControl, TransmissionMode, + }, + util::UnsignedByteField, +}; + +#[cfg(feature = "alloc")] +pub use alloc_mod::*; + +#[derive(Debug, PartialEq, Eq)] +pub struct FilePathTooLarge(pub usize); + +/// This trait is an abstraction for different Put Request structures which can be used +/// by Put Request consumers. +pub trait ReadablePutRequest { + fn destination_id(&self) -> UnsignedByteField; + fn source_file(&self) -> Option<&str>; + fn dest_file(&self) -> Option<&str>; + fn trans_mode(&self) -> Option; + fn closure_requested(&self) -> Option; + fn seg_ctrl(&self) -> Option; + + fn msgs_to_user(&self) -> Option>; + fn fault_handler_overrides(&self) -> Option>; + fn flow_label(&self) -> Option; + fn fs_requests(&self) -> Option>; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct PutRequest<'src_file, 'dest_file, 'msgs_to_user, 'fh_ovrds, 'flow_label, 'fs_requests> { + pub destination_id: UnsignedByteField, + source_file: Option<&'src_file str>, + dest_file: Option<&'dest_file str>, + pub trans_mode: Option, + pub closure_requested: Option, + pub seg_ctrl: Option, + pub msgs_to_user: Option<&'msgs_to_user [Tlv<'msgs_to_user>]>, + pub fault_handler_overrides: Option<&'fh_ovrds [Tlv<'fh_ovrds>]>, + pub flow_label: Option>, + pub fs_requests: Option<&'fs_requests [Tlv<'fs_requests>]>, +} + +impl<'src_file, 'dest_file, 'msgs_to_user, 'fh_ovrds, 'flow_label, 'fs_requests> + PutRequest<'src_file, 'dest_file, 'msgs_to_user, 'fh_ovrds, 'flow_label, 'fs_requests> +{ + #[allow(clippy::too_many_arguments)] + pub fn new( + destination_id: UnsignedByteField, + source_file: Option<&'src_file str>, + dest_file: Option<&'dest_file str>, + trans_mode: Option, + closure_requested: Option, + seg_ctrl: Option, + msgs_to_user: Option<&'msgs_to_user [Tlv<'msgs_to_user>]>, + fault_handler_overrides: Option<&'fh_ovrds [Tlv<'fh_ovrds>]>, + flow_label: Option>, + fs_requests: Option<&'fs_requests [Tlv<'fs_requests>]>, + ) -> Result { + generic_path_checks(source_file, dest_file)?; + Ok(Self { + destination_id, + source_file, + dest_file, + trans_mode, + closure_requested, + seg_ctrl, + msgs_to_user, + fault_handler_overrides, + flow_label, + fs_requests, + }) + } +} + +impl ReadablePutRequest for PutRequest<'_, '_, '_, '_, '_, '_> { + fn destination_id(&self) -> UnsignedByteField { + self.destination_id + } + + fn source_file(&self) -> Option<&str> { + self.source_file + } + + fn dest_file(&self) -> Option<&str> { + self.dest_file + } + + fn trans_mode(&self) -> Option { + self.trans_mode + } + + fn closure_requested(&self) -> Option { + self.closure_requested + } + + fn seg_ctrl(&self) -> Option { + self.seg_ctrl + } + + fn msgs_to_user(&self) -> Option> { + if let Some(msgs_to_user) = self.msgs_to_user { + return Some(msgs_to_user.iter().copied()); + } + None + } + + fn fault_handler_overrides(&self) -> Option> { + if let Some(fh_overrides) = self.fault_handler_overrides { + return Some(fh_overrides.iter().copied()); + } + None + } + + fn flow_label(&self) -> Option { + self.flow_label + } + + fn fs_requests(&self) -> Option> { + if let Some(fs_requests) = self.msgs_to_user { + return Some(fs_requests.iter().copied()); + } + None + } +} + +pub fn generic_path_checks( + source_file: Option<&str>, + dest_file: Option<&str>, +) -> Result<(), FilePathTooLarge> { + if let Some(src_file) = source_file { + if src_file.len() > u8::MAX as usize { + return Err(FilePathTooLarge(src_file.len())); + } + } + if let Some(dest_file) = dest_file { + if dest_file.len() > u8::MAX as usize { + return Err(FilePathTooLarge(dest_file.len())); + } + } + Ok(()) +} + +impl<'src_file, 'dest_file> PutRequest<'src_file, 'dest_file, 'static, 'static, 'static, 'static> { + pub fn new_regular_request( + dest_id: UnsignedByteField, + source_file: &'src_file str, + dest_file: &'dest_file str, + trans_mode: Option, + closure_requested: Option, + ) -> Result { + generic_path_checks(Some(source_file), Some(dest_file))?; + Ok(Self { + destination_id: dest_id, + source_file: Some(source_file), + dest_file: Some(dest_file), + trans_mode, + closure_requested, + seg_ctrl: None, + msgs_to_user: None, + fault_handler_overrides: None, + flow_label: None, + fs_requests: None, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct TlvWithInvalidType(pub(crate) ()); + +impl<'msgs_to_user> PutRequest<'static, 'static, 'msgs_to_user, 'static, 'static, 'static> { + pub fn new_msgs_to_user_only( + dest_id: UnsignedByteField, + msgs_to_user: &'msgs_to_user [Tlv<'msgs_to_user>], + ) -> Result { + Ok(Self { + destination_id: dest_id, + source_file: None, + dest_file: None, + trans_mode: None, + closure_requested: None, + seg_ctrl: None, + msgs_to_user: Some(msgs_to_user), + fault_handler_overrides: None, + flow_label: None, + fs_requests: None, + }) + } + + /// Uses [generic_tlv_list_type_check] to check the TLV type validity of all TLV fields. + pub fn check_tlv_type_validities(&self) -> bool { + generic_tlv_list_type_check(self.msgs_to_user, TlvType::MsgToUser); + if let Some(flow_label) = &self.flow_label { + if flow_label.tlv_type().is_none() { + return false; + } + if flow_label.tlv_type().unwrap() != TlvType::FlowLabel { + return false; + } + } + generic_tlv_list_type_check(self.fault_handler_overrides, TlvType::FaultHandler); + generic_tlv_list_type_check(self.fs_requests, TlvType::FilestoreRequest); + true + } +} + +pub fn generic_tlv_list_type_check( + opt_tlvs: Option<&[TlvProvider]>, + tlv_type: TlvType, +) -> bool { + if let Some(tlvs) = opt_tlvs { + for tlv in tlvs { + if tlv.tlv_type().is_none() { + return false; + } + if tlv.tlv_type().unwrap() != tlv_type { + return false; + } + } + } + true +} + +#[cfg(feature = "alloc")] +pub mod alloc_mod { + use core::str::Utf8Error; + + use super::*; + use alloc::string::ToString; + use spacepackets::{ + cfdp::tlv::{msg_to_user::MsgToUserTlv, ReadableTlv, TlvOwned, WritableTlv}, + ByteConversionError, + }; + + /// 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))] + pub struct PutRequestOwned { + pub destination_id: UnsignedByteField, + source_file: Option, + dest_file: Option, + pub trans_mode: Option, + pub closure_requested: Option, + pub seg_ctrl: Option, + pub msgs_to_user: Option>, + pub fault_handler_overrides: Option>, + pub flow_label: Option, + pub fs_requests: Option>, + } + + impl PutRequestOwned { + pub fn new_regular_request( + dest_id: UnsignedByteField, + source_file: &str, + dest_file: &str, + trans_mode: Option, + closure_requested: Option, + ) -> Result { + if source_file.len() > u8::MAX as usize { + return Err(FilePathTooLarge(source_file.len())); + } + if dest_file.len() > u8::MAX as usize { + return Err(FilePathTooLarge(dest_file.len())); + } + Ok(Self { + destination_id: dest_id, + source_file: Some(source_file.to_string()), + dest_file: Some(dest_file.to_string()), + trans_mode, + closure_requested, + seg_ctrl: None, + msgs_to_user: None, + fault_handler_overrides: None, + flow_label: None, + fs_requests: None, + }) + } + + pub fn new_msgs_to_user_only( + dest_id: UnsignedByteField, + msgs_to_user: &[MsgToUserTlv<'_>], + ) -> Result { + Ok(Self { + destination_id: dest_id, + source_file: None, + dest_file: None, + trans_mode: None, + closure_requested: None, + seg_ctrl: None, + msgs_to_user: Some(msgs_to_user.iter().map(|msg| msg.tlv.to_owned()).collect()), + fault_handler_overrides: None, + flow_label: None, + fs_requests: None, + }) + } + + /// Uses [generic_tlv_list_type_check] to check the TLV type validity of all TLV fields. + pub fn check_tlv_type_validities(&self) -> bool { + generic_tlv_list_type_check(self.msgs_to_user.as_deref(), TlvType::MsgToUser); + if let Some(flow_label) = &self.flow_label { + if flow_label.tlv_type().is_none() { + return false; + } + if flow_label.tlv_type().unwrap() != TlvType::FlowLabel { + return false; + } + } + generic_tlv_list_type_check( + self.fault_handler_overrides.as_deref(), + TlvType::FaultHandler, + ); + generic_tlv_list_type_check(self.fs_requests.as_deref(), TlvType::FilestoreRequest); + true + } + } + + impl From> for PutRequestOwned { + fn from(req: PutRequest<'_, '_, '_, '_, '_, '_>) -> Self { + Self { + destination_id: req.destination_id, + source_file: req.source_file.map(|s| s.into()), + dest_file: req.dest_file.map(|s| s.into()), + trans_mode: req.trans_mode, + closure_requested: req.closure_requested, + seg_ctrl: req.seg_ctrl, + msgs_to_user: req + .msgs_to_user + .map(|msgs_to_user| msgs_to_user.iter().map(|msg| msg.to_owned()).collect()), + fault_handler_overrides: req + .msgs_to_user + .map(|fh_overides| fh_overides.iter().map(|msg| msg.to_owned()).collect()), + flow_label: req + .flow_label + .map(|flow_label_tlv| flow_label_tlv.to_owned()), + fs_requests: req + .fs_requests + .map(|fs_requests| fs_requests.iter().map(|msg| msg.to_owned()).collect()), + } + } + } + + impl ReadablePutRequest for PutRequestOwned { + fn destination_id(&self) -> UnsignedByteField { + self.destination_id + } + + fn source_file(&self) -> Option<&str> { + self.source_file.as_deref() + } + + fn dest_file(&self) -> Option<&str> { + self.dest_file.as_deref() + } + + fn trans_mode(&self) -> Option { + self.trans_mode + } + + fn closure_requested(&self) -> Option { + self.closure_requested + } + + fn seg_ctrl(&self) -> Option { + self.seg_ctrl + } + + fn msgs_to_user(&self) -> Option> { + 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> { + 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 { + self.flow_label.as_ref().map(|tlv| tlv.as_tlv()) + } + + fn fs_requests(&self) -> Option> { + if let Some(requests) = &self.fs_requests { + return Some(requests.iter().map(|tlv_owned| tlv_owned.as_tlv())); + } + None + } + } + + pub struct StaticPutRequestFields { + pub destination_id: UnsignedByteField, + /// Static buffer to store source file path. + pub source_file_buf: [u8; u8::MAX as usize], + /// Current source path length. + pub source_file_len: usize, + /// Static buffer to store dest file path. + pub dest_file_buf: [u8; u8::MAX as usize], + /// Current destination path length. + pub dest_file_len: usize, + pub trans_mode: Option, + pub closure_requested: Option, + pub seg_ctrl: Option, + } + + impl Default for StaticPutRequestFields { + fn default() -> Self { + Self { + destination_id: UnsignedByteField::new(0, 0), + source_file_buf: [0; u8::MAX as usize], + source_file_len: Default::default(), + dest_file_buf: [0; u8::MAX as usize], + dest_file_len: Default::default(), + trans_mode: Default::default(), + closure_requested: Default::default(), + seg_ctrl: Default::default(), + } + } + } + + impl StaticPutRequestFields { + pub fn clear(&mut self) { + self.destination_id = UnsignedByteField::new(0, 0); + self.source_file_len = 0; + self.dest_file_len = 0; + self.trans_mode = None; + self.closure_requested = None; + self.seg_ctrl = None; + } + } + + /// This is a put request cache structure which can be used to cache [ReadablePutRequest]s + /// without requiring run-time allocation. The user must specify the static buffer sizes used + /// to store TLVs or list of TLVs. + pub struct StaticPutRequestCacher { + pub static_fields: StaticPutRequestFields, + opts_buf: alloc::vec::Vec, + opts_len: usize, // fs_request_start_end_pos: Option<(usize, usize)> + } + + impl StaticPutRequestCacher { + pub fn new(max_len_opts_buf: usize) -> Self { + Self { + static_fields: StaticPutRequestFields::default(), + opts_buf: alloc::vec![0; max_len_opts_buf], + opts_len: 0, + } + } + + pub fn set( + &mut self, + put_request: &impl ReadablePutRequest, + ) -> Result<(), ByteConversionError> { + self.static_fields.destination_id = put_request.destination_id(); + if let Some(source_file) = put_request.source_file() { + if source_file.len() > u8::MAX as usize { + return Err(ByteConversionError::ToSliceTooSmall { + found: self.static_fields.source_file_buf.len(), + expected: source_file.len(), + }); + } + self.static_fields.source_file_buf[..source_file.len()] + .copy_from_slice(source_file.as_bytes()); + self.static_fields.source_file_len = source_file.len(); + } + if let Some(dest_file) = put_request.dest_file() { + if dest_file.len() > u8::MAX as usize { + return Err(ByteConversionError::ToSliceTooSmall { + found: self.static_fields.source_file_buf.len(), + expected: dest_file.len(), + }); + } + self.static_fields.dest_file_buf[..dest_file.len()] + .copy_from_slice(dest_file.as_bytes()); + self.static_fields.dest_file_len = dest_file.len(); + } + self.static_fields.trans_mode = put_request.trans_mode(); + self.static_fields.closure_requested = put_request.closure_requested(); + self.static_fields.seg_ctrl = put_request.seg_ctrl(); + let mut current_idx = 0; + let mut store_tlv = |tlv: &Tlv| { + if current_idx + tlv.len_full() > self.opts_buf.len() { + return Err(ByteConversionError::ToSliceTooSmall { + found: self.opts_buf.len(), + expected: current_idx + tlv.len_full(), + }); + } + // We checked the buffer lengths, so this should never fail. + tlv.write_to_bytes(&mut self.opts_buf[current_idx..current_idx + tlv.len_full()]) + .unwrap(); + current_idx += tlv.len_full(); + Ok(()) + }; + if let Some(fs_req) = put_request.fs_requests() { + for fs_req in fs_req { + store_tlv(&fs_req)?; + } + } + if let Some(msgs_to_user) = put_request.msgs_to_user() { + for msg_to_user in msgs_to_user { + store_tlv(&msg_to_user)?; + } + } + self.opts_len = current_idx; + Ok(()) + } + + pub fn has_source_file(&self) -> bool { + self.static_fields.source_file_len > 0 + } + + pub fn has_dest_file(&self) -> bool { + self.static_fields.dest_file_len > 0 + } + + pub fn source_file(&self) -> Result<&str, Utf8Error> { + core::str::from_utf8( + &self.static_fields.source_file_buf[0..self.static_fields.source_file_len], + ) + } + + pub fn dest_file(&self) -> Result<&str, Utf8Error> { + core::str::from_utf8( + &self.static_fields.dest_file_buf[0..self.static_fields.dest_file_len], + ) + } + + pub fn opts_len(&self) -> usize { + self.opts_len + } + + pub fn opts_slice(&self) -> &[u8] { + &self.opts_buf[0..self.opts_len] + } + + /// This clears the cacher structure. This is a cheap operation because it only + /// sets [Option]al values to [None] and the length of stores TLVs to 0. + /// + /// Please note that this method will not set the values in the buffer to 0. + pub fn clear(&mut self) { + self.static_fields.clear(); + self.opts_len = 0; + } + } +} + +#[cfg(test)] +mod tests { + use std::string::String; + + use spacepackets::{ + cfdp::tlv::{msg_to_user::MsgToUserTlv, ReadableTlv}, + util::UbfU16, + }; + + use super::*; + + pub const DEST_ID: UbfU16 = UbfU16::new(5); + + #[test] + fn test_put_request_basic() { + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = PutRequest::new( + DEST_ID.into(), + Some(src_file), + Some(dest_file), + None, + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + let identical_request = + PutRequest::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + assert_eq!(put_request, identical_request); + } + + #[test] + fn test_put_request_path_checks_source_too_long() { + let mut invalid_path = String::from("/tmp/"); + invalid_path += "a".repeat(u8::MAX as usize).as_str(); + let dest_file = "/tmp/hello2.txt"; + let error = + PutRequest::new_regular_request(DEST_ID.into(), &invalid_path, dest_file, None, None); + assert!(error.is_err()); + let error = error.unwrap_err(); + assert_eq!(u8::MAX as usize + 5, error.0); + } + + #[test] + fn test_put_request_path_checks_dest_file_too_long() { + let mut invalid_path = String::from("/tmp/"); + invalid_path += "a".repeat(u8::MAX as usize).as_str(); + let source_file = "/tmp/hello2.txt"; + let error = + PutRequest::new_regular_request(DEST_ID.into(), source_file, &invalid_path, None, None); + assert!(error.is_err()); + let error = error.unwrap_err(); + assert_eq!(u8::MAX as usize + 5, error.0); + } + + #[test] + fn test_owned_put_request_path_checks_source_too_long() { + let mut invalid_path = String::from("/tmp/"); + invalid_path += "a".repeat(u8::MAX as usize).as_str(); + let dest_file = "/tmp/hello2.txt"; + let error = PutRequestOwned::new_regular_request( + DEST_ID.into(), + &invalid_path, + dest_file, + None, + None, + ); + assert!(error.is_err()); + let error = error.unwrap_err(); + assert_eq!(u8::MAX as usize + 5, error.0); + } + + #[test] + fn test_owned_put_request_path_checks_dest_file_too_long() { + let mut invalid_path = String::from("/tmp/"); + invalid_path += "a".repeat(u8::MAX as usize).as_str(); + let source_file = "/tmp/hello2.txt"; + let error = PutRequestOwned::new_regular_request( + DEST_ID.into(), + source_file, + &invalid_path, + None, + None, + ); + assert!(error.is_err()); + let error = error.unwrap_err(); + assert_eq!(u8::MAX as usize + 5, error.0); + } + + #[test] + fn test_put_request_basic_small_ctor() { + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = + PutRequest::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + assert_eq!(put_request.source_file(), Some(src_file)); + assert_eq!(put_request.dest_file(), Some(dest_file)); + assert_eq!(put_request.destination_id(), DEST_ID.into()); + assert_eq!(put_request.seg_ctrl(), None); + assert_eq!(put_request.closure_requested(), None); + assert_eq!(put_request.trans_mode(), None); + assert!(put_request.fs_requests().is_none()); + assert!(put_request.msgs_to_user().is_none()); + assert!(put_request.fault_handler_overrides().is_none()); + assert!(put_request.flow_label().is_none()); + } + + #[test] + fn test_put_request_owned_basic() { + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = + PutRequestOwned::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + assert_eq!(put_request.source_file(), Some(src_file)); + assert_eq!(put_request.dest_file(), Some(dest_file)); + assert_eq!(put_request.destination_id(), DEST_ID.into()); + assert_eq!(put_request.seg_ctrl(), None); + assert_eq!(put_request.closure_requested(), None); + assert_eq!(put_request.trans_mode(), None); + assert!(put_request.flow_label().is_none()); + assert!(put_request.fs_requests().is_none()); + assert!(put_request.msgs_to_user().is_none()); + assert!(put_request.fault_handler_overrides().is_none()); + assert!(put_request.flow_label().is_none()); + let put_request_cloned = put_request.clone(); + assert_eq!(put_request, put_request_cloned); + } + + #[test] + fn test_put_request_cacher_basic() { + let put_request_cached = StaticPutRequestCacher::new(128); + assert_eq!(put_request_cached.static_fields.source_file_len, 0); + assert_eq!(put_request_cached.static_fields.dest_file_len, 0); + assert_eq!(put_request_cached.opts_len(), 0); + assert_eq!(put_request_cached.opts_slice(), &[]); + } + + #[test] + fn test_put_request_cacher_set() { + let mut put_request_cached = StaticPutRequestCacher::new(128); + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = + PutRequest::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + put_request_cached.set(&put_request).unwrap(); + assert_eq!( + put_request_cached.static_fields.source_file_len, + src_file.len() + ); + assert_eq!( + put_request_cached.static_fields.dest_file_len, + dest_file.len() + ); + assert_eq!(put_request_cached.source_file().unwrap(), src_file); + assert_eq!(put_request_cached.dest_file().unwrap(), dest_file); + assert_eq!(put_request_cached.opts_len(), 0); + } + + #[test] + fn test_put_request_cacher_set_and_clear() { + let mut put_request_cached = StaticPutRequestCacher::new(128); + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = + PutRequest::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + put_request_cached.set(&put_request).unwrap(); + put_request_cached.clear(); + assert_eq!(put_request_cached.static_fields.source_file_len, 0); + assert_eq!(put_request_cached.static_fields.dest_file_len, 0); + assert_eq!(put_request_cached.opts_len(), 0); + } + + #[test] + fn test_messages_to_user_ctor_owned() { + let msg_to_user = MsgToUserTlv::new(&[1, 2, 3]).expect("creating message to user failed"); + let put_request = PutRequestOwned::new_msgs_to_user_only(DEST_ID.into(), &[msg_to_user]) + .expect("creating msgs to user only put request failed"); + let msg_to_user_iter = put_request.msgs_to_user(); + assert!(msg_to_user_iter.is_some()); + assert!(put_request.check_tlv_type_validities()); + let msg_to_user_iter = msg_to_user_iter.unwrap(); + for msg_to_user_tlv in msg_to_user_iter { + assert_eq!(msg_to_user_tlv.value(), msg_to_user.value()); + assert_eq!(msg_to_user_tlv.tlv_type().unwrap(), TlvType::MsgToUser); + } + } + + #[test] + fn test_messages_to_user_ctor() { + let msg_to_user = MsgToUserTlv::new(&[1, 2, 3]).expect("creating message to user failed"); + let binding = &[msg_to_user.to_tlv()]; + let put_request = PutRequest::new_msgs_to_user_only(DEST_ID.into(), binding) + .expect("creating msgs to user only put request failed"); + let msg_to_user_iter = put_request.msgs_to_user(); + assert!(put_request.check_tlv_type_validities()); + assert!(msg_to_user_iter.is_some()); + let msg_to_user_iter = msg_to_user_iter.unwrap(); + for msg_to_user_tlv in msg_to_user_iter { + assert_eq!(msg_to_user_tlv.value(), msg_to_user.value()); + assert_eq!(msg_to_user_tlv.tlv_type().unwrap(), TlvType::MsgToUser); + } + } + + #[test] + fn test_put_request_to_owned() { + let src_file = "/tmp/hello.txt"; + let dest_file = "/tmp/hello2.txt"; + let put_request = + PutRequest::new_regular_request(DEST_ID.into(), src_file, dest_file, None, None) + .unwrap(); + let put_request_owned: PutRequestOwned = put_request.into(); + assert_eq!(put_request_owned.destination_id(), DEST_ID.into()); + assert_eq!(put_request_owned.source_file().unwrap(), src_file); + assert_eq!(put_request_owned.dest_file().unwrap(), dest_file); + assert!(put_request_owned.msgs_to_user().is_none()); + assert!(put_request_owned.trans_mode().is_none()); + assert!(put_request_owned.closure_requested().is_none()); + } +} diff --git a/src/source.rs b/src/source.rs new file mode 100644 index 0000000..5854840 --- /dev/null +++ b/src/source.rs @@ -0,0 +1,1329 @@ +use core::{cell::RefCell, ops::ControlFlow, str::Utf8Error}; + +use spacepackets::{ + cfdp::{ + lv::Lv, + pdu::{ + eof::EofPdu, + file_data::{ + calculate_max_file_seg_len_for_max_packet_len_and_pdu_header, + FileDataPduCreatorWithReservedDatafield, + }, + finished::{DeliveryCode, FileStatus, FinishedPduReader}, + metadata::{MetadataGenericParams, MetadataPduCreator}, + CfdpPdu, CommonPduConfig, FileDirectiveType, PduError, PduHeader, WritablePduPacket, + }, + ConditionCode, Direction, LargeFileFlag, PduType, SegmentMetadataFlag, SegmentationControl, + TransmissionMode, + }, + util::{UnsignedByteField, UnsignedEnum}, + ByteConversionError, +}; + +use spacepackets::seq_count::SequenceCountProvider; + +use crate::{DummyPduProvider, GenericSendError, PduProvider}; + +use super::{ + filestore::{FilestoreError, VirtualFilestore}, + request::{ReadablePutRequest, StaticPutRequestCacher}, + user::{CfdpUser, TransactionFinishedParams}, + LocalEntityConfig, PacketTarget, PduSendProvider, RemoteEntityConfig, + RemoteEntityConfigProvider, State, TransactionId, UserFaultHookProvider, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum TransactionStep { + Idle = 0, + TransactionStart = 1, + SendingMetadata = 3, + SendingFileData = 4, + /// Re-transmitting missing packets in acknowledged mode + Retransmitting = 5, + SendingEof = 6, + WaitingForEofAck = 7, + WaitingForFinished = 8, + // SendingAckOfFinished = 9, + NoticeOfCompletion = 10, +} + +#[derive(Default)] +pub struct FileParams { + pub progress: u64, + pub segment_len: u64, + pub crc32: u32, + pub metadata_only: bool, + pub file_size: u64, + pub empty_file: bool, +} + +pub struct StateHelper { + state: super::State, + step: TransactionStep, + num_packets_ready: u32, +} + +#[derive(Debug)] +pub struct FinishedParams { + condition_code: ConditionCode, + delivery_code: DeliveryCode, + file_status: FileStatus, +} + +#[derive(Debug, derive_new::new)] +pub struct TransferState { + transaction_id: TransactionId, + remote_cfg: RemoteEntityConfig, + transmission_mode: super::TransmissionMode, + closure_requested: bool, + cond_code_eof: Option, + finished_params: Option, +} + +impl Default for StateHelper { + fn default() -> Self { + Self { + state: super::State::Idle, + step: TransactionStep::Idle, + num_packets_ready: 0, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum SourceError { + #[error("can not process packet type {pdu_type:?} with directive type {directive_type:?}")] + CantProcessPacketType { + pdu_type: PduType, + directive_type: Option, + }, + #[error("unexpected PDU")] + UnexpectedPdu { + pdu_type: PduType, + directive_type: Option, + }, + #[error("source handler is already busy with put request")] + PutRequestAlreadyActive, + #[error("error caching put request")] + PutRequestCaching(ByteConversionError), + #[error("filestore error: {0}")] + FilestoreError(#[from] FilestoreError), + #[error("source file does not have valid UTF8 format: {0}")] + SourceFileNotValidUtf8(Utf8Error), + #[error("destination file does not have valid UTF8 format: {0}")] + DestFileNotValidUtf8(Utf8Error), + #[error("error related to PDU creation")] + Pdu(#[from] PduError), + #[error("cfdp feature not implemented")] + NotImplemented, + #[error("issue sending PDU: {0}")] + SendError(#[from] GenericSendError), +} + +#[derive(Debug, thiserror::Error)] +pub enum PutRequestError { + #[error("error caching put request: {0}")] + Storage(#[from] ByteConversionError), + #[error("already busy with put request")] + AlreadyBusy, + #[error("no remote entity configuration found for {0:?}")] + NoRemoteCfgFound(UnsignedByteField), + #[error("source file does not have valid UTF8 format: {0}")] + SourceFileNotValidUtf8(#[from] Utf8Error), + #[error("source file does not exist")] + FileDoesNotExist, + #[error("filestore error: {0}")] + FilestoreError(#[from] FilestoreError), +} + +pub struct SourceHandler< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + SeqCountProvider: SequenceCountProvider, +> { + local_cfg: LocalEntityConfig, + pdu_sender: PduSender, + pdu_and_cksum_buffer: RefCell>, + put_request_cacher: StaticPutRequestCacher, + remote_cfg_table: RemoteCfgTable, + vfs: Vfs, + state_helper: StateHelper, + // Transfer related state information + tstate: Option, + // File specific transfer fields + fparams: FileParams, + // PDU configuration is cached so it can be re-used for all PDUs generated for file transfers. + pdu_conf: CommonPduConfig, + seq_count_provider: SeqCountProvider, +} + +impl< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + SeqCountProvider: SequenceCountProvider, + > SourceHandler +{ + pub fn new( + cfg: LocalEntityConfig, + pdu_sender: PduSender, + vfs: Vfs, + put_request_cacher: StaticPutRequestCacher, + pdu_and_cksum_buf_size: usize, + remote_cfg_table: RemoteCfgTable, + seq_count_provider: SeqCountProvider, + ) -> Self { + Self { + local_cfg: cfg, + remote_cfg_table, + pdu_sender, + pdu_and_cksum_buffer: RefCell::new(alloc::vec![0; pdu_and_cksum_buf_size]), + vfs, + put_request_cacher, + state_helper: Default::default(), + tstate: Default::default(), + fparams: Default::default(), + pdu_conf: Default::default(), + seq_count_provider, + } + } + + pub fn state_machine_no_packet( + &mut self, + cfdp_user: &mut impl CfdpUser, + ) -> Result { + self.state_machine(cfdp_user, None::<&DummyPduProvider>) + } + + /// This is the core function to drive the source handler. It is also used to insert + /// packets into the source handler. + /// + /// The state machine should either be called if a packet with the appropriate destination ID + /// is received, or periodically in IDLE periods to perform all CFDP related tasks, for example + /// checking for timeouts or missed file segments. + /// + /// The function returns the number of sent PDU packets on success. + pub fn state_machine( + &mut self, + cfdp_user: &mut impl CfdpUser, + packet_to_insert: Option<&impl PduProvider>, + ) -> Result { + if let Some(packet) = packet_to_insert { + self.insert_packet(cfdp_user, packet)?; + } + match self.state_helper.state { + super::State::Idle => { + // TODO: In acknowledged mode, add timer handling. + Ok(0) + } + super::State::Busy => self.fsm_busy(cfdp_user), + super::State::Suspended => { + // There is now way to suspend the handler currently anyway. + Ok(0) + } + } + } + + fn insert_packet( + &mut self, + _cfdp_user: &mut impl CfdpUser, + packet_to_insert: &impl PduProvider, + ) -> Result<(), SourceError> { + if packet_to_insert.packet_target()? != PacketTarget::SourceEntity { + // Unwrap is okay here, a PacketInfo for a file data PDU should always have the + // destination as the target. + return Err(SourceError::CantProcessPacketType { + pdu_type: packet_to_insert.pdu_type(), + directive_type: packet_to_insert.file_directive_type(), + }); + } + if packet_to_insert.pdu_type() == PduType::FileData { + // The [PacketInfo] API should ensure that file data PDUs can not be passed + // into a source entity, so this should never happen. + return Err(SourceError::UnexpectedPdu { + pdu_type: PduType::FileData, + directive_type: None, + }); + } + // Unwrap is okay here, the [PacketInfo] API should ensure that the directive type is + // always a valid value. + match packet_to_insert + .file_directive_type() + .expect("PDU directive type unexpectedly not set") + { + FileDirectiveType::FinishedPdu => self.handle_finished_pdu(packet_to_insert)?, + FileDirectiveType::NakPdu => self.handle_nak_pdu(), + FileDirectiveType::KeepAlivePdu => self.handle_keep_alive_pdu(), + FileDirectiveType::AckPdu => return Err(SourceError::NotImplemented), + FileDirectiveType::EofPdu + | FileDirectiveType::PromptPdu + | FileDirectiveType::MetadataPdu => { + return Err(SourceError::CantProcessPacketType { + pdu_type: packet_to_insert.pdu_type(), + directive_type: packet_to_insert.file_directive_type(), + }); + } + } + Ok(()) + } + + pub fn put_request( + &mut self, + put_request: &impl ReadablePutRequest, + ) -> Result<(), PutRequestError> { + if self.state_helper.state != super::State::Idle { + return Err(PutRequestError::AlreadyBusy); + } + self.put_request_cacher.set(put_request)?; + let remote_cfg = self.remote_cfg_table.get( + self.put_request_cacher + .static_fields + .destination_id + .value_const(), + ); + if remote_cfg.is_none() { + return Err(PutRequestError::NoRemoteCfgFound( + self.put_request_cacher.static_fields.destination_id, + )); + } + let remote_cfg = remote_cfg.unwrap(); + self.state_helper.num_packets_ready = 0; + let transmission_mode = if self.put_request_cacher.static_fields.trans_mode.is_some() { + self.put_request_cacher.static_fields.trans_mode.unwrap() + } else { + remote_cfg.default_transmission_mode + }; + let closure_requested = if self + .put_request_cacher + .static_fields + .closure_requested + .is_some() + { + self.put_request_cacher + .static_fields + .closure_requested + .unwrap() + } else { + remote_cfg.closure_requested_by_default + }; + if self.put_request_cacher.has_source_file() + && !self.vfs.exists(self.put_request_cacher.source_file()?)? + { + return Err(PutRequestError::FileDoesNotExist); + } + self.tstate = Some(TransferState::new( + TransactionId::new( + self.local_cfg().id, + UnsignedByteField::new( + SeqCountProvider::MAX_BIT_WIDTH / 8, + self.seq_count_provider.get_and_increment().into(), + ), + ), + *remote_cfg, + transmission_mode, + closure_requested, + None, + None, + )); + self.state_helper.state = super::State::Busy; + Ok(()) + } + + 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), + remote_cfg.max_packet_len, + None, + ); + if remote_cfg.max_file_segment_len.is_some() { + derived_max_seg_len = core::cmp::min( + remote_cfg.max_file_segment_len.unwrap(), + derived_max_seg_len, + ); + } + derived_max_seg_len as u64 + } + + #[inline] + pub fn transmission_mode(&self) -> Option { + self.tstate.as_ref().map(|v| v.transmission_mode) + } + + fn fsm_busy(&mut self, cfdp_user: &mut impl CfdpUser) -> Result { + let mut sent_packets = 0; + if self.state_helper.step == TransactionStep::Idle { + self.state_helper.step = TransactionStep::TransactionStart; + } + if self.state_helper.step == TransactionStep::TransactionStart { + self.handle_transaction_start(cfdp_user)?; + self.state_helper.step = TransactionStep::SendingMetadata; + } + if self.state_helper.step == TransactionStep::SendingMetadata { + self.prepare_and_send_metadata_pdu()?; + self.state_helper.step = TransactionStep::SendingFileData; + sent_packets += 1; + // return Ok(1); + } + if self.state_helper.step == TransactionStep::SendingFileData { + if let ControlFlow::Break(packets) = self.file_data_fsm()? { + sent_packets += packets; + // Exit for each file data PDU to allow flow control. + return Ok(sent_packets); + } + } + if self.state_helper.step == TransactionStep::SendingEof { + self.eof_fsm(cfdp_user)?; + sent_packets += 1; + // return Ok(1); + } + if self.state_helper.step == TransactionStep::WaitingForFinished { + /* + def _handle_wait_for_finish(self): + if ( + self.transmission_mode == TransmissionMode.ACKNOWLEDGED + and self.__handle_retransmission() + ): + return + if ( + self._inserted_pdu.pdu is None + or self._inserted_pdu.pdu_directive_type is None + or self._inserted_pdu.pdu_directive_type != DirectiveType.FINISHED_PDU + ): + if self._params.check_timer is not None: + if self._params.check_timer.timed_out(): + self._declare_fault(ConditionCode.CHECK_LIMIT_REACHED) + return + finished_pdu = self._inserted_pdu.to_finished_pdu() + self._inserted_pdu.pdu = None + self._params.finished_params = finished_pdu.finished_params + if self.transmission_mode == TransmissionMode.ACKNOWLEDGED: + self._prepare_finished_ack_packet(finished_pdu.condition_code) + self.states.step = TransactionStep.SENDING_ACK_OF_FINISHED + else: + self.states.step = TransactionStep.NOTICE_OF_COMPLETION + */ + } + if self.state_helper.step == TransactionStep::NoticeOfCompletion { + self.notice_of_completion(cfdp_user); + self.reset(); + } + Ok(sent_packets) + } + + fn eof_fsm(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), SourceError> { + let tstate = self.tstate.as_ref().unwrap(); + let checksum = self.vfs.calculate_checksum( + self.put_request_cacher.source_file().unwrap(), + tstate.remote_cfg.default_crc_type, + self.pdu_and_cksum_buffer.get_mut(), + )?; + self.prepare_and_send_eof_pdu(checksum)?; + let tstate = self.tstate.as_ref().unwrap(); + if self.local_cfg.indication_cfg.eof_sent { + cfdp_user.eof_sent_indication(&tstate.transaction_id); + } + if tstate.transmission_mode == TransmissionMode::Unacknowledged { + if tstate.closure_requested { + // TODO: Check timer handling. + self.state_helper.step = TransactionStep::WaitingForFinished; + } else { + self.state_helper.step = TransactionStep::NoticeOfCompletion; + } + } else { + // TODO: Start positive ACK procedure. + } + /* + if self.cfg.indication_cfg.eof_sent_indication_required: + assert self._params.transaction_id is not None + self.user.eof_sent_indication(self._params.transaction_id) + if self.transmission_mode == TransmissionMode.UNACKNOWLEDGED: + if self._params.closure_requested: + assert self._params.remote_cfg is not None + self._params.check_timer = ( + self.check_timer_provider.provide_check_timer( + local_entity_id=self.cfg.local_entity_id, + remote_entity_id=self._params.remote_cfg.entity_id, + entity_type=EntityType.SENDING, + ) + ) + self.states.step = TransactionStep.WAITING_FOR_FINISHED + else: + self.states.step = TransactionStep.NOTICE_OF_COMPLETION + else: + self._start_positive_ack_procedure() + */ + Ok(()) + } + + fn handle_transaction_start( + &mut self, + cfdp_user: &mut impl CfdpUser, + ) -> Result<(), SourceError> { + let tstate = self + .tstate + .as_ref() + .expect("transfer state unexpectedly empty"); + if !self.put_request_cacher.has_source_file() { + self.fparams.metadata_only = true; + } else { + let source_file = self + .put_request_cacher + .source_file() + .map_err(SourceError::SourceFileNotValidUtf8)?; + if !self.vfs.exists(source_file)? { + return Err(SourceError::FilestoreError( + FilestoreError::FileDoesNotExist, + )); + } + // We expect the destination file path to consist of valid UTF-8 characters as well. + self.put_request_cacher + .dest_file() + .map_err(SourceError::DestFileNotValidUtf8)?; + self.fparams.file_size = self.vfs.file_size(source_file)?; + if self.fparams.file_size > u32::MAX as u64 { + self.pdu_conf.file_flag = LargeFileFlag::Large + } else { + if self.fparams.file_size == 0 { + self.fparams.empty_file = true; + } + self.pdu_conf.file_flag = LargeFileFlag::Normal + } + } + // Both the source entity and destination entity ID field must have the same size. + // We use the larger of either the Put Request destination ID or the local entity ID + // as the size for the new entity IDs. + let larger_entity_width = core::cmp::max( + self.local_cfg.id.size(), + self.put_request_cacher.static_fields.destination_id.size(), + ); + let create_id = |cached_id: &UnsignedByteField| { + if larger_entity_width != cached_id.size() { + UnsignedByteField::new(larger_entity_width, cached_id.value_const()) + } else { + *cached_id + } + }; + self.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 = tstate.remote_cfg.crc_on_transmission_by_default.into(); + self.pdu_conf.transaction_seq_num = *tstate.transaction_id.seq_num(); + self.pdu_conf.trans_mode = tstate.transmission_mode; + self.fparams.segment_len = self.calculate_max_file_seg_len(&tstate.remote_cfg); + cfdp_user.transaction_indication(&tstate.transaction_id); + Ok(()) + } + + fn prepare_and_send_metadata_pdu(&mut self) -> Result<(), SourceError> { + let tstate = self + .tstate + .as_ref() + .expect("transfer state unexpectedly empty"); + let metadata_params = MetadataGenericParams::new( + tstate.closure_requested, + tstate.remote_cfg.default_crc_type, + self.fparams.file_size, + ); + if self.fparams.metadata_only { + let metadata_pdu = MetadataPduCreator::new( + PduHeader::new_no_file_data(self.pdu_conf, 0), + metadata_params, + Lv::new_empty(), + Lv::new_empty(), + self.put_request_cacher.opts_slice(), + ); + return self.pdu_send_helper(&metadata_pdu); + } + let metadata_pdu = MetadataPduCreator::new( + PduHeader::new_no_file_data(self.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(), + self.put_request_cacher.opts_slice(), + ); + self.pdu_send_helper(&metadata_pdu) + } + + fn file_data_fsm(&mut self) -> Result, SourceError> { + if self.transmission_mode().unwrap() == super::TransmissionMode::Acknowledged { + // TODO: Handle re-transmission + } + if !self.fparams.metadata_only + && self.fparams.progress < self.fparams.file_size + && self.send_progressing_file_data_pdu()? + { + return Ok(ControlFlow::Break(1)); + } + if self.fparams.empty_file || self.fparams.progress >= self.fparams.file_size { + // EOF is still expected. + self.state_helper.step = TransactionStep::SendingEof; + self.tstate.as_mut().unwrap().cond_code_eof = Some(ConditionCode::NoError); + } else if self.fparams.metadata_only { + // Special case: Metadata Only, no EOF required. + if self.tstate.as_ref().unwrap().closure_requested { + self.state_helper.step = TransactionStep::WaitingForFinished; + } else { + self.state_helper.step = TransactionStep::NoticeOfCompletion; + } + } + Ok(ControlFlow::Continue(())) + } + + 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. + let finished_params = if tstate.finished_params.is_none() { + TransactionFinishedParams { + id: tstate.transaction_id, + condition_code: ConditionCode::NoError, + delivery_code: DeliveryCode::Complete, + file_status: FileStatus::Unreported, + } + } else { + let finished_params = tstate.finished_params.as_ref().unwrap(); + TransactionFinishedParams { + id: tstate.transaction_id, + condition_code: finished_params.condition_code, + delivery_code: finished_params.delivery_code, + file_status: finished_params.file_status, + } + }; + cfdp_user.transaction_finished_indication(&finished_params); + } + } + + fn send_progressing_file_data_pdu(&mut self) -> Result { + // Should never be called, but use defensive programming here. + if self.fparams.progress >= self.fparams.file_size { + return Ok(false); + } + let read_len = if self.fparams.file_size < self.fparams.segment_len { + self.fparams.file_size + } else if self.fparams.progress + self.fparams.segment_len > self.fparams.file_size { + self.fparams.file_size - self.fparams.progress + } else { + self.fparams.segment_len + }; + let pdu_creator = FileDataPduCreatorWithReservedDatafield::new_no_seg_metadata( + PduHeader::new_for_file_data( + self.pdu_conf, + 0, + SegmentMetadataFlag::NotPresent, + SegmentationControl::NoRecordBoundaryPreservation, + ), + self.fparams.progress, + read_len, + ); + let mut unwritten_pdu = + pdu_creator.write_to_bytes_partially(self.pdu_and_cksum_buffer.get_mut())?; + self.vfs.read_data( + self.put_request_cacher.source_file().unwrap(), + self.fparams.progress, + read_len, + unwritten_pdu.file_data_field_mut(), + )?; + let written_len = unwritten_pdu.finish(); + self.pdu_sender.send_pdu( + PduType::FileData, + None, + &self.pdu_and_cksum_buffer.borrow()[0..written_len], + )?; + self.fparams.progress += read_len; + /* + """Generic function to prepare a file data PDU. This function can also be used to + re-transmit file data PDUs of segments which were already sent.""" + assert self._put_req is not None + assert self._put_req.source_file is not None + with open(self._put_req.source_file, "rb") as of: + file_data = self.user.vfs.read_from_opened_file(of, offset, read_len) + # TODO: Support for record continuation state not implemented yet. Segment metadata + # flag is therefore always set to False. Segment metadata support also omitted + # for now. Implementing those generically could be done in form of a callback, + # e.g. abstractmethod of this handler as a first way, another one being + # to expect the user to supply some helper class to split up a file + fd_params = FileDataParams( + file_data=file_data, offset=offset, segment_metadata=None + ) + file_data_pdu = FileDataPdu( + pdu_conf=self._params.pdu_conf, params=fd_params + ) + self._add_packet_to_be_sent(file_data_pdu) + */ + /* + """Prepare the next file data PDU, which also progresses the file copy operation. + + :return: True if a packet was prepared, False if PDU handling is done and the next steps + in the Copy File procedure can be performed + """ + # This function should only be called if file segments still need to be sent. + assert self._params.fp.progress < self._params.fp.file_size + if self._params.fp.file_size < self._params.fp.segment_len: + read_len = self._params.fp.file_size + else: + if ( + self._params.fp.progress + self._params.fp.segment_len + > self._params.fp.file_size + ): + read_len = self._params.fp.file_size - self._params.fp.progress + else: + read_len = self._params.fp.segment_len + self._prepare_file_data_pdu(self._params.fp.progress, read_len) + self._params.fp.progress += read_len + */ + Ok(true) + } + + fn prepare_and_send_eof_pdu(&mut self, checksum: u32) -> Result<(), SourceError> { + let tstate = self + .tstate + .as_ref() + .expect("transfer state unexpectedly empty"); + let eof_pdu = EofPdu::new( + PduHeader::new_no_file_data(self.pdu_conf, 0), + tstate.cond_code_eof.unwrap_or(ConditionCode::NoError), + checksum, + self.fparams.file_size, + None, + ); + self.pdu_send_helper(&eof_pdu)?; + Ok(()) + } + + fn pdu_send_helper(&self, pdu: &(impl WritablePduPacket + CfdpPdu)) -> Result<(), SourceError> { + let mut pdu_buffer_mut = self.pdu_and_cksum_buffer.borrow_mut(); + let written_len = pdu.write_to_bytes(&mut pdu_buffer_mut)?; + self.pdu_sender.send_pdu( + pdu.pdu_type(), + pdu.file_directive_type(), + &pdu_buffer_mut[0..written_len], + )?; + Ok(()) + } + + fn handle_finished_pdu(&mut self, pdu_provider: &impl PduProvider) -> Result<(), SourceError> { + // Ignore this packet when we are idle. + if self.state_helper.state == State::Idle { + return Ok(()); + } + if self.state_helper.step != TransactionStep::WaitingForFinished { + return Err(SourceError::UnexpectedPdu { + pdu_type: PduType::FileDirective, + directive_type: Some(FileDirectiveType::FinishedPdu), + }); + } + let finished_pdu = FinishedPduReader::new(pdu_provider.pdu())?; + // Unwrapping should be fine here, the transfer state is valid when we are not in IDLE + // mode. + self.tstate.as_mut().unwrap().finished_params = Some(FinishedParams { + condition_code: finished_pdu.condition_code(), + delivery_code: finished_pdu.delivery_code(), + file_status: finished_pdu.file_status(), + }); + if self.tstate.as_ref().unwrap().transmission_mode == TransmissionMode::Acknowledged { + // TODO: Send ACK packet here immediately and continue. + //self.state_helper.step = TransactionStep::SendingAckOfFinished; + } + self.state_helper.step = TransactionStep::NoticeOfCompletion; + + /* + if self.transmission_mode == TransmissionMode.ACKNOWLEDGED: + self._prepare_finished_ack_packet(finished_pdu.condition_code) + self.states.step = TransactionStep.SENDING_ACK_OF_FINISHED + else: + self.states.step = TransactionStep.NOTICE_OF_COMPLETION + */ + Ok(()) + } + + fn handle_nak_pdu(&mut self) {} + + fn handle_keep_alive_pdu(&mut self) {} + + /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. + pub fn step(&self) -> TransactionStep { + self.state_helper.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_helper.state + } + + pub fn local_cfg(&self) -> &LocalEntityConfig { + &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.state_helper = Default::default(); + self.tstate = None; + self.fparams = Default::default(); + } +} + +#[cfg(test)] +mod tests { + use std::{fs::OpenOptions, io::Write, path::PathBuf, vec::Vec}; + + use alloc::string::String; + use rand::Rng; + use spacepackets::{ + cfdp::{ + pdu::{ + file_data::FileDataPdu, finished::FinishedPduCreator, metadata::MetadataPduReader, + }, + ChecksumType, CrcFlag, + }, + util::UnsignedByteFieldU16, + }; + use tempfile::TempPath; + + use super::*; + use crate::{ + filestore::NativeFilestore, + request::PutRequestOwned, + tests::{basic_remote_cfg_table, SentPdu, TestCfdpSender, TestCfdpUser, TestFaultHandler}, + FaultHandler, IndicationConfig, PacketInfo, StdRemoteEntityConfigProvider, CRC_32, + }; + use spacepackets::seq_count::SeqCountProviderSimple; + + const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); + const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + const INVALID_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(5); + + fn init_full_filepaths_textfile() -> (TempPath, PathBuf) { + ( + tempfile::NamedTempFile::new().unwrap().into_temp_path(), + tempfile::TempPath::from_path("/tmp/test.txt").to_path_buf(), + ) + } + + type TestSourceHandler = SourceHandler< + TestCfdpSender, + TestFaultHandler, + NativeFilestore, + StdRemoteEntityConfigProvider, + SeqCountProviderSimple, + >; + + struct SourceHandlerTestbench { + handler: TestSourceHandler, + #[allow(dead_code)] + srcfile_handle: TempPath, + srcfile: String, + destfile: String, + max_packet_len: usize, + check_idle_on_drop: bool, + } + + impl SourceHandlerTestbench { + fn new( + crc_on_transmission_by_default: bool, + test_fault_handler: TestFaultHandler, + test_packet_sender: TestCfdpSender, + max_packet_len: usize, + ) -> Self { + let local_entity_cfg = LocalEntityConfig { + id: LOCAL_ID.into(), + indication_cfg: IndicationConfig::default(), + fault_handler: FaultHandler::new(test_fault_handler), + }; + let static_put_request_cacher = StaticPutRequestCacher::new(2048); + let (srcfile_handle, destfile) = init_full_filepaths_textfile(); + let srcfile = String::from(srcfile_handle.to_path_buf().to_str().unwrap()); + Self { + handler: SourceHandler::new( + local_entity_cfg, + test_packet_sender, + NativeFilestore::default(), + static_put_request_cacher, + 1024, + basic_remote_cfg_table( + REMOTE_ID, + max_packet_len, + crc_on_transmission_by_default, + ), + SeqCountProviderSimple::default(), + ), + srcfile_handle, + srcfile, + destfile: String::from(destfile.to_path_buf().to_str().unwrap()), + max_packet_len, + check_idle_on_drop: true, + } + } + + fn create_user(&self, next_expected_seq_num: u64, filesize: u64) -> TestCfdpUser { + TestCfdpUser::new( + next_expected_seq_num, + self.srcfile.clone(), + self.destfile.clone(), + filesize, + ) + } + + fn put_request( + &mut self, + put_request: &impl ReadablePutRequest, + ) -> Result<(), PutRequestError> { + self.handler.put_request(put_request) + } + + fn all_fault_queues_empty(&self) -> bool { + self.handler + .local_cfg + .user_fault_hook() + .borrow() + .all_queues_empty() + } + + fn pdu_queue_empty(&self) -> bool { + self.handler.pdu_sender.queue_empty() + } + + fn get_next_sent_pdu(&self) -> Option { + self.handler.pdu_sender.retrieve_next_pdu() + } + + fn common_pdu_check_for_file_transfer(&self, pdu_header: &PduHeader, crc_flag: CrcFlag) { + assert_eq!( + pdu_header.seg_ctrl(), + SegmentationControl::NoRecordBoundaryPreservation + ); + assert_eq!( + pdu_header.seg_metadata_flag(), + SegmentMetadataFlag::NotPresent + ); + assert_eq!(pdu_header.common_pdu_conf().source_id(), LOCAL_ID.into()); + assert_eq!(pdu_header.common_pdu_conf().dest_id(), REMOTE_ID.into()); + assert_eq!(pdu_header.common_pdu_conf().crc_flag, crc_flag); + assert_eq!( + pdu_header.common_pdu_conf().trans_mode, + TransmissionMode::Unacknowledged + ); + assert_eq!( + pdu_header.common_pdu_conf().direction, + Direction::TowardsReceiver + ); + assert_eq!( + pdu_header.common_pdu_conf().file_flag, + LargeFileFlag::Normal + ); + assert_eq!(pdu_header.common_pdu_conf().transaction_seq_num.size(), 2); + } + + fn generic_file_transfer( + &mut self, + cfdp_user: &mut TestCfdpUser, + with_closure: bool, + file_data: Vec, + ) -> (PduHeader, u32) { + let mut digest = CRC_32.digest(); + digest.update(&file_data); + let checksum = digest.finalize(); + cfdp_user.expected_full_src_name = self.srcfile.clone(); + cfdp_user.expected_full_dest_name = self.destfile.clone(); + cfdp_user.expected_file_size = file_data.len() as u64; + let put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + &self.srcfile, + &self.destfile, + Some(TransmissionMode::Unacknowledged), + Some(with_closure), + ) + .expect("creating put request failed"); + let (closure_requested, pdu_header) = self.common_no_acked_file_transfer( + cfdp_user, + put_request, + cfdp_user.expected_file_size, + ); + let mut current_offset = 0; + let chunks = file_data.chunks( + calculate_max_file_seg_len_for_max_packet_len_and_pdu_header( + &pdu_header, + self.max_packet_len, + None, + ), + ); + let mut fd_pdus = 0; + for segment in chunks { + self.check_next_file_pdu(current_offset, segment); + self.handler.state_machine_no_packet(cfdp_user).unwrap(); + fd_pdus += 1; + current_offset += segment.len() as u64; + } + self.common_eof_pdu_check( + cfdp_user, + closure_requested, + cfdp_user.expected_file_size, + checksum, + ); + (pdu_header, fd_pdus) + } + + // Returns a tuple. First parameter: Closure requested. Second parameter: PDU header of + // metadata PDU. + fn common_no_acked_file_transfer( + &mut self, + cfdp_user: &mut TestCfdpUser, + put_request: PutRequestOwned, + filesize: u64, + ) -> (bool, PduHeader) { + assert_eq!(cfdp_user.transaction_indication_call_count, 0); + assert_eq!(cfdp_user.eof_sent_call_count, 0); + + self.put_request(&put_request) + .expect("put_request call failed"); + assert_eq!(self.handler.state(), State::Busy); + assert_eq!(self.handler.step(), TransactionStep::Idle); + let sent_packets = self + .handler + .state_machine_no_packet(cfdp_user) + .expect("source handler FSM failure"); + assert_eq!(sent_packets, 2); + assert!(!self.pdu_queue_empty()); + let next_pdu = self.get_next_sent_pdu().unwrap(); + assert!(!self.pdu_queue_empty()); + assert_eq!(next_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + next_pdu.file_directive_type, + Some(FileDirectiveType::MetadataPdu) + ); + let metadata_pdu = + MetadataPduReader::new(&next_pdu.raw_pdu).expect("invalid metadata PDU format"); + let pdu_header = metadata_pdu.pdu_header(); + self.common_pdu_check_for_file_transfer(metadata_pdu.pdu_header(), CrcFlag::NoCrc); + assert_eq!( + metadata_pdu + .src_file_name() + .value_as_str() + .unwrap() + .unwrap(), + self.srcfile + ); + assert_eq!( + metadata_pdu + .dest_file_name() + .value_as_str() + .unwrap() + .unwrap(), + self.destfile + ); + assert_eq!(metadata_pdu.metadata_params().file_size, filesize); + assert_eq!( + metadata_pdu.metadata_params().checksum_type, + ChecksumType::Crc32 + ); + let closure_requested = if let Some(closure_requested) = put_request.closure_requested { + assert_eq!( + metadata_pdu.metadata_params().closure_requested, + closure_requested + ); + closure_requested + } else { + assert!(metadata_pdu.metadata_params().closure_requested); + metadata_pdu.metadata_params().closure_requested + }; + assert_eq!(metadata_pdu.options(), &[]); + (closure_requested, *pdu_header) + } + + fn check_next_file_pdu(&mut self, expected_offset: u64, expected_data: &[u8]) { + let next_pdu = self.get_next_sent_pdu().unwrap(); + assert_eq!(next_pdu.pdu_type, PduType::FileData); + assert!(next_pdu.file_directive_type.is_none()); + let fd_pdu = + FileDataPdu::from_bytes(&next_pdu.raw_pdu).expect("reading file data PDU failed"); + assert_eq!(fd_pdu.offset(), expected_offset); + assert_eq!(fd_pdu.file_data(), expected_data); + assert!(fd_pdu.segment_metadata().is_none()); + } + + fn common_eof_pdu_check( + &mut self, + cfdp_user: &mut TestCfdpUser, + closure_requested: bool, + filesize: u64, + checksum: u32, + ) { + let next_pdu = self.get_next_sent_pdu().unwrap(); + assert_eq!(next_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + next_pdu.file_directive_type, + Some(FileDirectiveType::EofPdu) + ); + let eof_pdu = EofPdu::from_bytes(&next_pdu.raw_pdu).expect("invalid EOF PDU format"); + self.common_pdu_check_for_file_transfer(eof_pdu.pdu_header(), CrcFlag::NoCrc); + assert_eq!(eof_pdu.condition_code(), ConditionCode::NoError); + assert_eq!(eof_pdu.file_size(), filesize); + assert_eq!(eof_pdu.file_checksum(), checksum); + assert_eq!( + eof_pdu + .pdu_header() + .common_pdu_conf() + .transaction_seq_num + .value_const(), + 0 + ); + if !closure_requested { + assert_eq!(self.handler.state(), State::Idle); + assert_eq!(self.handler.step(), TransactionStep::Idle); + } else { + assert_eq!(self.handler.state(), State::Busy); + assert_eq!(self.handler.step(), TransactionStep::WaitingForFinished); + } + assert_eq!(cfdp_user.transaction_indication_call_count, 1); + assert_eq!(cfdp_user.eof_sent_call_count, 1); + self.all_fault_queues_empty(); + } + + fn common_tiny_file_transfer( + &mut self, + cfdp_user: &mut TestCfdpUser, + with_closure: bool, + ) -> PduHeader { + let mut file = OpenOptions::new() + .write(true) + .open(&self.srcfile) + .expect("opening file failed"); + let content_str = "Hello World!"; + file.write_all(content_str.as_bytes()) + .expect("writing file content failed"); + drop(file); + let (pdu_header, fd_pdus) = self.generic_file_transfer( + cfdp_user, + with_closure, + content_str.as_bytes().to_vec(), + ); + assert_eq!(fd_pdus, 1); + pdu_header + } + + fn finish_handling(&mut self, user: &mut TestCfdpUser, pdu_header: PduHeader) { + let finished_pdu = FinishedPduCreator::new_default( + pdu_header, + DeliveryCode::Complete, + FileStatus::Retained, + ); + let finished_pdu_vec = finished_pdu.to_vec().unwrap(); + let packet_info = PacketInfo::new(&finished_pdu_vec).unwrap(); + self.handler + .state_machine(user, Some(&packet_info)) + .unwrap(); + } + } + + impl Drop for SourceHandlerTestbench { + fn drop(&mut self) { + self.all_fault_queues_empty(); + if self.check_idle_on_drop { + assert_eq!(self.handler.state(), State::Idle); + assert_eq!(self.handler.step(), TransactionStep::Idle); + } + } + } + + #[test] + fn test_basic() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + assert!(tb.handler.transmission_mode().is_none()); + assert!(tb.pdu_queue_empty()); + } + + #[test] + fn test_empty_file_transfer_not_acked_no_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + let filesize = 0; + let put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + &tb.srcfile, + &tb.destfile, + Some(TransmissionMode::Unacknowledged), + Some(false), + ) + .expect("creating put request failed"); + let mut cfdp_user = tb.create_user(0, filesize); + let (closure_requested, _) = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, filesize); + tb.common_eof_pdu_check( + &mut cfdp_user, + closure_requested, + filesize, + CRC_32.digest().finalize(), + ) + } + + #[test] + fn test_tiny_file_transfer_not_acked_no_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut cfdp_user = TestCfdpUser::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + tb.common_tiny_file_transfer(&mut cfdp_user, false); + } + + #[test] + fn test_tiny_file_transfer_not_acked_with_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + let mut cfdp_user = TestCfdpUser::default(); + let pdu_header = tb.common_tiny_file_transfer(&mut cfdp_user, true); + tb.finish_handling(&mut cfdp_user, pdu_header) + } + + #[test] + fn test_two_segment_file_transfer_not_acked_no_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 128); + let mut cfdp_user = TestCfdpUser::default(); + let mut file = OpenOptions::new() + .write(true) + .open(&tb.srcfile) + .expect("opening file failed"); + let mut rand_data = [0u8; 140]; + rand::thread_rng().fill(&mut rand_data[..]); + 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()); + assert_eq!(fd_pdus, 2); + } + + #[test] + fn test_two_segment_file_transfer_not_acked_with_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 128); + let mut cfdp_user = TestCfdpUser::default(); + let mut file = OpenOptions::new() + .write(true) + .open(&tb.srcfile) + .expect("opening file failed"); + let mut rand_data = [0u8; 140]; + rand::thread_rng().fill(&mut rand_data[..]); + file.write_all(&rand_data) + .expect("writing file content failed"); + drop(file); + let (pdu_header, fd_pdus) = + tb.generic_file_transfer(&mut cfdp_user, true, rand_data.to_vec()); + assert_eq!(fd_pdus, 2); + tb.finish_handling(&mut cfdp_user, pdu_header) + } + + #[test] + fn test_empty_file_transfer_not_acked_with_closure() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + let filesize = 0; + let put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + &tb.srcfile, + &tb.destfile, + Some(TransmissionMode::Unacknowledged), + Some(true), + ) + .expect("creating put request failed"); + let mut cfdp_user = tb.create_user(0, filesize); + let (closure_requested, pdu_header) = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, filesize); + tb.common_eof_pdu_check( + &mut cfdp_user, + closure_requested, + filesize, + CRC_32.digest().finalize(), + ); + tb.finish_handling(&mut cfdp_user, pdu_header) + } + + #[test] + fn test_put_request_no_remote_cfg() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + + let (srcfile, destfile) = init_full_filepaths_textfile(); + let srcfile_str = String::from(srcfile.to_str().unwrap()); + let destfile_str = String::from(destfile.to_str().unwrap()); + let put_request = PutRequestOwned::new_regular_request( + INVALID_ID.into(), + &srcfile_str, + &destfile_str, + Some(TransmissionMode::Unacknowledged), + Some(true), + ) + .expect("creating put request failed"); + let error = tb.handler.put_request(&put_request); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let PutRequestError::NoRemoteCfgFound(id) = error { + assert_eq!(id, INVALID_ID.into()); + } else { + panic!("unexpected error type: {:?}", error); + } + } + + #[test] + fn test_put_request_file_does_not_exist() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + + let file_which_does_not_exist = "/tmp/this_file_does_not_exist.txt"; + let destfile = "/tmp/tmp.txt"; + let put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + file_which_does_not_exist, + destfile, + Some(TransmissionMode::Unacknowledged), + Some(true), + ) + .expect("creating put request failed"); + let error = tb.put_request(&put_request); + assert!(error.is_err()); + let error = error.unwrap_err(); + if let PutRequestError::FileDoesNotExist = error { + } else { + panic!("unexpected error type: {:?}", error); + } + } +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..abd3fac --- /dev/null +++ b/src/time.rs @@ -0,0 +1,7 @@ +use core::fmt::Debug; + +/// Generic abstraction for a check/countdown timer. +pub trait CountdownProvider: Debug { + fn has_expired(&self) -> bool; + fn reset(&mut self); +} diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..5df8b2a --- /dev/null +++ b/src/user.rs @@ -0,0 +1,98 @@ +#[cfg(feature = "alloc")] +use spacepackets::cfdp::tlv::WritableTlv; +use spacepackets::{ + cfdp::{ + pdu::{ + file_data::SegmentMetadata, + finished::{DeliveryCode, FileStatus}, + }, + tlv::msg_to_user::MsgToUserTlv, + ConditionCode, + }, + util::UnsignedByteField, +}; + +use super::TransactionId; + +#[derive(Debug, Copy, Clone)] +pub struct TransactionFinishedParams { + pub id: TransactionId, + pub condition_code: ConditionCode, + pub delivery_code: DeliveryCode, + pub file_status: FileStatus, +} + +#[derive(Debug)] +pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> { + pub id: TransactionId, + pub source_id: UnsignedByteField, + pub file_size: u64, + pub src_file_name: &'src_file str, + pub dest_file_name: &'dest_file str, + pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>], +} + +#[cfg(feature = "alloc")] +#[derive(Debug)] +pub struct OwnedMetadataRecvdParams { + pub id: TransactionId, + pub source_id: UnsignedByteField, + pub file_size: u64, + pub src_file_name: alloc::string::String, + pub dest_file_name: alloc::string::String, + pub msgs_to_user: alloc::vec::Vec>, +} + +#[cfg(feature = "alloc")] +impl From> for OwnedMetadataRecvdParams { + fn from(value: MetadataReceivedParams) -> Self { + Self::from(&value) + } +} + +#[cfg(feature = "alloc")] +impl From<&MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams { + fn from(value: &MetadataReceivedParams) -> Self { + Self { + id: value.id, + source_id: value.source_id, + file_size: value.file_size, + src_file_name: value.src_file_name.into(), + dest_file_name: value.dest_file_name.into(), + msgs_to_user: value.msgs_to_user.iter().map(|tlv| tlv.to_vec()).collect(), + } + } +} + +#[derive(Debug)] +pub struct FileSegmentRecvdParams<'seg_meta> { + pub id: TransactionId, + pub offset: u64, + pub length: usize, + pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>, +} + +pub trait CfdpUser { + fn transaction_indication(&mut self, id: &TransactionId); + fn eof_sent_indication(&mut self, id: &TransactionId); + fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams); + fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams); + fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams); + // TODO: The standard does not strictly specify how the report information looks.. + fn report_indication(&mut self, id: &TransactionId); + fn suspended_indication(&mut self, id: &TransactionId, condition_code: ConditionCode); + fn resumed_indication(&mut self, id: &TransactionId, progress: u64); + fn fault_indication( + &mut self, + id: &TransactionId, + condition_code: ConditionCode, + progress: u64, + ); + fn abandoned_indication( + &mut self, + id: &TransactionId, + condition_code: ConditionCode, + progress: u64, + ); + fn eof_recvd_indication(&mut self, id: &TransactionId); +} diff --git a/tests/end-to-end.rs b/tests/end-to-end.rs new file mode 100644 index 0000000..bb4a285 --- /dev/null +++ b/tests/end-to-end.rs @@ -0,0 +1,352 @@ +//! This is an end-to-end integration tests using the CFDP abstractions provided by the library. +use std::{ + fs::OpenOptions, + io::Write, + sync::{atomic::AtomicBool, mpsc, Arc}, + thread, + time::Duration, +}; + +use cfdp::{ + dest::DestinationHandler, + filestore::NativeFilestore, + request::{PutRequestOwned, StaticPutRequestCacher}, + source::SourceHandler, + user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams}, + EntityType, IndicationConfig, LocalEntityConfig, PduWithInfo, RemoteEntityConfig, + StdCheckTimerCreator, TransactionId, UserFaultHookProvider, +}; +use spacepackets::{ + cfdp::{ChecksumType, ConditionCode, TransmissionMode}, + seq_count::SeqCountProviderSyncU16, + util::UnsignedByteFieldU16, +}; + +const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); +const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + +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 + ); + } + + 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 abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { + panic!( + "unexpected abandonment of transaction {:?}, condition code {:?}, progress {}", + transaction_id, cond, progress + ); + } + + 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 + ); + } +} + +pub struct ExampleCfdpUser { + entity_type: EntityType, + completion_signal: Arc, +} + +impl ExampleCfdpUser { + pub fn new(entity_type: EntityType, completion_signal: Arc) -> Self { + Self { + entity_type, + completion_signal, + } + } +} + +impl CfdpUser for ExampleCfdpUser { + fn transaction_indication(&mut self, id: &crate::TransactionId) { + println!( + "{:?} entity: Transaction indication for {:?}", + self.entity_type, id + ); + } + + fn eof_sent_indication(&mut self, id: &crate::TransactionId) { + println!( + "{:?} entity: EOF sent for transaction {:?}", + self.entity_type, id + ); + } + + fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams) { + println!( + "{:?} entity: Transaction finished: {:?}", + self.entity_type, finished_params + ); + self.completion_signal + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams) { + println!( + "{:?} entity: Metadata received: {:?}", + self.entity_type, md_recvd_params + ); + } + + fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams) { + println!( + "{:?} entity: File segment {:?} received", + self.entity_type, segment_recvd_params + ); + } + + fn report_indication(&mut self, _id: &crate::TransactionId) {} + + fn suspended_indication(&mut self, _id: &crate::TransactionId, _condition_code: ConditionCode) { + panic!("unexpected suspended indication"); + } + + fn resumed_indication(&mut self, _id: &crate::TransactionId, _progresss: u64) {} + + fn fault_indication( + &mut self, + _id: &crate::TransactionId, + _condition_code: ConditionCode, + _progress: u64, + ) { + panic!("unexpected fault indication"); + } + + fn abandoned_indication( + &mut self, + _id: &crate::TransactionId, + _condition_code: ConditionCode, + _progress: u64, + ) { + panic!("unexpected abandoned indication"); + } + + fn eof_recvd_indication(&mut self, id: &crate::TransactionId) { + println!( + "{:?} entity: EOF received for transaction {:?}", + self.entity_type, id + ); + } +} + +fn end_to_end_test(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(); + let stop_signal_ctrl = stop_signal_source.clone(); + + let completion_signal_source = Arc::new(AtomicBool::new(false)); + let completion_signal_source_main = completion_signal_source.clone(); + + let completion_signal_dest = Arc::new(AtomicBool::new(false)); + let completion_signal_dest_main = completion_signal_dest.clone(); + + let srcfile = tempfile::NamedTempFile::new().unwrap().into_temp_path(); + let mut file = OpenOptions::new() + .write(true) + .open(&srcfile) + .expect("opening file failed"); + file.write_all(FILE_DATA.as_bytes()) + .expect("writing file content failed"); + let destdir = tempfile::tempdir().expect("creating temp directory failed"); + let destfile = destdir.path().join("test.txt"); + + let local_cfg_source = LocalEntityConfig::new( + LOCAL_ID.into(), + IndicationConfig::default(), + ExampleFaultHandler::default(), + ); + let (source_tx, source_rx) = mpsc::channel::(); + let (dest_tx, dest_rx) = mpsc::channel::(); + let put_request_cacher = StaticPutRequestCacher::new(2048); + let remote_cfg_of_dest = RemoteEntityConfig::new_with_default_values( + REMOTE_ID.into(), + 1024, + with_closure, + false, + spacepackets::cfdp::TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + let seq_count_provider = SeqCountProviderSyncU16::default(); + let mut source_handler = SourceHandler::new( + local_cfg_source, + source_tx, + NativeFilestore::default(), + put_request_cacher, + 2048, + remote_cfg_of_dest, + seq_count_provider, + ); + let mut cfdp_user_source = ExampleCfdpUser::new(EntityType::Sending, completion_signal_source); + + let local_cfg_dest = LocalEntityConfig::new( + REMOTE_ID.into(), + IndicationConfig::default(), + ExampleFaultHandler::default(), + ); + let remote_cfg_of_source = RemoteEntityConfig::new_with_default_values( + LOCAL_ID.into(), + 1024, + true, + false, + spacepackets::cfdp::TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + let mut dest_handler = DestinationHandler::new( + local_cfg_dest, + 1024, + dest_tx, + NativeFilestore::default(), + remote_cfg_of_source, + StdCheckTimerCreator::default(), + ); + let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving, completion_signal_dest); + + let put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + srcfile.to_str().expect("invaid path string"), + destfile.to_str().expect("invaid path string"), + Some(TransmissionMode::Unacknowledged), + Some(with_closure), + ) + .expect("put request creation failed"); + + let start = std::time::Instant::now(); + + let jh_source = thread::spawn(move || { + source_handler + .put_request(&put_request) + .expect("put request failed"); + loop { + let mut next_delay = None; + let mut undelayed_call_count = 0; + let packet_info = match dest_rx.try_recv() { + Ok(pdu_with_info) => Some(pdu_with_info), + Err(e) => match e { + mpsc::TryRecvError::Empty => None, + mpsc::TryRecvError::Disconnected => { + panic!("unexpected disconnect from destination channel sender"); + } + }, + }; + match source_handler.state_machine(&mut cfdp_user_source, packet_info.as_ref()) { + Ok(sent_packets) => { + if sent_packets == 0 { + next_delay = Some(Duration::from_millis(50)); + } + } + Err(e) => { + println!("Source handler error: {}", e); + next_delay = Some(Duration::from_millis(50)); + } + } + if let Some(delay) = next_delay { + thread::sleep(delay); + } else { + undelayed_call_count += 1; + } + if stop_signal_source.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + // Safety feature against configuration errors. + if undelayed_call_count >= 200 { + panic!("Source handler state machine possible in permanent loop"); + } + } + }); + + let jh_dest = thread::spawn(move || { + loop { + let mut next_delay = None; + let mut undelayed_call_count = 0; + let packet_info = match source_rx.try_recv() { + Ok(pdu_with_info) => Some(pdu_with_info), + Err(e) => match e { + mpsc::TryRecvError::Empty => None, + mpsc::TryRecvError::Disconnected => { + panic!("unexpected disconnect from destination channel sender"); + } + }, + }; + match dest_handler.state_machine(&mut cfdp_user_dest, packet_info.as_ref()) { + Ok(sent_packets) => { + if sent_packets == 0 { + next_delay = Some(Duration::from_millis(50)); + } + } + Err(e) => { + println!("Source handler error: {}", e); + next_delay = Some(Duration::from_millis(50)); + } + } + if let Some(delay) = next_delay { + thread::sleep(delay); + } else { + undelayed_call_count += 1; + } + if stop_signal_dest.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + // Safety feature against configuration errors. + if undelayed_call_count >= 200 { + panic!("Destination handler state machine possible in permanent loop"); + } + } + }); + + loop { + if completion_signal_source_main.load(std::sync::atomic::Ordering::Relaxed) + && completion_signal_dest_main.load(std::sync::atomic::Ordering::Relaxed) + { + let file = std::fs::read_to_string(destfile).expect("reading file failed"); + assert_eq!(file, FILE_DATA); + // Stop the threads gracefully. + stop_signal_ctrl.store(true, std::sync::atomic::Ordering::Relaxed); + break; + } + if std::time::Instant::now() - start > Duration::from_secs(2) { + panic!("file transfer not finished in 2 seconds"); + } + std::thread::sleep(Duration::from_millis(50)); + } + + jh_source.join().unwrap(); + jh_dest.join().unwrap(); +} + +#[test] +fn end_to_end_test_no_closure() { + end_to_end_test(false); +} + +#[test] +fn end_to_end_test_with_closure() { + end_to_end_test(true); +}