From b87ccde07d6cb9f8612088b3f7264f38933a6eb6 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 | 67 + LICENSE-APACHE | 201 +++ NOTICE | 1 + README.md | 49 + automation/Dockerfile | 28 + automation/Jenkinsfile | 81 + coverage.py | 54 + examples/python-interop/.gitignore | 1 + examples/python-interop/README.md | 37 + examples/python-interop/main.py | 682 ++++++++ examples/python-interop/main.rs | 520 ++++++ examples/python-interop/requirements.txt | 1 + release-checklist.md | 25 + src/dest.rs | 2022 ++++++++++++++++++++++ src/filestore.rs | 838 +++++++++ src/lib.rs | 1538 ++++++++++++++++ src/request.rs | 782 +++++++++ src/source.rs | 1824 +++++++++++++++++++ src/time.rs | 7 + src/user.rs | 100 ++ tests/end-to-end.rs | 353 ++++ 24 files changed, 9299 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 examples/python-interop/.gitignore create mode 100644 examples/python-interop/README.md create mode 100755 examples/python-interop/main.py create mode 100644 examples/python-interop/main.rs create mode 100644 examples/python-interop/requirements.txt 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..c3adfe2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,67 @@ +[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 + +[dependencies.defmt] +version = "0.3" +optional = true + +[features] +default = ["std"] +std = [ + "alloc", + "thiserror", + "spacepackets/std" +] +alloc = [ + "hashbrown", + "spacepackets/alloc" +] +serde = ["dep:serde", "spacepackets/serde", "hashbrown/serde"] +defmt = ["dep:defmt", "spacepackets/defmt"] + +[dev-dependencies] +tempfile = "3" +rand = "0.8" +log = "0.4" +fern = "0.6" +chrono = "0.4" +clap = { version = "4", features = ["derive"] } + +[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..c40fa18 --- /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"--ignore \"examples/*\" -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/examples/python-interop/.gitignore b/examples/python-interop/.gitignore new file mode 100644 index 0000000..f9606a3 --- /dev/null +++ b/examples/python-interop/.gitignore @@ -0,0 +1 @@ +/venv diff --git a/examples/python-interop/README.md b/examples/python-interop/README.md new file mode 100644 index 0000000..a57e85c --- /dev/null +++ b/examples/python-interop/README.md @@ -0,0 +1,37 @@ +Python Interoperability Example for cfdp-rs +======= + +This example application showcases the interoperability of the CFDP handlers written in Rust +with a Python implementation which uses [cfdp-py](https://github.com/us-irs/cfdp-py) library. + +Both the Rust and the Python app exchange packet data units via a UDP interface and launch +both a destination and source handler. As such, they are both able to send and receive files. +Both applications can be started with the command line argument `-f` to initiate a file transfer. +You can run both applications with `-h` to get more information about the available options. + +## Running the Python App + +It is recommended to run the Python App in a dedicated virtual environment. For example, on a +Unix system you can use `python3 -m venv venv` and then `source venv/bin/activate` to create +and activate a virtual environment. + +After that, you can install the required dependencies using + +```sh +pip install -r requirements.txt +``` + +and then run the application using `./main.py` or `python3 main.py`. + +It is recommended to run `./main.py -h` first to get an overview of some possible options. +Running the Python App with `./main.py -f` will cause the Python App to start a file copy operation +with fixed temporary paths. + +## Running the Rust App + +You can run the Rust application using `cargo`, for example `cargo run --example python-interop`. +It is recommended to run `cargo run --example python-interop -- -h` to get an overview of some +possible launch options. + +Running the Rust App with `cargo run --example python-interop -- -f` will cause the Rust app to +start a file copy operation with fixed temporary paths. diff --git a/examples/python-interop/main.py b/examples/python-interop/main.py new file mode 100755 index 0000000..752b96a --- /dev/null +++ b/examples/python-interop/main.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python3 +from datetime import timedelta +from pathlib import Path +import os +import ipaddress +import tempfile +import socket +import select +import threading +import argparse +import logging +import time +import copy +from threading import Thread, Event +from typing import Any, Dict, List, Tuple, Optional +from multiprocessing import Queue +from queue import Empty + +from cfdppy.handler import DestHandler, RemoteEntityCfgTable, SourceHandler +from cfdppy.exceptions import InvalidDestinationId, SourceFileDoesNotExist +from cfdppy import ( + CfdpUserBase, + LocalEntityCfg, + PacketDestination, + PutRequest, + TransactionId, + get_packet_destination, + CfdpState, +) +from cfdppy.mib import ( + CheckTimerProvider, + DefaultFaultHandlerBase, + EntityType, + IndicationCfg, + RemoteEntityCfg, +) +from cfdppy.user import ( + FileSegmentRecvdParams, + MetadataRecvParams, + TransactionFinishedParams, + TransactionParams, +) +from spacepackets.cfdp import ChecksumType, ConditionCode, TransmissionMode +from spacepackets.cfdp.pdu import AbstractFileDirectiveBase, PduFactory, PduHolder +from spacepackets.cfdp.tlv import ( + MessageToUserTlv, + OriginatingTransactionId, + ProxyMessageType, + ProxyPutResponse, + ReservedCfdpMessage, +) +from spacepackets.cfdp.tlv.msg_to_user import ProxyPutResponseParams +from spacepackets.countdown import Countdown +from spacepackets.seqcount import SeqCountProvider +from spacepackets.util import ByteFieldU16, UnsignedByteField + + +PYTHON_ENTITY_ID = ByteFieldU16(1) +RUST_ENTITY_ID = ByteFieldU16(2) +# Enable all indications for both local and remote entity. +INDICATION_CFG = IndicationCfg() + +BASE_STR_SRC = "PY SRC" +BASE_STR_DEST = "PY DEST" + +FILE_CONTENT = "Hello World!\n" +FILE_SEGMENT_SIZE = 256 +MAX_PACKET_LEN = 512 + +# This queue is used to send put requests. +PUT_REQ_QUEUE = Queue() +# All telecommands which should go to the source handler should be put into this queue by +# the UDP server. +SOURCE_ENTITY_QUEUE = Queue() +# All telecommands which should go to the destination handler should be put into this queue by +# the UDP server. +DEST_ENTITY_QUEUE = Queue() +# All telemetry which should be sent to the remote entity is put into this queue and will then +# be sent by the UDP server. +TM_QUEUE = Queue() + +REMOTE_CFG_OF_PY_ENTITY = RemoteEntityCfg( + entity_id=PYTHON_ENTITY_ID, + max_packet_len=MAX_PACKET_LEN, + max_file_segment_len=FILE_SEGMENT_SIZE, + closure_requested=True, + crc_on_transmission=False, + default_transmission_mode=TransmissionMode.ACKNOWLEDGED, + crc_type=ChecksumType.CRC_32, +) + +REMOTE_CFG_OF_REMOTE_ENTITY = copy.copy(REMOTE_CFG_OF_PY_ENTITY) +REMOTE_CFG_OF_REMOTE_ENTITY.entity_id = RUST_ENTITY_ID + +RUST_PORT = 5111 +PY_PORT = 5222 + +_LOGGER = logging.getLogger(__name__) + + +class UdpServer(Thread): + def __init__( + self, + sleep_time: float, + addr: Tuple[str, int], + explicit_remote_addr: Optional[Tuple[str, int]], + tx_queue: Queue, + source_entity_rx_queue: Queue, + dest_entity_rx_queue: Queue, + stop_signal: Event, + ): + super().__init__() + self.sleep_time = sleep_time + self.udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + self.addr = addr + self.explicit_remote_addr = explicit_remote_addr + self.udp_socket.bind(addr) + self.tm_queue = tx_queue + self.last_sender = None + self.stop_signal = stop_signal + self.source_entity_queue = source_entity_rx_queue + self.dest_entity_queue = dest_entity_rx_queue + + def run(self): + _LOGGER.info(f"Starting UDP server on {self.addr}") + while True: + if self.stop_signal.is_set(): + break + self.periodic_operation() + time.sleep(self.sleep_time) + + def periodic_operation(self): + while True: + next_packet = self.poll_next_udp_packet() + if next_packet is None or next_packet.pdu is None: + break + # Perform PDU routing. + packet_dest = get_packet_destination(next_packet.pdu) + _LOGGER.debug(f"UDP server: Routing {next_packet} to {packet_dest}") + if packet_dest == PacketDestination.DEST_HANDLER: + self.dest_entity_queue.put(next_packet.pdu) + elif packet_dest == PacketDestination.SOURCE_HANDLER: + self.source_entity_queue.put(next_packet.pdu) + self.send_packets() + + def poll_next_udp_packet(self) -> Optional[PduHolder]: + ready = select.select([self.udp_socket], [], [], 0) + if ready[0]: + data, self.last_sender = self.udp_socket.recvfrom(4096) + return PduFactory.from_raw_to_holder(data) + return None + + def send_packets(self): + while True: + try: + next_tm = self.tm_queue.get(False) + if not isinstance(next_tm, bytes) and not isinstance( + next_tm, bytearray + ): + _LOGGER.error( + f"UDP server can only sent bytearray, received {next_tm}" + ) + continue + if self.explicit_remote_addr is not None: + self.udp_socket.sendto(next_tm, self.explicit_remote_addr) + elif self.last_sender is not None: + self.udp_socket.sendto(next_tm, self.last_sender) + else: + _LOGGER.warning( + "UDP Server: No packet destination found, dropping TM" + ) + except Empty: + break + + +class SourceEntityHandler(Thread): + def __init__( + self, + base_str: str, + verbose_level: int, + source_handler: SourceHandler, + put_req_queue: Queue, + source_entity_queue: Queue, + tm_queue: Queue, + stop_signal: Event, + ): + super().__init__() + self.base_str = base_str + self.verbose_level = verbose_level + self.source_handler = source_handler + self.put_req_queue = put_req_queue + self.source_entity_queue = source_entity_queue + self.tm_queue = tm_queue + self.stop_signal = stop_signal + + def _idle_handling(self) -> bool: + try: + put_req: PutRequest = self.put_req_queue.get(False) + _LOGGER.info(f"{self.base_str}: Handling Put Request: {put_req}") + if put_req.destination_id not in [PYTHON_ENTITY_ID, RUST_ENTITY_ID]: + _LOGGER.warning( + f"can only handle put requests target towards {RUST_ENTITY_ID} or " + f"{PYTHON_ENTITY_ID}" + ) + else: + try: + self.source_handler.put_request(put_req) + except SourceFileDoesNotExist as e: + _LOGGER.warning( + f"can not handle put request, source file {e.file} does not exist" + ) + return True + except Empty: + return False + + def _busy_handling(self): + # We are getting the packets from a Queue here, they could for example also be polled + # from a network. + packet_received = False + packet = None + try: + # We are getting the packets from a Queue here, they could for example also be polled + # from a network. + packet = self.source_entity_queue.get(False) + packet_received = True + except Empty: + pass + try: + packet_sent = self._call_source_state_machine(packet) + # If there is no work to do, put the thread to sleep. + if not packet_received and not packet_sent: + return False + except SourceFileDoesNotExist: + _LOGGER.warning("Source file does not exist") + self.source_handler.reset() + + def _call_source_state_machine( + self, packet: Optional[AbstractFileDirectiveBase] + ) -> bool: + """Returns whether a packet was sent.""" + + if packet is not None: + _LOGGER.debug(f"{self.base_str}: Inserting {packet}") + try: + fsm_result = self.source_handler.state_machine(packet) + except InvalidDestinationId as e: + _LOGGER.warning( + f"invalid destination ID {e.found_dest_id} on packet {packet}, expected " + f"{e.expected_dest_id}" + ) + fsm_result = self.source_handler.state_machine(None) + packet_sent = False + if fsm_result.states.num_packets_ready > 0: + while fsm_result.states.num_packets_ready > 0: + next_pdu_wrapper = self.source_handler.get_next_packet() + assert next_pdu_wrapper is not None + if self.verbose_level >= 1: + _LOGGER.debug( + f"{self.base_str}: Sending packet {next_pdu_wrapper.pdu}" + ) + # Send all packets which need to be sent. + self.tm_queue.put(next_pdu_wrapper.pack()) + packet_sent = True + return packet_sent + + def run(self): + _LOGGER.info(f"Starting {self.base_str}") + while True: + if self.stop_signal.is_set(): + break + if self.source_handler.state == CfdpState.IDLE: + if not self._idle_handling(): + time.sleep(0.2) + continue + if self.source_handler.state == CfdpState.BUSY: + if not self._busy_handling(): + time.sleep(0.2) + + +class DestEntityHandler(Thread): + def __init__( + self, + base_str: str, + verbose_level: int, + dest_handler: DestHandler, + dest_entity_queue: Queue, + tm_queue: Queue, + stop_signal: Event, + ): + super().__init__() + self.base_str = base_str + self.verbose_level = verbose_level + self.dest_handler = dest_handler + self.dest_entity_queue = dest_entity_queue + self.tm_queue = tm_queue + self.stop_signal = stop_signal + + def run(self): + _LOGGER.info( + f"Starting {self.base_str}. Local ID {self.dest_handler.cfg.local_entity_id}" + ) + while True: + packet_received = False + packet = None + if self.stop_signal.is_set(): + break + try: + packet = self.dest_entity_queue.get(False) + packet_received = True + except Empty: + pass + if packet is not None: + _LOGGER.debug(f"{self.base_str}: Inserting {packet}") + fsm_result = self.dest_handler.state_machine(packet) + packet_sent = False + if fsm_result.states.num_packets_ready > 0: + while fsm_result.states.num_packets_ready > 0: + next_pdu_wrapper = self.dest_handler.get_next_packet() + assert next_pdu_wrapper is not None + if self.verbose_level >= 1: + _LOGGER.debug( + f"{self.base_str}: Sending packet {next_pdu_wrapper.pdu}" + ) + self.tm_queue.put(next_pdu_wrapper.pack()) + packet_sent = True + # If there is no work to do, put the thread to sleep. + if not packet_received and not packet_sent: + time.sleep(0.5) + + +class CfdpFaultHandler(DefaultFaultHandlerBase): + def __init__(self, base_str: str): + self.base_str = base_str + super().__init__() + + def notice_of_suspension_cb( + self, transaction_id: TransactionId, cond: ConditionCode, progress: int + ): + _LOGGER.warning( + f"{self.base_str}: Received Notice of Suspension for transaction {transaction_id!r} " + f"with condition code {cond!r}. Progress: {progress}" + ) + + def notice_of_cancellation_cb( + self, transaction_id: TransactionId, cond: ConditionCode, progress: int + ): + _LOGGER.warning( + f"{self.base_str}: Received Notice of Cancellation for transaction {transaction_id!r} " + f"with condition code {cond!r}. Progress: {progress}" + ) + + def abandoned_cb( + self, transaction_id: TransactionId, cond: ConditionCode, progress: int + ): + _LOGGER.warning( + f"{self.base_str}: Abandoned fault for transaction {transaction_id!r} " + f"with condition code {cond!r}. Progress: {progress}" + ) + + def ignore_cb( + self, transaction_id: TransactionId, cond: ConditionCode, progress: int + ): + _LOGGER.warning( + f"{self.base_str}: Ignored fault for transaction {transaction_id!r} " + f"with condition code {cond!r}. Progress: {progress}" + ) + + +class CfdpUser(CfdpUserBase): + def __init__(self, base_str: str, put_req_queue: Queue): + self.base_str = base_str + self.put_req_queue = put_req_queue + # This is a dictionary where the key is the current transaction ID for a transaction which + # was triggered by a proxy request with a originating ID. + self.active_proxy_put_reqs: Dict[TransactionId, TransactionId] = {} + super().__init__() + + def transaction_indication( + self, + transaction_indication_params: TransactionParams, + ): + """This indication is used to report the transaction ID to the CFDP user""" + _LOGGER.info( + f"{self.base_str}: Transaction.indication for {transaction_indication_params.transaction_id}" + ) + if transaction_indication_params.originating_transaction_id is not None: + _LOGGER.info( + f"Originating Transaction ID: {transaction_indication_params.originating_transaction_id}" + ) + self.active_proxy_put_reqs.update( + { + transaction_indication_params.transaction_id: transaction_indication_params.originating_transaction_id + } + ) + + def eof_sent_indication(self, transaction_id: TransactionId): + _LOGGER.info(f"{self.base_str}: EOF-Sent.indication for {transaction_id}") + + def transaction_finished_indication(self, params: TransactionFinishedParams): + _LOGGER.info( + f"{self.base_str}: Transaction-Finished.indication for {params.transaction_id}." + ) + _LOGGER.info(f"Condition Code: {params.finished_params.condition_code!r}") + _LOGGER.info(f"Delivery Code: {params.finished_params.delivery_code!r}") + _LOGGER.info(f"File Status: {params.finished_params.file_status!r}") + if params.transaction_id in self.active_proxy_put_reqs: + proxy_put_response = ProxyPutResponse( + ProxyPutResponseParams.from_finished_params(params.finished_params) + ).to_generic_msg_to_user_tlv() + originating_id = self.active_proxy_put_reqs.get(params.transaction_id) + assert originating_id is not None + put_req = PutRequest( + destination_id=originating_id.source_id, + source_file=None, + dest_file=None, + trans_mode=None, + closure_requested=None, + msgs_to_user=[ + proxy_put_response, + OriginatingTransactionId( + originating_id + ).to_generic_msg_to_user_tlv(), + ], + ) + _LOGGER.info( + f"Requesting Proxy Put Response concluding Proxy Put originating from " + f"{originating_id}" + ) + self.put_req_queue.put(put_req) + self.active_proxy_put_reqs.pop(params.transaction_id) + + def metadata_recv_indication(self, params: MetadataRecvParams): + _LOGGER.info( + f"{self.base_str}: Metadata-Recv.indication for {params.transaction_id}." + ) + if params.msgs_to_user is not None: + self._handle_msgs_to_user(params.transaction_id, params.msgs_to_user) + + def _handle_msgs_to_user( + self, transaction_id: TransactionId, msgs_to_user: List[MessageToUserTlv] + ): + for msg_to_user in msgs_to_user: + if msg_to_user.is_reserved_cfdp_message(): + reserved_msg_tlv = msg_to_user.to_reserved_msg_tlv() + assert reserved_msg_tlv is not None + self._handle_reserved_cfdp_message(transaction_id, reserved_msg_tlv) + else: + _LOGGER.info(f"Received custom message to user: {msg_to_user}") + + def _handle_reserved_cfdp_message( + self, transaction_id: TransactionId, reserved_cfdp_msg: ReservedCfdpMessage + ): + if reserved_cfdp_msg.is_cfdp_proxy_operation(): + self._handle_cfdp_proxy_operation(transaction_id, reserved_cfdp_msg) + elif reserved_cfdp_msg.is_originating_transaction_id(): + _LOGGER.info( + f"Received originating transaction ID: " + f"{reserved_cfdp_msg.get_originating_transaction_id()}" + ) + + def _handle_cfdp_proxy_operation( + self, transaction_id: TransactionId, reserved_cfdp_msg: ReservedCfdpMessage + ): + if ( + reserved_cfdp_msg.get_cfdp_proxy_message_type() + == ProxyMessageType.PUT_REQUEST + ): + put_req_params = reserved_cfdp_msg.get_proxy_put_request_params() + _LOGGER.info(f"Received Proxy Put Request: {put_req_params}") + assert put_req_params is not None + put_req = PutRequest( + destination_id=put_req_params.dest_entity_id, + source_file=Path(put_req_params.source_file_as_path), + dest_file=Path(put_req_params.dest_file_as_path), + trans_mode=None, + closure_requested=None, + msgs_to_user=[ + OriginatingTransactionId( + transaction_id + ).to_generic_msg_to_user_tlv() + ], + ) + self.put_req_queue.put(put_req) + elif ( + reserved_cfdp_msg.get_cfdp_proxy_message_type() + == ProxyMessageType.PUT_RESPONSE + ): + put_response_params = reserved_cfdp_msg.get_proxy_put_response_params() + _LOGGER.info(f"Received Proxy Put Response: {put_response_params}") + + def file_segment_recv_indication(self, params: FileSegmentRecvdParams): + _LOGGER.info( + f"{self.base_str}: File-Segment-Recv.indication for {params.transaction_id}." + ) + + def report_indication(self, transaction_id: TransactionId, status_report: Any): + # TODO: p.28 of the CFDP standard specifies what information the status report parameter + # could contain. I think it would be better to not hardcode the type of the status + # report here, but something like Union[any, CfdpStatusReport] with CfdpStatusReport + # being an implementation which supports all three information suggestions would be + # nice + pass + + def suspended_indication( + self, transaction_id: TransactionId, cond_code: ConditionCode + ): + _LOGGER.info( + f"{self.base_str}: Suspended.indication for {transaction_id} | Condition Code: {cond_code}" + ) + + def resumed_indication(self, transaction_id: TransactionId, progress: int): + _LOGGER.info( + f"{self.base_str}: Resumed.indication for {transaction_id} | Progress: {progress} bytes" + ) + + def fault_indication( + self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int + ): + _LOGGER.info( + f"{self.base_str}: Fault.indication for {transaction_id} | Condition Code: {cond_code} | " + f"Progress: {progress} bytes" + ) + + def abandoned_indication( + self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int + ): + _LOGGER.info( + f"{self.base_str}: Abandoned.indication for {transaction_id} | Condition Code: {cond_code} |" + f" Progress: {progress} bytes" + ) + + def eof_recv_indication(self, transaction_id: TransactionId): + _LOGGER.info(f"{self.base_str}: EOF-Recv.indication for {transaction_id}") + + +class CustomCheckTimerProvider(CheckTimerProvider): + def provide_check_timer( + self, + local_entity_id: UnsignedByteField, + remote_entity_id: UnsignedByteField, + entity_type: EntityType, + ) -> Countdown: + return Countdown(timedelta(seconds=5.0)) + + +def main(): + parser = argparse.ArgumentParser( + prog="CFDP Local Entity Application", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument("-v", "--verbose", action="count", default=0) + parser.add_argument( + "-f", + help="Perform a file-copy operation", + action="store_true", + dest="file_copy", + ) + parser.add_argument( + "-m", + "--mode", + dest="transmission_mode", + help=( + f"Specify the transfer type{os.linesep}" + f' - "0" or "ack" for unacknowledged (Class 0) transfers{os.linesep}' + f' - "1" or "nak" for acknowledged (Class 1) transfers. Default value' + ), + default="nak", + ) + # Optional Boolean argument where you can specify True/False + parser.add_argument( + "-c", + type=bool, + nargs="?", + const=True, + default=None, + dest="closure_requested", + help="Request transaction closure for the unacknowledged mode", + ) + args = parser.parse_args() + stop_signal = threading.Event() + + logging_level = logging.INFO + if args.verbose >= 1: + logging_level = logging.DEBUG + + logging.basicConfig(level=logging_level) + + remote_cfg_table = RemoteEntityCfgTable() + remote_cfg_table.add_config(REMOTE_CFG_OF_REMOTE_ENTITY) + + src_fault_handler = CfdpFaultHandler(BASE_STR_SRC) + # 16 bit sequence count for transactions. + src_seq_count_provider = SeqCountProvider(16) + src_user = CfdpUser(BASE_STR_SRC, PUT_REQ_QUEUE) + check_timer_provider = CustomCheckTimerProvider() + source_handler = SourceHandler( + cfg=LocalEntityCfg(PYTHON_ENTITY_ID, INDICATION_CFG, src_fault_handler), + seq_num_provider=src_seq_count_provider, + remote_cfg_table=remote_cfg_table, + user=src_user, + check_timer_provider=check_timer_provider, + ) + source_entity_task = SourceEntityHandler( + BASE_STR_SRC, + logging_level, + source_handler, + PUT_REQ_QUEUE, + SOURCE_ENTITY_QUEUE, + TM_QUEUE, + stop_signal, + ) + + # Enable all indications. + dest_fault_handler = CfdpFaultHandler(BASE_STR_DEST) + dest_user = CfdpUser(BASE_STR_DEST, PUT_REQ_QUEUE) + dest_handler = DestHandler( + cfg=LocalEntityCfg(PYTHON_ENTITY_ID, INDICATION_CFG, dest_fault_handler), + user=dest_user, + remote_cfg_table=remote_cfg_table, + check_timer_provider=check_timer_provider, + ) + dest_entity_task = DestEntityHandler( + BASE_STR_DEST, + logging_level, + dest_handler, + DEST_ENTITY_QUEUE, + TM_QUEUE, + stop_signal, + ) + + # Address Any to accept CFDP packets from other address than localhost. + local_addr = ipaddress.ip_address("0.0.0.0") + # Localhost as default. + remote_addr = ipaddress.ip_address("127.0.0.1") + udp_server = UdpServer( + sleep_time=0.1, + addr=(str(local_addr), PY_PORT), + explicit_remote_addr=(str(remote_addr), RUST_PORT), + tx_queue=TM_QUEUE, + source_entity_rx_queue=SOURCE_ENTITY_QUEUE, + dest_entity_rx_queue=DEST_ENTITY_QUEUE, + stop_signal=stop_signal, + ) + + # Prepare a put request / file copy operation if the user specifies it. + if args.file_copy: + _LOGGER.info("Performing file copy operation") + transmission_mode = None + if args.transmission_mode == "ack": + transmission_mode = TransmissionMode.ACKNOWLEDGED + elif args.transmission_mode == "nak": + transmission_mode = TransmissionMode.UNACKNOWLEDGED + with tempfile.NamedTemporaryFile(delete=False) as srcfile: + srcfile.write(FILE_CONTENT.encode()) + srcfile_path = srcfile.name + tempdir = tempfile.TemporaryDirectory() + put_req = PutRequest( + destination_id=RUST_ENTITY_ID, + source_file=Path(srcfile_path), + dest_file=Path(tempdir.name).joinpath("test.txt"), + closure_requested=args.closure_requested, + trans_mode=transmission_mode, + ) + PUT_REQ_QUEUE.put(put_req) + + source_entity_task.start() + dest_entity_task.start() + udp_server.start() + + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + stop_signal.set() + + source_entity_task.join() + dest_entity_task.join() + udp_server.join() + + +if __name__ == "__main__": + main() diff --git a/examples/python-interop/main.rs b/examples/python-interop/main.rs new file mode 100644 index 0000000..8c775cf --- /dev/null +++ b/examples/python-interop/main.rs @@ -0,0 +1,520 @@ +use std::{ + fmt::Debug, + fs::OpenOptions, + io::{self, ErrorKind, Write}, + net::{IpAddr, Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket}, + sync::mpsc, + thread, + time::Duration, +}; + +use cfdp::{ + dest::DestinationHandler, + filestore::NativeFilestore, + request::{PutRequestOwned, StaticPutRequestCacher}, + source::SourceHandler, + user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams}, + EntityType, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, PduProvider, + RemoteEntityConfig, StdTimerCreator, TransactionId, UserFaultHookProvider, +}; +use clap::Parser; +use log::{debug, info, warn}; +use spacepackets::{ + cfdp::{ + pdu::{file_data::FileDataPdu, metadata::MetadataPduReader, PduError}, + ChecksumType, ConditionCode, TransmissionMode, + }, + seq_count::SeqCountProviderSyncU16, + util::{UnsignedByteFieldU16, UnsignedEnum}, +}; + +const PYTHON_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); +const RUST_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + +const RUST_PORT: u16 = 5111; +const PY_PORT: u16 = 5222; + +const LOG_LEVEL: log::LevelFilter = log::LevelFilter::Info; + +const FILE_DATA: &str = "Hello World!"; + +#[derive(Debug, Copy, Clone, clap::ValueEnum)] +pub enum TransmissionModeCli { + Nak, + Ack, +} + +#[derive(clap::Parser)] +#[command(about = "Arguments for executing a file copy operation")] +pub struct Cli { + #[arg(short, help = "Perform a file copy operation")] + file_copy: bool, + #[arg(short, default_value = "nak")] + mode: Option, + #[arg(short)] + closure_requested: Option, +} + +#[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, +} + +impl ExampleCfdpUser { + pub fn new(entity_type: EntityType) -> Self { + Self { entity_type } + } +} + +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 + ); + } + + 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 + ); + } +} + +pub struct UdpServer { + pub socket: UdpSocket, + recv_buf: Vec, + remote_addr: SocketAddr, + source_tc_tx: mpsc::Sender, + dest_tc_tx: mpsc::Sender, + source_tm_rx: mpsc::Receiver, + dest_tm_rx: mpsc::Receiver, +} + +#[derive(Debug, thiserror::Error)] +pub enum UdpServerError { + #[error(transparent)] + Io(#[from] io::Error), + #[error("pdu error: {0}")] + Pdu(#[from] PduError), + #[error("send error")] + Send, +} + +impl UdpServer { + pub fn new( + addr: A, + remote_addr: SocketAddr, + max_recv_size: usize, + source_tc_tx: mpsc::Sender, + dest_tc_tx: mpsc::Sender, + source_tm_rx: mpsc::Receiver, + dest_tm_rx: mpsc::Receiver, + ) -> Result { + let server = Self { + socket: UdpSocket::bind(addr)?, + recv_buf: vec![0; max_recv_size], + source_tc_tx, + dest_tc_tx, + remote_addr, + source_tm_rx, + dest_tm_rx, + }; + server.socket.set_nonblocking(true)?; + Ok(server) + } + + pub fn try_recv_tc( + &mut self, + ) -> Result, UdpServerError> { + let res = match self.socket.recv_from(&mut self.recv_buf) { + Ok(res) => res, + Err(e) => { + return if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut { + Ok(None) + } else { + Err(e.into()) + } + } + }; + let (_, from) = res; + self.remote_addr = from; + let pdu_owned = PduOwnedWithInfo::new_from_raw_packet(&self.recv_buf)?; + match pdu_owned.packet_target()? { + cfdp::PacketTarget::SourceEntity => { + self.source_tc_tx + .send(pdu_owned.clone()) + .map_err(|_| UdpServerError::Send)?; + } + cfdp::PacketTarget::DestEntity => { + self.dest_tc_tx + .send(pdu_owned.clone()) + .map_err(|_| UdpServerError::Send)?; + } + } + Ok(Some((pdu_owned, from))) + } + + pub fn recv_and_send_telemetry(&mut self) { + let tm_handler = |receiver: &mpsc::Receiver| { + while let Ok(tm) = receiver.try_recv() { + debug!("Sending PDU: {:?}", tm); + pdu_printout(&tm); + let result = self.socket.send_to(tm.pdu(), self.remote_addr()); + if let Err(e) = result { + warn!("Sending TM with UDP socket failed: {e}") + } + } + }; + tm_handler(&self.source_tm_rx); + tm_handler(&self.dest_tm_rx); + } + + pub fn remote_addr(&self) -> SocketAddr { + self.remote_addr + } +} + +fn pdu_printout(pdu: &PduOwnedWithInfo) { + match pdu.pdu_type() { + spacepackets::cfdp::PduType::FileDirective => match pdu.file_directive_type().unwrap() { + spacepackets::cfdp::pdu::FileDirectiveType::EofPdu => (), + spacepackets::cfdp::pdu::FileDirectiveType::FinishedPdu => (), + spacepackets::cfdp::pdu::FileDirectiveType::AckPdu => (), + spacepackets::cfdp::pdu::FileDirectiveType::MetadataPdu => { + let meta_pdu = + MetadataPduReader::new(pdu.pdu()).expect("creating metadata pdu failed"); + debug!("Metadata PDU: {:?}", meta_pdu) + } + spacepackets::cfdp::pdu::FileDirectiveType::NakPdu => (), + spacepackets::cfdp::pdu::FileDirectiveType::PromptPdu => (), + spacepackets::cfdp::pdu::FileDirectiveType::KeepAlivePdu => (), + }, + spacepackets::cfdp::PduType::FileData => { + let fd_pdu = FileDataPdu::from_bytes(pdu.pdu()).expect("creating file data pdu failed"); + debug!("File data PDU: {:?}", fd_pdu); + } + } +} + +fn main() { + let cli_args = Cli::parse(); + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + std::thread::current().name().expect("thread is not named"), + record.level(), + message + )) + }) + .level(LOG_LEVEL) + .chain(std::io::stdout()) + .apply() + .unwrap(); + + let srcfile = tempfile::NamedTempFile::new().unwrap().into_temp_path(); + let mut file = OpenOptions::new() + .write(true) + .open(&srcfile) + .expect("opening file failed"); + info!("created test source file {:?}", srcfile); + 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( + RUST_ID.into(), + IndicationConfig::default(), + ExampleFaultHandler::default(), + ); + let (source_tm_tx, source_tm_rx) = mpsc::channel::(); + let (dest_tm_tx, dest_tm_rx) = mpsc::channel::(); + let put_request_cacher = StaticPutRequestCacher::new(2048); + let remote_cfg_python = RemoteEntityConfig::new_with_default_values( + PYTHON_ID.into(), + 1024, + true, + false, + spacepackets::cfdp::TransmissionMode::Unacknowledged, + ChecksumType::Crc32C, + ); + let seq_count_provider = SeqCountProviderSyncU16::default(); + let mut source_handler = SourceHandler::new( + local_cfg_source, + source_tm_tx, + NativeFilestore::default(), + put_request_cacher, + 2048, + remote_cfg_python, + StdTimerCreator::default(), + seq_count_provider, + ); + let mut cfdp_user_source = ExampleCfdpUser::new(EntityType::Sending); + + let local_cfg_dest = LocalEntityConfig::new( + RUST_ID.into(), + IndicationConfig::default(), + ExampleFaultHandler::default(), + ); + let mut dest_handler = DestinationHandler::new( + local_cfg_dest, + 1024, + dest_tm_tx, + NativeFilestore::default(), + remote_cfg_python, + StdTimerCreator::default(), + ); + let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving); + + let put_request = if cli_args.file_copy { + Some( + PutRequestOwned::new_regular_request( + PYTHON_ID.into(), + srcfile.to_str().expect("invaid path string"), + destfile.to_str().expect("invaid path string"), + cli_args.mode.map(|m| match m { + TransmissionModeCli::Ack => TransmissionMode::Acknowledged, + TransmissionModeCli::Nak => TransmissionMode::Unacknowledged, + }), + cli_args.closure_requested, + ) + .expect("put request creation failed"), + ) + } else { + None + }; + + let (source_tc_tx, source_tc_rx) = mpsc::channel(); + let (dest_tc_tx, dest_tc_rx) = mpsc::channel(); + + let local_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), RUST_PORT); + let remote_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), PY_PORT); + let mut udp_server = UdpServer::new( + local_addr, + remote_addr, + 2048, + source_tc_tx, + dest_tc_tx, + source_tm_rx, + dest_tm_rx, + ) + .expect("creating UDP server failed"); + + let jh_source = thread::Builder::new() + .name("cfdp src entity".to_string()) + .spawn(move || { + info!("Starting RUST SRC"); + if let Some(put_request) = put_request { + info!("RUST SRC: Performing put request: {:?}", put_request); + 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 source_tc_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) => { + warn!("cfdp src entity error: {}", e); + next_delay = Some(Duration::from_millis(50)); + } + } + if let Some(delay) = next_delay { + thread::sleep(delay); + } else { + undelayed_call_count += 1; + } + // Safety feature against configuration errors. + if undelayed_call_count >= 200 { + panic!("Source handler state machine possible in permanent loop"); + } + } + }) + .unwrap(); + + let jh_dest = thread::Builder::new() + .name("cfdp dest entity".to_string()) + .spawn(move || { + info!("Starting RUST DEST. Local ID {}", RUST_ID.value()); + loop { + let mut next_delay = None; + let mut undelayed_call_count = 0; + let packet_info = match dest_tc_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!("Dest handler error: {}", e); + // TODO: I'd prefer a proper cancel request if a transfer is active.. + dest_handler.reset(); + next_delay = Some(Duration::from_millis(50)); + } + } + if let Some(delay) = next_delay { + thread::sleep(delay); + } else { + undelayed_call_count += 1; + } + // Safety feature against configuration errors. + if undelayed_call_count >= 200 { + panic!("Destination handler state machine possible in permanent loop"); + } + } + }) + .unwrap(); + + let jh_udp_server = thread::Builder::new() + .name("cfdp udp server".to_string()) + .spawn(move || { + info!("Starting UDP server on {}", remote_addr); + loop { + loop { + match udp_server.try_recv_tc() { + Ok(result) => match result { + Some((pdu, _addr)) => { + debug!("Received PDU on UDP server: {:?}", pdu); + pdu_printout(&pdu); + } + None => break, + }, + Err(e) => { + warn!("UDP server error: {}", e); + break; + } + } + } + udp_server.recv_and_send_telemetry(); + thread::sleep(Duration::from_millis(50)); + } + }) + .unwrap(); + + jh_source.join().unwrap(); + jh_dest.join().unwrap(); + jh_udp_server.join().unwrap(); +} diff --git a/examples/python-interop/requirements.txt b/examples/python-interop/requirements.txt new file mode 100644 index 0000000..5fce759 --- /dev/null +++ b/examples/python-interop/requirements.txt @@ -0,0 +1 @@ +cfdp-py @ git+https://github.com/us-irs/cfdp-py.git@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..8b7d48e --- /dev/null +++ b/src/dest.rs @@ -0,0 +1,2022 @@ +//! # CFDP Destination Entity Module +//! +//! The [DestinationHandler] is the primary component of this module which converts the PDUs sent +//! from a remote source entity back to a file. A file copy operation on the receiver side +//! is started with the reception of a Metadata PDU, for example one generated by the +//! [spacepackets::cfdp::pdu::metadata::MetadataPduCreator]. After that, file packet PDUs, for +//! example generated with the [spacepackets::cfdp::pdu::file_data] module, can be inserted into +//! the destination handler and will be assembled into a file. +//! +//! A destination entity might still generate packets which need to be sent back to the source +//! entity of the file transfer. However, this handler allows freedom of communication like the +//! source entity by using a user-provided [PduSendProvider] to send all generated PDUs. +//! +//! The transaction will be finished when following conditions are met: +//! +//! 1. A valid EOF PDU, for example one generated by the [spacepackets::cfdp::pdu::eof::EofPdu] +//! helper, has been inserted into the class. +//! 2. The file checksum verification has been successful. If this is not the case for the +//! unacknowledged mode, the handler will re-attempt the checksum calculation up to a certain +//! threshold called the check limit. If the threshold is reached, the transaction will +//! finish with a failure. +//! +//! ### Unacknowledged mode with closure +//! +//! 3. Finished PDU has been sent back to the remote side. +//! +//! ### Acknowledged mode (*not implemented yet*) +//! +//! 3. An EOF ACK PDU has been sent back to the remote side. +//! 4. A Finished PDU has been sent back to the remote side. +//! 5. A Finished PDU ACK was received. +use crate::{user::TransactionFinishedParams, DummyPduProvider, GenericSendError, PduProvider}; +use core::str::{from_utf8, from_utf8_unchecked, Utf8Error}; + +use super::{ + filestore::{FilestoreError, NativeFilestore, VirtualFilestore}, + user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams}, + CountdownProvider, EntityType, LocalEntityConfig, PacketTarget, PduSendProvider, + RemoteEntityConfig, RemoteEntityConfigProvider, State, StdCountdown, + StdRemoteEntityConfigProvider, StdTimerCreator, TimerContext, TimerCreatorProvider, + 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: [u8; u8::MAX as usize * 2], + dest_file_path_len: usize, +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum CompletionDisposition { + Completed = 0, + Cancelled = 1, +} + +/// This enumeration models the different transaction steps of the destination entity handler. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum TransactionStep { + Idle = 0, + TransactionStart = 1, + 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, + // file_size_eof: u64, + metadata_only: bool, + condition_code: ConditionCode, + delivery_code: DeliveryCode, + fault_location_finished: Option, + 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(), + // file_size_eof: Default::default(), + metadata_only: false, + condition_code: ConditionCode::NoError, + delivery_code: DeliveryCode::Incomplete, + fault_location_finished: None, + 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: [0; u8::MAX as usize * 2], + dest_file_path_len: 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")] + WrongStateForFileData, + #[error("can not process EOF PDUs in current state")] + WrongStateForEof, + // 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 using the user provided [PduSendProvider]. +/// +/// The handler requires the [alloc] feature but will allocated all required memory on construction +/// time. This means that the handler is still suitable for embedded systems where run-time +/// allocation is prohibited. Furthermore, it uses the [VirtualFilestore] abstraction to allow +/// usage on systems without a [std] filesystem. +/// +/// This handler is able to deal with file copy operations to directories, similarly to how the +/// UNIX tool `cp` works. If the destination path is a directory instead of a regular full path, +/// the source path base file name will be appended to the destination path to form the resulting +/// new full path. +/// +// This handler also does not support concurrency out of the box but is flexible enough to be used +/// in different concurrent contexts. For example, you can dynamically create new handlers and +/// run them inside a thread pool, or move the newly created handler to a new thread.""" +pub struct DestinationHandler< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + CheckTimerCreator: TimerCreatorProvider, + CheckTimerProvider: CountdownProvider, +> { + 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, + StdTimerCreator, + StdCountdown, +>; + +impl< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + TimerCreator: TimerCreatorProvider, + Countdown: CountdownProvider, + > DestinationHandler +{ + /// Constructs a new destination handler. + /// + /// # Arguments + /// + /// * `local_cfg` - The local CFDP entity configuration. + /// * `max_packet_len` - The maximum expected generated packet size in bytes. Each time a + /// packet is sent, it will be buffered inside an internal buffer. The length of this buffer + /// will be determined by this parameter. This parameter can either be a known upper bound, + /// or it can specifically be determined by the largest packet size parameter of all remote + /// entity configurations in the passed `remote_cfg_table`. + /// * `pdu_sender` - [PduSendProvider] used to send generated PDU packets. + /// * `vfs` - [VirtualFilestore] implementation used by the handler, which decouples the CFDP + /// implementation from the underlying filestore/filesystem. This allows to use this handler + /// for embedded systems where a standard runtime might not be available. + /// * `remote_cfg_table` - The [RemoteEntityConfigProvider] used to look up remote + /// entities and target specific configuration for file copy operations. + /// * `check_timer_creator` - [TimerCreatorProvider] used by the CFDP handler to generate + /// timers required by various tasks. This allows to use this handler for embedded systems + /// where the standard time APIs might not be available. + pub fn new( + local_cfg: LocalEntityConfig, + max_packet_len: usize, + pdu_sender: PduSender, + vfs: Vfs, + remote_cfg_table: RemoteCfgTable, + timer_creator: TimerCreator, + ) -> 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: 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 and periodically 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) + } + } + } + + /// This function models the Cancel.request CFDP primitive and is the recommended way + /// to cancel a transaction. It will cause a Notice Of Cancellation at this entity. + /// Please note that the state machine might still be active because a cancelled transfer + /// might still require some packets to be sent to the remote sender entity. + /// + /// If no unexpected errors occur, this function returns whether the current transfer + /// was cancelled. It returns [false] if the state machine is currently idle or if there + /// is a transaction ID missmatch. + pub fn cancel_request(&mut self, transaction_id: &TransactionId) -> bool { + if self.state() == super::State::Idle { + return false; + } + if let Some(active_id) = self.transaction_id() { + if active_id == *transaction_id { + self.trigger_notice_of_completion_cancelled( + ConditionCode::CancelRequestReceived, + EntityIdTlv::new(self.local_cfg.id), + ); + + self.step = TransactionStep::TransferCompletion; + return true; + } + } + false + } + + /// Returns [None] if the state machine is IDLE, and the transmission mode of the current + /// 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::WrongStateForFileData); + } + let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; + if self.local_cfg.indication_cfg.file_segment_recv { + user.file_segment_recvd_indication(&FileSegmentRecvdParams { + id: self.tstate().transaction_id.unwrap(), + offset: fd_pdu.offset(), + length: fd_pdu.file_data().len(), + segment_metadata: fd_pdu.segment_metadata(), + }); + } + if let Err(e) = self.vfs.write_data( + // Safety: It was already verified that the path is valid during the transaction start. + unsafe { + from_utf8_unchecked( + //from_utf8( + &self.tparams.file_properties.dest_path_buf + [0..self.tparams.file_properties.dest_file_path_len], + ) + }, + 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::WrongStateForEof); + } + 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 { + // This is an EOF (Cancel), perform Cancel Response Procedures according to chapter + // 4.6.6 of the standard. + self.trigger_notice_of_completion_cancelled( + eof_pdu.condition_code(), + EntityIdTlv::new(self.tparams.remote_cfg.unwrap().entity_id), + ); + self.tparams.tstate.progress = eof_pdu.file_size(); + if eof_pdu.file_size() > 0 { + self.tparams.tstate.delivery_code = DeliveryCode::Incomplete; + } else { + self.tparams.tstate.delivery_code = DeliveryCode::Complete; + } + // TODO: The cancel EOF also supplies a checksum and a progress number. We could cross + // check that checksum, but how would we deal with a checksum failure? The standard + // does not specify anything for this case.. It could be part of the status report + // issued to the user though. + true + }; + if regular_transfer_finish { + self.file_transfer_complete_transition(); + } + Ok(()) + } + + fn trigger_notice_of_completion_cancelled( + &mut self, + cond_code: ConditionCode, + fault_location: EntityIdTlv, + ) { + self.tparams.tstate.completion_disposition = CompletionDisposition::Cancelled; + self.tparams.tstate.condition_code = cond_code; + self.tparams.tstate.fault_location_finished = Some(fault_location); + // For anything larger than 0, we'd have to do a checksum check to verify whether + // the delivery is really complete, and we need the EOF checksum for that.. + if self.tparams.tstate.progress == 0 { + self.tparams.tstate.delivery_code = DeliveryCode::Complete; + } + } + + /// Returns whether the transfer can be completed regularly. + 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( + checksum, + // Safety: It was already verified that the path is valid during the transaction start. + unsafe { + from_utf8_unchecked( + &self.tparams.file_properties.dest_path_buf + [0..self.tparams.file_properties.dest_file_path_len], + ) + }, + self.tparams.metadata_params().checksum_type, + self.tparams.tstate.progress, + &mut self.tparams.cksum_buf, + ) { + Ok(checksum_success) => { + file_delivery_complete = checksum_success; + if !checksum_success { + self.tparams.tstate.delivery_code = DeliveryCode::Incomplete; + self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure; + } + } + 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_countdown( + TimerContext::CheckLimit { + local_id: self.local_cfg.id, + remote_id: self.tparams.remote_cfg.unwrap().entity_id, + entity_type: EntityType::Receiving, + }, + )); + self.tparams.tstate.current_check_count = 0; + } + + 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 { + let result = self.transaction_start(cfdp_user); + if let Err(e) = result { + // If we can not even start the transaction properly, reset the handler. + // We could later let the user do this optionally, but for now this is the safer + // approach to prevent inconsistent states of the handler. + self.reset(); + return Err(e); + } + } + 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], + )?; + self.tparams.file_properties.dest_path_buf[0..dest_name.len()] + .copy_from_slice(dest_name.as_bytes()); + self.tparams.file_properties.dest_file_path_len = dest_name.len(); + let source_id = self.tparams.pdu_conf.source_id(); + let id = TransactionId::new(source_id, self.tparams.pdu_conf.transaction_seq_num); + 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); + + if self.vfs.exists(dest_name)? && self.vfs.is_dir(dest_name)? { + // 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 + // Safety: It was already verified that the path is valid during the transaction start. + let source_file_name = self.vfs.file_name(src_name)?; + if source_file_name.is_none() { + return Err(DestError::PathConcat); + } + let source_name = source_file_name.unwrap(); + self.tparams.file_properties.dest_path_buf[dest_name.len()] = b'/'; + self.tparams.file_properties.dest_path_buf + [dest_name.len() + 1..dest_name.len() + 1 + source_name.len()] + .copy_from_slice(source_name.as_bytes()); + self.tparams.file_properties.dest_file_path_len += 1 + source_name.len(); + } + let dest_path_str = from_utf8( + &self.tparams.file_properties.dest_path_buf + [0..self.tparams.file_properties.dest_file_path_len], + )?; + if self.vfs.exists(dest_path_str)? { + self.vfs.truncate_file(dest_path_str)?; + } else { + self.vfs.create_file(dest_path_str)?; + } + self.tparams.tstate.file_status = FileStatus::Retained; + self.step = TransactionStep::ReceivingFileDataPdus; + 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; + 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 + { + // Safety: We already verified that the path is valid during the transaction start. + let dest_path = unsafe { + from_utf8_unchecked( + &self.tparams.file_properties.dest_path_buf + [0..self.tparams.file_properties.dest_file_path_len], + ) + }; + if self.vfs.exists(dest_path)? && self.vfs.is_file(dest_path)? { + self.vfs.remove_file(dest_path)?; + } + self.tstate_mut().file_status = FileStatus::DiscardDeliberately; + } + 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, EntityIdTlv::new(self.local_cfg().id)); + } + 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, + fault_location: EntityIdTlv, + ) { + self.step = TransactionStep::TransferCompletion; + self.tstate_mut().condition_code = condition_code; + self.tstate_mut().fault_location_finished = Some(fault_location); + self.tstate_mut().completion_disposition = CompletionDisposition::Cancelled; + } + + fn notice_of_suspension(&mut self) { + // TODO: Implement suspension handling. + } + + fn abandon_transaction(&mut self) { + self.reset(); + } + + /// This function is public to allow completely resetting the handler, but it is explicitely + /// discouraged to do this. CFDP has mechanism to detect issues and errors on itself. + /// Resetting the handler might interfere with these mechanisms and lead to unexpected + /// behaviour. + pub fn reset(&mut self) { + self.step = TransactionStep::Idle; + self.state = State::Idle; + // self.packets_to_send_ctx.packet_available = false; + self.tparams.reset(); + } + + 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 { + FinishedPduCreator::new_generic( + pdu_header, + tstate.condition_code, + tstate.delivery_code, + tstate.file_status, + &[], + tstate.fault_location_finished, + ) + }; + 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, + path::{Path, PathBuf}, + 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, UnsignedByteFieldU8}, + }; + + use crate::{ + filestore::NativeFilestore, + tests::{ + basic_remote_cfg_table, SentPdu, TestCfdpSender, TestCfdpUser, TestFaultHandler, + LOCAL_ID, REMOTE_ID, + }, + CountdownProvider, FaultHandler, IndicationConfig, PduRawWithInfo, + StdRemoteEntityConfigProvider, TimerCreatorProvider, CRC_32, + }; + + use super::*; + + #[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 TimerCreatorProvider for TestCheckTimerCreator { + type Countdown = TestCheckTimer; + + fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown { + 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, + closure_requested: bool, + pdu_header: PduHeader, + expected_full_data: Vec, + expected_file_size: u64, + buf: [u8; 512], + } + + impl DestHandlerTestbench { + fn new_with_fixed_paths(fault_handler: TestFaultHandler, closure_requested: bool) -> Self { + let (src_path, dest_path) = init_full_filepaths_textfile(); + assert!(!Path::exists(&dest_path)); + Self::new(fault_handler, closure_requested, true, src_path, dest_path) + } + + fn new( + fault_handler: TestFaultHandler, + closure_requested: bool, + check_dest_file: bool, + src_path: PathBuf, + dest_path: PathBuf, + ) -> 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 handler = Self { + check_timer_expired, + handler: dest_handler, + src_path, + closure_requested, + dest_path, + check_dest_file, + 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 get_next_pdu(&mut self) -> Option { + self.handler.pdu_sender.retrieve_next_pdu() + } + + 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 = PduRawWithInfo::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], + ) -> PduRawWithInfo<'a> { + let written_len = pdu + .write_to_bytes(buf) + .expect("writing metadata PDU failed"); + PduRawWithInfo::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_cancelling_idle_fsm() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut dest_handler = default_dest_handler(fault_handler, test_sender, Arc::default()); + assert!(!dest_handler.cancel_request(&TransactionId::new( + UnsignedByteFieldU8::new(0).into(), + UnsignedByteFieldU8::new(0).into() + ))); + } + + #[test] + fn test_empty_file_transfer_not_acked_no_closure() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + 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_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(file_size); + tb.generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + 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_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(file_size); + tb.generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + 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_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(file_size); + let transaction_id = tb + .generic_transfer_init(&mut test_user, file_size) + .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_with_fixed_paths(fault_handler, false); + let mut test_user = testbench.test_user_from_cached_paths(file_size); + let transaction_id = testbench + .generic_transfer_init(&mut test_user, file_size) + .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_with_fixed_paths(fault_handler, true); + let mut test_user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + 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_finished_pdu_insertion_rejected() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + tb.check_dest_file = false; + let mut user = tb.test_user_from_cached_paths(0); + let finished_pdu = FinishedPduCreator::new_default( + PduHeader::new_no_file_data(CommonPduConfig::default(), 0), + DeliveryCode::Complete, + FileStatus::Retained, + ); + let finished_pdu_raw = finished_pdu.to_vec().unwrap(); + let packet_info = PduRawWithInfo::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_with_fixed_paths(fault_handler, true); + let mut user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut user, 0) + .expect("transfer init failed"); + 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_with_fixed_paths(fault_handler, true); + let mut user = tb.test_user_from_cached_paths(file_size); + tb.generic_transfer_init(&mut user, file_size) + .expect("transfer init failed"); + 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(); + } + + #[test] + fn test_file_copy_to_directory() { + let fault_handler = TestFaultHandler::default(); + let src_path = tempfile::TempPath::from_path("/tmp/test.txt").to_path_buf(); + let dest_path = tempfile::TempDir::new().unwrap(); + let mut dest_path_buf = dest_path.into_path(); + let mut tb = DestHandlerTestbench::new( + fault_handler, + false, + false, + src_path.clone(), + dest_path_buf.clone(), + ); + dest_path_buf.push(src_path.file_name().unwrap()); + tb.dest_path = dest_path_buf; + let mut test_user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut test_user, 0) + .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_tranfer_cancellation_empty_file_with_eof_pdu() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(0); + tb.generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let cancel_eof = EofPdu::new( + tb.pdu_header, + ConditionCode::CancelRequestReceived, + 0, + 0, + Some(EntityIdTlv::new(LOCAL_ID.into())), + ); + let packets = tb + .handler + .state_machine( + &mut test_user, + Some(&PduRawWithInfo::new(&cancel_eof.to_vec().unwrap()).unwrap()), + ) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 0); + } + + fn generic_tranfer_cancellation_partial_file_with_eof_pdu(with_closure: bool) { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = 5; + + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, with_closure); + let mut test_user = tb.test_user_from_cached_paths(file_size); + tb.generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) + .expect("file data insertion failed"); + // Checksum is currently ignored on remote side.. we still supply it, according to the + // standard. + let mut digest = CRC_32.digest(); + digest.update(&file_data[0..5]); + let checksum = digest.finalize(); + let cancel_eof = EofPdu::new( + tb.pdu_header, + ConditionCode::CancelRequestReceived, + checksum, + 5, + Some(EntityIdTlv::new(LOCAL_ID.into())), + ); + let packets = tb + .handler + .state_machine( + &mut test_user, + Some(&PduRawWithInfo::new(&cancel_eof.to_vec().unwrap()).unwrap()), + ) + .expect("state machine call with EOF insertion failed"); + if with_closure { + assert_eq!(packets, 1); + let finished_pdu = tb.handler.pdu_sender.retrieve_next_pdu().unwrap(); + assert_eq!(finished_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + finished_pdu.file_directive_type.unwrap(), + FileDirectiveType::FinishedPdu + ); + let finished_pdu = FinishedPduReader::from_bytes(&finished_pdu.raw_pdu).unwrap(); + assert_eq!( + finished_pdu.condition_code(), + ConditionCode::CancelRequestReceived + ); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!( + finished_pdu + .fault_location() + .expect("no fault location set"), + EntityIdTlv::new(LOCAL_ID.into()) + ); + } else { + assert_eq!(packets, 0); + } + tb.expected_file_size = file_size; + tb.expected_full_data = file_data[0..file_size as usize].to_vec(); + } + + #[test] + fn test_tranfer_cancellation_partial_file_with_eof_pdu_no_closure() { + generic_tranfer_cancellation_partial_file_with_eof_pdu(false); + } + #[test] + fn test_tranfer_cancellation_partial_file_with_eof_pdu_with_closure() { + generic_tranfer_cancellation_partial_file_with_eof_pdu(true); + } + + #[test] + fn test_tranfer_cancellation_empty_file_with_cancel_api() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let mut test_user = tb.test_user_from_cached_paths(0); + let transaction_id = tb + .generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.handler.cancel_request(&transaction_id); + let packets = tb + .handler + .state_machine_no_packet(&mut test_user) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 0); + } + + #[test] + fn test_tranfer_cancellation_empty_file_with_cancel_api_and_closure() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); + let mut test_user = tb.test_user_from_cached_paths(0); + let transaction_id = tb + .generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.handler.cancel_request(&transaction_id); + let packets = tb + .handler + .state_machine_no_packet(&mut test_user) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 1); + let next_pdu = tb.get_next_pdu().unwrap(); + assert_eq!(next_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + next_pdu.file_directive_type.unwrap(), + FileDirectiveType::FinishedPdu + ); + let finished_pdu = + FinishedPduReader::new(&next_pdu.raw_pdu).expect("finished pdu read failed"); + assert_eq!( + finished_pdu.condition_code(), + ConditionCode::CancelRequestReceived + ); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + // Empty file, so still complete. + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete); + assert_eq!( + finished_pdu.fault_location(), + Some(EntityIdTlv::new(REMOTE_ID.into())) + ); + } + + #[test] + fn test_tranfer_cancellation_partial_file_with_cancel_api_and_closure() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = 5; + + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, true); + let mut test_user = tb.test_user_from_cached_paths(file_size); + let transaction_id = tb + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) + .expect("file data insertion failed"); + + tb.handler.cancel_request(&transaction_id); + let packets = tb + .handler + .state_machine_no_packet(&mut test_user) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 1); + let next_pdu = tb.get_next_pdu().unwrap(); + assert_eq!(next_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + next_pdu.file_directive_type.unwrap(), + FileDirectiveType::FinishedPdu + ); + let finished_pdu = + FinishedPduReader::new(&next_pdu.raw_pdu).expect("finished pdu read failed"); + assert_eq!( + finished_pdu.condition_code(), + ConditionCode::CancelRequestReceived + ); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Incomplete); + assert_eq!( + finished_pdu.fault_location(), + Some(EntityIdTlv::new(REMOTE_ID.into())) + ); + tb.expected_file_size = file_size; + tb.expected_full_data = file_data[0..file_size as usize].to_vec(); + } + + // Only incomplete received files will be removed. + #[test] + fn test_tranfer_cancellation_file_disposition_not_done_for_empty_file() { + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + let remote_cfg_mut = tb + .handler + .remote_cfg_table + .get_mut(LOCAL_ID.value()) + .unwrap(); + remote_cfg_mut.disposition_on_cancellation = true; + let mut test_user = tb.test_user_from_cached_paths(0); + let transaction_id = tb + .generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + + tb.handler.cancel_request(&transaction_id); + let packets = tb + .handler + .state_machine_no_packet(&mut test_user) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 0); + } + + #[test] + fn test_tranfer_cancellation_file_disposition_not_done_for_incomplete_file() { + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); + let file_size = file_data.len() as u64; + + let fault_handler = TestFaultHandler::default(); + let mut tb = DestHandlerTestbench::new_with_fixed_paths(fault_handler, false); + tb.check_dest_file = false; + let remote_cfg_mut = tb + .handler + .remote_cfg_table + .get_mut(LOCAL_ID.value()) + .unwrap(); + remote_cfg_mut.disposition_on_cancellation = true; + let mut test_user = tb.test_user_from_cached_paths(file_size); + let transaction_id = tb + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + tb.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + tb.generic_file_data_insert(&mut test_user, 0, &file_data[0..5]) + .expect("file data insertion failed"); + + tb.handler.cancel_request(&transaction_id); + let packets = tb + .handler + .state_machine_no_packet(&mut test_user) + .expect("state machine call with EOF insertion failed"); + assert_eq!(packets, 0); + // File was disposed. + assert!(!Path::exists(tb.dest_path())); + } +} diff --git a/src/filestore.rs b/src/filestore.rs new file mode 100644 index 0000000..9af7022 --- /dev/null +++ b/src/filestore.rs @@ -0,0 +1,838 @@ +use alloc::string::{String, ToString}; +use core::fmt::Display; +use spacepackets::cfdp::ChecksumType; +use spacepackets::ByteConversionError; +#[cfg(feature = "std")] +use std::error::Error; +use std::path::Path; +#[cfg(feature = "std")] +pub use std_mod::*; + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum FilestoreError { + FileDoesNotExist, + FileAlreadyExists, + DirDoesNotExist, + Permission, + IsNotFile, + IsNotDirectory, + ByteConversion(ByteConversionError), + Io { + raw_errno: Option, + string: String, + }, + ChecksumTypeNotImplemented(ChecksumType), + Utf8Error, + Other, +} + +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) + } + FilestoreError::Utf8Error => { + write!(f, "utf8 error") + } + FilestoreError::Other => { + write!(f, "some filestore error occured") + } + } + } +} + +impl Error for FilestoreError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + FilestoreError::ByteConversion(e) => Some(e), + _ => None, + } + } +} + +#[cfg(feature = "std")] +impl From 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; + + /// Extract the file name part of a full path. + /// + /// This method should behave similarly to the [std::path::Path::file_name] method. + fn file_name<'a>(&self, full_path: &'a str) -> Result, FilestoreError>; + + 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, + size_to_verify: u64, + 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, + expected_checksum: u32, + file_path: &str, + checksum_type: ChecksumType, + size_to_verify: u64, + verification_buf: &mut [u8], + ) -> Result { + Ok( + self.calculate_checksum(file_path, checksum_type, size_to_verify, verification_buf)? + == expected_checksum, + ) + } +} + +#[cfg(feature = "std")] +pub mod std_mod { + + use crc::Crc; + + use crate::{CRC_32, CRC_32C}; + + 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 file_name<'a>(&self, full_path: &'a str) -> Result, FilestoreError> { + let path = Path::new(full_path); + path.file_name() + .map(|s| s.to_str()) + .ok_or(FilestoreError::Utf8Error) + } + + 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, + size_to_verify: u64, + verification_buf: &mut [u8], + ) -> Result { + let mut calc_with_crc_lib = |crc: Crc| -> Result { + let mut digest = crc.digest(); + let mut buf_reader = BufReader::new(File::open(file_path)?); + let mut remaining_bytes = size_to_verify; + while remaining_bytes > 0 { + // Read the smaller of the remaining bytes or the buffer size + let bytes_to_read = remaining_bytes.min(verification_buf.len() as u64) as usize; + let bytes_read = buf_reader.read(&mut verification_buf[0..bytes_to_read])?; + + if bytes_read == 0 { + break; // Reached end of file + } + digest.update(&verification_buf[0..bytes_read]); + remaining_bytes -= bytes_read as u64; + } + Ok(digest.finalize()) + }; + match checksum_type { + ChecksumType::Modular => self.calc_modular_checksum(file_path), + ChecksumType::Crc32 => calc_with_crc_lib(CRC_32), + ChecksumType::Crc32C => calc_with_crc_lib(CRC_32C), + 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( + checksum, + file_path.to_str().unwrap(), + ChecksumType::Modular, + EXAMPLE_DATA_CFDP.len() as u64, + &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( + 0, + 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( + 0, + 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..c358bee --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1538 @@ +//! This module contains the implementation of the CCSDS File Delivery Protocol (CFDP) high level +//! abstractions as specified in CCSDS 727.0-B-5. +//! +//! The basic idea of CFDP is to convert files of any size into a stream of packets called packet +//! data units (PDU). CFPD has an unacknowledged and acknowledged mode, with the option to request +//! a transaction closure for the unacknowledged mode. Using the unacknowledged mode with no +//! transaction closure is applicable for simplex communication paths, while the unacknowledged +//! mode with closure is the easiest way to get a confirmation of a successful file transfer, +//! including a CRC check on the remote side to verify file integrity. The acknowledged mode is +//! the most complex mode which includes multiple mechanism to ensure succesfull packet transaction +//! even for unreliable connections, including lost segment detection. As such, it can be compared +//! to a specialized TCP for file transfers with remote systems. +//! +//! The core of these high-level components are the [crate::dest::DestinationHandler] and the +//! [crate::source::SourceHandler] component. These model the CFDP destination and source entity +//! respectively. You can find high-level and API documentation for both handlers in the respective +//! [crate::dest] and [crate::source] module. +//! +//! # Examples +//! +//! This library currently features two example application which showcase how the provided +//! components could be used to provide CFDP services. +//! Both examples feature implementations of the [UserFaultHookProvider] and the [user::CfdpUser] +//! trait which simply print some information to the console to monitor the progress of a file +//! copy operation. +//! +//! The [end-to-end test](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/tests/end-to-end.rs) +//! is an integration tests which spawns a CFDP source entity and a CFDP destination entity, +//! moves them to separate threads and then performs a small file copy operation. +//! You can run the integration test for a transfer with no closure and with printout to the +//! standard console by running: +//! +//! ```sh +//! cargo test end_to_end_test_no_closure -- --nocapture +//! ``` +//! +//! or with closure: +//! +//! ```sh +//! cargo test end_to_end_test_with_closure -- --nocapture +//! ``` +//! +//! The [Python Interoperability](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/examples/python-interop) +//! example showcases the interoperability of the CFDP handlers written in Rust with a Python +//! implementation. The dedicated example documentation shows how to run this example. +#![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_ISCSI, CRC_32_ISO_HDLC}; +#[cfg(feature = "std")] +use hashbrown::HashMap; + +#[cfg(feature = "alloc")] +pub use alloc_mod::*; +use core::time::Duration; +#[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")] +pub use std_mod::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum EntityType { + Sending, + Receiving, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TimerContext { + CheckLimit { + local_id: UnsignedByteField, + remote_id: UnsignedByteField, + entity_type: EntityType, + }, + NakActivity { + expiry_time: Duration, + }, + PositiveAck { + expiry_time: Duration, + }, +} + +/// 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 TimerCreatorProvider { + type Countdown: CountdownProvider; + + fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown; +} + +/// 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)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct RemoteEntityConfig { + pub entity_id: UnsignedByteField, + pub max_packet_len: usize, + 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; +} + +/// This is a thin wrapper around a [HashMap] to store remote entity configurations. +/// It implements the full [RemoteEntityConfigProvider] trait. +#[cfg(feature = "std")] +#[derive(Default, Debug)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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() + } +} + +/// This is a thin wrapper around a [alloc::vec::Vec] to store remote entity configurations. +/// It implements the full [RemoteEntityConfigProvider] trait. +#[cfg(feature = "alloc")] +#[derive(Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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 + } +} + +/// A remote entity configurations also implements the [RemoteEntityConfigProvider], but the +/// [RemoteEntityConfigProvider::add_config] and [RemoteEntityConfigProvider::remove_config] +/// are no-ops and always returns [false]. +impl RemoteEntityConfigProvider for RemoteEntityConfig { + 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 [UserFaultHookProvider] 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); +} + +/// Dummy fault hook which implements [UserFaultHookProvider] but only provides empty +/// implementations. +#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)] +pub struct DummyFaultHook {} + +impl UserFaultHookProvider for DummyFaultHook { + fn notice_of_suspension_cb( + &mut self, + _transaction_id: TransactionId, + _cond: ConditionCode, + _progress: u64, + ) { + } + + 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 [UserFaultHookProvider]. +/// +/// 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 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct IndicationConfig { + pub eof_sent: bool, + pub eof_recv: bool, + 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, + } + } +} + +/// Each CFDP entity handler has a [LocalEntityConfig]uration. +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(PduOwnedWithInfo::new( + pdu_type, + file_directive_type, + raw_pdu.to_vec(), + )) + .map_err(|_| GenericSendError::RxDisconnected)?; + Ok(()) + } + } + + /// Simple implementation of the [CountdownProvider] trait assuming a standard runtime. + #[derive(Debug)] + pub struct StdCountdown { + expiry_time: Duration, + start_time: std::time::Instant, + } + + impl StdCountdown { + pub fn new(expiry_time: Duration) -> Self { + Self { + expiry_time, + start_time: std::time::Instant::now(), + } + } + + pub fn expiry_time_seconds(&self) -> u64 { + self.expiry_time.as_secs() + } + } + + impl CountdownProvider for StdCountdown { + fn has_expired(&self) -> bool { + if self.start_time.elapsed() > self.expiry_time { + return true; + } + false + } + + fn reset(&mut self) { + self.start_time = std::time::Instant::now(); + } + } + + pub struct StdTimerCreator { + pub check_limit_timeout: Duration, + } + + impl StdTimerCreator { + pub const fn new(check_limit_timeout: Duration) -> Self { + Self { + check_limit_timeout, + } + } + } + + impl Default for StdTimerCreator { + fn default() -> Self { + Self::new(Duration::from_secs(5)) + } + } + + impl TimerCreatorProvider for StdTimerCreator { + type Countdown = StdCountdown; + + fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown { + match timer_context { + TimerContext::CheckLimit { + local_id: _, + remote_id: _, + entity_type: _, + } => StdCountdown::new(self.check_limit_timeout), + TimerContext::NakActivity { expiry_time } => StdCountdown::new(expiry_time), + TimerContext::PositiveAck { expiry_time } => StdCountdown::new(expiry_time), + } + } + } +} + +/// 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))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +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, +} + +/// [crc::Crc] instance using [crc::CRC_32_ISO_HDLC]. +/// +/// SANA registry entry: , +/// Entry in CRC catalogue: +pub const CRC_32: Crc = Crc::::new(&CRC_32_ISO_HDLC); +/// [crc::Crc] instance using [crc::CRC_32_ISCSI]. +/// +/// SANA registry entry: , +/// Entry in CRC catalogue: +pub const CRC_32C: Crc = Crc::::new(&CRC_32_ISCSI); + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PacketTarget { + SourceEntity, + DestEntity, +} + +/// Generic trait which models a raw CFDP packet data unit (PDU) block with some additional context +/// information. +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 PduRawWithInfo<'raw_packet> { + pdu_type: PduType, + file_directive_type: Option, + packet_len: usize, + raw_packet: &'raw_packet [u8], +} + +pub fn determine_packet_target(raw_pdu: &[u8]) -> Result { + let (header, header_len) = PduHeader::from_bytes(raw_pdu)?; + if header.pdu_type() == PduType::FileData { + return Ok(PacketTarget::DestEntity); + } + let file_directive_type = FileDirectiveType::try_from(raw_pdu[header_len]).map_err(|_| { + PduError::InvalidDirectiveType { + found: raw_pdu[header_len], + expected: None, + } + })?; + 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> PduRawWithInfo<'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 PduRawWithInfo<'_> { + 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.raw_packet) + } +} + +#[cfg(feature = "alloc")] +pub mod alloc_mod { + use spacepackets::cfdp::{ + pdu::{FileDirectiveType, PduError}, + PduType, + }; + + use crate::{determine_packet_target, PacketTarget, PduProvider, PduRawWithInfo}; + + #[derive(Debug, PartialEq, Eq, Clone)] + pub struct PduOwnedWithInfo { + pub pdu_type: PduType, + pub file_directive_type: Option, + pub pdu: alloc::vec::Vec, + } + + impl PduOwnedWithInfo { + pub fn new_from_raw_packet(raw_packet: &[u8]) -> Result { + Ok(PduRawWithInfo::new(raw_packet)?.into()) + } + + pub fn new( + pdu_type: PduType, + file_directive_type: Option, + pdu: alloc::vec::Vec, + ) -> Self { + Self { + pdu_type, + file_directive_type, + pdu, + } + } + } + + impl From> for PduOwnedWithInfo { + fn from(value: PduRawWithInfo) -> Self { + Self::new( + value.pdu_type(), + value.file_directive_type(), + value.raw_packet().to_vec(), + ) + } + } + + impl PduProvider for PduOwnedWithInfo { + 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.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, StdCountdown}; + + 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 = PduRawWithInfo::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 = PduRawWithInfo::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 = PduRawWithInfo::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 = StdCountdown::new(Duration::from_secs(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 = StdTimerCreator::new(Duration::from_secs(1)); + let check_timer = std_check_timer_creator.create_countdown(TimerContext::NakActivity { + expiry_time: Duration::from_secs(1), + }); + 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..f822969 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,782 @@ +use spacepackets::{ + cfdp::{ + tlv::{GenericTlv, Tlv, TlvType}, + SegmentationControl, TransmissionMode, + }, + util::UnsignedByteField, +}; + +#[cfg(feature = "alloc")] +pub use alloc_mod::*; + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FilePathTooLarge(pub usize); + +/// This trait is an abstraction for different Put Request structures which can be used +/// 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)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TlvWithInvalidType(pub(crate) ()); + +impl<'msgs_to_user> PutRequest<'static, 'static, 'msgs_to_user, 'static, 'static, 'static> { + 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))] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + 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..fbd1299 --- /dev/null +++ b/src/source.rs @@ -0,0 +1,1824 @@ +//! # CFDP Source Entity Module +//! +//! The [SourceHandler] is the primary component of this module which converts a +//! [ReadablePutRequest] into all packet data units (PDUs) which need to be sent to a remote +//! CFDP entity to perform a File Copy operation to a remote entity. +//! +//! The source entity allows freedom communication by using a user-provided [PduSendProvider] +//! to send all generated PDUs. It should be noted that for regular file transfers, each +//! [SourceHandler::state_machine] call will map to one generated file data PDU. This allows +//! flow control for the user of the state machine. +//! +//! The [SourceHandler::state_machine] will generally perform the following steps after a valid +//! put request was received through the [SourceHandler::put_request] method: +//! +//! 1. Generate the Metadata PDU to be sent to a remote CFDP entity. You can use the +//! [spacepackets::cfdp::pdu::metadata::MetadataPduReader] to inspect the generated PDU. +//! 2. Generate all File Data PDUs to be sent to a remote CFDP entity if applicable (file not +//! empty). The PDU(s) can be inspected using the [spacepackets::cfdp::pdu::file_data::FileDataPdu] reader. +//! 3. Generate an EOF PDU to be sent to a remote CFDP entity. The PDU can be inspected using +//! the [spacepackets::cfdp::pdu::eof::EofPdu] reader. +//! +//! If this is an unacknowledged transfer with no transaction closure, the file transfer will be +//! done after these steps. In any other case: +//! +//! ### Unacknowledged transfer with requested closure +//! +//! 4. A Finished PDU will be awaited, for example one generated using +//! [spacepackets::cfdp::pdu::finished::FinishedPduCreator]. +//! +//! ### Acknowledged transfer (*not implemented yet*) +//! +//! 4. A EOF ACK packet will be awaited, for example one generated using +//! [spacepackets::cfdp::pdu::ack::AckPdu]. +//! 5. A Finished PDU will be awaited, for example one generated using +//! [spacepackets::cfdp::pdu::finished::FinishedPduCreator]. +//! 6. A finished PDU ACK packet will be generated to be sent to the remote CFDP entity. +//! The [spacepackets::cfdp::pdu::finished::FinishedPduReader] can be used to inspect the +//! generated PDU. +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::{ + time::CountdownProvider, DummyPduProvider, EntityType, GenericSendError, PduProvider, + TimerCreatorProvider, +}; + +use super::{ + filestore::{FilestoreError, VirtualFilestore}, + request::{ReadablePutRequest, StaticPutRequestCacher}, + user::{CfdpUser, TransactionFinishedParams}, + LocalEntityConfig, PacketTarget, PduSendProvider, RemoteEntityConfig, + RemoteEntityConfigProvider, State, TransactionId, UserFaultHookProvider, +}; + +/// This enumeration models the different transaction steps of the source entity handler. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum TransactionStep { + Idle = 0, + TransactionStart = 1, + 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: {0}")] + 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), +} + +/// This is the primary CFDP source handler. It models the CFDP source entity, which is +/// primarily responsible for handling put requests to send files to another CFDP destination +/// entity. +/// +/// As such, it contains a state machine to perform all operations necessary to perform a +/// source-to-destination file transfer. This class uses the user provides [PduSendProvider] to +/// send the CFDP PDU packets generated by the state machine. +/// +/// The following core functions are the primary interface: +/// +/// 1. [Self::put_request] can be used to start transactions, most notably to start +/// and perform a Copy File procedure to send a file or to send a Proxy Put Request to request +/// a file. +/// 2. [Self::state_machine] is the primary interface to execute an +/// active file transfer. It generates the necessary CFDP PDUs for this process. +/// This method is also used to insert received packets with the appropriate destination ID +/// and target handler type into the state machine. +/// +/// A put request will only be accepted if the handler is in the idle state. +/// +/// The handler requires the [alloc] feature but will allocated all required memory on construction +/// time. This means that the handler is still suitable for embedded systems where run-time +/// allocation is prohibited. Furthermore, it uses the [VirtualFilestore] abstraction to allow +/// usage on systems without a [std] filesystem. +/// This handler does not support concurrency out of the box. Instead, if concurrent handling +/// is required, it is recommended to create a new handler and run all active handlers inside a +/// thread pool, or move the newly created handler to a new thread. +pub struct SourceHandler< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + TimerCreator: TimerCreatorProvider, + Countdown: CountdownProvider, + 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, + countdown: Option, + timer_creator: TimerCreator, + seq_count_provider: SeqCountProvider, +} + +impl< + PduSender: PduSendProvider, + UserFaultHook: UserFaultHookProvider, + Vfs: VirtualFilestore, + RemoteCfgTable: RemoteEntityConfigProvider, + TimerCreator: TimerCreatorProvider, + Countdown: CountdownProvider, + SeqCountProvider: SequenceCountProvider, + > + SourceHandler< + PduSender, + UserFaultHook, + Vfs, + RemoteCfgTable, + TimerCreator, + Countdown, + SeqCountProvider, + > +{ + /// Creates a new instance of a source handler. + /// + /// # Arguments + /// + /// * `cfg` - The local entity configuration for this source handler. + /// * `pdu_sender` - [PduSendProvider] provider used to send CFDP PDUs generated by the handler. + /// * `vfs` - [VirtualFilestore] implementation used by the handler, which decouples the CFDP + /// implementation from the underlying filestore/filesystem. This allows to use this handler + /// for embedded systems where a standard runtime might not be available. + /// * `put_request_cacher` - The put request cacher is used cache put requests without + /// requiring run-time allocation. + /// * `pdu_and_cksum_buf_size` - The handler requires a buffer to generate PDUs and perform + /// checksum calculations. The user can specify the size of this buffer, so this should be + /// set to the maximum expected PDU size or a conservative upper bound for this size, for + /// example 2048 or 4096 bytes. + /// * `remote_cfg_table` - The [RemoteEntityConfigProvider] used to look up remote + /// entities and target specific configuration for file copy operations. + /// * `timer_creator` - [TimerCreatorProvider] used by the CFDP handler to generate + /// timers required by various tasks. This allows to use this handler for embedded systems + /// where the standard time APIs might not be available. + /// * `seq_count_provider` - The [SequenceCountProvider] used to generate the [TransactionId] + /// which contains an incrementing counter. + #[allow(clippy::too_many_arguments)] + pub fn new( + cfg: LocalEntityConfig, + pdu_sender: PduSender, + vfs: Vfs, + put_request_cacher: StaticPutRequestCacher, + pdu_and_cksum_buf_size: usize, + remote_cfg_table: RemoteCfgTable, + timer_creator: TimerCreator, + 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(), + countdown: None, + timer_creator, + seq_count_provider, + } + } + + /// Calls [Self::state_machine], without inserting a packet. + 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, + pdu: Option<&impl PduProvider>, + ) -> Result { + if let Some(packet) = pdu { + 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, pdu), + 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(()) + } + + /// This function is used to pass a put request to the source handler, which is + /// also used to start a file copy operation. As such, this function models the Put.request + /// CFDP primtiive. + + /// Please note that the source handler can also process one put request at a time. + /// The caller is responsible of creating a new source handler, one handler can only handle + /// one file copy request at a time. + 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); + } + + let transaction_id = TransactionId::new( + self.local_cfg().id, + UnsignedByteField::new( + SeqCountProvider::MAX_BIT_WIDTH / 8, + self.seq_count_provider.get_and_increment().into(), + ), + ); + // 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 + } + }; + + // Set PDU configuration fields which are important for generating PDUs. + 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 = remote_cfg.crc_on_transmission_by_default.into(); + self.pdu_conf.transaction_seq_num = *transaction_id.seq_num(); + self.pdu_conf.trans_mode = transmission_mode; + self.fparams.segment_len = self.calculate_max_file_seg_len(remote_cfg); + + // Set up the transfer context structure. + self.tstate = Some(TransferState { + transaction_id, + remote_cfg: *remote_cfg, + transmission_mode, + closure_requested, + cond_code_eof: None, + finished_params: None, + }); + self.state_helper.state = super::State::Busy; + Ok(()) + } + + /// This functions models the Cancel.request CFDP primitive and is the recommended way to + /// cancel a transaction. + /// + /// This method will cause a Notice of Cancellation at this entity if a transaction is active + /// and the passed transaction ID matches the currently active transaction ID. Please note + /// that the state machine might still be active because a cancelled transfer might still + /// require some packets to be sent to the remote receiver entity. + /// + /// If not unexpected errors occur, this method returns [true] if the transfer was cancelled + /// propery and [false] if there is no transaction active or the passed transaction ID and the + /// active ID do not match. + pub fn cancel_request( + &mut self, + user: &mut impl CfdpUser, + transaction_id: &TransactionId, + ) -> Result { + if self.state_helper.state == super::State::Idle { + return Ok(false); + } + if let Some(active_id) = self.transaction_id() { + if active_id == *transaction_id { + self.notice_of_cancellation(user, ConditionCode::CancelRequestReceived)?; + return Ok(true); + } + } + Ok(false) + } + + fn fsm_busy( + &mut self, + user: &mut impl CfdpUser, + pdu: Option<&impl PduProvider>, + ) -> 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(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; + } + 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(user)?; + sent_packets += 1; + } + if self.state_helper.step == TransactionStep::WaitingForFinished { + self.handle_wait_for_finished_pdu(user, pdu)?; + } + if self.state_helper.step == TransactionStep::NoticeOfCompletion { + self.notice_of_completion(user); + self.reset(); + } + Ok(sent_packets) + } + + fn handle_wait_for_finished_pdu( + &mut self, + user: &mut impl CfdpUser, + packet: Option<&impl PduProvider>, + ) -> Result<(), SourceError> { + if let Some(packet) = packet { + if let Some(FileDirectiveType::FinishedPdu) = packet.file_directive_type() { + let finished_pdu = FinishedPduReader::new(packet.pdu())?; + 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.transmission_mode().unwrap() == TransmissionMode::Acknowledged { + // TODO: Ack packet handling + self.state_helper.step = TransactionStep::NoticeOfCompletion; + } else { + self.state_helper.step = TransactionStep::NoticeOfCompletion; + } + return Ok(()); + } + } + // If we reach this state, countdown is definitely valid instance. + if self.countdown.as_ref().unwrap().has_expired() { + self.declare_fault(user, ConditionCode::CheckLimitReached)?; + } + /* + 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 + */ + Ok(()) + } + + fn eof_fsm(&mut self, 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.fparams.file_size, + self.pdu_and_cksum_buffer.get_mut(), + )?; + self.prepare_and_send_eof_pdu(user, checksum)?; + let tstate = self.tstate.as_ref().unwrap(); + if tstate.transmission_mode == TransmissionMode::Unacknowledged { + if tstate.closure_requested { + self.countdown = Some(self.timer_creator.create_countdown( + crate::TimerContext::CheckLimit { + local_id: self.local_cfg.id, + remote_id: tstate.remote_cfg.entity_id, + entity_type: EntityType::Sending, + }, + )); + 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 + } + } + 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 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 + } + + 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, + cfdp_user: &mut impl CfdpUser, + 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.progress, + None, + ); + self.pdu_send_helper(&eof_pdu)?; + if self.local_cfg.indication_cfg.eof_sent { + cfdp_user.eof_sent_indication(&tstate.transaction_id); + } + 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) {} + + pub fn transaction_id(&self) -> Option { + self.tstate.as_ref().map(|v| v.transaction_id) + } + + /// Returns the [TransmissionMode] for the active file operation. + #[inline] + pub fn transmission_mode(&self) -> Option { + self.tstate.as_ref().map(|v| v.transmission_mode) + } + + /// Get the [TransactionStep], which denotes the exact step of a pending CFDP transaction when + /// applicable. + pub fn step(&self) -> TransactionStep { + self.state_helper.step + } + + pub fn state(&self) -> State { + self.state_helper.state + } + + pub fn local_cfg(&self) -> &LocalEntityConfig { + &self.local_cfg + } + + fn declare_fault( + &mut self, + user: &mut impl CfdpUser, + cond: ConditionCode, + ) -> Result<(), SourceError> { + // Need to cache those in advance, because a notice of cancellation can reset the handler. + let transaction_id = self.tstate.as_ref().unwrap().transaction_id; + let progress = self.fparams.progress; + let fh = self.local_cfg.fault_handler.get_fault_handler(cond); + match fh { + spacepackets::cfdp::FaultHandlerCode::NoticeOfCancellation => { + if let ControlFlow::Break(_) = self.notice_of_cancellation(user, cond)? { + return Ok(()); + } + } + spacepackets::cfdp::FaultHandlerCode::NoticeOfSuspension => { + self.notice_of_suspension(); + } + spacepackets::cfdp::FaultHandlerCode::IgnoreError => (), + spacepackets::cfdp::FaultHandlerCode::AbandonTransaction => self.abandon_transaction(), + } + self.local_cfg + .fault_handler + .report_fault(transaction_id, cond, progress); + Ok(()) + } + + fn notice_of_cancellation( + &mut self, + user: &mut impl CfdpUser, + condition_code: ConditionCode, + ) -> Result, SourceError> { + let transaction_id = self.tstate.as_ref().unwrap().transaction_id; + // CFDP standard 4.11.2.2.3: Any fault declared in the course of transferring + // the EOF (cancel) PDU must result in abandonment of the transaction. + if let Some(cond_code_eof) = self.tstate.as_ref().unwrap().cond_code_eof { + if cond_code_eof != ConditionCode::NoError { + // Still call the abandonment callback to ensure the fault is logged. + self.local_cfg + .fault_handler + .user_hook + .get_mut() + .abandoned_cb(transaction_id, cond_code_eof, self.fparams.progress); + self.abandon_transaction(); + return Ok(ControlFlow::Break(())); + } + } + + let tstate = self.tstate.as_mut().unwrap(); + tstate.cond_code_eof = Some(condition_code); + // As specified in 4.11.2.2, prepare an EOF PDU to be sent to the remote entity. Supply + // the checksum for the file copy progress sent so far. + let checksum = self.vfs.calculate_checksum( + self.put_request_cacher.source_file().unwrap(), + tstate.remote_cfg.default_crc_type, + self.fparams.progress, + self.pdu_and_cksum_buffer.get_mut(), + )?; + self.prepare_and_send_eof_pdu(user, checksum)?; + if self.transmission_mode().unwrap() == TransmissionMode::Unacknowledged { + // We are done. + self.reset(); + } else { + self.state_helper.step = TransactionStep::WaitingForEofAck; + } + Ok(ControlFlow::Continue(())) + } + + fn notice_of_suspension(&mut self) {} + + fn abandon_transaction(&mut self) { + // I guess an abandoned transaction just stops whatever the handler is doing and resets + // it to a clean state.. The implementation for this is quite easy. + self.reset(); + } + + /* + def _notice_of_cancellation(self, condition_code: ConditionCode) -> bool: + """Returns whether the fault declaration handler can returns prematurely.""" + # CFDP standard 4.11.2.2.3: Any fault declared in the course of transferring + # the EOF (cancel) PDU must result in abandonment of the transaction. + if ( + self._params.cond_code_eof is not None + and self._params.cond_code_eof != ConditionCode.NO_ERROR + ): + assert self._params.transaction_id is not None + # We still call the abandonment callback to ensure the fault is logged. + self.cfg.default_fault_handlers.abandoned_cb( + self._params.transaction_id, + self._params.cond_code_eof, + self._params.fp.progress, + ) + self._abandon_transaction() + return False + self._params.cond_code_eof = condition_code + # As specified in 4.11.2.2, prepare an EOF PDU to be sent to the remote entity. Supply + # the checksum for the file copy progress sent so far. + self._prepare_eof_pdu(self._checksum_calculation(self._params.fp.progress)) + self.states.step = TransactionStep.SENDING_EOF + return True + */ + + /// 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(); + self.countdown = None; + } +} + +#[cfg(test)] +mod tests { + use core::time::Duration; + use std::{fs::OpenOptions, io::Write, path::PathBuf, thread, 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, + source::TransactionStep, + tests::{basic_remote_cfg_table, SentPdu, TestCfdpSender, TestCfdpUser, TestFaultHandler}, + FaultHandler, IndicationConfig, PduRawWithInfo, StdCountdown, + StdRemoteEntityConfigProvider, StdTimerCreator, 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, + StdTimerCreator, + StdCountdown, + SeqCountProviderSimple, + >; + + struct SourceHandlerTestbench { + handler: TestSourceHandler, + #[allow(dead_code)] + srcfile_handle: TempPath, + srcfile: String, + destfile: String, + max_packet_len: usize, + check_idle_on_drop: bool, + } + + #[allow(dead_code)] + struct TransferInfo { + id: TransactionId, + closure_requested: bool, + pdu_header: PduHeader, + } + + 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, + ), + StdTimerCreator::new(core::time::Duration::from_millis(100)), + 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 set_check_limit_timeout(&mut self, timeout: Duration) { + self.handler.timer_creator.check_limit_timeout = timeout; + } + + 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() + } + + #[allow(dead_code)] + fn test_fault_handler(&self) -> &RefCell { + self.handler.local_cfg.user_fault_hook() + } + + fn test_fault_handler_mut(&mut self) -> &mut RefCell { + self.handler.local_cfg.user_fault_hook_mut() + } + + 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 transaction_info = 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( + &transaction_info.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, + transaction_info.closure_requested, + cfdp_user.expected_file_size, + checksum, + ); + (transaction_info.pdu_header, fd_pdus) + } + + fn common_no_acked_file_transfer( + &mut self, + cfdp_user: &mut TestCfdpUser, + put_request: PutRequestOwned, + filesize: u64, + ) -> TransferInfo { + 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 id = self.handler.transaction_id().unwrap(); + 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(), &[]); + TransferInfo { + pdu_header: *pdu_header, + closure_requested, + id, + } + } + + 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 = PduRawWithInfo::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 transaction_info = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, filesize); + tb.common_eof_pdu_check( + &mut cfdp_user, + transaction_info.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 transaction_info = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, filesize); + tb.common_eof_pdu_check( + &mut cfdp_user, + transaction_info.closure_requested, + filesize, + CRC_32.digest().finalize(), + ); + tb.finish_handling(&mut cfdp_user, transaction_info.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 !matches!(error, PutRequestError::FileDoesNotExist) { + panic!("unexpected error type: {:?}", error); + } + } + + #[test] + fn test_finished_pdu_check_timeout() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 512); + tb.set_check_limit_timeout(Duration::from_millis(45)); + 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 transaction_info = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, filesize); + let expected_id = tb.handler.transaction_id().unwrap(); + tb.common_eof_pdu_check( + &mut cfdp_user, + transaction_info.closure_requested, + filesize, + CRC_32.digest().finalize(), + ); + // After 50 ms delay, we run into a timeout, which leads to a check limit error + // declaration -> leads to a notice of cancellation -> leads to an EOF PDU with the + // appropriate error code. + thread::sleep(Duration::from_millis(50)); + assert_eq!( + tb.handler.state_machine_no_packet(&mut cfdp_user).unwrap(), + 0 + ); + let next_pdu = tb.get_next_sent_pdu().unwrap(); + let eof_pdu = EofPdu::from_bytes(&next_pdu.raw_pdu).expect("invalid EOF PDU format"); + tb.common_pdu_check_for_file_transfer(eof_pdu.pdu_header(), CrcFlag::NoCrc); + assert_eq!(eof_pdu.condition_code(), ConditionCode::CheckLimitReached); + assert_eq!(eof_pdu.file_size(), 0); + assert_eq!(eof_pdu.file_checksum(), 0); + + // Cancellation fault should have been triggered. + let fault_handler = tb.test_fault_handler_mut(); + let fh_ref_mut = fault_handler.get_mut(); + assert!(!fh_ref_mut.cancellation_queue_empty()); + assert_eq!(fh_ref_mut.notice_of_cancellation_queue.len(), 1); + let (id, cond_code, progress) = fh_ref_mut.notice_of_cancellation_queue.pop_back().unwrap(); + assert_eq!(id, expected_id); + assert_eq!(cond_code, ConditionCode::CheckLimitReached); + assert_eq!(progress, 0); + fh_ref_mut.all_queues_empty(); + } + + #[test] + fn test_cancelled_transfer_empty_file() { + 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); + assert_eq!(cfdp_user.transaction_indication_call_count, 0); + assert_eq!(cfdp_user.eof_sent_call_count, 0); + + tb.put_request(&put_request) + .expect("put_request call failed"); + assert_eq!(tb.handler.state(), State::Busy); + assert_eq!(tb.handler.step(), TransactionStep::Idle); + assert!(tb.get_next_sent_pdu().is_none()); + let id = tb.handler.transaction_id().unwrap(); + tb.handler + .cancel_request(&mut cfdp_user, &id) + .expect("transaction cancellation failed"); + assert_eq!(tb.handler.state(), State::Idle); + assert_eq!(tb.handler.step(), TransactionStep::Idle); + // EOF (Cancel) PDU will be generated + let eof_pdu = tb + .get_next_sent_pdu() + .expect("no EOF PDU generated like expected"); + assert_eq!( + eof_pdu.file_directive_type.unwrap(), + FileDirectiveType::EofPdu + ); + let eof_pdu = EofPdu::from_bytes(&eof_pdu.raw_pdu).unwrap(); + assert_eq!( + eof_pdu.condition_code(), + ConditionCode::CancelRequestReceived + ); + assert_eq!(eof_pdu.file_checksum(), 0); + assert_eq!(eof_pdu.file_size(), 0); + tb.common_pdu_check_for_file_transfer(eof_pdu.pdu_header(), CrcFlag::NoCrc); + } + + #[test] + fn test_cancelled_transfer_mid_transfer() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let mut tb = SourceHandlerTestbench::new(false, fault_handler, test_sender, 128); + 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 put_request = PutRequestOwned::new_regular_request( + REMOTE_ID.into(), + &tb.srcfile, + &tb.destfile, + Some(TransmissionMode::Unacknowledged), + Some(false), + ) + .expect("creating put request failed"); + let file_size = rand_data.len() as u64; + let mut cfdp_user = tb.create_user(0, file_size); + let transaction_info = + tb.common_no_acked_file_transfer(&mut cfdp_user, put_request, file_size); + let mut chunks = rand_data.chunks( + calculate_max_file_seg_len_for_max_packet_len_and_pdu_header( + &transaction_info.pdu_header, + tb.max_packet_len, + None, + ), + ); + let mut digest = CRC_32.digest(); + let first_chunk = chunks.next().expect("no chunk found"); + digest.update(first_chunk); + let checksum = digest.finalize(); + let next_packet = tb.get_next_sent_pdu().unwrap(); + assert_eq!(next_packet.pdu_type, PduType::FileData); + let fd_pdu = FileDataPdu::from_bytes(&next_packet.raw_pdu).unwrap(); + assert_eq!(fd_pdu.file_data(), &rand_data[0..first_chunk.len()]); + let expected_id = tb.handler.transaction_id().unwrap(); + assert!(tb + .handler + .cancel_request(&mut cfdp_user, &expected_id) + .expect("cancellation failed")); + assert_eq!(tb.handler.state(), State::Idle); + assert_eq!(tb.handler.step(), TransactionStep::Idle); + let next_packet = tb.get_next_sent_pdu().unwrap(); + assert_eq!(next_packet.pdu_type, PduType::FileDirective); + assert_eq!( + next_packet.file_directive_type.unwrap(), + FileDirectiveType::EofPdu + ); + // As specified in 4.11.2.2 of the standard, the file size will be the progress of the + // file copy operation so far, and the checksum is calculated for that progress. + let eof_pdu = EofPdu::from_bytes(&next_packet.raw_pdu).expect("EOF PDU creation failed"); + assert_eq!(eof_pdu.file_size(), first_chunk.len() as u64); + assert_eq!(eof_pdu.file_checksum(), checksum); + assert_eq!( + eof_pdu.condition_code(), + ConditionCode::CancelRequestReceived + ); + } +} 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..f0a0931 --- /dev/null +++ b/src/user.rs @@ -0,0 +1,100 @@ +#[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)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct TransactionFinishedParams { + pub id: TransactionId, + pub condition_code: ConditionCode, + 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..0f5bbca --- /dev/null +++ b/tests/end-to-end.rs @@ -0,0 +1,353 @@ +//! 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, PduOwnedWithInfo, RemoteEntityConfig, + StdTimerCreator, 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, + StdTimerCreator::default(), + 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, + StdTimerCreator::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); +}