diff --git a/Makefile b/Makefile index b52ec12..31248c3 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,71 @@ CC ?= cc -# Minimal Makefile: only build and run the unit test binary. -CFLAGS ?= -O2 -Iinclude -Wall -Wextra -std=c11 +AR ?= ar + +# Optimisation and instrumentation only. tools/coverage-html.sh overrides this +# to add gcov instrumentation, so the standard, include paths and warning set +# below stay identical between a normal build and a coverage build. +OPT ?= -O2 + +WARNINGS = -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wcast-align -Wcast-qual \ + -Wpointer-arith -Wformat=2 -Wmissing-prototypes -Wstrict-prototypes \ + -Wredundant-decls -Wundef + +# Build flags shared by the library, example and tests. +CFLAGS = $(OPT) -std=c99 -Iinclude $(WARNINGS) + BUILD_DIR = build +OBJ_DIR = $(BUILD_DIR)/obj +LIB_DIR = $(BUILD_DIR)/lib +LIB = $(LIB_DIR)/libcfdp.a + +SRCS = $(wildcard src/*.c) +OBJS = $(patsubst src/%.c,$(OBJ_DIR)/%.o,$(SRCS)) + +# Test entry point plus one test file per source module. +TEST_SRCS = $(wildcard tests/*.c) +TEST_HDRS = $(wildcard tests/*.h) +TEST_OBJS = $(patsubst tests/%.c,$(OBJ_DIR)/tests/%.o,$(TEST_SRCS)) + CTEST_PATH = $(BUILD_DIR)/tests/ctest +EXAMPLE_PATH = $(BUILD_DIR)/examples/example + +all: lib ctest example -all: ctest +lib: $(LIB) + +$(LIB): $(OBJS) + mkdir -p $(dir $@) + $(AR) rcs $@ $(OBJS) + +$(OBJ_DIR)/%.o: src/%.c + mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ ctest: $(CTEST_PATH) -$(CTEST_PATH): tests/unit_tests.c +# Test objects live under $(OBJ_DIR) so gcov's .gcno/.gcda files stay inside +# $(BUILD_DIR) instead of being dropped in the repository root. +$(OBJ_DIR)/tests/%.o: tests/%.c $(TEST_HDRS) + mkdir -p $(dir $@) + $(CC) $(CFLAGS) -Itests -c $< -o $@ + +$(CTEST_PATH): $(TEST_OBJS) $(LIB) + mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(TEST_OBJS) $(LIB) -o $@ + +example: $(EXAMPLE_PATH) + +$(EXAMPLE_PATH): examples/example.c $(LIB) mkdir -p $(dir $@) - $(CC) $(CFLAGS) -Iinclude tests/unit_tests.c -o $(CTEST_PATH) + $(CC) $(CFLAGS) examples/example.c $(LIB) -o $@ run: ctest $(CTEST_PATH) +coverage-html: + bash tools/coverage-html.sh + clean: rm -rf $(BUILD_DIR) -coverage-html: - bash tools/coverage_html.sh - -.PHONY: all ctest run clean +.PHONY: all lib ctest example run clean coverage-html diff --git a/README.md b/README.md index 329fa00..1a45e7b 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,88 @@ -# ProjectName -Project description. -Template repo for minimal embedded C implementations of CCSDS / ECSS standards. +# EmbeddedCFDP +A minimal, dependency-free embedded C implementation of the **CCSDS File +Delivery Protocol (CFDP)** wire format. Part of the OpenSpaceCode initiative — +reusable, standards-aligned components for small-scale space applications. ## Standards Compliance -- **CCSDS 000.0-X-Y**: +- **CCSDS 727.0-B-5**: CCSDS File Delivery Protocol (CFDP) — Blue Book. + +This library implements the *basic* protocol layer: serialisation and +deserialisation of the fundamental PDUs. The transaction state machine, timers, +retransmission and filestore are out of scope. See +[`docs/727x0b5e1.pdf`](docs/727x0b5e1.pdf) for the standard itself. ## Features ### Core Protocol Implementation +- **Fixed PDU header** (§5.1) — full flag set, variable-length entity IDs and + transaction sequence numbers (1–8 octets), 32- and 64-bit (large file) modes. +- **File Data PDU** (§5.3) — segment offset plus file data, including the + record continuation state and segment metadata when the header's segment + metadata flag is set. +- **File Directive PDUs** (§5.2) — EOF, Finished, ACK, Metadata, NAK, + Prompt and Keep Alive. The NAK codec carries up to + `CFDP_NAK_MAX_SEGMENT_REQUESTS` (32 by default, overridable) segment + requests, and rejects a PDU needing more rather than dropping the excess. +- **LV and TLV parameters** (§5.1.8, §5.1.9, §5.4) — filestore requests and + responses, messages to user, fault handler overrides, flow labels and + entity IDs. +- **Fault Location** (§5.2.2, §5.2.3) — encoded and decoded directly by the EOF + and Finished codecs, which refuse to emit a fault condition without it. +- **Modular file checksum** (§4.2.2) — streaming, segment-order independent. +- **CRC length accounting** (§4.1.3.2) — `cfdp_pdu_payload_size()` returns the + payload length with any trailing CRC excluded, so the CRC octets are never + decoded as file data or as part of a TLV chain. + +### Not Implemented + +- **CRC computation and checking** (§4.1) — the header's CRC flag is encoded + and the trailer's length is accounted for, but no CRC value is computed or + verified; supply and check it in the caller. +- **Checksum types** other than modular (§4.2.2) — including the null checksum. +- **Transaction procedures** (§4.3–§4.12) and **user operations** (§6). ### Design Principles +- **No heap usage** — every buffer is caller-supplied. +- **No external dependencies** — C99, standard library headers only. +- **Round-trip symmetry** — every `_serialize` has a matching `_deserialize`. +- **Big-endian on the wire**, native-endian in the API. ## Project Structure ``` -EmbeddedSpacePacket/ +EmbeddedCFDP/ ├── include/ -│ └── +│ ├── cfdp.h # Umbrella header +│ ├── cfdp_common.h # Enums, constants, shared helpers +│ ├── cfdp_endian.h # Big-endian integer helpers +│ ├── cfdp_checksum.h # Modular file checksum +│ ├── cfdp_pdu.h # Fixed PDU header + File Data PDU +│ ├── cfdp_directive.h # File Directive PDUs +│ └── cfdp_tlv.h # LV and TLV parameters ├── src/ -│ └── +│ ├── cfdp_checksum.c +│ ├── cfdp_pdu.c +│ ├── cfdp_directive.c +│ └── cfdp_tlv.c ├── examples/ -│ └── +│ └── example.c # Build-and-parse a small-file transfer ├── tests/ -│ ├── cunit.h # Minimal test framework -│ └── unit_tests.c # Unit tests -├── scripts/ -│ └── coverage_html.sh # Coverage report -├── build/ # Build artifacts +│ ├── cunit.h # Minimal test framework +│ ├── test_runners.h # Per-module test runner declarations +│ ├── test_cfdp_checksum.c +│ ├── test_cfdp_pdu.c +│ ├── test_cfdp_directive.c +│ ├── test_cfdp_tlv.c +│ └── unit_tests.c # Test entry point +├── docs/ +│ └── 727x0b5e1.pdf # CCSDS 727.0-B-5 Blue Book +├── tools/ +│ └── coverage-html.sh # Coverage report +├── build/ # Build artifacts ├── Makefile └── README.md ``` @@ -45,14 +97,22 @@ make Builds the static library, the example binary and the test binary in `build/`. +The C standard, include paths and warning set are fixed in the `Makefile` and apply to the +library, example and tests alike. Only the optimisation/instrumentation flags are meant to be +overridden, via `OPT`: + +```bash +make OPT="-O0 -g" +``` + ### Build Library Only ```bash make lib -# Produces: build/ +# Produces: build/lib/libcfdp.a ``` -### Build Example +### Build and Run the Example ```bash make example @@ -62,29 +122,22 @@ make example ### Run Tests ```bash -make ctest -./build/tests/ctest +make run ``` ### Coverage (HTML) -Requires `gcovr` installed in your system: - -```bash -sudo apt install gcovr -``` - -Generate coverage report: +Requires `gcovr`: ```bash +pip install gcovr make coverage-html +# Prints a line/branch summary +# Output: build/coverage/index.html ``` -Output report: - -```bash -build/coverage/index.html -``` +The script rebuilds with `OPT="-O0 -g --coverage"`, so instrumentation is the only difference +from a normal build. ### Clean @@ -94,59 +147,175 @@ make clean ## Quick Start -### Step 1 +### Step 1 — Serialise a header and an EOF PDU ```c +#include "cfdp.h" + +uint8_t buf[64]; + +cfdp_pdu_header_t hdr = {0}; +hdr.version = CFDP_PROTOCOL_VERSION; +hdr.pdu_type = CFDP_PDU_TYPE_DIRECTIVE; +hdr.direction = CFDP_DIRECTION_TOWARD_RECEIVER; +hdr.transmission_mode = CFDP_TRANS_MODE_UNACKNOWLEDGED; +hdr.large_file_flag = CFDP_FILE_SIZE_SMALL; +hdr.entity_id_length = 1; +hdr.transaction_seq_length = 2; +hdr.source_entity_id = 1; +hdr.transaction_seq_number = 42; +hdr.destination_entity_id = 2; + +cfdp_eof_pdu_t eof = {0}; +eof.condition_code = CFDP_COND_NO_ERROR; +eof.file_checksum = cfdp_checksum_compute(file, file_len); +eof.file_size = file_len; + +size_t hlen = cfdp_pdu_header_size(&hdr); +size_t plen = cfdp_eof_serialize(&eof, hdr.large_file_flag, buf + hlen, sizeof(buf) - hlen); +hdr.data_field_length = (uint16_t)plen; +cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf)); +size_t total = hlen + plen; /* bytes to transmit */ +``` + +### Step 2 — Parse a received PDU +```c +cfdp_pdu_header_t hdr; +size_t hlen = cfdp_pdu_header_deserialize(rx, rx_len, &hdr); + +const uint8_t *payload = rx + hlen; + +/* The data field length counts the CRC when the CRC flag is set (§4.1.3.2), + * so never pass hdr.data_field_length straight to a payload codec. */ +size_t payload_len = cfdp_pdu_payload_size(&hdr); + +if (hdr.pdu_type == CFDP_PDU_TYPE_DIRECTIVE) { + cfdp_directive_code_t code; + cfdp_pdu_directive_code(payload, payload_len, &code); + /* dispatch on `code` (CFDP_DIRECTIVE_EOF, ...) */ +} else { + cfdp_file_data_pdu_t fd; + cfdp_file_data_deserialize(payload, payload_len, hdr.large_file_flag, + hdr.segment_metadata_flag, &fd); + /* write fd.file_data_len octets at fd.offset */ +} ``` -### Step 2 +## API Reference + +### PDU header (`cfdp_pdu.h`) ```c +size_t cfdp_pdu_header_size(const cfdp_pdu_header_t *hdr); +size_t cfdp_pdu_header_serialize(const cfdp_pdu_header_t *hdr, uint8_t *buf, size_t buf_len); +size_t cfdp_pdu_header_deserialize(const uint8_t *buf, size_t buf_len, cfdp_pdu_header_t *hdr); +size_t cfdp_pdu_payload_size(const cfdp_pdu_header_t *hdr); +``` +### File Data (`cfdp_pdu.h`) + +```c +size_t cfdp_file_data_serialize(const cfdp_file_data_pdu_t *fd, cfdp_large_file_flag_t large, + cfdp_seg_metadata_flag_t seg_meta, uint8_t *buf, size_t buf_len); +size_t cfdp_file_data_deserialize(const uint8_t *buf, size_t buf_len, cfdp_large_file_flag_t large, + cfdp_seg_metadata_flag_t seg_meta, cfdp_file_data_pdu_t *fd); ``` -### Step X +Both take the header's large-file and segment-metadata flags, which select the +data field layout (§5.3). Pass the same values carried in the header actually +sent or received: the serialiser rejects a payload whose segment metadata +disagrees with the flag, since that mismatch would silently shift the offset +field at the peer. +### Directives (`cfdp_directive.h`) -## API Reference +`cfdp_{eof,finished,ack,metadata,nak,prompt,keep_alive}_{serialize,deserialize}()` +— each returns the number of octets written or consumed, or `0` on error. -### Lifecycle +Pass `cfdp_pdu_payload_size(&hdr)` as the data field length when decoding, not +`hdr.data_field_length`, so a trailing CRC is not parsed as PDU content. -```c +The ACK codec accepts only EOF and Finished as the acknowledged directive, the +only two table 5-8 allows, and derives the directive subtype code from it — +`0001` for Finished, `0000` for EOF — so `cfdp_ack_pdu_t` has no subtype field +to set wrongly. A received ACK of any other directive, or with a subtype that +disagrees with its directive code, is rejected. -``` +`cfdp_nak_deserialize()` rejects a data field whose segment requests do not fill +it exactly, and one carrying more requests than `cfdp_nak_pdu_t` can hold — +truncating would tell the sender that gaps it never saw had been satisfied, so +they would never be retransmitted. Raise `CFDP_NAK_MAX_SEGMENT_REQUESTS` (it +sizes the struct: 32 requests is 536 octets on a 64-bit build) on links where the receiver +routinely reports more gaps than the default. -### Building a Packet +### LV/TLV parameters (`cfdp_tlv.h`) ```c - +size_t cfdp_lv_serialize(const char *value, uint8_t value_len, uint8_t *buf, size_t buf_len); +size_t cfdp_tlv_serialize(const cfdp_tlv_t *tlv, uint8_t *buf, size_t buf_len); +size_t cfdp_entity_id_tlv_serialize(uint64_t entity_id, uint8_t id_len, + uint8_t *buf, size_t buf_len); +size_t cfdp_fault_handler_tlv_serialize(cfdp_condition_code_t condition_code, + cfdp_fault_handler_code_t handler_code, + uint8_t *buf, size_t buf_len); +size_t cfdp_filestore_request_tlv_serialize(const cfdp_filestore_request_t *req, + uint8_t *buf, size_t buf_len); +size_t cfdp_filestore_response_tlv_serialize(const cfdp_filestore_response_t *resp, + uint8_t *buf, size_t buf_len); ``` -### Utilities +Each has a matching `_deserialize()`. Metadata options and Finished filestore +responses are carried as pre-encoded TLV chains: build one with these codecs, +point the PDU struct at it, and walk a received chain by advancing through +`cfdp_tlv_deserialize()` by its return value. ```c - +/* Attach a filestore request to a Metadata PDU. */ +uint8_t options[64]; +cfdp_filestore_request_t req = {0}; +req.action_code = CFDP_FS_ACTION_CREATE_DIRECTORY; +req.first_filename = "logs"; +req.first_filename_len = 4; + +md.options = options; +md.options_len = (uint16_t)cfdp_filestore_request_tlv_serialize(&req, options, sizeof(options)); ``` -### Types +### Checksum (`cfdp_checksum.h`) ```c - +uint32_t cfdp_checksum_update(uint32_t checksum, uint64_t offset, const uint8_t *data, size_t len); +uint32_t cfdp_checksum_compute(const uint8_t *data, size_t len); ``` -## Memory Usage (Estimated) +### Return-value convention -- **Library (stripped)**: -- **Serialization buffer**: -- **No heap usage**: all allocations are caller-supplied +Every `_serialize` / `_deserialize` function returns the number of octets +written or consumed, and `0` on any error (NULL argument, buffer too small, or +malformed input). -## CCSDS XXX — Notes +## Memory Usage (Estimated) + +- **Library (stripped)**: a few kilobytes of `.text`; no static state. +- **No heap usage**: all allocations are caller-supplied. +- **Serialization buffers**: caller-sized; a full PDU header is at most + `CFDP_PDU_HEADER_MAX_LEN` (28) octets. ## Limitations +- Optional TLV parameters (fault location, filestore requests/responses, + messages to user) are not encoded or decoded. +- The 16-bit CRC value is not computed or checked. The flag is preserved and + `cfdp_pdu_payload_size()` excludes the trailer from the payload length, but + computing and verifying the CRC itself is left to the caller. +- No transaction state machine, timers or retransmission logic. + ## References +- CCSDS 727.0-B-5, *CCSDS File Delivery Protocol (CFDP)*, Blue Book. +- CCSDS 720.1-G-4, *CFDP — Part 1: Introduction and Overview*, Green Book. + ## License -See LICENSE file. \ No newline at end of file +See [LICENSE](LICENSE) file. diff --git a/docs/727x0b5e1.pdf b/docs/727x0b5e1.pdf new file mode 100644 index 0000000..d011171 Binary files /dev/null and b/docs/727x0b5e1.pdf differ diff --git a/examples/example.c b/examples/example.c new file mode 100644 index 0000000..bdc4fb1 --- /dev/null +++ b/examples/example.c @@ -0,0 +1,160 @@ +/** + * @file example.c + * @brief Worked example: build and parse a minimal CFDP file transfer + * + * Assembles the three PDUs of a tiny unacknowledged-mode transfer — Metadata, + * File Data and EOF — for an in-memory "file", prints each PDU as hex, then + * parses them back and verifies the file checksum. + * Demonstrates CCSDS 727.0-B-5 (CCSDS File Delivery Protocol). + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp.h" + +#include + +static const uint8_t g_file[] = "OpenSpaceCode CFDP demo payload"; +static const size_t g_file_len = sizeof(g_file) - 1U; /* drop the NUL terminator */ + +static void print_hex(const char *label, const uint8_t *buf, size_t len) +{ + printf("%-10s (%2zu octets):", label, len); + for (size_t i = 0; i < len; i++) + { + printf(" %02X", buf[i]); + } + printf("\n"); +} + +static void fill_common_header(cfdp_pdu_header_t *hdr, cfdp_pdu_type_t type) +{ + hdr->version = CFDP_PROTOCOL_VERSION; + hdr->pdu_type = type; + hdr->direction = CFDP_DIRECTION_TOWARD_RECEIVER; + hdr->transmission_mode = CFDP_TRANS_MODE_UNACKNOWLEDGED; + hdr->crc_flag = CFDP_CRC_ABSENT; + hdr->large_file_flag = CFDP_FILE_SIZE_SMALL; + hdr->segmentation_control = CFDP_SEG_CTRL_BOUNDARIES_NOT_PRESERVED; + hdr->segment_metadata_flag = CFDP_SEG_METADATA_ABSENT; + hdr->entity_id_length = 1; + hdr->transaction_seq_length = 2; + hdr->source_entity_id = 1; + hdr->transaction_seq_number = 42; + hdr->destination_entity_id = 2; +} + +static size_t emit_pdu(const char *label, + cfdp_pdu_header_t *hdr, + const uint8_t *payload, + size_t payload_len, + uint8_t *out, + size_t out_len) +{ + size_t hlen = cfdp_pdu_header_size(hdr); + hdr->data_field_length = (uint16_t)payload_len; + if ((cfdp_pdu_header_serialize(hdr, out, out_len) == 0) || (out_len < hlen + payload_len)) + { + return 0; + } + for (size_t i = 0; i < payload_len; i++) + { + out[hlen + i] = payload[i]; + } + print_hex(label, out, hlen + payload_len); + return hlen + payload_len; +} + +static void build_metadata(uint8_t *out, size_t out_len) +{ + cfdp_pdu_header_t hdr; + fill_common_header(&hdr, CFDP_PDU_TYPE_DIRECTIVE); + + cfdp_metadata_pdu_t md = {0}; + md.closure_requested = false; + md.checksum_type = CFDP_CHECKSUM_MODULAR; + md.file_size = g_file_len; + md.source_filename = "src.dat"; + md.source_filename_len = 7; + md.destination_filename = "dst.dat"; + md.destination_filename_len = 7; + + uint8_t payload[64]; + size_t plen = cfdp_metadata_serialize(&md, hdr.large_file_flag, payload, sizeof(payload)); + emit_pdu("Metadata", &hdr, payload, plen, out, out_len); +} + +static void build_file_data(uint8_t *out, size_t out_len) +{ + cfdp_pdu_header_t hdr; + fill_common_header(&hdr, CFDP_PDU_TYPE_FILE_DATA); + + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 0; + fd.file_data = g_file; + fd.file_data_len = g_file_len; + + uint8_t payload[64]; + size_t plen = cfdp_file_data_serialize(&fd, + hdr.large_file_flag, + hdr.segment_metadata_flag, + payload, + sizeof(payload)); + emit_pdu("File Data", &hdr, payload, plen, out, out_len); +} + +static void build_eof(uint8_t *out, size_t out_len) +{ + cfdp_pdu_header_t hdr; + fill_common_header(&hdr, CFDP_PDU_TYPE_DIRECTIVE); + + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_NO_ERROR; + eof.file_checksum = cfdp_checksum_compute(g_file, g_file_len); + eof.file_size = g_file_len; + + uint8_t payload[16]; + size_t plen = cfdp_eof_serialize(&eof, hdr.large_file_flag, payload, sizeof(payload)); + emit_pdu("EOF", &hdr, payload, plen, out, out_len); +} + +static void parse_and_verify(const uint8_t *pdu, size_t pdu_len) +{ + cfdp_pdu_header_t hdr; + size_t hlen = cfdp_pdu_header_deserialize(pdu, pdu_len, &hdr); + + /* The data field length counts the CRC when the CRC flag is set (§4.1.3.2); + * handing that raw length to the payload codec would append the CRC octets + * to the file. */ + size_t payload_len = cfdp_pdu_payload_size(&hdr); + + cfdp_file_data_pdu_t fd; + cfdp_file_data_deserialize(&pdu[hlen], + payload_len, + hdr.large_file_flag, + hdr.segment_metadata_flag, + &fd); + + uint32_t checksum = cfdp_checksum_update(0, fd.offset, fd.file_data, fd.file_data_len); + printf("\nReceiver reconstructed %zu octets at offset %llu, checksum 0x%08X\n", + fd.file_data_len, + (unsigned long long)fd.offset, + checksum); + printf("Expected file checksum: 0x%08X\n", + cfdp_checksum_compute(g_file, g_file_len)); +} + +int main(void) +{ + uint8_t metadata_pdu[96]; + uint8_t file_data_pdu[96]; + uint8_t eof_pdu[32]; + + printf("=== CFDP small-file transfer (unacknowledged mode) ===\n\n"); + build_metadata(metadata_pdu, sizeof(metadata_pdu)); + build_file_data(file_data_pdu, sizeof(file_data_pdu)); + build_eof(eof_pdu, sizeof(eof_pdu)); + + parse_and_verify(file_data_pdu, sizeof(file_data_pdu)); + return 0; +} diff --git a/include/cfdp.h b/include/cfdp.h new file mode 100644 index 0000000..21fab05 --- /dev/null +++ b/include/cfdp.h @@ -0,0 +1,23 @@ +/** + * @file cfdp.h + * @brief Umbrella header for the EmbeddedCFDP library + * + * Aggregates the public CFDP modules: common definitions, the PDU header and + * File Data codec, the File Directive codecs, the LV/TLV parameter codecs and + * the file checksum. + * Implements a basic subset of CCSDS 727.0-B-5 (CCSDS File Delivery Protocol). + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_H +#define CFDP_H + +#include "cfdp_checksum.h" +#include "cfdp_common.h" +#include "cfdp_directive.h" +#include "cfdp_pdu.h" +#include "cfdp_tlv.h" + +#endif /* CFDP_H */ diff --git a/include/cfdp_checksum.h b/include/cfdp_checksum.h new file mode 100644 index 0000000..2acf334 --- /dev/null +++ b/include/cfdp_checksum.h @@ -0,0 +1,51 @@ +/** + * @file cfdp_checksum.h + * @brief CFDP 32-bit modular file checksum + * + * Implements the legacy modular checksum as per CCSDS 727.0-B-5 §4.2.2. + * The checksum is the arithmetic sum, modulo 2^32, of the 4-octet words + * formed by the file contents aligned to their absolute offset within the + * file. Because each octet contributes independently of the others, the + * checksum can be accumulated segment-by-segment and in any order. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_CHECKSUM_H +#define CFDP_CHECKSUM_H + +#include +#include + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Fold one file segment into a running modular checksum. + * + * Suitable for streaming: call once per received or transmitted File Data + * segment, passing the checksum returned by the previous call. Segments may + * be supplied in any order and need not be aligned to a 4-octet boundary. + * + * @param[in] checksum Running checksum so far (0 for the first segment). + * @param[in] offset Absolute offset of @p data within the file, in octets. + * @param[in] data Segment octets; may be NULL only when @p len is 0. + * @param[in] len Number of octets in @p data. + * @return The updated checksum. + */ +uint32_t cfdp_checksum_update(uint32_t checksum, uint64_t offset, const uint8_t *data, size_t len); + +/** + * @brief Compute the modular checksum of a whole in-memory file. + * + * Convenience wrapper equivalent to cfdp_checksum_update(0, 0, data, len). + * + * @param[in] data File octets; may be NULL only when @p len is 0. + * @param[in] len File length in octets. + * @return The file checksum. + */ +uint32_t cfdp_checksum_compute(const uint8_t *data, size_t len); + +#endif /* CFDP_CHECKSUM_H */ diff --git a/include/cfdp_common.h b/include/cfdp_common.h new file mode 100644 index 0000000..bd9dca4 --- /dev/null +++ b/include/cfdp_common.h @@ -0,0 +1,254 @@ +/** + * @file cfdp_common.h + * @brief Common CFDP constants, enumerations and shared helpers + * + * Defines the protocol-level enumerations (PDU types, directive codes, + * condition codes, etc.) shared by every CFDP module. + * Implements the field encodings of CCSDS 727.0-B-5 (CCSDS File Delivery + * Protocol), Section 5. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_COMMON_H +#define CFDP_COMMON_H + +#include +#include + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +/** @brief CFDP protocol version carried in the PDU header (CCSDS 727.0-B-5 §5.1.2). */ +#define CFDP_PROTOCOL_VERSION 1U + +/** @brief Minimum length in octets of an entity ID or transaction sequence number. */ +#define CFDP_ID_LEN_MIN 1U + +/** @brief Maximum length in octets of an entity ID or transaction sequence number. */ +#define CFDP_ID_LEN_MAX 8U + +/** @brief Size of the fixed part of the PDU header, before the variable-length IDs. */ +#define CFDP_PDU_HEADER_FIXED_LEN 4U + +/** @brief Largest possible PDU header: fixed part plus three 8-octet identifier fields. */ +#define CFDP_PDU_HEADER_MAX_LEN (CFDP_PDU_HEADER_FIXED_LEN + 3U * CFDP_ID_LEN_MAX) + +/** @brief Octets the 16-bit CRC occupies at the end of the PDU data field (§4.1.3.2). */ +#define CFDP_PDU_CRC_LEN 2U + +/** @brief Largest segment metadata field a File Data PDU can carry (§5.3, table 5-14). */ +#define CFDP_SEGMENT_METADATA_MAX_LEN 63U + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief PDU Type flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_PDU_TYPE_DIRECTIVE = 0, /**< PDU carries a File Directive. */ + CFDP_PDU_TYPE_FILE_DATA = 1 /**< PDU carries File Data. */ +} cfdp_pdu_type_t; + +/** + * @brief Direction flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_DIRECTION_TOWARD_RECEIVER = 0, /**< PDU travels toward the file receiver. */ + CFDP_DIRECTION_TOWARD_SENDER = 1 /**< PDU travels toward the file sender. */ +} cfdp_direction_t; + +/** + * @brief Transmission Mode flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_TRANS_MODE_ACKNOWLEDGED = 0, /**< Class 2: reliable, acknowledged transfer. */ + CFDP_TRANS_MODE_UNACKNOWLEDGED = 1 /**< Class 1: unreliable, unacknowledged transfer. */ +} cfdp_transmission_mode_t; + +/** + * @brief CRC flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_CRC_ABSENT = 0, /**< No 16-bit CRC trails the PDU data field. */ + CFDP_CRC_PRESENT = 1 /**< A 16-bit CRC trails the PDU data field. */ +} cfdp_crc_flag_t; + +/** + * @brief Large File flag (CCSDS 727.0-B-5 §5.1.2). + * + * Selects the width of file offsets and sizes on the wire. + */ +typedef enum +{ + CFDP_FILE_SIZE_SMALL = 0, /**< Offsets and sizes are 32-bit (4 octets). */ + CFDP_FILE_SIZE_LARGE = 1 /**< Offsets and sizes are 64-bit (8 octets). */ +} cfdp_large_file_flag_t; + +/** + * @brief Segmentation Control flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_SEG_CTRL_BOUNDARIES_NOT_PRESERVED = 0, /**< Record boundaries are not preserved. */ + CFDP_SEG_CTRL_BOUNDARIES_PRESERVED = 1 /**< Record boundaries are preserved. */ +} cfdp_seg_ctrl_t; + +/** + * @brief Segment Metadata flag (CCSDS 727.0-B-5 §5.1.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_SEG_METADATA_ABSENT = 0, /**< File Data PDUs carry no segment metadata. */ + CFDP_SEG_METADATA_PRESENT = 1 /**< File Data PDUs carry segment metadata. */ +} cfdp_seg_metadata_flag_t; + +/** + * @brief Record continuation state of a File Data segment (CCSDS 727.0-B-5 §5.3). + * + * Present only when the PDU header's Segment Metadata flag is + * ::CFDP_SEG_METADATA_PRESENT. Enum values equal the 2-bit wire pattern + * directly. + */ +typedef enum +{ + CFDP_RECORD_CONT_NEITHER = 0, /**< Neither the start nor the end of any record. */ + CFDP_RECORD_CONT_START = 1, /**< Starts a record that continues past this PDU. */ + CFDP_RECORD_CONT_END = 2, /**< Ends a record that began in a prior PDU. */ + CFDP_RECORD_CONT_START_AND_END = 3 /**< Carries one or more complete records. */ +} cfdp_record_continuation_t; + +/** + * @brief File Directive codes (CCSDS 727.0-B-5 §5.4, Table 5-4). + * + * Enum values equal the 1-octet directive code on the wire. + */ +typedef enum +{ + CFDP_DIRECTIVE_EOF = 0x04, /**< End-of-File PDU. */ + CFDP_DIRECTIVE_FINISHED = 0x05, /**< Finished PDU. */ + CFDP_DIRECTIVE_ACK = 0x06, /**< Acknowledgement PDU. */ + CFDP_DIRECTIVE_METADATA = 0x07, /**< Metadata PDU. */ + CFDP_DIRECTIVE_NAK = 0x08, /**< Negative Acknowledgement PDU. */ + CFDP_DIRECTIVE_PROMPT = 0x09, /**< Prompt PDU. */ + CFDP_DIRECTIVE_KEEP_ALIVE = 0x0C /**< Keep Alive PDU. */ +} cfdp_directive_code_t; + +/** + * @brief Condition codes (CCSDS 727.0-B-5 §5.5, Table 5-5). + * + * Enum values equal the 4-bit condition code on the wire. + */ +typedef enum +{ + CFDP_COND_NO_ERROR = 0x0, /**< No error. */ + CFDP_COND_POSITIVE_ACK_LIMIT_REACHED = 0x1, /**< Positive ACK limit reached. */ + CFDP_COND_KEEP_ALIVE_LIMIT_REACHED = 0x2, /**< Keep Alive limit reached. */ + CFDP_COND_INVALID_TRANSMISSION_MODE = 0x3, /**< Invalid transmission mode. */ + CFDP_COND_FILESTORE_REJECTION = 0x4, /**< Filestore rejection. */ + CFDP_COND_FILE_CHECKSUM_FAILURE = 0x5, /**< File checksum failure. */ + CFDP_COND_FILE_SIZE_ERROR = 0x6, /**< File size error. */ + CFDP_COND_NAK_LIMIT_REACHED = 0x7, /**< NAK limit reached. */ + CFDP_COND_INACTIVITY_DETECTED = 0x8, /**< Inactivity detected. */ + CFDP_COND_INVALID_FILE_STRUCTURE = 0x9, /**< Invalid file structure. */ + CFDP_COND_CHECK_LIMIT_REACHED = 0xA, /**< Check limit reached. */ + CFDP_COND_UNSUPPORTED_CHECKSUM_TYPE = 0xB, /**< Unsupported checksum type. */ + CFDP_COND_SUSPEND_REQUEST_RECEIVED = 0xE, /**< Suspend request received. */ + CFDP_COND_CANCEL_REQUEST_RECEIVED = 0xF /**< Cancel request received. */ +} cfdp_condition_code_t; + +/** + * @brief Delivery code carried in the Finished PDU (CCSDS 727.0-B-5 §5.4.2). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_DELIVERY_COMPLETE = 0, /**< Data complete: whole file delivered. */ + CFDP_DELIVERY_INCOMPLETE = 1 /**< Data incomplete: file not fully delivered. */ +} cfdp_delivery_code_t; + +/** + * @brief File status carried in the Finished PDU (CCSDS 727.0-B-5 §5.4.2). + * + * Enum values equal the 2-bit wire pattern directly. + */ +typedef enum +{ + CFDP_FILE_STATUS_DISCARDED = 0, /**< Deliberately discarded. */ + CFDP_FILE_STATUS_DISCARDED_FILESTORE_REJECTION = 1, /**< Discarded on filestore rejection. */ + CFDP_FILE_STATUS_RETAINED = 2, /**< Retained in the filestore. */ + CFDP_FILE_STATUS_UNREPORTED = 3 /**< File status not reported. */ +} cfdp_file_status_t; + +/** + * @brief Transaction status carried in the ACK PDU (CCSDS 727.0-B-5 §5.4.3). + * + * Enum values equal the 2-bit wire pattern directly. + */ +typedef enum +{ + CFDP_TXN_STATUS_UNDEFINED = 0, /**< Transaction status undefined. */ + CFDP_TXN_STATUS_ACTIVE = 1, /**< Transaction is active. */ + CFDP_TXN_STATUS_TERMINATED = 2, /**< Transaction is terminated. */ + CFDP_TXN_STATUS_UNRECOGNIZED = 3 /**< Transaction is unrecognized. */ +} cfdp_transaction_status_t; + +/** + * @brief Checksum algorithm identifier (CCSDS 727.0-B-5 §5.2.5; SANA registry). + * + * Enum values equal the 4-bit checksum type field in the Metadata PDU. + */ +typedef enum +{ + CFDP_CHECKSUM_MODULAR = 0, /**< Legacy 32-bit modular checksum. */ + CFDP_CHECKSUM_NULL = 15 /**< Null checksum: value is always zero. */ +} cfdp_checksum_type_t; + +/** + * @brief Prompt PDU response type (CCSDS 727.0-B-5 §5.4.5). + * + * Enum values equal the 1-bit wire pattern directly. + */ +typedef enum +{ + CFDP_PROMPT_NAK = 0, /**< Prompt the receiver to issue a NAK. */ + CFDP_PROMPT_KEEP_ALIVE = 1 /**< Prompt the receiver to issue a Keep Alive. */ +} cfdp_prompt_response_t; + +/* ------------------------------------------------------------------------- + * Inline Helpers + * ---------------------------------------------------------------------- */ + +/** + * @brief Number of octets used to encode file offsets and sizes. + * + * @param[in] large_file_flag Large File flag from the PDU header. + * @return 8 for a large file, 4 for a small file. + */ +static inline uint8_t cfdp_file_size_octets(cfdp_large_file_flag_t large_file_flag) +{ + return (large_file_flag == CFDP_FILE_SIZE_LARGE) ? 8U : 4U; +} + +#endif /* CFDP_COMMON_H */ diff --git a/include/cfdp_directive.h b/include/cfdp_directive.h new file mode 100644 index 0000000..cfc8a38 --- /dev/null +++ b/include/cfdp_directive.h @@ -0,0 +1,366 @@ +/** + * @file cfdp_directive.h + * @brief CFDP File Directive PDU codecs + * + * Serialises and deserialises the File Directive PDUs used to control a CFDP + * file transfer: EOF, Finished, ACK, Metadata, NAK, Prompt and Keep Alive. + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.2 and + * Section 5.4. + * + * @note Option TLVs are carried as pre-encoded spans: build or walk them with + * the codecs in cfdp_tlv.h. The Fault Location TLV, being mandatory on a + * fault condition, is encoded and decoded directly. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_DIRECTIVE_H +#define CFDP_DIRECTIVE_H + +#include "cfdp_common.h" +#include "cfdp_tlv.h" + +#include +#include + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +/** + * @brief Maximum number of segment requests a NAK PDU may carry. + * + * Sizes ::cfdp_nak_pdu_t and bounds both NAK codecs, which reject a PDU + * needing more rather than silently dropping the excess. A NAK data field can + * hold far more than this, so raise it (define it before including this header) + * on links where the receiver routinely reports more gaps than the default. + */ +#ifndef CFDP_NAK_MAX_SEGMENT_REQUESTS +# define CFDP_NAK_MAX_SEGMENT_REQUESTS 32U +#endif + +/** @brief Directive subtype code of an ACK of a Finished PDU (§5.2.4, table 5-8). */ +#define CFDP_ACK_SUBTYPE_FINISHED 1U + +/** @brief Directive subtype code of an ACK of any other file directive (§5.2.4, table 5-8). */ +#define CFDP_ACK_SUBTYPE_OTHER 0U + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief End-of-File PDU contents (CCSDS 727.0-B-5 §5.2.2). + * + * @note The Fault Location is omitted when @p condition_code is 'No error' and + * required otherwise; serialisation fails if a fault condition is given + * without one. On decode @p fault_location_len is 0 when absent. + */ +typedef struct +{ + cfdp_condition_code_t condition_code; /**< Condition at the sending entity. */ + uint32_t file_checksum; /**< Modular checksum of the whole file. */ + uint64_t file_size; /**< Total file size in octets. */ + uint64_t fault_location_entity_id; /**< Entity that initiated cancellation. */ + uint8_t fault_location_len; /**< Octets of that entity ID; 0 when omitted. */ +} cfdp_eof_pdu_t; + +/** + * @brief Finished PDU contents (CCSDS 727.0-B-5 §5.2.3). + * + * @note @p filestore_responses is a pre-encoded chain of Filestore Response + * TLVs in caller-owned memory — one per Filestore Request of the + * Metadata PDU — built or walked with cfdp_filestore_response_tlv_*(). + * The Fault Location is required unless @p condition_code is 'No error' + * or 'Unsupported checksum type'. + */ +typedef struct +{ + cfdp_condition_code_t condition_code; /**< Condition at the receiving entity. */ + cfdp_delivery_code_t delivery_code; /**< Data complete or incomplete. */ + cfdp_file_status_t file_status; /**< Fate of the delivered file. */ + const uint8_t *filestore_responses; /**< Filestore Response TLVs, or NULL. */ + uint16_t filestore_responses_len; /**< Octets in @p filestore_responses. */ + uint64_t fault_location_entity_id; /**< Entity that initiated cancellation. */ + uint8_t fault_location_len; /**< Octets of that entity ID; 0 when omitted. */ +} cfdp_finished_pdu_t; + +/** + * @brief Acknowledgement PDU contents (CCSDS 727.0-B-5 §5.2.4). + * + * @note Table 5-8 fixes the directive subtype code as a function of the + * acknowledged directive — ::CFDP_ACK_SUBTYPE_FINISHED for an ACK of a + * Finished PDU, ::CFDP_ACK_SUBTYPE_OTHER for every other — so it is not a + * field here: the codec derives it on encode and rejects a PDU carrying + * the wrong value on decode. + */ +typedef struct +{ + cfdp_directive_code_t ack_directive_code; /**< Directive being acknowledged (EOF/Finished). */ + cfdp_condition_code_t condition_code; /**< Condition code being acknowledged. */ + cfdp_transaction_status_t transaction_status; /**< Sender's view of the transaction. */ +} cfdp_ack_pdu_t; + +/** + * @brief Metadata PDU contents (CCSDS 727.0-B-5 §5.2.5). + * + * @note @p source_filename, @p destination_filename and @p options point into + * caller-owned memory; the library neither copies nor frees them. + * @p options is a pre-encoded chain of option TLVs — filestore requests, + * messages to user, fault handler overrides and flow labels — built or + * walked with the codecs in cfdp_tlv.h. + */ +typedef struct +{ + bool closure_requested; /**< Whether transaction closure is requested. */ + cfdp_checksum_type_t checksum_type; /**< Checksum algorithm identifier. */ + uint64_t file_size; /**< Total file size in octets. */ + const char *source_filename; /**< Source file name (may be NULL when empty). */ + uint8_t source_filename_len; /**< Source file name length in octets. */ + const char *destination_filename; /**< Destination file name (may be NULL when empty). */ + uint8_t destination_filename_len; /**< Destination file name length in octets. */ + const uint8_t *options; /**< Option TLVs, or NULL when none. */ + uint16_t options_len; /**< Octets in @p options. */ +} cfdp_metadata_pdu_t; + +/** + * @brief A single NAK segment request (CCSDS 727.0-B-5 §5.2.6). + */ +typedef struct +{ + uint64_t start_offset; /**< Offset of the first missing octet. */ + uint64_t end_offset; /**< Offset one past the last missing octet. */ +} cfdp_segment_request_t; + +/** + * @brief Negative Acknowledgement PDU contents (CCSDS 727.0-B-5 §5.2.6). + * + * @note A NAK carrying more than ::CFDP_NAK_MAX_SEGMENT_REQUESTS requests is + * rejected rather than truncated: every decoded request is reported in + * @p segment_requests, so @p segment_request_count is never a partial + * view of what the peer asked for. + */ +typedef struct +{ + uint64_t start_of_scope; /**< Start offset of the reported scope. */ + uint64_t end_of_scope; /**< End offset of the reported scope. */ + cfdp_segment_request_t segment_requests[CFDP_NAK_MAX_SEGMENT_REQUESTS]; /**< Missing ranges. */ + size_t segment_request_count; /**< Number of valid entries in @p segment_requests. */ +} cfdp_nak_pdu_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Serialise an EOF PDU data field (directive code plus contents). + * + * @param[in] eof EOF contents to serialise. + * @param[in] large_file_flag Selects a 32- or 64-bit file size field. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, buffer too small, or a + * fault condition code with no fault location). + */ +size_t cfdp_eof_serialize(const cfdp_eof_pdu_t *eof, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise an EOF PDU data field. + * + * @note Deliberately lenient: unlike cfdp_eof_serialize(), this does not + * enforce the §5.2.2 Fault Location rule. A PDU is accepted whether or + * not the TLV matches its condition code; check @p eof->fault_location_len + * if the rule matters to the caller. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[in] large_file_flag Selects a 32- or 64-bit file size field. + * @param[out] eof Decoded EOF contents, including the Fault + * Location TLV when one is present. + * @return Bytes consumed, or 0 on error (NULL args, wrong directive code, + * truncated input, or a malformed trailing TLV). + */ +size_t cfdp_eof_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_eof_pdu_t *eof); + +/** + * @brief Serialise a Finished PDU data field. + * + * @param[in] fin Finished contents to serialise. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (including a fault condition code with + * no fault location). + */ +size_t cfdp_finished_serialize(const cfdp_finished_pdu_t *fin, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise a Finished PDU data field. + * + * @note Deliberately lenient: unlike cfdp_finished_serialize(), this does not + * enforce the §5.2.3 Fault Location rule. A PDU is accepted whether or + * not the TLV matches its condition code; check @p fin->fault_location_len + * if the rule matters to the caller. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[out] fin Decoded Finished contents; @p filestore_responses spans + * the Filestore Response TLVs within @p buf. + * @return Bytes consumed, or 0 on error (including a TLV other than a + * Filestore Response or Fault Location). + */ +size_t cfdp_finished_deserialize(const uint8_t *buf, size_t buf_len, cfdp_finished_pdu_t *fin); + +/** + * @brief Serialise an ACK PDU data field. + * + * The directive subtype code is derived from @p ack->ack_directive_code per + * table 5-8; it cannot be supplied, so a mismatched pair cannot be emitted. + * + * @param[in] ack ACK contents to serialise; @p ack->ack_directive_code + * must be ::CFDP_DIRECTIVE_EOF or ::CFDP_DIRECTIVE_FINISHED, + * the only directives table 5-8 acknowledges. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, buffer too small, or an + * acknowledged directive other than EOF or Finished). + */ +size_t cfdp_ack_serialize(const cfdp_ack_pdu_t *ack, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise an ACK PDU data field. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[out] ack Decoded ACK contents; left untouched on error. + * @return Bytes consumed, or 0 on error (NULL args, wrong directive code, + * truncated input, an acknowledged directive other than EOF or + * Finished, or a directive subtype code that table 5-8 does not allow + * for the acknowledged directive). + */ +size_t cfdp_ack_deserialize(const uint8_t *buf, size_t buf_len, cfdp_ack_pdu_t *ack); + +/** + * @brief Serialise a Metadata PDU data field. + * + * @param[in] md Metadata contents to serialise. + * @param[in] large_file_flag Selects a 32- or 64-bit file size field. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +size_t cfdp_metadata_serialize(const cfdp_metadata_pdu_t *md, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a Metadata PDU data field. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[in] large_file_flag Selects a 32- or 64-bit file size field. + * @param[out] md Decoded contents; file name and option pointers + * index into @p buf. Any octets after the + * destination file name become @p options. + * @return Bytes consumed, or 0 on error. + */ +size_t cfdp_metadata_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_metadata_pdu_t *md); + +/** + * @brief Serialise a NAK PDU data field. + * + * @param[in] nak NAK contents to serialise. + * @param[in] large_file_flag Selects 32- or 64-bit offset fields. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (including too many segment requests). + */ +size_t cfdp_nak_serialize(const cfdp_nak_pdu_t *nak, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a NAK PDU data field. + * + * Rejects a data field whose segment requests do not fill it exactly, and one + * carrying more than ::CFDP_NAK_MAX_SEGMENT_REQUESTS requests. Dropping the + * excess would leave the sender believing it had satisfied the NAK, so the + * unreported gaps would never be retransmitted. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets — use + * cfdp_pdu_payload_size(), not the header's raw + * data field length. + * @param[in] large_file_flag Selects 32- or 64-bit offset fields. + * @param[out] nak Decoded contents. + * @return Bytes consumed (equal to @p buf_len), or 0 on error (NULL args, + * wrong directive code, truncated scope, a segment request array that + * does not fill the data field, or more requests than can be stored). + */ +size_t cfdp_nak_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_nak_pdu_t *nak); + +/** + * @brief Serialise a Prompt PDU data field. + * + * @param[in] response Prompt response type (NAK or Keep Alive). + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +size_t cfdp_prompt_serialize(cfdp_prompt_response_t response, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise a Prompt PDU data field. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[out] response Decoded prompt response type. + * @return Bytes consumed, or 0 on error. + */ +size_t cfdp_prompt_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_prompt_response_t *response); + +/** + * @brief Serialise a Keep Alive PDU data field. + * + * @param[in] progress Receiver's reported file progress in octets. + * @param[in] large_file_flag Selects a 32- or 64-bit progress field. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +size_t cfdp_keep_alive_serialize(uint64_t progress, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a Keep Alive PDU data field. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[in] large_file_flag Selects a 32- or 64-bit progress field. + * @param[out] progress Decoded file progress in octets. + * @return Bytes consumed, or 0 on error. + */ +size_t cfdp_keep_alive_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + uint64_t *progress); + +#endif /* CFDP_DIRECTIVE_H */ diff --git a/include/cfdp_endian.h b/include/cfdp_endian.h new file mode 100644 index 0000000..6f6f1e1 --- /dev/null +++ b/include/cfdp_endian.h @@ -0,0 +1,51 @@ +/** + * @file cfdp_endian.h + * @brief Big-endian (network order) integer serialisation helpers + * + * CFDP encodes every multi-octet field in big-endian order + * (CCSDS 727.0-B-5 §5.1). These small inline helpers keep the module + * serialisers free of hand-rolled shift/mask loops. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_ENDIAN_H +#define CFDP_ENDIAN_H + +#include + +/** + * @brief Write an unsigned integer to a buffer in big-endian order. + * + * @param[out] buf Destination buffer, must hold at least @p nbytes octets. + * @param[in] value Value to encode; only the low @p nbytes octets are used. + * @param[in] nbytes Number of octets to write (1..8). + */ +static inline void cfdp_write_uint(uint8_t *buf, uint64_t value, uint8_t nbytes) +{ + for (uint8_t i = 0; i < nbytes; i++) + { + buf[nbytes - 1U - i] = (uint8_t)(value & 0xFFU); + value >>= 8; + } +} + +/** + * @brief Read a big-endian unsigned integer from a buffer. + * + * @param[in] buf Source buffer, must hold at least @p nbytes octets. + * @param[in] nbytes Number of octets to read (1..8). + * @return The decoded value. + */ +static inline uint64_t cfdp_read_uint(const uint8_t *buf, uint8_t nbytes) +{ + uint64_t value = 0; + for (uint8_t i = 0; i < nbytes; i++) + { + value = (value << 8) | (uint64_t)buf[i]; + } + return value; +} + +#endif /* CFDP_ENDIAN_H */ diff --git a/include/cfdp_pdu.h b/include/cfdp_pdu.h new file mode 100644 index 0000000..4c44a9e --- /dev/null +++ b/include/cfdp_pdu.h @@ -0,0 +1,181 @@ +/** + * @file cfdp_pdu.h + * @brief CFDP PDU fixed header and File Data PDU codec + * + * Serialises and deserialises the fixed PDU header shared by every CFDP PDU + * and the File Data PDU payload. + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.1 + * (fixed PDU header) and Section 5.3 (File Data PDU). + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_PDU_H +#define CFDP_PDU_H + +#include "cfdp_common.h" + +#include +#include + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief Fixed PDU header shared by every CFDP PDU (CCSDS 727.0-B-5 §5.1). + * + * @note @p entity_id_length and @p transaction_seq_length hold the actual + * octet counts (1..8), not the (count-1) form used on the wire. + */ +typedef struct +{ + uint8_t version; /**< Must be ::CFDP_PROTOCOL_VERSION. */ + cfdp_pdu_type_t pdu_type; /**< Directive or File Data. */ + cfdp_direction_t direction; /**< Toward receiver or sender. */ + cfdp_transmission_mode_t transmission_mode; /**< Acknowledged or unacknowledged. */ + cfdp_crc_flag_t crc_flag; /**< Whether a CRC trails the PDU. */ + cfdp_large_file_flag_t large_file_flag; /**< Selects 32- or 64-bit file fields. */ + uint16_t data_field_length; /**< PDU data field length in octets. */ + cfdp_seg_ctrl_t segmentation_control; /**< Record boundary preservation. */ + cfdp_seg_metadata_flag_t segment_metadata_flag; /**< Segment metadata present flag. */ + uint8_t entity_id_length; /**< Entity ID length in octets (1..8). */ + uint8_t transaction_seq_length; /**< Transaction sequence length in octets (1..8). */ + uint64_t source_entity_id; /**< Source entity ID. */ + uint64_t transaction_seq_number; /**< Transaction sequence number. */ + uint64_t destination_entity_id; /**< Destination entity ID. */ +} cfdp_pdu_header_t; + +/** + * @brief File Data PDU payload (CCSDS 727.0-B-5 §5.3, table 5-14). + * + * @note @p file_data and @p segment_metadata point into caller-owned memory; + * the library neither copies nor frees them. + * @note @p record_continuation, @p segment_metadata and @p segment_metadata_len + * are carried on the wire only when the PDU header's Segment Metadata + * flag is ::CFDP_SEG_METADATA_PRESENT, and must be left zero/NULL when it + * is not — the codecs reject a payload that disagrees with the flag. + */ +typedef struct +{ + uint64_t offset; /**< Offset of this segment within the file, in octets. */ + const uint8_t *file_data; /**< File data octets. */ + size_t file_data_len; /**< Number of file data octets. */ + cfdp_record_continuation_t record_continuation; /**< Record boundaries within this segment. */ + const uint8_t *segment_metadata; /**< Segment metadata octets, or NULL. */ + uint8_t segment_metadata_len; /**< Segment metadata length, 0..63 octets. */ +} cfdp_file_data_pdu_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Total on-wire size of the header described by @p hdr. + * + * @param[in] hdr Header whose identifier lengths determine the size. + * @return Header size in octets, or 0 if @p hdr is NULL or its identifier + * lengths are out of the 1..8 range. + */ +size_t cfdp_pdu_header_size(const cfdp_pdu_header_t *hdr); + +/** + * @brief Serialise a fixed PDU header into a caller-supplied buffer. + * + * The caller is responsible for setting @p hdr->data_field_length to the + * length of the payload that will follow the header. + * + * @param[in] hdr Header to serialise. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, a version other than + * ::CFDP_PROTOCOL_VERSION, bad identifier lengths, or buffer too small). + */ +size_t cfdp_pdu_header_serialize(const cfdp_pdu_header_t *hdr, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise a fixed PDU header from a buffer. + * + * @param[in] buf Input buffer positioned at the start of the header. + * @param[in] buf_len Number of octets available in @p buf. + * @param[out] hdr Decoded header; may be partly written on error. + * @return Header size in octets consumed, or 0 on error (NULL args, a version + * other than ::CFDP_PROTOCOL_VERSION, or truncated header). + */ +size_t cfdp_pdu_header_deserialize(const uint8_t *buf, size_t buf_len, cfdp_pdu_header_t *hdr); + +/** + * @brief Length of a received PDU's payload, excluding any trailing CRC. + * + * §4.1.3.2 places the 16-bit CRC in the final octets of the PDU data field and + * counts it in the data field length, so @p hdr->data_field_length is not the + * payload length when the CRC flag is set. Pass the result of this function, + * not @p hdr->data_field_length, to the payload deserialisers; feeding them the + * raw data field length would decode the CRC octets as file data or as part of + * a TLV chain. + * + * @param[in] hdr Decoded header of the received PDU. + * @return Payload length in octets, or 0 if @p hdr is NULL or the data field is + * too short to hold the CRC its flag claims. Every valid CFDP PDU has a + * non-empty payload, so 0 is unambiguously an error. + */ +size_t cfdp_pdu_payload_size(const cfdp_pdu_header_t *hdr); + +/** + * @brief Serialise a File Data PDU payload (§5.3, table 5-14). + * + * Writes the data field only; serialise the fixed header separately. When + * @p segment_metadata_flag is ::CFDP_SEG_METADATA_PRESENT the record + * continuation state, segment metadata length and segment metadata precede the + * offset; otherwise the data field starts at the offset. + * + * @param[in] fd File Data payload to serialise. + * @param[in] large_file_flag Selects a 32- or 64-bit offset field. + * @param[in] segment_metadata_flag Segment Metadata flag of the PDU header; + * must match the header actually sent. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, buffer too small, segment + * metadata longer than ::CFDP_SEGMENT_METADATA_MAX_LEN, or segment + * metadata supplied while @p segment_metadata_flag is absent). + */ +size_t cfdp_file_data_serialize(const cfdp_file_data_pdu_t *fd, + cfdp_large_file_flag_t large_file_flag, + cfdp_seg_metadata_flag_t segment_metadata_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a File Data PDU payload from a data-field slice (§5.3). + * + * @param[in] buf Data field, positioned at its first octet. + * @param[in] buf_len Payload length in octets — use + * cfdp_pdu_payload_size(), not the header's + * raw data field length, or a trailing CRC + * would be decoded as file data. + * @param[in] large_file_flag Selects a 32- or 64-bit offset field. + * @param[in] segment_metadata_flag Segment Metadata flag from the PDU header; + * selects the data field layout. + * @param[out] fd Decoded payload; @p fd->file_data and + * @p fd->segment_metadata point into @p buf. + * @return Bytes consumed (equal to @p buf_len), or 0 on error (NULL args or a + * data field too short for the layout the flags describe). + */ +size_t cfdp_file_data_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_seg_metadata_flag_t segment_metadata_flag, + cfdp_file_data_pdu_t *fd); + +/** + * @brief Peek the directive code of a File Directive PDU data field. + * + * @param[in] buf Data field, positioned at the directive code octet. + * @param[in] buf_len Length of the data field in octets. + * @param[out] code Decoded directive code. + * @return true on success, false if @p buf is NULL or @p buf_len is 0. + */ +bool cfdp_pdu_directive_code(const uint8_t *buf, size_t buf_len, cfdp_directive_code_t *code); + +#endif /* CFDP_PDU_H */ diff --git a/include/cfdp_tlv.h b/include/cfdp_tlv.h new file mode 100644 index 0000000..3ef1e94 --- /dev/null +++ b/include/cfdp_tlv.h @@ -0,0 +1,323 @@ +/** + * @file cfdp_tlv.h + * @brief CFDP LV and TLV parameter codecs + * + * Serialises and deserialises the Length-Value and Type-Length-Value objects + * carried by the File Directive PDUs: filestore requests and responses, + * messages to user, fault handler overrides, flow labels and entity IDs. + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.1.8, + * Section 5.1.9 and Section 5.4. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_TLV_H +#define CFDP_TLV_H + +#include "cfdp_common.h" + +#include +#include +#include + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +/** @brief Octets preceding the value of an LV object: the length field. */ +#define CFDP_LV_HEADER_LEN 1U + +/** @brief Octets preceding the value of a TLV object: the type and length fields. */ +#define CFDP_TLV_HEADER_LEN 2U + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief TLV type codes (CCSDS 727.0-B-5 §5.4). + * + * Enum values equal the 1-octet Type field on the wire. + */ +typedef enum +{ + CFDP_TLV_FILESTORE_REQUEST = 0x00, /**< Filestore request (§5.4.1). */ + CFDP_TLV_FILESTORE_RESPONSE = 0x01, /**< Filestore response (§5.4.2). */ + CFDP_TLV_MESSAGE_TO_USER = 0x02, /**< Message to user, opaque value (§5.4.3). */ + CFDP_TLV_FAULT_HANDLER_OVERRIDE = 0x04, /**< Fault handler override (§5.4.4). */ + CFDP_TLV_FLOW_LABEL = 0x05, /**< Flow label, opaque value (§5.4.5). */ + CFDP_TLV_ENTITY_ID = 0x06 /**< Entity ID, used as fault location (§5.4.6). */ +} cfdp_tlv_type_t; + +/** + * @brief Filestore request action codes (CCSDS 727.0-B-5 §5.4.1, table 5-16). + * + * Enum values equal the 4-bit action code on the wire. + */ +typedef enum +{ + CFDP_FS_ACTION_CREATE_FILE = 0x0, /**< Create the named file. */ + CFDP_FS_ACTION_DELETE_FILE = 0x1, /**< Delete the named file. */ + CFDP_FS_ACTION_RENAME_FILE = 0x2, /**< Rename first file to second. */ + CFDP_FS_ACTION_APPEND_FILE = 0x3, /**< Append second file to first. */ + CFDP_FS_ACTION_REPLACE_FILE = 0x4, /**< Replace first file's contents with second's. */ + CFDP_FS_ACTION_CREATE_DIRECTORY = 0x5, /**< Create the named directory. */ + CFDP_FS_ACTION_REMOVE_DIRECTORY = 0x6, /**< Remove the named directory. */ + CFDP_FS_ACTION_DENY_FILE = 0x7, /**< Delete the named file if present. */ + CFDP_FS_ACTION_DENY_DIRECTORY = 0x8 /**< Remove the named directory if present. */ +} cfdp_filestore_action_t; + +/** + * @brief Filestore response status codes (CCSDS 727.0-B-5 §5.4.2, table 5-18). + * + * @note Codes 0x1..0x3 are action specific — for example 0x1 means 'file does + * not exist' for a Delete but 'create not allowed' for a Create. See + * table 5-18 for the meaning under each action code. + */ +typedef enum +{ + CFDP_FS_STATUS_SUCCESSFUL = 0x0, /**< Action performed successfully. */ + CFDP_FS_STATUS_ERROR_1 = 0x1, /**< First action-specific failure. */ + CFDP_FS_STATUS_ERROR_2 = 0x2, /**< Second action-specific failure. */ + CFDP_FS_STATUS_ERROR_3 = 0x3, /**< Third action-specific failure. */ + CFDP_FS_STATUS_NOT_PERFORMED = 0xF /**< Action not performed. */ +} cfdp_filestore_status_t; + +/** + * @brief Fault handler codes (CCSDS 727.0-B-5 §5.4.4, table 5-19). + * + * Enum values equal the 4-bit handler code on the wire. + */ +typedef enum +{ + CFDP_HANDLER_RESERVED = 0x0, /**< Reserved; rejected by the codecs. */ + CFDP_HANDLER_NOTICE_OF_CANCELLATION = 0x1, /**< Issue a Notice of Cancellation. */ + CFDP_HANDLER_NOTICE_OF_SUSPENSION = 0x2, /**< Issue a Notice of Suspension. */ + CFDP_HANDLER_IGNORE_ERROR = 0x3, /**< Ignore the error. */ + CFDP_HANDLER_ABANDON_TRANSACTION = 0x4 /**< Abandon the transaction. */ +} cfdp_fault_handler_code_t; + +/** + * @brief A generic TLV object (CCSDS 727.0-B-5 §5.1.9). + * + * @note @p value points into caller-owned memory; the library neither copies + * nor frees it. + */ +typedef struct +{ + uint8_t type; /**< Type field; one of ::cfdp_tlv_type_t. */ + uint8_t length; /**< Value length in octets. */ + const uint8_t *value; /**< Value octets, or NULL when @p length is zero. */ +} cfdp_tlv_t; + +/** + * @brief Filestore Request TLV contents (CCSDS 727.0-B-5 §5.4.1, table 5-15). + * + * @note Both file names point into caller-owned memory. The second file name + * is present only for the action codes listed in table 5-16; see + * ::cfdp_filestore_action_has_second_filename. + */ +typedef struct +{ + cfdp_filestore_action_t action_code; /**< Filestore action to perform. */ + const char *first_filename; /**< First file name (may be NULL when empty). */ + uint8_t first_filename_len; /**< First file name length in octets. */ + const char *second_filename; /**< Second file name (may be NULL when empty). */ + uint8_t second_filename_len; /**< Second file name length in octets. */ +} cfdp_filestore_request_t; + +/** + * @brief Filestore Response TLV contents (CCSDS 727.0-B-5 §5.4.2, table 5-17). + * + * @note All three values point into caller-owned memory. + */ +typedef struct +{ + cfdp_filestore_action_t action_code; /**< Action the response reports on. */ + cfdp_filestore_status_t status_code; /**< Outcome of the action. */ + const char *first_filename; /**< First file name (may be NULL when empty). */ + uint8_t first_filename_len; /**< First file name length in octets. */ + const char *second_filename; /**< Second file name (may be NULL when empty). */ + uint8_t second_filename_len; /**< Second file name length in octets. */ + const char *message; /**< Implementation-specific message (may be NULL). */ + uint8_t message_len; /**< Message length in octets. */ +} cfdp_filestore_response_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Whether an action code carries a second file name (table 5-16). + * + * @param[in] action_code Filestore action code. + * @return true for Rename, Append and Replace; false for every other action. + */ +bool cfdp_filestore_action_has_second_filename(cfdp_filestore_action_t action_code); + +/** + * @brief Serialise an LV object: a 1-octet length followed by the value. + * + * @param[in] value Value octets; may be NULL only when @p value_len is 0. + * @param[in] value_len Value length in octets. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (missing value or buffer too small). + */ +size_t cfdp_lv_serialize(const char *value, uint8_t value_len, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise an LV object, pointing @p value into @p buf. + * + * @param[in] buf Input buffer positioned at the length octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] value Set to the value octets, or NULL when the value is empty. + * @param[out] value_len Set to the value length in octets. + * @return Bytes consumed, or 0 on error (NULL args or truncated input). + */ +size_t cfdp_lv_deserialize(const uint8_t *buf, + size_t buf_len, + const char **value, + uint8_t *value_len); + +/** + * @brief Serialise a TLV object: type, length, then the value. + * + * @param[in] tlv TLV to serialise; @p value may be NULL only when the + * length is 0. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +size_t cfdp_tlv_serialize(const cfdp_tlv_t *tlv, uint8_t *buf, size_t buf_len); + +/** + * @brief Deserialise a TLV object, pointing @p tlv->value into @p buf. + * + * Successive calls advance through a chain of TLVs by the returned length. + * + * @param[in] buf Input buffer positioned at the type octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] tlv Decoded TLV. + * @return Bytes consumed, or 0 on error. + */ +size_t cfdp_tlv_deserialize(const uint8_t *buf, size_t buf_len, cfdp_tlv_t *tlv); + +/** + * @brief Serialise an Entity ID TLV, used as a Fault Location (§5.4.6). + * + * @param[in] entity_id Entity at which transaction cancellation was initiated. + * @param[in] id_len Octets used to encode @p entity_id (1..8). + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (bad length or buffer too small). + */ +size_t cfdp_entity_id_tlv_serialize(uint64_t entity_id, + uint8_t id_len, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise an Entity ID TLV (§5.4.6). + * + * @param[in] buf Input buffer positioned at the type octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] entity_id Decoded entity ID. + * @param[out] id_len Octets the entity ID occupied on the wire. + * @return Bytes consumed, or 0 on error (wrong type or bad length). + */ +size_t cfdp_entity_id_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + uint64_t *entity_id, + uint8_t *id_len); + +/** + * @brief Serialise a Fault Handler Override TLV (§5.4.4). + * + * @param[in] condition_code Fault condition the override applies to; 'No error', + * 'Suspend.request received', 'Cancel.request + * received' and the reserved codes are not faults. + * @param[in] handler_code Handler to apply; one of the four defined in + * table 5-19 (::CFDP_HANDLER_RESERVED is rejected). + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL buffer, buffer too small, a + * non-fault condition code, or a reserved handler code). + */ +size_t cfdp_fault_handler_tlv_serialize(cfdp_condition_code_t condition_code, + cfdp_fault_handler_code_t handler_code, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a Fault Handler Override TLV (§5.4.4). + * + * @param[in] buf Input buffer positioned at the type octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] condition_code Decoded condition code; left untouched on error. + * @param[out] handler_code Decoded handler code; left untouched on error. + * @return Bytes consumed, or 0 on error (NULL args, truncated or mistyped TLV, + * a value length other than 1, a non-fault condition code, or a + * reserved handler code). + */ +size_t cfdp_fault_handler_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_condition_code_t *condition_code, + cfdp_fault_handler_code_t *handler_code); + +/** + * @brief Serialise a Filestore Request TLV (§5.4.1). + * + * @param[in] req Request contents; the second file name is written only + * for the action codes that carry one. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, buffer too small, an action + * code outside table 5-16, or a value too long for the TLV). + */ +size_t cfdp_filestore_request_tlv_serialize(const cfdp_filestore_request_t *req, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a Filestore Request TLV (§5.4.1). + * + * @param[in] buf Input buffer positioned at the type octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] req Decoded request; file names index into @p buf. + * @return Bytes consumed, or 0 on error (NULL args, truncated or mistyped TLV, + * an action code outside table 5-16, or malformed file names). + */ +size_t cfdp_filestore_request_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_filestore_request_t *req); + +/** + * @brief Serialise a Filestore Response TLV (§5.4.2). + * + * @param[in] resp Response contents; the second file name is written only + * for the action codes that carry one. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args, buffer too small, an action + * code outside table 5-16, or a value too long for the TLV). + */ +size_t cfdp_filestore_response_tlv_serialize(const cfdp_filestore_response_t *resp, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a Filestore Response TLV (§5.4.2). + * + * @param[in] buf Input buffer positioned at the type octet. + * @param[in] buf_len Octets available in @p buf. + * @param[out] resp Decoded response; file names and message index into @p buf. + * @return Bytes consumed, or 0 on error (NULL args, truncated or mistyped TLV, + * an action code outside table 5-16, or malformed names or message). + */ +size_t cfdp_filestore_response_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_filestore_response_t *resp); + +#endif /* CFDP_TLV_H */ diff --git a/src/cfdp_checksum.c b/src/cfdp_checksum.c new file mode 100644 index 0000000..2985abe --- /dev/null +++ b/src/cfdp_checksum.c @@ -0,0 +1,36 @@ +/** + * @file cfdp_checksum.c + * @brief CFDP 32-bit modular file checksum + * + * Implements the legacy modular checksum as per CCSDS 727.0-B-5 §4.2.2. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_checksum.h" + +uint32_t cfdp_checksum_update(uint32_t checksum, uint64_t offset, const uint8_t *data, size_t len) +{ + if ((!data) || (len == 0)) + { + return checksum; + } + + for (size_t i = 0; i < len; i++) + { + /* Each octet lands in one of the four byte lanes of a 4-octet word + * according to its absolute position in the file, so the running sum + * is independent of how the file was split into segments. */ + uint8_t lane = (uint8_t)((offset + i) & 0x3U); + uint8_t shift = (uint8_t)(8U * (3U - lane)); + checksum += ((uint32_t)data[i]) << shift; + } + + return checksum; +} + +uint32_t cfdp_checksum_compute(const uint8_t *data, size_t len) +{ + return cfdp_checksum_update(0, 0, data, len); +} diff --git a/src/cfdp_directive.c b/src/cfdp_directive.c new file mode 100644 index 0000000..4346d0b --- /dev/null +++ b/src/cfdp_directive.c @@ -0,0 +1,584 @@ +/** + * @file cfdp_directive.c + * @brief CFDP File Directive PDU codecs + * + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.2 and + * Section 5.4. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_directive.h" + +#include "cfdp_endian.h" + +#include + +/** + * @brief Whether a condition code obliges a PDU to carry a Fault Location. + * + * @param[in] condition_code Condition code of the PDU being built. + * @return true when the Fault Location TLV must be present (§5.2.3). + */ +static bool cfdp_fault_location_required(cfdp_condition_code_t condition_code) +{ + return (condition_code != CFDP_COND_NO_ERROR) && + (condition_code != CFDP_COND_UNSUPPORTED_CHECKSUM_TYPE); +} + +size_t cfdp_eof_serialize(const cfdp_eof_pdu_t *eof, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len) +{ + if ((!eof) || (!buf)) + { + return 0; + } + + /* §5.2.2: the Fault Location is omitted only on 'No error'. Emitting a + * fault condition without it would produce a malformed EOF PDU. */ + bool with_fault_location = (eof->condition_code != CFDP_COND_NO_ERROR); + if ((with_fault_location) && (eof->fault_location_len == 0)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t size = (size_t)fs + 6U; + if (buf_len < size) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + buf[1] = (uint8_t)(((uint8_t)eof->condition_code & 0xFU) << 4); + cfdp_write_uint(&buf[2], eof->file_checksum, 4); + cfdp_write_uint(&buf[6], eof->file_size, fs); + + if (!with_fault_location) + { + return size; + } + + size_t n = cfdp_entity_id_tlv_serialize(eof->fault_location_entity_id, + eof->fault_location_len, + &buf[size], + buf_len - size); + if (n == 0) + { + return 0; + } + + return size + n; +} + +size_t cfdp_eof_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_eof_pdu_t *eof) +{ + if ((!buf) || (!eof)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t size = (size_t)fs + 6U; + if ((buf_len < size) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_EOF)) + { + return 0; + } + + eof->condition_code = (cfdp_condition_code_t)((buf[1] >> 4) & 0xFU); + eof->file_checksum = (uint32_t)cfdp_read_uint(&buf[2], 4); + eof->file_size = cfdp_read_uint(&buf[6], fs); + eof->fault_location_entity_id = 0; + eof->fault_location_len = 0; + + if (buf_len == size) + { + return size; + } + + size_t n = cfdp_entity_id_tlv_deserialize(&buf[size], + buf_len - size, + &eof->fault_location_entity_id, + &eof->fault_location_len); + if (n == 0) + { + return 0; + } + + return size + n; +} + +size_t cfdp_finished_serialize(const cfdp_finished_pdu_t *fin, uint8_t *buf, size_t buf_len) +{ + if ((!fin) || (!buf) || (buf_len < 2U) || + ((!fin->filestore_responses) && (fin->filestore_responses_len > 0))) + { + return 0; + } + + /* §5.2.3: the Fault Location is omitted only on 'No error' and + * 'Unsupported checksum type'. */ + bool with_fault_location = cfdp_fault_location_required(fin->condition_code); + if ((with_fault_location) && (fin->fault_location_len == 0)) + { + return 0; + } + + size_t pos = 2U; + if (buf_len < pos + fin->filestore_responses_len) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_FINISHED; + buf[1] = + (uint8_t)((((uint8_t)fin->condition_code & 0xFU) << 4) | + (((uint8_t)fin->delivery_code & 0x1U) << 2) | ((uint8_t)fin->file_status & 0x3U)); + + if (fin->filestore_responses_len > 0) + { + memcpy(&buf[pos], fin->filestore_responses, fin->filestore_responses_len); + pos += fin->filestore_responses_len; + } + + if (!with_fault_location) + { + return pos; + } + + size_t n = cfdp_entity_id_tlv_serialize(fin->fault_location_entity_id, + fin->fault_location_len, + &buf[pos], + buf_len - pos); + if (n == 0) + { + return 0; + } + + return pos + n; +} + +/** + * @brief Decode the TLV chain trailing a Finished PDU's fixed octets. + * + * Filestore Responses are reported as the span they occupy in @p buf; an + * Entity ID TLV is decoded into the Fault Location fields. §5.2.3 admits no + * other TLV here, and the responses all precede the Fault Location. + * + * @param[in] buf Data field, positioned at the directive code. + * @param[in] buf_len Length of the data field in octets. + * @param[out] fin Finished contents receiving the decoded TLVs. + * @return Bytes consumed in total, or 0 on a malformed or unexpected TLV. + */ +static size_t cfdp_finished_parse_tlvs(const uint8_t *buf, size_t buf_len, cfdp_finished_pdu_t *fin) +{ + size_t pos = 2U; + size_t responses_start = pos; + + while (pos < buf_len) + { + cfdp_tlv_t tlv; + size_t n = cfdp_tlv_deserialize(&buf[pos], buf_len - pos, &tlv); + if (n == 0) + { + return 0; + } + + if (tlv.type == (uint8_t)CFDP_TLV_FILESTORE_RESPONSE) + { + if (fin->fault_location_len > 0) + { + return 0; + } + fin->filestore_responses = &buf[responses_start]; + fin->filestore_responses_len = (uint16_t)((pos + n) - responses_start); + } + else if (tlv.type == (uint8_t)CFDP_TLV_ENTITY_ID) + { + if (cfdp_entity_id_tlv_deserialize(&buf[pos], + buf_len - pos, + &fin->fault_location_entity_id, + &fin->fault_location_len) == 0) + { + return 0; + } + } + else + { + return 0; + } + + pos += n; + } + + return pos; +} + +size_t cfdp_finished_deserialize(const uint8_t *buf, size_t buf_len, cfdp_finished_pdu_t *fin) +{ + if ((!buf) || (!fin) || (buf_len < 2U) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_FINISHED)) + { + return 0; + } + + fin->condition_code = (cfdp_condition_code_t)((buf[1] >> 4) & 0xFU); + fin->delivery_code = (cfdp_delivery_code_t)((buf[1] >> 2) & 0x1U); + fin->file_status = (cfdp_file_status_t)(buf[1] & 0x3U); + fin->filestore_responses = NULL; + fin->filestore_responses_len = 0; + fin->fault_location_entity_id = 0; + fin->fault_location_len = 0; + + return cfdp_finished_parse_tlvs(buf, buf_len, fin); +} + +/** + * @brief Whether a directive may be acknowledged (table 5-8). + * + * @param[in] ack_directive_code Directive the ACK acknowledges. + * @return true for EOF and Finished, the only acknowledged directives. + */ +static bool cfdp_ack_directive_valid(cfdp_directive_code_t ack_directive_code) +{ + return (ack_directive_code == CFDP_DIRECTIVE_EOF) || + (ack_directive_code == CFDP_DIRECTIVE_FINISHED); +} + +/** + * @brief Directive subtype code table 5-8 requires for an acknowledged PDU. + * + * @param[in] ack_directive_code Directive the ACK acknowledges. + * @return ::CFDP_ACK_SUBTYPE_FINISHED for a Finished PDU, else + * ::CFDP_ACK_SUBTYPE_OTHER. + */ +static uint8_t cfdp_ack_directive_subtype(cfdp_directive_code_t ack_directive_code) +{ + return (ack_directive_code == CFDP_DIRECTIVE_FINISHED) ? CFDP_ACK_SUBTYPE_FINISHED + : CFDP_ACK_SUBTYPE_OTHER; +} + +size_t cfdp_ack_serialize(const cfdp_ack_pdu_t *ack, uint8_t *buf, size_t buf_len) +{ + if ((!ack) || (!buf) || (buf_len < 3U) || (!cfdp_ack_directive_valid(ack->ack_directive_code))) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_ACK; + buf[1] = (uint8_t)((((uint8_t)ack->ack_directive_code & 0xFU) << 4) | + (cfdp_ack_directive_subtype(ack->ack_directive_code) & 0xFU)); + buf[2] = (uint8_t)((((uint8_t)ack->condition_code & 0xFU) << 4) | + ((uint8_t)ack->transaction_status & 0x3U)); + + return 3; +} + +size_t cfdp_ack_deserialize(const uint8_t *buf, size_t buf_len, cfdp_ack_pdu_t *ack) +{ + if ((!buf) || (!ack) || (buf_len < 3U) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_ACK)) + { + return 0; + } + + cfdp_directive_code_t acknowledged = (cfdp_directive_code_t)((buf[1] >> 4) & 0xFU); + + /* Table 5-8 admits only EOF and Finished as acknowledged directives, and + * fixes the subtype for each, so anything else is not a valid ACK. */ + if ((!cfdp_ack_directive_valid(acknowledged)) || + ((buf[1] & 0xFU) != cfdp_ack_directive_subtype(acknowledged))) + { + return 0; + } + + ack->ack_directive_code = acknowledged; + ack->condition_code = (cfdp_condition_code_t)((buf[2] >> 4) & 0xFU); + ack->transaction_status = (cfdp_transaction_status_t)(buf[2] & 0x3U); + + return 3; +} + +size_t cfdp_metadata_serialize(const cfdp_metadata_pdu_t *md, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len) +{ + if ((!md) || (!buf) || ((!md->options) && (md->options_len > 0))) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t need = (size_t)fs + (size_t)md->source_filename_len + + (size_t)md->destination_filename_len + (size_t)md->options_len + 4U; + if (buf_len < need) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_METADATA; + buf[1] = + (uint8_t)(((md->closure_requested ? 1U : 0U) << 6) | ((uint8_t)md->checksum_type & 0xFU)); + size_t pos = 2; + cfdp_write_uint(&buf[pos], md->file_size, fs); + pos += fs; + + size_t n = + cfdp_lv_serialize(md->source_filename, md->source_filename_len, &buf[pos], buf_len - pos); + if (n == 0) + { + return 0; + } + pos += n; + + n = cfdp_lv_serialize(md->destination_filename, + md->destination_filename_len, + &buf[pos], + buf_len - pos); + if (n == 0) + { + return 0; + } + pos += n; + + if (md->options_len > 0) + { + memcpy(&buf[pos], md->options, md->options_len); + pos += md->options_len; + } + + return pos; +} + +size_t cfdp_metadata_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_metadata_pdu_t *md) +{ + if ((!buf) || (!md)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + if ((buf_len < (size_t)fs + 2U) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_METADATA)) + { + return 0; + } + + md->closure_requested = (((buf[1] >> 6) & 0x1U) != 0); + md->checksum_type = (cfdp_checksum_type_t)(buf[1] & 0xFU); + size_t pos = 2; + md->file_size = cfdp_read_uint(&buf[pos], fs); + pos += fs; + + size_t n = cfdp_lv_deserialize(&buf[pos], + buf_len - pos, + &md->source_filename, + &md->source_filename_len); + if (n == 0) + { + return 0; + } + pos += n; + + n = cfdp_lv_deserialize(&buf[pos], + buf_len - pos, + &md->destination_filename, + &md->destination_filename_len); + if (n == 0) + { + return 0; + } + pos += n; + + /* Whatever follows the file names is the option TLV chain (§5.2.5). */ + md->options = (buf_len > pos) ? &buf[pos] : NULL; + md->options_len = (uint16_t)(buf_len - pos); + + return buf_len; +} + +size_t cfdp_nak_serialize(const cfdp_nak_pdu_t *nak, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len) +{ + if ((!nak) || (!buf) || (nak->segment_request_count > CFDP_NAK_MAX_SEGMENT_REQUESTS)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t pair = 2U * (size_t)fs; + size_t size = 1U + pair + nak->segment_request_count * pair; + if (buf_len < size) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + size_t pos = 1; + cfdp_write_uint(&buf[pos], nak->start_of_scope, fs); + pos += fs; + cfdp_write_uint(&buf[pos], nak->end_of_scope, fs); + pos += fs; + + for (size_t i = 0; i < nak->segment_request_count; i++) + { + cfdp_write_uint(&buf[pos], nak->segment_requests[i].start_offset, fs); + pos += fs; + cfdp_write_uint(&buf[pos], nak->segment_requests[i].end_offset, fs); + pos += fs; + } + + return pos; +} + +/** + * @brief Decode the segment request array filling a NAK PDU's data field. + * + * @param[in] buf Data field, positioned at the first segment request. + * @param[in] buf_len Octets remaining in the data field. + * @param[in] fs Octets per file-size-sensitive offset field. + * @param[out] nak NAK contents receiving the decoded requests. + * @return Bytes consumed, or 0 if the requests do not fill @p buf_len exactly + * or there are more than ::CFDP_NAK_MAX_SEGMENT_REQUESTS of them. + */ +static size_t cfdp_nak_parse_segment_requests(const uint8_t *buf, + size_t buf_len, + uint8_t fs, + cfdp_nak_pdu_t *nak) +{ + size_t pair = 2U * (size_t)fs; + + /* §5.2.6: the data field is a whole number of segment requests. A ragged + * tail means the PDU is malformed, not that it ends early. Dropping + * requests that do not fit would leave the sender believing it had + * satisfied the NAK, so the missing ranges would never be retransmitted. */ + size_t count = buf_len / pair; + if (((buf_len % pair) != 0) || (count > CFDP_NAK_MAX_SEGMENT_REQUESTS)) + { + return 0; + } + + size_t pos = 0; + nak->segment_request_count = 0; + for (size_t i = 0; i < count; i++) + { + nak->segment_requests[i].start_offset = cfdp_read_uint(&buf[pos], fs); + pos += fs; + nak->segment_requests[i].end_offset = cfdp_read_uint(&buf[pos], fs); + pos += fs; + nak->segment_request_count++; + } + + return pos; +} + +size_t cfdp_nak_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_nak_pdu_t *nak) +{ + if ((!buf) || (!nak)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t pair = 2U * (size_t)fs; + if ((buf_len < 1U + pair) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_NAK)) + { + return 0; + } + + size_t pos = 1; + nak->start_of_scope = cfdp_read_uint(&buf[pos], fs); + pos += fs; + nak->end_of_scope = cfdp_read_uint(&buf[pos], fs); + pos += fs; + + size_t n = cfdp_nak_parse_segment_requests(&buf[pos], buf_len - pos, fs, nak); + if ((n == 0) && (buf_len != pos)) + { + return 0; + } + + return pos + n; +} + +size_t cfdp_prompt_serialize(cfdp_prompt_response_t response, uint8_t *buf, size_t buf_len) +{ + if ((!buf) || (buf_len < 2U)) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_PROMPT; + buf[1] = (uint8_t)(((uint8_t)response & 0x1U) << 7); + + return 2; +} + +size_t cfdp_prompt_deserialize(const uint8_t *buf, size_t buf_len, cfdp_prompt_response_t *response) +{ + if ((!buf) || (!response) || (buf_len < 2U) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_PROMPT)) + { + return 0; + } + + *response = (cfdp_prompt_response_t)((buf[1] >> 7) & 0x1U); + + return 2; +} + +size_t cfdp_keep_alive_serialize(uint64_t progress, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len) +{ + if (!buf) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t size = (size_t)fs + 1U; + if (buf_len < size) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_KEEP_ALIVE; + cfdp_write_uint(&buf[1], progress, fs); + + return size; +} + +size_t cfdp_keep_alive_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + uint64_t *progress) +{ + if ((!buf) || (!progress)) + { + return 0; + } + + uint8_t fs = cfdp_file_size_octets(large_file_flag); + size_t size = (size_t)fs + 1U; + if ((buf_len < size) || (buf[0] != (uint8_t)CFDP_DIRECTIVE_KEEP_ALIVE)) + { + return 0; + } + + *progress = cfdp_read_uint(&buf[1], fs); + + return size; +} diff --git a/src/cfdp_pdu.c b/src/cfdp_pdu.c new file mode 100644 index 0000000..735987c --- /dev/null +++ b/src/cfdp_pdu.c @@ -0,0 +1,332 @@ +/** + * @file cfdp_pdu.c + * @brief CFDP PDU fixed header and File Data PDU codec + * + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.1 + * (fixed PDU header) and Section 5.3 (File Data PDU). + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_pdu.h" + +#include "cfdp_endian.h" + +#include + +/** + * @brief Whether an identifier length is within the CFDP 1..8 octet range. + * + * @param[in] len Identifier length in octets. + * @return true if @p len is a legal entity ID / sequence number length. + */ +static bool cfdp_id_len_valid(uint8_t len) +{ + return (len >= CFDP_ID_LEN_MIN) && (len <= CFDP_ID_LEN_MAX); +} + +size_t cfdp_pdu_header_size(const cfdp_pdu_header_t *hdr) +{ + if (!hdr) + { + return 0; + } + if ((!cfdp_id_len_valid(hdr->entity_id_length)) || + (!cfdp_id_len_valid(hdr->transaction_seq_length))) + { + return 0; + } + return CFDP_PDU_HEADER_FIXED_LEN + (size_t)(2U * hdr->entity_id_length) + + hdr->transaction_seq_length; +} + +/** + * @brief Pack the first octet of the fixed header (flags nibble/bit fields). + * + * @param[in] hdr Header supplying the flag fields. + * @return The encoded octet 0. + */ +static uint8_t cfdp_pack_octet0(const cfdp_pdu_header_t *hdr) +{ + return (uint8_t)(((hdr->version & 0x7U) << 5) | (((uint8_t)hdr->pdu_type & 0x1U) << 4) | + (((uint8_t)hdr->direction & 0x1U) << 3) | + (((uint8_t)hdr->transmission_mode & 0x1U) << 2) | + (((uint8_t)hdr->crc_flag & 0x1U) << 1) | + ((uint8_t)hdr->large_file_flag & 0x1U)); +} + +/** + * @brief Pack the fourth octet of the fixed header (identifier lengths). + * + * @param[in] hdr Header supplying the segmentation flags and ID lengths. + * @return The encoded octet 3. + */ +static uint8_t cfdp_pack_octet3(const cfdp_pdu_header_t *hdr) +{ + return (uint8_t)((((uint8_t)hdr->segmentation_control & 0x1U) << 7) | + (((hdr->entity_id_length - 1U) & 0x7U) << 4) | + (((uint8_t)hdr->segment_metadata_flag & 0x1U) << 3) | + ((hdr->transaction_seq_length - 1U) & 0x7U)); +} + +size_t cfdp_pdu_header_serialize(const cfdp_pdu_header_t *hdr, uint8_t *buf, size_t buf_len) +{ + if ((!hdr) || (!buf) || (hdr->version != CFDP_PROTOCOL_VERSION)) + { + return 0; + } + + size_t size = cfdp_pdu_header_size(hdr); + if ((size == 0) || (buf_len < size)) + { + return 0; + } + + buf[0] = cfdp_pack_octet0(hdr); + cfdp_write_uint(&buf[1], hdr->data_field_length, 2); + buf[3] = cfdp_pack_octet3(hdr); + + size_t pos = CFDP_PDU_HEADER_FIXED_LEN; + cfdp_write_uint(&buf[pos], hdr->source_entity_id, hdr->entity_id_length); + pos += hdr->entity_id_length; + cfdp_write_uint(&buf[pos], hdr->transaction_seq_number, hdr->transaction_seq_length); + pos += hdr->transaction_seq_length; + cfdp_write_uint(&buf[pos], hdr->destination_entity_id, hdr->entity_id_length); + + return size; +} + +/** + * @brief Decode the two bitpacked octets of the fixed header into @p hdr. + * + * @param[in] buf Input buffer positioned at octet 0 (at least 4 octets). + * @param[out] hdr Header receiving the decoded flag and length fields. + */ +static void cfdp_unpack_flags(const uint8_t *buf, cfdp_pdu_header_t *hdr) +{ + uint8_t o0 = buf[0]; + uint8_t o3 = buf[3]; + + hdr->version = (uint8_t)((o0 >> 5) & 0x7U); + hdr->pdu_type = (cfdp_pdu_type_t)((o0 >> 4) & 0x1U); + hdr->direction = (cfdp_direction_t)((o0 >> 3) & 0x1U); + hdr->transmission_mode = (cfdp_transmission_mode_t)((o0 >> 2) & 0x1U); + hdr->crc_flag = (cfdp_crc_flag_t)((o0 >> 1) & 0x1U); + hdr->large_file_flag = (cfdp_large_file_flag_t)(o0 & 0x1U); + + hdr->data_field_length = (uint16_t)cfdp_read_uint(&buf[1], 2); + + hdr->segmentation_control = (cfdp_seg_ctrl_t)((o3 >> 7) & 0x1U); + hdr->entity_id_length = (uint8_t)(((o3 >> 4) & 0x7U) + 1U); + hdr->segment_metadata_flag = (cfdp_seg_metadata_flag_t)((o3 >> 3) & 0x1U); + hdr->transaction_seq_length = (uint8_t)((o3 & 0x7U) + 1U); +} + +size_t cfdp_pdu_header_deserialize(const uint8_t *buf, size_t buf_len, cfdp_pdu_header_t *hdr) +{ + if ((!buf) || (!hdr) || (buf_len < CFDP_PDU_HEADER_FIXED_LEN)) + { + return 0; + } + + cfdp_unpack_flags(buf, hdr); + + /* §5.1.2: only version '001' is defined. Other versions may lay out the + * header or data field differently, so decoding one here would misread it. */ + if (hdr->version != CFDP_PROTOCOL_VERSION) + { + return 0; + } + + /* cfdp_unpack_flags always yields identifier lengths of 1..8 octets, so the + * size == 0 arm is unreachable here; it guards against future decoding + * changes, and keeps the line's branches out of the coverage report. */ + size_t size = cfdp_pdu_header_size(hdr); + if ((size == 0) || (buf_len < size)) /* GCOVR_EXCL_BR_LINE */ + { + return 0; + } + + size_t pos = CFDP_PDU_HEADER_FIXED_LEN; + hdr->source_entity_id = cfdp_read_uint(&buf[pos], hdr->entity_id_length); + pos += hdr->entity_id_length; + hdr->transaction_seq_number = cfdp_read_uint(&buf[pos], hdr->transaction_seq_length); + pos += hdr->transaction_seq_length; + hdr->destination_entity_id = cfdp_read_uint(&buf[pos], hdr->entity_id_length); + + return size; +} + +size_t cfdp_pdu_payload_size(const cfdp_pdu_header_t *hdr) +{ + if (!hdr) + { + return 0; + } + if (hdr->crc_flag != CFDP_CRC_PRESENT) + { + return hdr->data_field_length; + } + if (hdr->data_field_length < CFDP_PDU_CRC_LEN) + { + return 0; + } + + return (size_t)hdr->data_field_length - CFDP_PDU_CRC_LEN; +} + +/** + * @brief Whether a File Data payload agrees with the Segment Metadata flag. + * + * A payload carrying segment metadata under an absent flag (or the reverse) + * would put the offset at a different place than the header announces, which + * the peer decodes as file data at a wild offset rather than as an error. + * + * @param[in] fd Payload to check. + * @param[in] segment_metadata_flag Segment Metadata flag of the PDU header. + * @return true when @p fd may be encoded under @p segment_metadata_flag. + */ +static bool cfdp_segment_metadata_consistent(const cfdp_file_data_pdu_t *fd, + cfdp_seg_metadata_flag_t segment_metadata_flag) +{ + if (segment_metadata_flag != CFDP_SEG_METADATA_PRESENT) + { + return (fd->segment_metadata_len == 0) && (!fd->segment_metadata); + } + + return (fd->segment_metadata_len <= CFDP_SEGMENT_METADATA_MAX_LEN) && + ((fd->segment_metadata) || (fd->segment_metadata_len == 0)); +} + +/** + * @brief Write the record continuation state, metadata length and metadata. + * + * @param[in] fd Payload supplying the segment metadata fields. + * @param[out] buf Output buffer positioned at the first data field octet. + * @return Bytes written. + */ +static size_t cfdp_segment_metadata_write(const cfdp_file_data_pdu_t *fd, uint8_t *buf) +{ + buf[0] = (uint8_t)((((uint8_t)fd->record_continuation & 0x3U) << 6) | + (fd->segment_metadata_len & 0x3FU)); + + if (fd->segment_metadata_len > 0) + { + memcpy(&buf[1], fd->segment_metadata, fd->segment_metadata_len); + } + + return (size_t)fd->segment_metadata_len + 1U; +} + +size_t cfdp_file_data_serialize(const cfdp_file_data_pdu_t *fd, + cfdp_large_file_flag_t large_file_flag, + cfdp_seg_metadata_flag_t segment_metadata_flag, + uint8_t *buf, + size_t buf_len) +{ + if ((!fd) || (!buf) || ((!fd->file_data) && (fd->file_data_len > 0)) || + (!cfdp_segment_metadata_consistent(fd, segment_metadata_flag))) + { + return 0; + } + + bool with_metadata = (segment_metadata_flag == CFDP_SEG_METADATA_PRESENT); + size_t metadata_size = with_metadata ? ((size_t)fd->segment_metadata_len + 1U) : 0U; + uint8_t offset_octets = cfdp_file_size_octets(large_file_flag); + size_t size = metadata_size + (size_t)offset_octets + fd->file_data_len; + if (buf_len < size) + { + return 0; + } + + size_t pos = with_metadata ? cfdp_segment_metadata_write(fd, buf) : 0U; + cfdp_write_uint(&buf[pos], fd->offset, offset_octets); + pos += offset_octets; + + if (fd->file_data_len > 0) + { + memcpy(&buf[pos], fd->file_data, fd->file_data_len); + pos += fd->file_data_len; + } + + return pos; +} + +/** + * @brief Decode the segment metadata preceding the offset, when present. + * + * @param[in] buf Data field, positioned at its first octet. + * @param[in] buf_len Payload length in octets. + * @param[out] fd Payload receiving the segment metadata fields. + * @return Bytes consumed, or 0 if the data field is too short. + */ +static size_t cfdp_segment_metadata_read(const uint8_t *buf, + size_t buf_len, + cfdp_file_data_pdu_t *fd) +{ + if (buf_len < 1U) + { + return 0; + } + + fd->record_continuation = (cfdp_record_continuation_t)((buf[0] >> 6) & 0x3U); + fd->segment_metadata_len = (uint8_t)(buf[0] & 0x3FU); + if (buf_len < (size_t)fd->segment_metadata_len + 1U) + { + return 0; + } + + fd->segment_metadata = (fd->segment_metadata_len > 0) ? &buf[1] : NULL; + + return (size_t)fd->segment_metadata_len + 1U; +} + +size_t cfdp_file_data_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_seg_metadata_flag_t segment_metadata_flag, + cfdp_file_data_pdu_t *fd) +{ + if ((!buf) || (!fd)) + { + return 0; + } + + fd->record_continuation = CFDP_RECORD_CONT_NEITHER; + fd->segment_metadata = NULL; + fd->segment_metadata_len = 0; + + size_t pos = 0; + if (segment_metadata_flag == CFDP_SEG_METADATA_PRESENT) + { + pos = cfdp_segment_metadata_read(buf, buf_len, fd); + if (pos == 0) + { + return 0; + } + } + + uint8_t offset_octets = cfdp_file_size_octets(large_file_flag); + if (buf_len < pos + offset_octets) + { + return 0; + } + + fd->offset = cfdp_read_uint(&buf[pos], offset_octets); + pos += offset_octets; + fd->file_data = (buf_len > pos) ? &buf[pos] : NULL; + fd->file_data_len = buf_len - pos; + + return buf_len; +} + +bool cfdp_pdu_directive_code(const uint8_t *buf, size_t buf_len, cfdp_directive_code_t *code) +{ + if ((!buf) || (!code) || (buf_len == 0)) + { + return false; + } + *code = (cfdp_directive_code_t)buf[0]; + return true; +} diff --git a/src/cfdp_tlv.c b/src/cfdp_tlv.c new file mode 100644 index 0000000..b40edcc --- /dev/null +++ b/src/cfdp_tlv.c @@ -0,0 +1,480 @@ +/** + * @file cfdp_tlv.c + * @brief CFDP LV and TLV parameter codecs + * + * Implements CCSDS 727.0-B-5 (CCSDS File Delivery Protocol), Section 5.1.8 + * (LV objects), Section 5.1.9 (TLV objects) and Section 5.4 (TLV parameters). + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_tlv.h" + +#include "cfdp_endian.h" + +#include + +bool cfdp_filestore_action_has_second_filename(cfdp_filestore_action_t action_code) +{ + return (action_code == CFDP_FS_ACTION_RENAME_FILE) || + (action_code == CFDP_FS_ACTION_APPEND_FILE) || + (action_code == CFDP_FS_ACTION_REPLACE_FILE); +} + +/** + * @brief Whether a filestore action code is defined by table 5-16. + * + * Only '0000'-'1000' are defined; the 4-bit field can carry '1001'-'1111', + * which a receiving filestore could not act on. + * + * @param[in] action_code Filestore action code to check. + * @return true for Create File through Deny Directory. + */ +static bool cfdp_filestore_action_valid(cfdp_filestore_action_t action_code) +{ + /* The unsigned cast also rejects negative values forced into the enum. */ + return (uint32_t)action_code <= (uint32_t)CFDP_FS_ACTION_DENY_DIRECTORY; +} + +/** + * @brief Action code carried in the first value octet of a filestore TLV. + * + * @param[in] tlv Decoded filestore TLV with a value at least one octet long. + * @return The 4-bit action code. + */ +static cfdp_filestore_action_t cfdp_filestore_tlv_action(const cfdp_tlv_t *tlv) +{ + return (cfdp_filestore_action_t)((tlv->value[0] >> 4) & 0xFU); +} + +size_t cfdp_lv_serialize(const char *value, uint8_t value_len, uint8_t *buf, size_t buf_len) +{ + if ((!buf) || ((!value) && (value_len > 0)) || + (buf_len < (size_t)value_len + CFDP_LV_HEADER_LEN)) + { + return 0; + } + + buf[0] = value_len; + if (value_len > 0) + { + memcpy(&buf[CFDP_LV_HEADER_LEN], value, value_len); + } + + return (size_t)value_len + CFDP_LV_HEADER_LEN; +} + +size_t cfdp_lv_deserialize(const uint8_t *buf, + size_t buf_len, + const char **value, + uint8_t *value_len) +{ + if ((!buf) || (!value) || (!value_len) || (buf_len < CFDP_LV_HEADER_LEN)) + { + return 0; + } + + uint8_t len = buf[0]; + if (buf_len < (size_t)len + CFDP_LV_HEADER_LEN) + { + return 0; + } + + *value = (len > 0) ? (const char *)&buf[CFDP_LV_HEADER_LEN] : NULL; + *value_len = len; + + return (size_t)len + CFDP_LV_HEADER_LEN; +} + +size_t cfdp_tlv_serialize(const cfdp_tlv_t *tlv, uint8_t *buf, size_t buf_len) +{ + if ((!tlv) || (!buf) || ((!tlv->value) && (tlv->length > 0)) || + (buf_len < (size_t)tlv->length + CFDP_TLV_HEADER_LEN)) + { + return 0; + } + + buf[0] = tlv->type; + buf[1] = tlv->length; + if (tlv->length > 0) + { + memcpy(&buf[CFDP_TLV_HEADER_LEN], tlv->value, tlv->length); + } + + return (size_t)tlv->length + CFDP_TLV_HEADER_LEN; +} + +size_t cfdp_tlv_deserialize(const uint8_t *buf, size_t buf_len, cfdp_tlv_t *tlv) +{ + if ((!buf) || (!tlv) || (buf_len < CFDP_TLV_HEADER_LEN)) + { + return 0; + } + + uint8_t length = buf[1]; + if (buf_len < (size_t)length + CFDP_TLV_HEADER_LEN) + { + return 0; + } + + tlv->type = buf[0]; + tlv->length = length; + tlv->value = (length > 0) ? &buf[CFDP_TLV_HEADER_LEN] : NULL; + + return (size_t)length + CFDP_TLV_HEADER_LEN; +} + +size_t cfdp_entity_id_tlv_serialize(uint64_t entity_id, + uint8_t id_len, + uint8_t *buf, + size_t buf_len) +{ + if ((!buf) || (id_len < CFDP_ID_LEN_MIN) || (id_len > CFDP_ID_LEN_MAX) || + (buf_len < (size_t)id_len + CFDP_TLV_HEADER_LEN)) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_TLV_ENTITY_ID; + buf[1] = id_len; + cfdp_write_uint(&buf[CFDP_TLV_HEADER_LEN], entity_id, id_len); + + return (size_t)id_len + CFDP_TLV_HEADER_LEN; +} + +size_t cfdp_entity_id_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + uint64_t *entity_id, + uint8_t *id_len) +{ + cfdp_tlv_t tlv; + if ((!entity_id) || (!id_len) || (cfdp_tlv_deserialize(buf, buf_len, &tlv) == 0) || + (tlv.type != (uint8_t)CFDP_TLV_ENTITY_ID) || (tlv.length < CFDP_ID_LEN_MIN) || + (tlv.length > CFDP_ID_LEN_MAX)) + { + return 0; + } + + *entity_id = cfdp_read_uint(tlv.value, tlv.length); + *id_len = tlv.length; + + return (size_t)tlv.length + CFDP_TLV_HEADER_LEN; +} + +/** + * @brief Whether a Fault Handler Override may name this condition (table 5-19). + * + * Only faults can be overridden: 'No error', 'Suspend.request received' and + * 'Cancel.request received' are not faults, and '1100'-'1101' are reserved in + * table 5-5. That leaves exactly the contiguous range '0001'-'1011'. + * + * @param[in] condition_code Condition code to check. + * @return true for a fault condition code. + */ +static bool cfdp_fault_condition_valid(cfdp_condition_code_t condition_code) +{ + return (condition_code >= CFDP_COND_POSITIVE_ACK_LIMIT_REACHED) && + (condition_code <= CFDP_COND_UNSUPPORTED_CHECKSUM_TYPE); +} + +/** + * @brief Whether a handler code is defined by table 5-19. + * + * '0000' is reserved for future expansion and '0101'-'1111' are reserved. + * + * @param[in] handler_code Handler code to check. + * @return true for one of the four defined fault handlers. + */ +static bool cfdp_fault_handler_valid(cfdp_fault_handler_code_t handler_code) +{ + return (handler_code >= CFDP_HANDLER_NOTICE_OF_CANCELLATION) && + (handler_code <= CFDP_HANDLER_ABANDON_TRANSACTION); +} + +size_t cfdp_fault_handler_tlv_serialize(cfdp_condition_code_t condition_code, + cfdp_fault_handler_code_t handler_code, + uint8_t *buf, + size_t buf_len) +{ + if ((!buf) || (buf_len < CFDP_TLV_HEADER_LEN + 1U) || + (!cfdp_fault_condition_valid(condition_code)) || (!cfdp_fault_handler_valid(handler_code))) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_TLV_FAULT_HANDLER_OVERRIDE; + buf[1] = 1U; + buf[2] = (uint8_t)((((uint8_t)condition_code & 0xFU) << 4) | ((uint8_t)handler_code & 0xFU)); + + return CFDP_TLV_HEADER_LEN + 1U; +} + +size_t cfdp_fault_handler_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_condition_code_t *condition_code, + cfdp_fault_handler_code_t *handler_code) +{ + cfdp_tlv_t tlv; + if ((!condition_code) || (!handler_code) || (cfdp_tlv_deserialize(buf, buf_len, &tlv) == 0) || + (tlv.type != (uint8_t)CFDP_TLV_FAULT_HANDLER_OVERRIDE) || (tlv.length != 1U)) + { + return 0; + } + + cfdp_condition_code_t condition = (cfdp_condition_code_t)((tlv.value[0] >> 4) & 0xFU); + cfdp_fault_handler_code_t handler = (cfdp_fault_handler_code_t)(tlv.value[0] & 0xFU); + if ((!cfdp_fault_condition_valid(condition)) || (!cfdp_fault_handler_valid(handler))) + { + return 0; + } + + *condition_code = condition; + *handler_code = handler; + + return CFDP_TLV_HEADER_LEN + 1U; +} + +/** + * @brief Write the one or two file name LVs shared by the filestore TLVs. + * + * @param[in] first First file name; may be NULL only when empty. + * @param[in] first_len First file name length in octets. + * @param[in] second Second file name; read only when @p has_second. + * @param[in] second_len Second file name length in octets. + * @param[in] has_second Whether the action code carries a second file name. + * @param[out] buf Output buffer positioned at the first name's LV. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +static size_t cfdp_filestore_names_serialize(const char *first, + uint8_t first_len, + const char *second, + uint8_t second_len, + bool has_second, + uint8_t *buf, + size_t buf_len) +{ + size_t pos = cfdp_lv_serialize(first, first_len, buf, buf_len); + if (pos == 0) + { + return 0; + } + + if (has_second) + { + size_t n = cfdp_lv_serialize(second, second_len, &buf[pos], buf_len - pos); + if (n == 0) + { + return 0; + } + pos += n; + } + + return pos; +} + +/** + * @brief Read the one or two file name LVs shared by the filestore TLVs. + * + * @param[in] buf Input buffer positioned at the first name's LV. + * @param[in] buf_len Octets available in @p buf. + * @param[in] has_second Whether the action code carries a second file name. + * @param[out] first Set to the first file name, which indexes into @p buf. + * @param[out] first_len Set to the first file name length. + * @param[out] second Set to the second file name when @p has_second. + * @param[out] second_len Set to the second file name length when @p has_second. + * @return Bytes consumed, or 0 on error. + */ +static size_t cfdp_filestore_names_deserialize(const uint8_t *buf, + size_t buf_len, + bool has_second, + const char **first, + uint8_t *first_len, + const char **second, + uint8_t *second_len) +{ + size_t pos = cfdp_lv_deserialize(buf, buf_len, first, first_len); + if (pos == 0) + { + return 0; + } + + if (has_second) + { + size_t n = cfdp_lv_deserialize(&buf[pos], buf_len - pos, second, second_len); + if (n == 0) + { + return 0; + } + pos += n; + } + + return pos; +} + +/** + * @brief Finish a filestore TLV by writing its type and value length fields. + * + * @param[out] buf Buffer holding the already-written value. + * @param[in] type TLV type code. + * @param[in] end Offset one past the last value octet. + * @return @p end, or 0 when the value exceeds the 255-octet TLV length field. + */ +static size_t cfdp_filestore_tlv_finish(uint8_t *buf, cfdp_tlv_type_t type, size_t end) +{ + size_t value_len = end - CFDP_TLV_HEADER_LEN; + if (value_len > UINT8_MAX) + { + return 0; + } + + buf[0] = (uint8_t)type; + buf[1] = (uint8_t)value_len; + + return end; +} + +size_t cfdp_filestore_request_tlv_serialize(const cfdp_filestore_request_t *req, + uint8_t *buf, + size_t buf_len) +{ + if ((!req) || (!buf) || (buf_len < CFDP_TLV_HEADER_LEN + 1U) || + (!cfdp_filestore_action_valid(req->action_code))) + { + return 0; + } + + size_t pos = CFDP_TLV_HEADER_LEN; + buf[pos] = (uint8_t)(((uint8_t)req->action_code & 0xFU) << 4); + pos += 1U; + + size_t n = + cfdp_filestore_names_serialize(req->first_filename, + req->first_filename_len, + req->second_filename, + req->second_filename_len, + cfdp_filestore_action_has_second_filename(req->action_code), + &buf[pos], + buf_len - pos); + if (n == 0) + { + return 0; + } + + return cfdp_filestore_tlv_finish(buf, CFDP_TLV_FILESTORE_REQUEST, pos + n); +} + +size_t cfdp_filestore_request_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_filestore_request_t *req) +{ + cfdp_tlv_t tlv; + if ((!req) || (cfdp_tlv_deserialize(buf, buf_len, &tlv) == 0) || + (tlv.type != (uint8_t)CFDP_TLV_FILESTORE_REQUEST) || (tlv.length < 1U) || + (!cfdp_filestore_action_valid(cfdp_filestore_tlv_action(&tlv)))) + { + return 0; + } + + memset(req, 0, sizeof(*req)); + req->action_code = cfdp_filestore_tlv_action(&tlv); + + size_t n = cfdp_filestore_names_deserialize( + &tlv.value[1], + (size_t)tlv.length - 1U, + cfdp_filestore_action_has_second_filename(req->action_code), + &req->first_filename, + &req->first_filename_len, + &req->second_filename, + &req->second_filename_len); + + /* The names must account for the whole TLV value: no trailing octets. */ + if ((n == 0) || (n + 1U != (size_t)tlv.length)) + { + return 0; + } + + return (size_t)tlv.length + CFDP_TLV_HEADER_LEN; +} + +size_t cfdp_filestore_response_tlv_serialize(const cfdp_filestore_response_t *resp, + uint8_t *buf, + size_t buf_len) +{ + if ((!resp) || (!buf) || (buf_len < CFDP_TLV_HEADER_LEN + 1U) || + (!cfdp_filestore_action_valid(resp->action_code))) + { + return 0; + } + + size_t pos = CFDP_TLV_HEADER_LEN; + buf[pos] = + (uint8_t)((((uint8_t)resp->action_code & 0xFU) << 4) | ((uint8_t)resp->status_code & 0xFU)); + pos += 1U; + + size_t n = + cfdp_filestore_names_serialize(resp->first_filename, + resp->first_filename_len, + resp->second_filename, + resp->second_filename_len, + cfdp_filestore_action_has_second_filename(resp->action_code), + &buf[pos], + buf_len - pos); + if (n == 0) + { + return 0; + } + pos += n; + + n = cfdp_lv_serialize(resp->message, resp->message_len, &buf[pos], buf_len - pos); + if (n == 0) + { + return 0; + } + + return cfdp_filestore_tlv_finish(buf, CFDP_TLV_FILESTORE_RESPONSE, pos + n); +} + +size_t cfdp_filestore_response_tlv_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_filestore_response_t *resp) +{ + cfdp_tlv_t tlv; + if ((!resp) || (cfdp_tlv_deserialize(buf, buf_len, &tlv) == 0) || + (tlv.type != (uint8_t)CFDP_TLV_FILESTORE_RESPONSE) || (tlv.length < 1U) || + (!cfdp_filestore_action_valid(cfdp_filestore_tlv_action(&tlv)))) + { + return 0; + } + + memset(resp, 0, sizeof(*resp)); + resp->action_code = cfdp_filestore_tlv_action(&tlv); + resp->status_code = (cfdp_filestore_status_t)(tlv.value[0] & 0xFU); + + size_t pos = cfdp_filestore_names_deserialize( + &tlv.value[1], + (size_t)tlv.length - 1U, + cfdp_filestore_action_has_second_filename(resp->action_code), + &resp->first_filename, + &resp->first_filename_len, + &resp->second_filename, + &resp->second_filename_len); + if (pos == 0) + { + return 0; + } + + size_t n = cfdp_lv_deserialize(&tlv.value[1U + pos], + (size_t)tlv.length - 1U - pos, + &resp->message, + &resp->message_len); + + /* Names plus message must account for the whole TLV value. */ + if ((n == 0) || (pos + n + 1U != (size_t)tlv.length)) + { + return 0; + } + + return (size_t)tlv.length + CFDP_TLV_HEADER_LEN; +} diff --git a/tests/test_cfdp_checksum.c b/tests/test_cfdp_checksum.c new file mode 100644 index 0000000..e2eb2fc --- /dev/null +++ b/tests/test_cfdp_checksum.c @@ -0,0 +1,63 @@ +/** + * @file test_cfdp_checksum.c + * @brief Unit tests for the CFDP 32-bit modular file checksum + * + * Exercises src/cfdp_checksum.c against CCSDS 727.0-B-5 §4.2.2 with + * known-vector and streamed-accumulation checks. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp.h" +#include "cunit.h" +#include "test_runners.h" + +static int test_checksum_known(void) +{ + const uint8_t a[] = {0x01, 0x02, 0x03, 0x04}; + ASSERT_TRUE(cfdp_checksum_compute(a, sizeof(a)) == 0x01020304U); + + const uint8_t b[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + ASSERT_TRUE(cfdp_checksum_compute(b, sizeof(b)) == 0x06020304U); + + /* Segment-by-segment accumulation must match a single-shot computation. */ + uint32_t streamed = cfdp_checksum_update(0, 0, a, 2); + streamed = cfdp_checksum_update(streamed, 2, &a[2], 2); + ASSERT_TRUE(streamed == cfdp_checksum_compute(a, sizeof(a))); + return 0; +} + +static int test_checksum_no_data(void) +{ + const uint8_t data[] = {0xFF}; + + /* Both guard conditions must leave the running checksum untouched. */ + ASSERT_TRUE(cfdp_checksum_update(0x11223344U, 0, NULL, 4) == 0x11223344U); + ASSERT_TRUE(cfdp_checksum_update(0x11223344U, 0, data, 0) == 0x11223344U); + ASSERT_TRUE(cfdp_checksum_compute(NULL, 8) == 0); + return 0; +} + +static int test_checksum_offset_lanes(void) +{ + const uint8_t data[] = {0x01, 0x02}; + + /* An octet's lane follows its absolute file offset, not its index in the + * segment, so the same two octets weigh differently at offset 1 and 4. */ + ASSERT_TRUE(cfdp_checksum_update(0, 1, data, sizeof(data)) == 0x00010200U); + ASSERT_TRUE(cfdp_checksum_update(0, 4, data, sizeof(data)) == 0x01020000U); + return 0; +} + +test_result_t test_cfdp_checksum_run_all(void) +{ + RUN_TEST(test_checksum_known); + RUN_TEST(test_checksum_no_data); + RUN_TEST(test_checksum_offset_lanes); + + /* cunit.h keeps its tally in file-local statics, so these counters cover + * only the tests run above. */ + test_result_t result = {cunit_total_tests - cunit_overall_failures, cunit_total_tests}; + return result; +} diff --git a/tests/test_cfdp_directive.c b/tests/test_cfdp_directive.c new file mode 100644 index 0000000..15692b9 --- /dev/null +++ b/tests/test_cfdp_directive.c @@ -0,0 +1,863 @@ +/** + * @file test_cfdp_directive.c + * @brief Unit tests for the File Directive PDU codecs + * + * Exercises src/cfdp_directive.c against CCSDS 727.0-B-5 Section 5.2 and + * Section 5.4 with round-trip and known-vector checks. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp.h" +#include "cunit.h" +#include "test_runners.h" + +#include + +static int test_eof_roundtrip(void) +{ + const uint8_t expected[] = {0x04, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x10}; + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_NO_ERROR; + eof.file_checksum = 0x01020304U; + eof.file_size = 16; + + uint8_t buf[16]; + size_t n = cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_EQ_INT(sizeof(expected), n); + ASSERT_EQ_MEM(expected, buf, sizeof(expected)); + + cfdp_eof_pdu_t out = {0}; + size_t m = cfdp_eof_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out); + ASSERT_EQ_INT(n, m); + ASSERT_EQ_INT(CFDP_COND_NO_ERROR, out.condition_code); + ASSERT_TRUE(out.file_checksum == 0x01020304U); + ASSERT_TRUE(out.file_size == 16); + return 0; +} + +static int test_finished_roundtrip(void) +{ + cfdp_finished_pdu_t fin = {0}; + /* 'Unsupported checksum type' is the one fault condition that carries no + * Fault Location (§5.2.3). */ + fin.condition_code = CFDP_COND_UNSUPPORTED_CHECKSUM_TYPE; + fin.delivery_code = CFDP_DELIVERY_INCOMPLETE; + fin.file_status = CFDP_FILE_STATUS_RETAINED; + + uint8_t buf[8]; + size_t n = cfdp_finished_serialize(&fin, buf, sizeof(buf)); + ASSERT_EQ_INT(2, n); + + cfdp_finished_pdu_t out = {0}; + ASSERT_EQ_INT(2, cfdp_finished_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_COND_UNSUPPORTED_CHECKSUM_TYPE, out.condition_code); + ASSERT_EQ_INT(CFDP_DELIVERY_INCOMPLETE, out.delivery_code); + ASSERT_EQ_INT(CFDP_FILE_STATUS_RETAINED, out.file_status); + ASSERT_TRUE(!out.filestore_responses); + ASSERT_EQ_INT(0, out.fault_location_len); + + /* The nominal close of a successful transfer carries no TLVs at all. */ + cfdp_finished_pdu_t nominal = {0}; + nominal.condition_code = CFDP_COND_NO_ERROR; + nominal.delivery_code = CFDP_DELIVERY_COMPLETE; + nominal.file_status = CFDP_FILE_STATUS_RETAINED; + ASSERT_EQ_INT(2, cfdp_finished_serialize(&nominal, buf, sizeof(buf))); + ASSERT_EQ_INT(0x02, buf[1]); + return 0; +} + +/** + * @brief Encode two Filestore Response TLVs for the Finished PDU tests. + */ +static size_t build_filestore_responses(uint8_t *buf, size_t buf_len) +{ + cfdp_filestore_response_t resp = {0}; + resp.action_code = CFDP_FS_ACTION_DELETE_FILE; + resp.status_code = CFDP_FS_STATUS_SUCCESSFUL; + resp.first_filename = "a.dat"; + resp.first_filename_len = 5; + + size_t n = cfdp_filestore_response_tlv_serialize(&resp, buf, buf_len); + + resp.action_code = CFDP_FS_ACTION_CREATE_DIRECTORY; + resp.status_code = CFDP_FS_STATUS_NOT_PERFORMED; + resp.first_filename = "dir"; + resp.first_filename_len = 3; + + return n + cfdp_filestore_response_tlv_serialize(&resp, &buf[n], buf_len - n); +} + +static int test_finished_filestore_and_fault_location(void) +{ + uint8_t responses[64]; + size_t responses_len = build_filestore_responses(responses, sizeof(responses)); + + cfdp_finished_pdu_t fin = {0}; + fin.condition_code = CFDP_COND_FILESTORE_REJECTION; + fin.delivery_code = CFDP_DELIVERY_INCOMPLETE; + fin.file_status = CFDP_FILE_STATUS_DISCARDED_FILESTORE_REJECTION; + fin.filestore_responses = responses; + fin.filestore_responses_len = (uint16_t)responses_len; + fin.fault_location_entity_id = 9; + fin.fault_location_len = 1; + + uint8_t buf[96]; + size_t n = cfdp_finished_serialize(&fin, buf, sizeof(buf)); + ASSERT_EQ_INT(2 + responses_len + 3, n); + + cfdp_finished_pdu_t out = {0}; + ASSERT_EQ_INT(n, cfdp_finished_deserialize(buf, n, &out)); + ASSERT_EQ_INT(responses_len, out.filestore_responses_len); + ASSERT_EQ_MEM(responses, out.filestore_responses, responses_len); + ASSERT_TRUE(out.fault_location_entity_id == 9ULL); + ASSERT_EQ_INT(1, out.fault_location_len); + + /* The reported span decodes back into the two responses it was built from. */ + cfdp_filestore_response_t first = {0}; + size_t consumed = + cfdp_filestore_response_tlv_deserialize(out.filestore_responses, responses_len, &first); + ASSERT_TRUE(consumed > 0); + ASSERT_EQ_INT(CFDP_FS_ACTION_DELETE_FILE, first.action_code); + + cfdp_filestore_response_t second = {0}; + ASSERT_TRUE(cfdp_filestore_response_tlv_deserialize(&out.filestore_responses[consumed], + responses_len - consumed, + &second) > 0); + ASSERT_EQ_INT(CFDP_FS_ACTION_CREATE_DIRECTORY, second.action_code); + return 0; +} + +static int test_finished_serialize_invalid_args(void) +{ + uint8_t responses[64]; + size_t responses_len = build_filestore_responses(responses, sizeof(responses)); + + cfdp_finished_pdu_t fin = {0}; + fin.condition_code = CFDP_COND_FILESTORE_REJECTION; + + /* A fault condition demands a Fault Location (§5.2.3). */ + uint8_t buf[96]; + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, buf, sizeof(buf))); + + fin.fault_location_entity_id = 9; + fin.fault_location_len = 1; + ASSERT_EQ_INT(5, cfdp_finished_serialize(&fin, buf, sizeof(buf))); + + /* No room for the Fault Location TLV after the fixed octets. */ + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, buf, 3)); + + fin.filestore_responses_len = (uint16_t)responses_len; + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, buf, sizeof(buf))); + + fin.filestore_responses = responses; + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, buf, responses_len)); + return 0; +} + +static int test_finished_deserialize_bad_tlvs(void) +{ + cfdp_finished_pdu_t out = {0}; + + /* Only Filestore Response and Entity ID TLVs may follow (§5.2.3). */ + const uint8_t wrong_type[] = {CFDP_DIRECTIVE_FINISHED, 0x00, CFDP_TLV_MESSAGE_TO_USER, 0x00}; + ASSERT_EQ_INT(0, cfdp_finished_deserialize(wrong_type, sizeof(wrong_type), &out)); + + /* A Filestore Response after the Fault Location breaks the field order. */ + const uint8_t out_of_order[] = {CFDP_DIRECTIVE_FINISHED, + 0x00, + CFDP_TLV_ENTITY_ID, + 0x01, + 0x07, + CFDP_TLV_FILESTORE_RESPONSE, + 0x02, + 0x10, + 0x00}; + ASSERT_EQ_INT(0, cfdp_finished_deserialize(out_of_order, sizeof(out_of_order), &out)); + + /* A TLV whose length runs past the end of the data field. */ + const uint8_t truncated[] = {CFDP_DIRECTIVE_FINISHED, 0x00, CFDP_TLV_FILESTORE_RESPONSE, 0x04}; + ASSERT_EQ_INT(0, cfdp_finished_deserialize(truncated, sizeof(truncated), &out)); + + /* An Entity ID TLV carrying an illegal identifier length. */ + const uint8_t bad_entity[] = {CFDP_DIRECTIVE_FINISHED, 0x00, CFDP_TLV_ENTITY_ID, 0x00}; + ASSERT_EQ_INT(0, cfdp_finished_deserialize(bad_entity, sizeof(bad_entity), &out)); + return 0; +} + +static int test_ack_roundtrip(void) +{ + cfdp_ack_pdu_t ack = {0}; + ack.ack_directive_code = CFDP_DIRECTIVE_FINISHED; + ack.condition_code = CFDP_COND_NO_ERROR; + ack.transaction_status = CFDP_TXN_STATUS_ACTIVE; + + uint8_t buf[8]; + size_t n = cfdp_ack_serialize(&ack, buf, sizeof(buf)); + ASSERT_EQ_INT(3, n); + + cfdp_ack_pdu_t out = {0}; + ASSERT_EQ_INT(3, cfdp_ack_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_DIRECTIVE_FINISHED, out.ack_directive_code); + ASSERT_EQ_INT(CFDP_TXN_STATUS_ACTIVE, out.transaction_status); + return 0; +} + +static int test_ack_exact_bytes(void) +{ + /* Table 5-8: acknowledged directive code and subtype share octet 1; the + * condition code, two spare bits and transaction status share octet 2. */ + cfdp_ack_pdu_t ack = {0}; + ack.ack_directive_code = CFDP_DIRECTIVE_FINISHED; + ack.condition_code = CFDP_COND_FILE_CHECKSUM_FAILURE; + ack.transaction_status = CFDP_TXN_STATUS_TERMINATED; + + uint8_t buf[8]; + ASSERT_EQ_INT(3, cfdp_ack_serialize(&ack, buf, sizeof(buf))); + + const uint8_t expected_finished[] = {0x06, 0x51, 0x52}; + ASSERT_EQ_MEM(expected_finished, buf, sizeof(expected_finished)); + + /* An ACK of EOF takes subtype '0000' instead. */ + ack.ack_directive_code = CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(3, cfdp_ack_serialize(&ack, buf, sizeof(buf))); + + const uint8_t expected_eof[] = {0x06, 0x40, 0x52}; + ASSERT_EQ_MEM(expected_eof, buf, sizeof(expected_eof)); + return 0; +} + +static int test_ack_subtype_is_derived(void) +{ + /* The subtype is not a caller field, so an ACK of Finished always carries + * '0001' and an ACK of any other directive always carries '0000'. */ + cfdp_ack_pdu_t ack = {0}; + uint8_t buf[8]; + + ack.ack_directive_code = CFDP_DIRECTIVE_FINISHED; + ASSERT_EQ_INT(3, cfdp_ack_serialize(&ack, buf, sizeof(buf))); + ASSERT_EQ_INT(CFDP_ACK_SUBTYPE_FINISHED, buf[1] & 0x0FU); + + ack.ack_directive_code = CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(3, cfdp_ack_serialize(&ack, buf, sizeof(buf))); + ASSERT_EQ_INT(CFDP_ACK_SUBTYPE_OTHER, buf[1] & 0x0FU); + return 0; +} + +static int test_ack_deserialize_rejects_wrong_subtype(void) +{ + cfdp_ack_pdu_t out = {0}; + + /* ACK of Finished must carry subtype '0001', not '0000'. */ + uint8_t finished_zero[] = {0x06, 0x50, 0x00}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(finished_zero, sizeof(finished_zero), &out)); + + /* ACK of EOF must carry subtype '0000', not '0001'. */ + uint8_t eof_one[] = {0x06, 0x41, 0x00}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(eof_one, sizeof(eof_one), &out)); + + /* Any other subtype value is equally invalid. */ + uint8_t finished_seven[] = {0x06, 0x57, 0x00}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(finished_seven, sizeof(finished_seven), &out)); + + /* The conformant encodings of both still decode. */ + uint8_t finished_ok[] = {0x06, 0x51, 0x00}; + ASSERT_EQ_INT(3, cfdp_ack_deserialize(finished_ok, sizeof(finished_ok), &out)); + ASSERT_EQ_INT(CFDP_DIRECTIVE_FINISHED, out.ack_directive_code); + + uint8_t eof_ok[] = {0x06, 0x40, 0x00}; + ASSERT_EQ_INT(3, cfdp_ack_deserialize(eof_ok, sizeof(eof_ok), &out)); + ASSERT_EQ_INT(CFDP_DIRECTIVE_EOF, out.ack_directive_code); + return 0; +} + +static int test_ack_rejects_unacknowledged_directives(void) +{ + /* Table 5-8: only EOF and Finished PDUs are acknowledged. Every other + * 4-bit directive code - defined or reserved - is rejected on encode. */ + cfdp_ack_pdu_t ack = {0}; + uint8_t buf[8]; + for (uint8_t code = 0x0; code <= 0xF; code++) + { + ack.ack_directive_code = (cfdp_directive_code_t)code; + size_t expected = + ((code == CFDP_DIRECTIVE_EOF) || (code == CFDP_DIRECTIVE_FINISHED)) ? 3U : 0U; + ASSERT_EQ_INT(expected, cfdp_ack_serialize(&ack, buf, sizeof(buf))); + } + return 0; +} + +static int test_ack_deserialize_rejects_unacknowledged_directives(void) +{ + cfdp_ack_pdu_t out = {0}; + out.ack_directive_code = CFDP_DIRECTIVE_FINISHED; + out.condition_code = CFDP_COND_FILE_SIZE_ERROR; + out.transaction_status = CFDP_TXN_STATUS_TERMINATED; + + /* ACK of Metadata, with the subtype '0000' table 5-8 would give it. */ + const uint8_t metadata[] = {0x06, 0x70, 0x01}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(metadata, sizeof(metadata), &out)); + + /* ACK of NAK, and of the reserved directive code '0000'. */ + const uint8_t nak[] = {0x06, 0x80, 0x01}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(nak, sizeof(nak), &out)); + const uint8_t reserved[] = {0x06, 0x00, 0x01}; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(reserved, sizeof(reserved), &out)); + + /* A rejected ACK leaves the output as it was. */ + ASSERT_EQ_INT(CFDP_DIRECTIVE_FINISHED, out.ack_directive_code); + ASSERT_EQ_INT(CFDP_COND_FILE_SIZE_ERROR, out.condition_code); + ASSERT_EQ_INT(CFDP_TXN_STATUS_TERMINATED, out.transaction_status); + return 0; +} + +static int test_metadata_roundtrip(void) +{ + cfdp_metadata_pdu_t md = {0}; + md.closure_requested = true; + md.checksum_type = CFDP_CHECKSUM_MODULAR; + md.file_size = 1024; + md.source_filename = "input.bin"; + md.source_filename_len = 9; + md.destination_filename = "output.bin"; + md.destination_filename_len = 10; + + uint8_t buf[64]; + size_t n = cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_TRUE(n > 0); + + cfdp_metadata_pdu_t out = {0}; + size_t m = cfdp_metadata_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out); + ASSERT_EQ_INT(n, m); + ASSERT_TRUE(out.closure_requested); + ASSERT_TRUE(out.file_size == 1024); + ASSERT_EQ_INT(9, out.source_filename_len); + ASSERT_EQ_MEM("input.bin", out.source_filename, 9); + ASSERT_EQ_INT(10, out.destination_filename_len); + ASSERT_EQ_MEM("output.bin", out.destination_filename, 10); + return 0; +} + +static int test_nak_roundtrip(void) +{ + cfdp_nak_pdu_t nak = {0}; + nak.start_of_scope = 0; + nak.end_of_scope = 4096; + nak.segment_request_count = 2; + nak.segment_requests[0].start_offset = 100; + nak.segment_requests[0].end_offset = 200; + nak.segment_requests[1].start_offset = 300; + nak.segment_requests[1].end_offset = 400; + + uint8_t buf[64]; + size_t n = cfdp_nak_serialize(&nak, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_EQ_INT(1 + 2 * 4 + 2 * (2 * 4), n); + + cfdp_nak_pdu_t out = {0}; + size_t m = cfdp_nak_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out); + ASSERT_EQ_INT(n, m); + ASSERT_EQ_INT(2, out.segment_request_count); + ASSERT_TRUE(out.end_of_scope == 4096); + ASSERT_TRUE(out.segment_requests[1].start_offset == 300); + ASSERT_TRUE(out.segment_requests[1].end_offset == 400); + return 0; +} + +static int test_prompt_keepalive_roundtrip(void) +{ + uint8_t buf[16]; + size_t n = cfdp_prompt_serialize(CFDP_PROMPT_KEEP_ALIVE, buf, sizeof(buf)); + ASSERT_EQ_INT(2, n); + cfdp_prompt_response_t resp; + ASSERT_EQ_INT(2, cfdp_prompt_deserialize(buf, n, &resp)); + ASSERT_EQ_INT(CFDP_PROMPT_KEEP_ALIVE, resp); + + n = cfdp_keep_alive_serialize(0x0A0B0C0DULL, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_EQ_INT(5, n); + uint64_t progress = 0; + ASSERT_EQ_INT(5, cfdp_keep_alive_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &progress)); + ASSERT_TRUE(progress == 0x0A0B0C0DULL); + return 0; +} + +static int test_eof_serialize_invalid_args(void) +{ + cfdp_eof_pdu_t eof = {0}; + uint8_t buf[16]; + + ASSERT_EQ_INT(0, cfdp_eof_serialize(NULL, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, 9)); + return 0; +} + +static int test_eof_deserialize_invalid_args(void) +{ + uint8_t buf[10] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + cfdp_eof_pdu_t eof = {0}; + + ASSERT_EQ_INT(0, cfdp_eof_deserialize(NULL, sizeof(buf), CFDP_FILE_SIZE_SMALL, &eof)); + ASSERT_EQ_INT(0, cfdp_eof_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, NULL)); + ASSERT_EQ_INT(0, cfdp_eof_deserialize(buf, 9, CFDP_FILE_SIZE_SMALL, &eof)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_FINISHED; + ASSERT_EQ_INT(0, cfdp_eof_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &eof)); + return 0; +} + +static int test_eof_large_file_roundtrip(void) +{ + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_NO_ERROR; + eof.file_checksum = 0xAABBCCDDU; + eof.file_size = 0x0000000100000000ULL; + + uint8_t buf[24]; + size_t n = cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_LARGE, buf, sizeof(buf)); + ASSERT_EQ_INT(14, n); + + cfdp_eof_pdu_t out = {0}; + ASSERT_EQ_INT(n, cfdp_eof_deserialize(buf, n, CFDP_FILE_SIZE_LARGE, &out)); + ASSERT_TRUE(out.file_checksum == 0xAABBCCDDU); + ASSERT_TRUE(out.file_size == eof.file_size); + ASSERT_EQ_INT(0, out.fault_location_len); + return 0; +} + +static int test_eof_fault_location_roundtrip(void) +{ + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_FILE_SIZE_ERROR; + eof.file_checksum = 0x01020304U; + eof.file_size = 16; + eof.fault_location_entity_id = 0xBEEF; + eof.fault_location_len = 2; + + uint8_t buf[24]; + size_t n = cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + + /* 10 fixed octets, then a 2-octet Entity ID TLV with its type and length. */ + ASSERT_EQ_INT(14, n); + ASSERT_EQ_INT(CFDP_TLV_ENTITY_ID, buf[10]); + ASSERT_EQ_INT(2, buf[11]); + ASSERT_EQ_INT(0xBE, buf[12]); + ASSERT_EQ_INT(0xEF, buf[13]); + + cfdp_eof_pdu_t out = {0}; + ASSERT_EQ_INT(n, cfdp_eof_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out)); + ASSERT_EQ_INT(CFDP_COND_FILE_SIZE_ERROR, out.condition_code); + ASSERT_TRUE(out.fault_location_entity_id == 0xBEEFULL); + ASSERT_EQ_INT(2, out.fault_location_len); + return 0; +} + +static int test_eof_fault_location_required(void) +{ + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_FILESTORE_REJECTION; + eof.file_size = 16; + + /* §5.2.2 requires the Fault Location on any condition but 'No error'. */ + uint8_t buf[24]; + ASSERT_EQ_INT(0, cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + eof.fault_location_entity_id = 3; + eof.fault_location_len = 1; + ASSERT_EQ_INT(13, cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + /* The TLV must still fit once the fixed part has been written. */ + ASSERT_EQ_INT(0, cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, 11)); + return 0; +} + +static int test_eof_deserialize_bad_fault_location(void) +{ + cfdp_eof_pdu_t eof = {0}; + eof.condition_code = CFDP_COND_NO_ERROR; + eof.file_size = 16; + + uint8_t buf[24]; + size_t n = cfdp_eof_serialize(&eof, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + + /* Trailing octets that are not a well-formed Entity ID TLV. */ + buf[n] = (uint8_t)CFDP_TLV_FLOW_LABEL; + buf[n + 1U] = 1; + buf[n + 2U] = 9; + cfdp_eof_pdu_t out = {0}; + ASSERT_EQ_INT(0, cfdp_eof_deserialize(buf, n + 3U, CFDP_FILE_SIZE_SMALL, &out)); + return 0; +} + +static int test_finished_invalid_args(void) +{ + cfdp_finished_pdu_t fin = {0}; + uint8_t buf[2] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_FINISHED; + + ASSERT_EQ_INT(0, cfdp_finished_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_finished_serialize(&fin, buf, 1)); + + ASSERT_EQ_INT(0, cfdp_finished_deserialize(NULL, sizeof(buf), &fin)); + ASSERT_EQ_INT(0, cfdp_finished_deserialize(buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(0, cfdp_finished_deserialize(buf, 1, &fin)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, cfdp_finished_deserialize(buf, sizeof(buf), &fin)); + return 0; +} + +static int test_ack_invalid_args(void) +{ + /* A well-formed ACK of EOF, so only the argument under test is at fault. */ + cfdp_ack_pdu_t ack = {0}; + ack.ack_directive_code = CFDP_DIRECTIVE_EOF; + uint8_t buf[3] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_ACK; + buf[1] = 0x40; + + ASSERT_EQ_INT(0, cfdp_ack_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_ack_serialize(&ack, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_ack_serialize(&ack, buf, 2)); + + ASSERT_EQ_INT(0, cfdp_ack_deserialize(NULL, sizeof(buf), &ack)); + ASSERT_EQ_INT(0, cfdp_ack_deserialize(buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(0, cfdp_ack_deserialize(buf, 2, &ack)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, cfdp_ack_deserialize(buf, sizeof(buf), &ack)); + return 0; +} + +static int test_metadata_empty_filenames(void) +{ + cfdp_metadata_pdu_t md = {0}; + md.checksum_type = CFDP_CHECKSUM_NULL; + md.file_size = 42; + + uint8_t buf[16]; + size_t n = cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_EQ_INT(8, n); + + cfdp_metadata_pdu_t out = {0}; + ASSERT_EQ_INT(n, cfdp_metadata_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out)); + ASSERT_TRUE(!out.closure_requested); + ASSERT_EQ_INT(CFDP_CHECKSUM_NULL, out.checksum_type); + ASSERT_TRUE(!out.source_filename); + ASSERT_EQ_INT(0, out.source_filename_len); + ASSERT_TRUE(!out.destination_filename); + ASSERT_EQ_INT(0, out.destination_filename_len); + return 0; +} + +static int test_metadata_with_options(void) +{ + /* A Filestore Request followed by a Message to User, the two option TLVs a + * Metadata PDU most commonly carries (§5.2.5). */ + uint8_t options[64]; + cfdp_filestore_request_t req = {0}; + req.action_code = CFDP_FS_ACTION_CREATE_DIRECTORY; + req.first_filename = "logs"; + req.first_filename_len = 4; + size_t options_len = cfdp_filestore_request_tlv_serialize(&req, options, sizeof(options)); + + cfdp_tlv_t message = {0}; + message.type = (uint8_t)CFDP_TLV_MESSAGE_TO_USER; + message.length = 4; + message.value = (const uint8_t *)"cfdp"; + options_len += + cfdp_tlv_serialize(&message, &options[options_len], sizeof(options) - options_len); + + cfdp_metadata_pdu_t md = {0}; + md.file_size = 512; + md.source_filename = "s.dat"; + md.source_filename_len = 5; + md.destination_filename = "d.dat"; + md.destination_filename_len = 5; + md.options = options; + md.options_len = (uint16_t)options_len; + + uint8_t buf[96]; + size_t n = cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf)); + ASSERT_EQ_INT(4 + 5 + 5 + 4 + options_len, n); + + cfdp_metadata_pdu_t out = {0}; + ASSERT_EQ_INT(n, cfdp_metadata_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, &out)); + ASSERT_EQ_INT(options_len, out.options_len); + ASSERT_EQ_MEM(options, out.options, options_len); + + cfdp_filestore_request_t decoded = {0}; + ASSERT_TRUE(cfdp_filestore_request_tlv_deserialize(out.options, out.options_len, &decoded) > 0); + ASSERT_EQ_INT(CFDP_FS_ACTION_CREATE_DIRECTORY, decoded.action_code); + ASSERT_EQ_MEM("logs", decoded.first_filename, 4); + return 0; +} + +static int test_metadata_serialize_invalid_args(void) +{ + cfdp_metadata_pdu_t md = {0}; + md.source_filename = "input.bin"; + md.source_filename_len = 9; + md.destination_filename = "output.bin"; + md.destination_filename_len = 10; + + uint8_t buf[64]; + ASSERT_EQ_INT(0, cfdp_metadata_serialize(NULL, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, 26)); + + /* A non-zero name length with no name is rejected by each name field. */ + md.source_filename = NULL; + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + md.source_filename = "input.bin"; + md.destination_filename = NULL; + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + /* Options declared but not supplied, then options that do not fit. */ + static const uint8_t flow_label[] = {CFDP_TLV_FLOW_LABEL, 0x02, 'a', 'b'}; + md.destination_filename = "output.bin"; + md.options_len = (uint16_t)sizeof(flow_label); + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + md.options = flow_label; + ASSERT_EQ_INT(0, cfdp_metadata_serialize(&md, CFDP_FILE_SIZE_SMALL, buf, 30)); + return 0; +} + +static int test_metadata_deserialize_invalid_args(void) +{ + uint8_t buf[16] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_METADATA; + cfdp_metadata_pdu_t md = {0}; + + ASSERT_EQ_INT(0, cfdp_metadata_deserialize(NULL, sizeof(buf), CFDP_FILE_SIZE_SMALL, &md)); + ASSERT_EQ_INT(0, cfdp_metadata_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, NULL)); + ASSERT_EQ_INT(0, cfdp_metadata_deserialize(buf, 5, CFDP_FILE_SIZE_SMALL, &md)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, cfdp_metadata_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &md)); + return 0; +} + +static int test_metadata_deserialize_truncated_names(void) +{ + cfdp_metadata_pdu_t md = {0}; + + /* Directive code, flags and a 4-octet file size, then nothing: the source + * name's length octet is missing. */ + const uint8_t no_source[] = {CFDP_DIRECTIVE_METADATA, 0x00, 0x00, 0x00, 0x00, 0x00}; + ASSERT_EQ_INT( + 0, + cfdp_metadata_deserialize(no_source, sizeof(no_source), CFDP_FILE_SIZE_SMALL, &md)); + + /* The source name claims 5 octets but only 1 follows. */ + const uint8_t short_source[] = + {CFDP_DIRECTIVE_METADATA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 'a'}; + ASSERT_EQ_INT( + 0, + cfdp_metadata_deserialize(short_source, sizeof(short_source), CFDP_FILE_SIZE_SMALL, &md)); + + /* An empty source name consumes the last octet, leaving no destination. */ + const uint8_t no_destination[] = {CFDP_DIRECTIVE_METADATA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + ASSERT_EQ_INT(0, + cfdp_metadata_deserialize(no_destination, + sizeof(no_destination), + CFDP_FILE_SIZE_SMALL, + &md)); + return 0; +} + +static int test_nak_serialize_invalid_args(void) +{ + cfdp_nak_pdu_t nak = {0}; + nak.segment_request_count = 1; + nak.segment_requests[0].start_offset = 100; + nak.segment_requests[0].end_offset = 200; + + uint8_t buf[16]; + ASSERT_EQ_INT(0, cfdp_nak_serialize(NULL, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_nak_serialize(&nak, CFDP_FILE_SIZE_SMALL, NULL, sizeof(buf))); + + /* One request needs 17 octets: 1 directive, 8 scope, 8 request. */ + ASSERT_EQ_INT(0, cfdp_nak_serialize(&nak, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + + nak.segment_request_count = CFDP_NAK_MAX_SEGMENT_REQUESTS + 1U; + ASSERT_EQ_INT(0, cfdp_nak_serialize(&nak, CFDP_FILE_SIZE_SMALL, buf, sizeof(buf))); + return 0; +} + +static int test_nak_deserialize_invalid_args(void) +{ + uint8_t buf[9] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + cfdp_nak_pdu_t nak = {0}; + + ASSERT_EQ_INT(0, cfdp_nak_deserialize(NULL, sizeof(buf), CFDP_FILE_SIZE_SMALL, &nak)); + ASSERT_EQ_INT(0, cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, NULL)); + ASSERT_EQ_INT(0, cfdp_nak_deserialize(buf, 8, CFDP_FILE_SIZE_SMALL, &nak)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &nak)); + return 0; +} + +static int test_nak_deserialize_rejects_excess_segment_requests(void) +{ + /* One segment request more than the decoder can store. Truncating would + * tell the sender that gaps it was never shown had been satisfied. */ + uint8_t buf[1 + 8 + (CFDP_NAK_MAX_SEGMENT_REQUESTS + 1U) * 8U] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + + cfdp_nak_pdu_t out = {0}; + ASSERT_EQ_INT(0, cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &out)); + return 0; +} + +static int test_nak_deserialize_accepts_full_segment_requests(void) +{ + /* Exactly the capacity must still decode, and account for every octet. */ + uint8_t buf[1 + 8 + CFDP_NAK_MAX_SEGMENT_REQUESTS * 8U] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + + cfdp_nak_pdu_t out = {0}; + size_t n = cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &out); + ASSERT_EQ_INT(sizeof(buf), n); + ASSERT_EQ_INT(CFDP_NAK_MAX_SEGMENT_REQUESTS, out.segment_request_count); + return 0; +} + +static int test_nak_deserialize_no_segment_requests(void) +{ + /* §5.2.6 allows N = 0: scope only, with no gaps to report. */ + uint8_t buf[9] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + buf[5] = 0x10; + + cfdp_nak_pdu_t out; + memset(&out, 0xFF, sizeof(out)); + ASSERT_EQ_INT(sizeof(buf), cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &out)); + ASSERT_EQ_INT(0, out.segment_request_count); + ASSERT_TRUE(out.start_of_scope == 0); + ASSERT_TRUE(out.end_of_scope == 0x10000000U); + return 0; +} + +static int test_nak_deserialize_rejects_ragged_tail(void) +{ + /* §5.2.6: the segment requests fill the data field exactly. Three trailing + * octets are a malformed PDU, not a request that ends early. */ + uint8_t buf[1 + 8 + 8 + 3] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + + cfdp_nak_pdu_t out = {0}; + ASSERT_EQ_INT(0, cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &out)); + + /* The same data field without the stray octets is well formed. */ + ASSERT_EQ_INT(17, cfdp_nak_deserialize(buf, 17, CFDP_FILE_SIZE_SMALL, &out)); + ASSERT_EQ_INT(1, out.segment_request_count); + + /* A large-file NAK needs 16-octet requests, so the same 8 trailing octets + * that were a whole request above are now a ragged tail. */ + uint8_t large[1 + 16 + 8] = {0}; + large[0] = (uint8_t)CFDP_DIRECTIVE_NAK; + ASSERT_EQ_INT(0, cfdp_nak_deserialize(large, sizeof(large), CFDP_FILE_SIZE_LARGE, &out)); + return 0; +} + +static int test_prompt_nak_roundtrip(void) +{ + uint8_t buf[2]; + ASSERT_EQ_INT(2, cfdp_prompt_serialize(CFDP_PROMPT_NAK, buf, sizeof(buf))); + ASSERT_EQ_INT(0x00, buf[1]); + + cfdp_prompt_response_t resp = CFDP_PROMPT_KEEP_ALIVE; + ASSERT_EQ_INT(2, cfdp_prompt_deserialize(buf, sizeof(buf), &resp)); + ASSERT_EQ_INT(CFDP_PROMPT_NAK, resp); + return 0; +} + +static int test_prompt_invalid_args(void) +{ + uint8_t buf[2] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_PROMPT; + cfdp_prompt_response_t resp; + + ASSERT_EQ_INT(0, cfdp_prompt_serialize(CFDP_PROMPT_NAK, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_prompt_serialize(CFDP_PROMPT_NAK, buf, 1)); + + ASSERT_EQ_INT(0, cfdp_prompt_deserialize(NULL, sizeof(buf), &resp)); + ASSERT_EQ_INT(0, cfdp_prompt_deserialize(buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(0, cfdp_prompt_deserialize(buf, 1, &resp)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, cfdp_prompt_deserialize(buf, sizeof(buf), &resp)); + return 0; +} + +static int test_keep_alive_invalid_args(void) +{ + uint8_t buf[5] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_KEEP_ALIVE; + uint64_t progress = 0; + + ASSERT_EQ_INT(0, cfdp_keep_alive_serialize(0, CFDP_FILE_SIZE_SMALL, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_keep_alive_serialize(0, CFDP_FILE_SIZE_SMALL, buf, 4)); + + ASSERT_EQ_INT(0, + cfdp_keep_alive_deserialize(NULL, sizeof(buf), CFDP_FILE_SIZE_SMALL, &progress)); + ASSERT_EQ_INT(0, cfdp_keep_alive_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, NULL)); + ASSERT_EQ_INT(0, cfdp_keep_alive_deserialize(buf, 4, CFDP_FILE_SIZE_SMALL, &progress)); + + buf[0] = (uint8_t)CFDP_DIRECTIVE_EOF; + ASSERT_EQ_INT(0, + cfdp_keep_alive_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &progress)); + return 0; +} + +test_result_t test_cfdp_directive_run_all(void) +{ + RUN_TEST(test_eof_roundtrip); + RUN_TEST(test_eof_large_file_roundtrip); + RUN_TEST(test_eof_fault_location_roundtrip); + RUN_TEST(test_eof_fault_location_required); + RUN_TEST(test_eof_deserialize_bad_fault_location); + RUN_TEST(test_eof_serialize_invalid_args); + RUN_TEST(test_eof_deserialize_invalid_args); + RUN_TEST(test_finished_roundtrip); + RUN_TEST(test_finished_filestore_and_fault_location); + RUN_TEST(test_finished_serialize_invalid_args); + RUN_TEST(test_finished_deserialize_bad_tlvs); + RUN_TEST(test_finished_invalid_args); + RUN_TEST(test_ack_roundtrip); + RUN_TEST(test_ack_exact_bytes); + RUN_TEST(test_ack_subtype_is_derived); + RUN_TEST(test_ack_deserialize_rejects_wrong_subtype); + RUN_TEST(test_ack_rejects_unacknowledged_directives); + RUN_TEST(test_ack_deserialize_rejects_unacknowledged_directives); + RUN_TEST(test_ack_invalid_args); + RUN_TEST(test_metadata_roundtrip); + RUN_TEST(test_metadata_empty_filenames); + RUN_TEST(test_metadata_with_options); + RUN_TEST(test_metadata_serialize_invalid_args); + RUN_TEST(test_metadata_deserialize_invalid_args); + RUN_TEST(test_metadata_deserialize_truncated_names); + RUN_TEST(test_nak_roundtrip); + RUN_TEST(test_nak_serialize_invalid_args); + RUN_TEST(test_nak_deserialize_invalid_args); + RUN_TEST(test_nak_deserialize_rejects_excess_segment_requests); + RUN_TEST(test_nak_deserialize_accepts_full_segment_requests); + RUN_TEST(test_nak_deserialize_no_segment_requests); + RUN_TEST(test_nak_deserialize_rejects_ragged_tail); + RUN_TEST(test_prompt_keepalive_roundtrip); + RUN_TEST(test_prompt_nak_roundtrip); + RUN_TEST(test_prompt_invalid_args); + RUN_TEST(test_keep_alive_invalid_args); + + /* cunit.h keeps its tally in file-local statics, so these counters cover + * only the tests run above. */ + test_result_t result = {cunit_total_tests - cunit_overall_failures, cunit_total_tests}; + return result; +} diff --git a/tests/test_cfdp_pdu.c b/tests/test_cfdp_pdu.c new file mode 100644 index 0000000..d7c62eb --- /dev/null +++ b/tests/test_cfdp_pdu.c @@ -0,0 +1,570 @@ +/** + * @file test_cfdp_pdu.c + * @brief Unit tests for the fixed PDU header and File Data PDU codec + * + * Exercises src/cfdp_pdu.c against CCSDS 727.0-B-5 Section 5.1 (fixed PDU + * header) and Section 5.3 (File Data PDU) with round-trip and known-vector + * checks. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp.h" +#include "cunit.h" +#include "test_runners.h" + +#include + +static void fill_header(cfdp_pdu_header_t *hdr) +{ + memset(hdr, 0, sizeof(*hdr)); + hdr->version = CFDP_PROTOCOL_VERSION; + hdr->pdu_type = CFDP_PDU_TYPE_DIRECTIVE; + hdr->direction = CFDP_DIRECTION_TOWARD_RECEIVER; + hdr->transmission_mode = CFDP_TRANS_MODE_ACKNOWLEDGED; + hdr->crc_flag = CFDP_CRC_ABSENT; + hdr->large_file_flag = CFDP_FILE_SIZE_SMALL; + hdr->data_field_length = 10; + hdr->segmentation_control = CFDP_SEG_CTRL_BOUNDARIES_NOT_PRESERVED; + hdr->segment_metadata_flag = CFDP_SEG_METADATA_ABSENT; + hdr->entity_id_length = 1; + hdr->transaction_seq_length = 1; + hdr->source_entity_id = 1; + hdr->transaction_seq_number = 2; + hdr->destination_entity_id = 3; +} + +static int test_header_exact_bytes(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN]; + size_t n = cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf)); + + const uint8_t expected[] = {0x20, 0x00, 0x0A, 0x00, 0x01, 0x02, 0x03}; + ASSERT_EQ_INT(sizeof(expected), n); + ASSERT_EQ_MEM(expected, buf, sizeof(expected)); + return 0; +} + +static int test_header_all_flags(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + hdr.pdu_type = CFDP_PDU_TYPE_FILE_DATA; + hdr.direction = CFDP_DIRECTION_TOWARD_SENDER; + hdr.transmission_mode = CFDP_TRANS_MODE_UNACKNOWLEDGED; + hdr.crc_flag = CFDP_CRC_PRESENT; + hdr.large_file_flag = CFDP_FILE_SIZE_LARGE; + + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN]; + size_t n = cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf)); + ASSERT_TRUE(n > 0); + ASSERT_EQ_INT(0x3F, buf[0]); + return 0; +} + +static int test_header_roundtrip_large_ids(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + hdr.large_file_flag = CFDP_FILE_SIZE_LARGE; + hdr.entity_id_length = 4; + hdr.transaction_seq_length = 8; + hdr.source_entity_id = 0x11223344ULL; + hdr.transaction_seq_number = 0x0102030405060708ULL; + hdr.destination_entity_id = 0xAABBCCDDULL; + + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN]; + size_t n = cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf)); + ASSERT_EQ_INT(4 + 2 * 4 + 8, n); + + cfdp_pdu_header_t out; + size_t m = cfdp_pdu_header_deserialize(buf, n, &out); + ASSERT_EQ_INT(n, m); + ASSERT_EQ_INT(hdr.entity_id_length, out.entity_id_length); + ASSERT_EQ_INT(hdr.transaction_seq_length, out.transaction_seq_length); + ASSERT_EQ_INT(CFDP_FILE_SIZE_LARGE, out.large_file_flag); + ASSERT_TRUE(out.source_entity_id == hdr.source_entity_id); + ASSERT_TRUE(out.transaction_seq_number == hdr.transaction_seq_number); + ASSERT_TRUE(out.destination_entity_id == hdr.destination_entity_id); + return 0; +} + +static int test_file_data_roundtrip(void) +{ + const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x42}; + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 0x12345678ULL; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + + uint8_t buf[32]; + size_t n = cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf)); + ASSERT_EQ_INT(4 + sizeof(payload), n); + + cfdp_file_data_pdu_t out = {0}; + size_t m = + cfdp_file_data_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, CFDP_SEG_METADATA_ABSENT, &out); + ASSERT_EQ_INT(n, m); + ASSERT_TRUE(out.offset == fd.offset); + ASSERT_EQ_INT(sizeof(payload), out.file_data_len); + ASSERT_EQ_MEM(payload, out.file_data, sizeof(payload)); + return 0; +} + +static int test_file_data_empty_payload(void) +{ + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 0x0000BEEFULL; + + uint8_t buf[8]; + size_t n = cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf)); + ASSERT_EQ_INT(4, n); + + cfdp_file_data_pdu_t out = {0}; + ASSERT_EQ_INT( + 4, + cfdp_file_data_deserialize(buf, n, CFDP_FILE_SIZE_SMALL, CFDP_SEG_METADATA_ABSENT, &out)); + ASSERT_TRUE(out.offset == fd.offset); + ASSERT_TRUE(!out.file_data); + ASSERT_EQ_INT(0, out.file_data_len); + return 0; +} + +static int test_file_data_large_file_roundtrip(void) +{ + const uint8_t payload[] = {0x11, 0x22}; + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 0x0102030405060708ULL; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + + uint8_t buf[32]; + size_t n = cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_LARGE, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf)); + ASSERT_EQ_INT(8 + sizeof(payload), n); + + cfdp_file_data_pdu_t out = {0}; + ASSERT_EQ_INT( + n, + cfdp_file_data_deserialize(buf, n, CFDP_FILE_SIZE_LARGE, CFDP_SEG_METADATA_ABSENT, &out)); + ASSERT_TRUE(out.offset == fd.offset); + ASSERT_EQ_MEM(payload, out.file_data, sizeof(payload)); + return 0; +} + +static int test_directive_code_peek(void) +{ + const uint8_t buf[] = {CFDP_DIRECTIVE_METADATA, 0x00}; + cfdp_directive_code_t code; + ASSERT_TRUE(cfdp_pdu_directive_code(buf, sizeof(buf), &code)); + ASSERT_EQ_INT(CFDP_DIRECTIVE_METADATA, code); + + ASSERT_TRUE(!cfdp_pdu_directive_code(NULL, sizeof(buf), &code)); + ASSERT_TRUE(!cfdp_pdu_directive_code(buf, sizeof(buf), NULL)); + ASSERT_TRUE(!cfdp_pdu_directive_code(buf, 0, &code)); + return 0; +} + +static int test_header_size_invalid_lengths(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + ASSERT_EQ_INT(7, cfdp_pdu_header_size(&hdr)); + ASSERT_EQ_INT(0, cfdp_pdu_header_size(NULL)); + + /* Both identifier lengths are rejected below 1 and above 8 octets. */ + fill_header(&hdr); + hdr.entity_id_length = 0; + ASSERT_EQ_INT(0, cfdp_pdu_header_size(&hdr)); + hdr.entity_id_length = (uint8_t)(CFDP_ID_LEN_MAX + 1U); + ASSERT_EQ_INT(0, cfdp_pdu_header_size(&hdr)); + + fill_header(&hdr); + hdr.transaction_seq_length = 0; + ASSERT_EQ_INT(0, cfdp_pdu_header_size(&hdr)); + hdr.transaction_seq_length = (uint8_t)(CFDP_ID_LEN_MAX + 1U); + ASSERT_EQ_INT(0, cfdp_pdu_header_size(&hdr)); + return 0; +} + +static int test_header_serialize_invalid_args(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN]; + + ASSERT_EQ_INT(0, cfdp_pdu_header_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_pdu_header_serialize(&hdr, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_pdu_header_serialize(&hdr, buf, 4)); + + hdr.entity_id_length = 0; + ASSERT_EQ_INT(0, cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf))); + return 0; +} + +static int test_header_deserialize_invalid_args(void) +{ + cfdp_pdu_header_t hdr; + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN] = {0}; + buf[0] = 0x20; /* version '001', so only the argument under test is at fault */ + + ASSERT_EQ_INT(0, cfdp_pdu_header_deserialize(NULL, sizeof(buf), &hdr)); + ASSERT_EQ_INT(0, cfdp_pdu_header_deserialize(buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(0, cfdp_pdu_header_deserialize(buf, 2, &hdr)); + + /* Octet 3 asks for 8-octet identifiers, so the 4 octets supplied cannot + * hold the identifier fields the header announces. */ + const uint8_t truncated[] = {0x20, 0x00, 0x0A, 0x77}; + ASSERT_EQ_INT(0, cfdp_pdu_header_deserialize(truncated, sizeof(truncated), &hdr)); + return 0; +} + +static int test_header_serialize_rejects_other_versions(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + uint8_t buf[CFDP_PDU_HEADER_MAX_LEN]; + + /* §5.1.2, table 5-1: the version field is '001'. Version 9 is included + * because masking it to 3 bits would silently emit '001'. */ + const uint8_t bad_versions[] = {0, 2, 7, 9}; + for (size_t i = 0; i < sizeof(bad_versions); i++) + { + hdr.version = bad_versions[i]; + ASSERT_EQ_INT(0, cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf))); + } + + hdr.version = CFDP_PROTOCOL_VERSION; + ASSERT_EQ_INT(7, cfdp_pdu_header_serialize(&hdr, buf, sizeof(buf))); + return 0; +} + +static int test_header_deserialize_rejects_other_versions(void) +{ + cfdp_pdu_header_t hdr; + + /* A complete header with 1-octet IDs; only the version bits vary. The + * other octet-0 bits are left clear. */ + uint8_t buf[] = {0x00, 0x00, 0x0A, 0x00, 0x01, 0x02, 0x03}; + for (uint8_t version = 0; version <= 7; version++) + { + buf[0] = (uint8_t)(version << 5); + size_t expected = (version == CFDP_PROTOCOL_VERSION) ? sizeof(buf) : 0U; + ASSERT_EQ_INT(expected, cfdp_pdu_header_deserialize(buf, sizeof(buf), &hdr)); + } + return 0; +} + +static int test_file_data_serialize_invalid_args(void) +{ + const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x42}; + cfdp_file_data_pdu_t fd = {0}; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + + uint8_t buf[8]; + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(NULL, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf))); + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + NULL, + sizeof(buf))); + + /* 4 offset octets plus 5 payload octets do not fit in 8. */ + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf))); + + cfdp_file_data_pdu_t no_data = {0}; + no_data.file_data_len = 3; + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&no_data, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf))); + return 0; +} + +static int test_file_data_deserialize_invalid_args(void) +{ + const uint8_t buf[8] = {0}; + cfdp_file_data_pdu_t fd = {0}; + + ASSERT_EQ_INT(0, + cfdp_file_data_deserialize(NULL, + sizeof(buf), + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + &fd)); + ASSERT_EQ_INT(0, + cfdp_file_data_deserialize(buf, + sizeof(buf), + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + NULL)); + ASSERT_EQ_INT( + 0, + cfdp_file_data_deserialize(buf, 3, CFDP_FILE_SIZE_SMALL, CFDP_SEG_METADATA_ABSENT, &fd)); + return 0; +} + +static int test_file_data_segment_metadata_exact_bytes(void) +{ + const uint8_t metadata[] = {0xAA, 0xBB, 0xCC}; + const uint8_t payload[] = {'H', 'I'}; + + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 16; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + fd.record_continuation = CFDP_RECORD_CONT_END; + fd.segment_metadata = metadata; + fd.segment_metadata_len = sizeof(metadata); + + uint8_t buf[32]; + size_t n = cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + buf, + sizeof(buf)); + + /* Table 5-14: record continuation state (2 bits) and segment metadata + * length (6 bits) share the first octet, then the metadata, then the + * offset, then the file data. 0x83 is state '10' with length 3. */ + const uint8_t expected[] = {0x83, 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0x00, 0x10, 'H', 'I'}; + ASSERT_EQ_INT(sizeof(expected), n); + ASSERT_EQ_MEM(expected, buf, sizeof(expected)); + return 0; +} + +static int test_file_data_segment_metadata_roundtrip(void) +{ + const uint8_t metadata[] = {0x01, 0x02}; + const uint8_t payload[] = {'a', 'b', 'c'}; + + cfdp_file_data_pdu_t fd = {0}; + fd.offset = 0x1122334455667788ULL; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + fd.record_continuation = CFDP_RECORD_CONT_START_AND_END; + fd.segment_metadata = metadata; + fd.segment_metadata_len = sizeof(metadata); + + uint8_t buf[64]; + size_t n = cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_LARGE, + CFDP_SEG_METADATA_PRESENT, + buf, + sizeof(buf)); + ASSERT_TRUE(n > 0); + + cfdp_file_data_pdu_t out; + ASSERT_EQ_INT( + n, + cfdp_file_data_deserialize(buf, n, CFDP_FILE_SIZE_LARGE, CFDP_SEG_METADATA_PRESENT, &out)); + ASSERT_TRUE(out.offset == fd.offset); + ASSERT_EQ_INT(CFDP_RECORD_CONT_START_AND_END, out.record_continuation); + ASSERT_EQ_INT(sizeof(metadata), out.segment_metadata_len); + ASSERT_EQ_MEM(metadata, out.segment_metadata, sizeof(metadata)); + ASSERT_EQ_INT(sizeof(payload), out.file_data_len); + ASSERT_EQ_MEM(payload, out.file_data, sizeof(payload)); + return 0; +} + +static int test_file_data_segment_metadata_absent_fields_cleared(void) +{ + const uint8_t wire[] = {0x00, 0x00, 0x00, 0x04, 'x'}; + + cfdp_file_data_pdu_t out; + memset(&out, 0xFF, sizeof(out)); + ASSERT_EQ_INT(sizeof(wire), + cfdp_file_data_deserialize(wire, + sizeof(wire), + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + &out)); + ASSERT_EQ_INT(CFDP_RECORD_CONT_NEITHER, out.record_continuation); + ASSERT_EQ_INT(0, out.segment_metadata_len); + ASSERT_TRUE(!out.segment_metadata); + ASSERT_TRUE(out.offset == 4); + return 0; +} + +static int test_file_data_segment_metadata_mismatched_flag(void) +{ + const uint8_t metadata[] = {0xAA}; + const uint8_t payload[] = {'x'}; + + cfdp_file_data_pdu_t fd = {0}; + fd.file_data = payload; + fd.file_data_len = sizeof(payload); + fd.segment_metadata = metadata; + fd.segment_metadata_len = sizeof(metadata); + + uint8_t buf[32]; + + /* Metadata supplied while the header flag says absent would put the offset + * somewhere the peer does not look for it. */ + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_ABSENT, + buf, + sizeof(buf))); + + /* The 6-bit length field cannot express more than 63 octets. */ + fd.segment_metadata_len = CFDP_SEGMENT_METADATA_MAX_LEN + 1U; + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + buf, + sizeof(buf))); + + /* A present flag with a NULL metadata pointer but a non-zero length. */ + fd.segment_metadata = NULL; + fd.segment_metadata_len = 1; + ASSERT_EQ_INT(0, + cfdp_file_data_serialize(&fd, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + buf, + sizeof(buf))); + return 0; +} + +static int test_file_data_segment_metadata_truncated(void) +{ + cfdp_file_data_pdu_t out; + + /* Empty data field: no room for the record continuation octet. */ + const uint8_t empty[] = {0x00}; + ASSERT_EQ_INT(0, + cfdp_file_data_deserialize(empty, + 0, + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + &out)); + + /* Announces 5 metadata octets but carries 2. */ + const uint8_t short_metadata[] = {0x05, 0xAA, 0xBB}; + ASSERT_EQ_INT(0, + cfdp_file_data_deserialize(short_metadata, + sizeof(short_metadata), + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + &out)); + + /* Metadata complete, but the 4-octet offset is truncated. */ + const uint8_t short_offset[] = {0x01, 0xAA, 0x00, 0x00}; + ASSERT_EQ_INT(0, + cfdp_file_data_deserialize(short_offset, + sizeof(short_offset), + CFDP_FILE_SIZE_SMALL, + CFDP_SEG_METADATA_PRESENT, + &out)); + return 0; +} + +static int test_pdu_payload_size_excludes_crc(void) +{ + cfdp_pdu_header_t hdr; + fill_header(&hdr); + hdr.data_field_length = 10; + + hdr.crc_flag = CFDP_CRC_ABSENT; + ASSERT_EQ_INT(10, cfdp_pdu_payload_size(&hdr)); + + /* §4.1.3.2: the CRC sits in the final octets of the data field and its + * length is counted in the data field length. */ + hdr.crc_flag = CFDP_CRC_PRESENT; + ASSERT_EQ_INT(8, cfdp_pdu_payload_size(&hdr)); + + /* A data field too short to hold the CRC it claims is malformed. */ + hdr.data_field_length = 1; + ASSERT_EQ_INT(0, cfdp_pdu_payload_size(&hdr)); + + ASSERT_EQ_INT(0, cfdp_pdu_payload_size(NULL)); + return 0; +} + +static int test_file_data_crc_octets_not_file_data(void) +{ + /* A File Data PDU whose data field is a 4-octet offset, three file octets + * and a 2-octet CRC. Only the three file octets may reach the file. */ + const uint8_t data_field[] = {0x00, 0x00, 0x00, 0x00, 'A', 'B', 'C', 0x12, 0x34}; + + cfdp_pdu_header_t hdr; + fill_header(&hdr); + hdr.pdu_type = CFDP_PDU_TYPE_FILE_DATA; + hdr.crc_flag = CFDP_CRC_PRESENT; + hdr.data_field_length = (uint16_t)sizeof(data_field); + + cfdp_file_data_pdu_t fd; + size_t payload_len = cfdp_pdu_payload_size(&hdr); + ASSERT_EQ_INT(sizeof(data_field) - CFDP_PDU_CRC_LEN, payload_len); + ASSERT_EQ_INT(payload_len, + cfdp_file_data_deserialize(data_field, + payload_len, + hdr.large_file_flag, + hdr.segment_metadata_flag, + &fd)); + ASSERT_EQ_INT(3, fd.file_data_len); + ASSERT_EQ_MEM("ABC", fd.file_data, 3); + return 0; +} + +test_result_t test_cfdp_pdu_run_all(void) +{ + RUN_TEST(test_header_exact_bytes); + RUN_TEST(test_header_all_flags); + RUN_TEST(test_header_roundtrip_large_ids); + RUN_TEST(test_file_data_roundtrip); + RUN_TEST(test_file_data_empty_payload); + RUN_TEST(test_file_data_large_file_roundtrip); + RUN_TEST(test_directive_code_peek); + RUN_TEST(test_header_size_invalid_lengths); + RUN_TEST(test_header_serialize_invalid_args); + RUN_TEST(test_header_deserialize_invalid_args); + RUN_TEST(test_header_serialize_rejects_other_versions); + RUN_TEST(test_header_deserialize_rejects_other_versions); + RUN_TEST(test_file_data_serialize_invalid_args); + RUN_TEST(test_file_data_deserialize_invalid_args); + RUN_TEST(test_file_data_segment_metadata_exact_bytes); + RUN_TEST(test_file_data_segment_metadata_roundtrip); + RUN_TEST(test_file_data_segment_metadata_absent_fields_cleared); + RUN_TEST(test_file_data_segment_metadata_mismatched_flag); + RUN_TEST(test_file_data_segment_metadata_truncated); + RUN_TEST(test_pdu_payload_size_excludes_crc); + RUN_TEST(test_file_data_crc_octets_not_file_data); + + /* cunit.h keeps its tally in file-local statics, so these counters cover + * only the tests run above. */ + test_result_t result = {cunit_total_tests - cunit_overall_failures, cunit_total_tests}; + return result; +} diff --git a/tests/test_cfdp_tlv.c b/tests/test_cfdp_tlv.c new file mode 100644 index 0000000..58e68e2 --- /dev/null +++ b/tests/test_cfdp_tlv.c @@ -0,0 +1,671 @@ +/** + * @file test_cfdp_tlv.c + * @brief Unit tests for the LV and TLV parameter codecs + * + * Exercises src/cfdp_tlv.c against CCSDS 727.0-B-5 Section 5.1.8, Section 5.1.9 + * and Section 5.4 with round-trip and known-vector checks. + * See also: docs/727x0b5e1.pdf + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp.h" +#include "cunit.h" +#include "test_runners.h" + +static int test_lv_roundtrip(void) +{ + uint8_t buf[16]; + ASSERT_EQ_INT(4, cfdp_lv_serialize("abc", 3, buf, sizeof(buf))); + ASSERT_EQ_INT(3, buf[0]); + ASSERT_EQ_MEM("abc", &buf[1], 3); + + const char *value = NULL; + uint8_t value_len = 0; + ASSERT_EQ_INT(4, cfdp_lv_deserialize(buf, sizeof(buf), &value, &value_len)); + ASSERT_EQ_INT(3, value_len); + ASSERT_EQ_MEM("abc", value, 3); + + /* An empty value is a bare length octet and decodes to a NULL pointer. */ + ASSERT_EQ_INT(1, cfdp_lv_serialize(NULL, 0, buf, sizeof(buf))); + ASSERT_EQ_INT(1, cfdp_lv_deserialize(buf, sizeof(buf), &value, &value_len)); + ASSERT_TRUE(!value); + ASSERT_EQ_INT(0, value_len); + return 0; +} + +static int test_lv_invalid_args(void) +{ + uint8_t buf[4] = {3, 'a', 'b', 'c'}; + const char *value = NULL; + uint8_t value_len = 0; + + ASSERT_EQ_INT(0, cfdp_lv_serialize("abc", 3, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_lv_serialize(NULL, 3, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_lv_serialize("abc", 3, buf, 3)); + + ASSERT_EQ_INT(0, cfdp_lv_deserialize(NULL, sizeof(buf), &value, &value_len)); + ASSERT_EQ_INT(0, cfdp_lv_deserialize(buf, sizeof(buf), NULL, &value_len)); + ASSERT_EQ_INT(0, cfdp_lv_deserialize(buf, sizeof(buf), &value, NULL)); + ASSERT_EQ_INT(0, cfdp_lv_deserialize(buf, 0, &value, &value_len)); + ASSERT_EQ_INT(0, cfdp_lv_deserialize(buf, 3, &value, &value_len)); + return 0; +} + +static int test_tlv_roundtrip(void) +{ + const uint8_t message[] = {'c', 'f', 'd', 'p'}; + cfdp_tlv_t tlv = {0}; + tlv.type = (uint8_t)CFDP_TLV_MESSAGE_TO_USER; + tlv.length = sizeof(message); + tlv.value = message; + + uint8_t buf[16]; + ASSERT_EQ_INT(2 + sizeof(message), cfdp_tlv_serialize(&tlv, buf, sizeof(buf))); + ASSERT_EQ_INT(0x02, buf[0]); + ASSERT_EQ_INT(4, buf[1]); + + cfdp_tlv_t out = {0}; + ASSERT_EQ_INT(2 + sizeof(message), cfdp_tlv_deserialize(buf, sizeof(buf), &out)); + ASSERT_EQ_INT(CFDP_TLV_MESSAGE_TO_USER, out.type); + ASSERT_EQ_INT(sizeof(message), out.length); + ASSERT_EQ_MEM(message, out.value, sizeof(message)); + + /* A zero-length TLV is legal and carries no value pointer. */ + cfdp_tlv_t empty = {0}; + empty.type = (uint8_t)CFDP_TLV_FLOW_LABEL; + ASSERT_EQ_INT(2, cfdp_tlv_serialize(&empty, buf, sizeof(buf))); + ASSERT_EQ_INT(2, cfdp_tlv_deserialize(buf, sizeof(buf), &out)); + ASSERT_TRUE(!out.value); + return 0; +} + +static int test_tlv_invalid_args(void) +{ + const uint8_t value[] = {0xAA}; + cfdp_tlv_t tlv = {0}; + tlv.type = (uint8_t)CFDP_TLV_FLOW_LABEL; + tlv.length = 1; + tlv.value = value; + + uint8_t buf[4] = {0x05, 0x01, 0xAA, 0x00}; + cfdp_tlv_t out = {0}; + + ASSERT_EQ_INT(0, cfdp_tlv_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_tlv_serialize(&tlv, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_tlv_serialize(&tlv, buf, 2)); + + cfdp_tlv_t no_value = tlv; + no_value.value = NULL; + ASSERT_EQ_INT(0, cfdp_tlv_serialize(&no_value, buf, sizeof(buf))); + + ASSERT_EQ_INT(0, cfdp_tlv_deserialize(NULL, sizeof(buf), &out)); + ASSERT_EQ_INT(0, cfdp_tlv_deserialize(buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(0, cfdp_tlv_deserialize(buf, 1, &out)); + ASSERT_EQ_INT(0, cfdp_tlv_deserialize(buf, 2, &out)); + return 0; +} + +static int test_entity_id_tlv_roundtrip(void) +{ + uint8_t buf[16]; + ASSERT_EQ_INT(3, cfdp_entity_id_tlv_serialize(0x2A, 1, buf, sizeof(buf))); + ASSERT_EQ_INT(CFDP_TLV_ENTITY_ID, buf[0]); + ASSERT_EQ_INT(1, buf[1]); + ASSERT_EQ_INT(0x2A, buf[2]); + + uint64_t entity_id = 0; + uint8_t id_len = 0; + ASSERT_EQ_INT(3, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, &id_len)); + ASSERT_TRUE(entity_id == 0x2AULL); + ASSERT_EQ_INT(1, id_len); + + ASSERT_EQ_INT(10, cfdp_entity_id_tlv_serialize(0x0102030405060708ULL, 8, buf, sizeof(buf))); + ASSERT_EQ_INT(10, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, &id_len)); + ASSERT_TRUE(entity_id == 0x0102030405060708ULL); + ASSERT_EQ_INT(8, id_len); + return 0; +} + +static int test_entity_id_tlv_invalid_args(void) +{ + uint8_t buf[16] = {0}; + uint64_t entity_id = 0; + uint8_t id_len = 0; + + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_serialize(1, 1, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_serialize(1, 0, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_serialize(1, 9, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_serialize(1, 4, buf, 5)); + + ASSERT_EQ_INT(3, cfdp_entity_id_tlv_serialize(7, 1, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), NULL, &id_len)); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, NULL)); + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, 1, &entity_id, &id_len)); + + /* Right shape, wrong type; then the right type with an illegal ID length. */ + buf[0] = (uint8_t)CFDP_TLV_FLOW_LABEL; + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, &id_len)); + buf[0] = (uint8_t)CFDP_TLV_ENTITY_ID; + buf[1] = 0; + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, &id_len)); + buf[1] = 9; + ASSERT_EQ_INT(0, cfdp_entity_id_tlv_deserialize(buf, sizeof(buf), &entity_id, &id_len)); + return 0; +} + +static int test_fault_handler_tlv_roundtrip(void) +{ + uint8_t buf[8]; + size_t n = cfdp_fault_handler_tlv_serialize(CFDP_COND_FILESTORE_REJECTION, + CFDP_HANDLER_IGNORE_ERROR, + buf, + sizeof(buf)); + ASSERT_EQ_INT(3, n); + ASSERT_EQ_INT(CFDP_TLV_FAULT_HANDLER_OVERRIDE, buf[0]); + ASSERT_EQ_INT(1, buf[1]); + ASSERT_EQ_INT(0x43, buf[2]); + + cfdp_condition_code_t condition = CFDP_COND_NO_ERROR; + cfdp_fault_handler_code_t handler = CFDP_HANDLER_RESERVED; + ASSERT_EQ_INT(3, cfdp_fault_handler_tlv_deserialize(buf, n, &condition, &handler)); + ASSERT_EQ_INT(CFDP_COND_FILESTORE_REJECTION, condition); + ASSERT_EQ_INT(CFDP_HANDLER_IGNORE_ERROR, handler); + return 0; +} + +static int test_fault_handler_tlv_invalid_args(void) +{ + uint8_t buf[8] = {0}; + cfdp_condition_code_t condition = CFDP_COND_NO_ERROR; + cfdp_fault_handler_code_t handler = CFDP_HANDLER_RESERVED; + + /* A valid condition/handler pair, so only the buffer is at fault. */ + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_serialize(CFDP_COND_INACTIVITY_DETECTED, + CFDP_HANDLER_IGNORE_ERROR, + NULL, + sizeof(buf))); + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_serialize(CFDP_COND_INACTIVITY_DETECTED, + CFDP_HANDLER_IGNORE_ERROR, + buf, + 2)); + + ASSERT_EQ_INT(3, + cfdp_fault_handler_tlv_serialize(CFDP_COND_NAK_LIMIT_REACHED, + CFDP_HANDLER_ABANDON_TRANSACTION, + buf, + sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_fault_handler_tlv_deserialize(buf, sizeof(buf), NULL, &handler)); + ASSERT_EQ_INT(0, cfdp_fault_handler_tlv_deserialize(buf, sizeof(buf), &condition, NULL)); + ASSERT_EQ_INT(0, cfdp_fault_handler_tlv_deserialize(buf, 1, &condition, &handler)); + + buf[0] = (uint8_t)CFDP_TLV_FLOW_LABEL; + ASSERT_EQ_INT(0, cfdp_fault_handler_tlv_deserialize(buf, sizeof(buf), &condition, &handler)); + buf[0] = (uint8_t)CFDP_TLV_FAULT_HANDLER_OVERRIDE; + buf[1] = 2; + ASSERT_EQ_INT(0, cfdp_fault_handler_tlv_deserialize(buf, sizeof(buf), &condition, &handler)); + return 0; +} + +static int test_fault_handler_tlv_rejects_non_fault_conditions(void) +{ + uint8_t buf[8]; + + /* Table 5-19: these conditions are not faults, so cannot be overridden. */ + const cfdp_condition_code_t not_faults[] = {CFDP_COND_NO_ERROR, + CFDP_COND_SUSPEND_REQUEST_RECEIVED, + CFDP_COND_CANCEL_REQUEST_RECEIVED, + (cfdp_condition_code_t)0xC, + (cfdp_condition_code_t)0xD}; + for (size_t i = 0; i < sizeof(not_faults) / sizeof(not_faults[0]); i++) + { + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_serialize(not_faults[i], + CFDP_HANDLER_IGNORE_ERROR, + buf, + sizeof(buf))); + } + + /* Every fault condition, '0001' through '1011', is accepted. */ + for (uint8_t c = 0x1; c <= 0xB; c++) + { + ASSERT_EQ_INT(3, + cfdp_fault_handler_tlv_serialize((cfdp_condition_code_t)c, + CFDP_HANDLER_IGNORE_ERROR, + buf, + sizeof(buf))); + } + return 0; +} + +static int test_fault_handler_tlv_rejects_reserved_handlers(void) +{ + uint8_t buf[8]; + + /* '0000' and '0101'-'1111' are reserved in table 5-19. */ + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_serialize(CFDP_COND_FILE_SIZE_ERROR, + CFDP_HANDLER_RESERVED, + buf, + sizeof(buf))); + for (uint8_t h = 0x5; h <= 0xF; h++) + { + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_serialize(CFDP_COND_FILE_SIZE_ERROR, + (cfdp_fault_handler_code_t)h, + buf, + sizeof(buf))); + } + + /* The four defined handlers are accepted. */ + for (uint8_t h = 0x1; h <= 0x4; h++) + { + ASSERT_EQ_INT(3, + cfdp_fault_handler_tlv_serialize(CFDP_COND_FILE_SIZE_ERROR, + (cfdp_fault_handler_code_t)h, + buf, + sizeof(buf))); + } + return 0; +} + +static int test_fault_handler_tlv_deserialize_rejects_invalid_codes(void) +{ + cfdp_condition_code_t condition = CFDP_COND_FILE_SIZE_ERROR; + cfdp_fault_handler_code_t handler = CFDP_HANDLER_ABANDON_TRANSACTION; + + /* Condition 'No error' with a valid handler. */ + const uint8_t no_error[] = {0x04, 0x01, 0x03}; + ASSERT_EQ_INT( + 0, + cfdp_fault_handler_tlv_deserialize(no_error, sizeof(no_error), &condition, &handler)); + + /* Condition 'Cancel.request received' with a valid handler. */ + const uint8_t cancel[] = {0x04, 0x01, 0xF3}; + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_deserialize(cancel, sizeof(cancel), &condition, &handler)); + + /* A fault condition with the reserved handler '0000'. */ + const uint8_t reserved_zero[] = {0x04, 0x01, 0x50}; + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_deserialize(reserved_zero, + sizeof(reserved_zero), + &condition, + &handler)); + + /* A fault condition with the reserved handler '0101'. */ + const uint8_t reserved_five[] = {0x04, 0x01, 0x55}; + ASSERT_EQ_INT(0, + cfdp_fault_handler_tlv_deserialize(reserved_five, + sizeof(reserved_five), + &condition, + &handler)); + + /* A rejected TLV leaves the outputs as they were. */ + ASSERT_EQ_INT(CFDP_COND_FILE_SIZE_ERROR, condition); + ASSERT_EQ_INT(CFDP_HANDLER_ABANDON_TRANSACTION, handler); + + /* 'File checksum failure' -> 'Notice of Suspension' decodes. */ + const uint8_t valid[] = {0x04, 0x01, 0x52}; + ASSERT_EQ_INT(3, + cfdp_fault_handler_tlv_deserialize(valid, sizeof(valid), &condition, &handler)); + ASSERT_EQ_INT(CFDP_COND_FILE_CHECKSUM_FAILURE, condition); + ASSERT_EQ_INT(CFDP_HANDLER_NOTICE_OF_SUSPENSION, handler); + return 0; +} + +static int test_filestore_action_second_filename(void) +{ + ASSERT_TRUE(cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_RENAME_FILE)); + ASSERT_TRUE(cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_APPEND_FILE)); + ASSERT_TRUE(cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_REPLACE_FILE)); + + ASSERT_TRUE(!cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_CREATE_FILE)); + ASSERT_TRUE(!cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_DELETE_FILE)); + ASSERT_TRUE(!cfdp_filestore_action_has_second_filename(CFDP_FS_ACTION_DENY_DIRECTORY)); + return 0; +} + +static int test_filestore_request_roundtrip(void) +{ + cfdp_filestore_request_t req = {0}; + req.action_code = CFDP_FS_ACTION_DELETE_FILE; + req.first_filename = "old.dat"; + req.first_filename_len = 7; + + uint8_t buf[32]; + size_t n = cfdp_filestore_request_tlv_serialize(&req, buf, sizeof(buf)); + ASSERT_EQ_INT(2 + 1 + 1 + 7, n); + ASSERT_EQ_INT(CFDP_TLV_FILESTORE_REQUEST, buf[0]); + ASSERT_EQ_INT(9, buf[1]); + ASSERT_EQ_INT(0x10, buf[2]); + + cfdp_filestore_request_t out = {0}; + ASSERT_EQ_INT(n, cfdp_filestore_request_tlv_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_FS_ACTION_DELETE_FILE, out.action_code); + ASSERT_EQ_INT(7, out.first_filename_len); + ASSERT_EQ_MEM("old.dat", out.first_filename, 7); + ASSERT_TRUE(!out.second_filename); + ASSERT_EQ_INT(0, out.second_filename_len); + return 0; +} + +static int test_filestore_request_second_filename(void) +{ + cfdp_filestore_request_t req = {0}; + req.action_code = CFDP_FS_ACTION_RENAME_FILE; + req.first_filename = "a.dat"; + req.first_filename_len = 5; + req.second_filename = "b.dat"; + req.second_filename_len = 5; + + uint8_t buf[32]; + size_t n = cfdp_filestore_request_tlv_serialize(&req, buf, sizeof(buf)); + ASSERT_EQ_INT(2 + 1 + 6 + 6, n); + + cfdp_filestore_request_t out = {0}; + ASSERT_EQ_INT(n, cfdp_filestore_request_tlv_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_FS_ACTION_RENAME_FILE, out.action_code); + ASSERT_EQ_MEM("a.dat", out.first_filename, 5); + ASSERT_EQ_MEM("b.dat", out.second_filename, 5); + return 0; +} + +static int test_filestore_request_invalid_args(void) +{ + cfdp_filestore_request_t req = {0}; + req.action_code = CFDP_FS_ACTION_RENAME_FILE; + req.first_filename = "a.dat"; + req.first_filename_len = 5; + req.second_filename = "b.dat"; + req.second_filename_len = 5; + + uint8_t buf[32]; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(&req, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(&req, buf, 2)); + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(&req, buf, 9)); + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(&req, buf, 14)); + + size_t n = cfdp_filestore_request_tlv_serialize(&req, buf, sizeof(buf)); + cfdp_filestore_request_t out = {0}; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_deserialize(buf, n, NULL)); + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_deserialize(buf, 1, &out)); + + buf[0] = (uint8_t)CFDP_TLV_FLOW_LABEL; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_deserialize(buf, n, &out)); + + /* Right type, but a value that cannot hold even the action octet. */ + const uint8_t empty_value[] = {CFDP_TLV_FILESTORE_REQUEST, 0x00}; + ASSERT_EQ_INT(0, + cfdp_filestore_request_tlv_deserialize(empty_value, sizeof(empty_value), &out)); + return 0; +} + +static int test_filestore_request_malformed_value(void) +{ + cfdp_filestore_request_t out = {0}; + + /* The name LV claims 5 octets but the TLV value holds 2. */ + const uint8_t truncated[] = {CFDP_TLV_FILESTORE_REQUEST, 0x03, 0x10, 0x05, 'a'}; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_deserialize(truncated, sizeof(truncated), &out)); + + /* A well-formed name LV followed by an octet the format does not allow. */ + const uint8_t trailing[] = {CFDP_TLV_FILESTORE_REQUEST, 0x04, 0x10, 0x01, 'a', 0xFF}; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_deserialize(trailing, sizeof(trailing), &out)); + + /* Rename carries two names, but only the first is present. */ + const uint8_t missing_second[] = {CFDP_TLV_FILESTORE_REQUEST, 0x03, 0x20, 0x01, 'a'}; + ASSERT_EQ_INT( + 0, + cfdp_filestore_request_tlv_deserialize(missing_second, sizeof(missing_second), &out)); + return 0; +} + +static int test_filestore_response_roundtrip(void) +{ + cfdp_filestore_response_t resp = {0}; + resp.action_code = CFDP_FS_ACTION_CREATE_FILE; + resp.status_code = CFDP_FS_STATUS_NOT_PERFORMED; + resp.first_filename = "new.dat"; + resp.first_filename_len = 7; + resp.message = "no space"; + resp.message_len = 8; + + uint8_t buf[64]; + size_t n = cfdp_filestore_response_tlv_serialize(&resp, buf, sizeof(buf)); + ASSERT_EQ_INT(2 + 1 + 8 + 9, n); + ASSERT_EQ_INT(CFDP_TLV_FILESTORE_RESPONSE, buf[0]); + ASSERT_EQ_INT(0x0F, buf[2]); + + cfdp_filestore_response_t out = {0}; + ASSERT_EQ_INT(n, cfdp_filestore_response_tlv_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_FS_ACTION_CREATE_FILE, out.action_code); + ASSERT_EQ_INT(CFDP_FS_STATUS_NOT_PERFORMED, out.status_code); + ASSERT_EQ_MEM("new.dat", out.first_filename, 7); + ASSERT_EQ_MEM("no space", out.message, 8); + ASSERT_TRUE(!out.second_filename); + return 0; +} + +static int test_filestore_response_second_filename(void) +{ + cfdp_filestore_response_t resp = {0}; + resp.action_code = CFDP_FS_ACTION_REPLACE_FILE; + resp.status_code = CFDP_FS_STATUS_ERROR_2; + resp.first_filename = "a.dat"; + resp.first_filename_len = 5; + resp.second_filename = "b.dat"; + resp.second_filename_len = 5; + + uint8_t buf[64]; + size_t n = cfdp_filestore_response_tlv_serialize(&resp, buf, sizeof(buf)); + ASSERT_EQ_INT(2 + 1 + 6 + 6 + 1, n); + + cfdp_filestore_response_t out = {0}; + ASSERT_EQ_INT(n, cfdp_filestore_response_tlv_deserialize(buf, n, &out)); + ASSERT_EQ_INT(CFDP_FS_STATUS_ERROR_2, out.status_code); + ASSERT_EQ_MEM("a.dat", out.first_filename, 5); + ASSERT_EQ_MEM("b.dat", out.second_filename, 5); + ASSERT_TRUE(!out.message); + ASSERT_EQ_INT(0, out.message_len); + return 0; +} + +static int test_filestore_response_invalid_args(void) +{ + cfdp_filestore_response_t resp = {0}; + resp.action_code = CFDP_FS_ACTION_DELETE_FILE; + resp.first_filename = "a.dat"; + resp.first_filename_len = 5; + resp.message = "gone"; + resp.message_len = 4; + + uint8_t buf[64]; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(&resp, NULL, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(&resp, buf, 2)); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(&resp, buf, 8)); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(&resp, buf, 12)); + + size_t n = cfdp_filestore_response_tlv_serialize(&resp, buf, sizeof(buf)); + cfdp_filestore_response_t out = {0}; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(buf, n, NULL)); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(buf, 1, &out)); + + buf[0] = (uint8_t)CFDP_TLV_FILESTORE_REQUEST; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(buf, n, &out)); + + const uint8_t empty_value[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x00}; + ASSERT_EQ_INT(0, + cfdp_filestore_response_tlv_deserialize(empty_value, sizeof(empty_value), &out)); + return 0; +} + +static int test_filestore_response_malformed_value(void) +{ + cfdp_filestore_response_t out = {0}; + + /* The name LV claims 5 octets but the TLV value holds 2. */ + const uint8_t bad_name[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x03, 0x10, 0x05, 'a'}; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(bad_name, sizeof(bad_name), &out)); + + /* Name LV present, message LV missing. */ + const uint8_t no_message[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x03, 0x10, 0x01, 'a'}; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(no_message, sizeof(no_message), &out)); + + /* Name and message LVs, then an octet the format does not allow. */ + const uint8_t trailing[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x05, 0x10, 0x01, 'a', 0x00, 0xFF}; + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_deserialize(trailing, sizeof(trailing), &out)); + return 0; +} + +static int test_filestore_request_rejects_undefined_actions(void) +{ + /* Both names supplied, so every defined action encodes regardless of + * whether it carries a second file name. */ + cfdp_filestore_request_t req = {0}; + req.first_filename = "a"; + req.first_filename_len = 1; + req.second_filename = "b"; + req.second_filename_len = 1; + + uint8_t buf[16]; + for (uint8_t code = 0x0; code <= 0xF; code++) + { + req.action_code = (cfdp_filestore_action_t)code; + size_t n = cfdp_filestore_request_tlv_serialize(&req, buf, sizeof(buf)); + + /* Table 5-16 defines '0000'-'1000' only. */ + if (code <= CFDP_FS_ACTION_DENY_DIRECTORY) + { + ASSERT_TRUE(n > 0); + } + else + { + ASSERT_EQ_INT(0, n); + } + } + return 0; +} + +static int test_filestore_response_rejects_undefined_actions(void) +{ + cfdp_filestore_response_t resp = {0}; + resp.first_filename = "a"; + resp.first_filename_len = 1; + resp.second_filename = "b"; + resp.second_filename_len = 1; + + uint8_t buf[16]; + for (uint8_t code = 0x0; code <= 0xF; code++) + { + resp.action_code = (cfdp_filestore_action_t)code; + size_t n = cfdp_filestore_response_tlv_serialize(&resp, buf, sizeof(buf)); + + /* Table 5-17: the action code is as for the Filestore Request TLV. */ + if (code <= CFDP_FS_ACTION_DENY_DIRECTORY) + { + ASSERT_TRUE(n > 0); + } + else + { + ASSERT_EQ_INT(0, n); + } + } + return 0; +} + +static int test_filestore_tlv_deserialize_rejects_undefined_actions(void) +{ + cfdp_filestore_request_t req = {0}; + cfdp_filestore_response_t resp = {0}; + + /* Well-formed apart from the action code: one empty file name LV. */ + const uint8_t req_action_9[] = {CFDP_TLV_FILESTORE_REQUEST, 0x02, 0x90, 0x00}; + ASSERT_EQ_INT(0, + cfdp_filestore_request_tlv_deserialize(req_action_9, sizeof(req_action_9), &req)); + const uint8_t req_action_f[] = {CFDP_TLV_FILESTORE_REQUEST, 0x02, 0xF0, 0x00}; + ASSERT_EQ_INT(0, + cfdp_filestore_request_tlv_deserialize(req_action_f, sizeof(req_action_f), &req)); + + /* Deny Directory, '1000', is the last defined action and still decodes. */ + const uint8_t req_action_8[] = {CFDP_TLV_FILESTORE_REQUEST, 0x02, 0x80, 0x00}; + ASSERT_EQ_INT(4, + cfdp_filestore_request_tlv_deserialize(req_action_8, sizeof(req_action_8), &req)); + ASSERT_EQ_INT(CFDP_FS_ACTION_DENY_DIRECTORY, req.action_code); + + /* Responses: an empty file name LV and an empty message LV. */ + const uint8_t resp_action_b[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x03, 0xB0, 0x00, 0x00}; + ASSERT_EQ_INT( + 0, + cfdp_filestore_response_tlv_deserialize(resp_action_b, sizeof(resp_action_b), &resp)); + + const uint8_t resp_action_8[] = {CFDP_TLV_FILESTORE_RESPONSE, 0x03, 0x80, 0x00, 0x00}; + ASSERT_EQ_INT( + 5, + cfdp_filestore_response_tlv_deserialize(resp_action_8, sizeof(resp_action_8), &resp)); + ASSERT_EQ_INT(CFDP_FS_ACTION_DENY_DIRECTORY, resp.action_code); + return 0; +} + +static int test_filestore_tlv_value_too_long(void) +{ + static char name[255]; + memset(name, 'x', sizeof(name)); + + /* Two 255-octet names plus the action octet overflow the 8-bit TLV length. */ + cfdp_filestore_request_t req = {0}; + req.action_code = CFDP_FS_ACTION_RENAME_FILE; + req.first_filename = name; + req.first_filename_len = (uint8_t)sizeof(name); + req.second_filename = name; + req.second_filename_len = (uint8_t)sizeof(name); + + static uint8_t buf[600]; + ASSERT_EQ_INT(0, cfdp_filestore_request_tlv_serialize(&req, buf, sizeof(buf))); + + cfdp_filestore_response_t resp = {0}; + resp.action_code = CFDP_FS_ACTION_RENAME_FILE; + resp.first_filename = name; + resp.first_filename_len = (uint8_t)sizeof(name); + resp.second_filename = name; + resp.second_filename_len = (uint8_t)sizeof(name); + ASSERT_EQ_INT(0, cfdp_filestore_response_tlv_serialize(&resp, buf, sizeof(buf))); + return 0; +} + +test_result_t test_cfdp_tlv_run_all(void) +{ + RUN_TEST(test_lv_roundtrip); + RUN_TEST(test_lv_invalid_args); + RUN_TEST(test_tlv_roundtrip); + RUN_TEST(test_tlv_invalid_args); + RUN_TEST(test_entity_id_tlv_roundtrip); + RUN_TEST(test_entity_id_tlv_invalid_args); + RUN_TEST(test_fault_handler_tlv_roundtrip); + RUN_TEST(test_fault_handler_tlv_invalid_args); + RUN_TEST(test_fault_handler_tlv_rejects_non_fault_conditions); + RUN_TEST(test_fault_handler_tlv_rejects_reserved_handlers); + RUN_TEST(test_fault_handler_tlv_deserialize_rejects_invalid_codes); + RUN_TEST(test_filestore_action_second_filename); + RUN_TEST(test_filestore_request_roundtrip); + RUN_TEST(test_filestore_request_second_filename); + RUN_TEST(test_filestore_request_invalid_args); + RUN_TEST(test_filestore_request_malformed_value); + RUN_TEST(test_filestore_response_roundtrip); + RUN_TEST(test_filestore_response_second_filename); + RUN_TEST(test_filestore_response_invalid_args); + RUN_TEST(test_filestore_response_malformed_value); + RUN_TEST(test_filestore_tlv_value_too_long); + RUN_TEST(test_filestore_request_rejects_undefined_actions); + RUN_TEST(test_filestore_response_rejects_undefined_actions); + RUN_TEST(test_filestore_tlv_deserialize_rejects_undefined_actions); + + /* cunit.h keeps its tally in file-local statics, so these counters cover + * only the tests run above. */ + test_result_t result = {cunit_total_tests - cunit_overall_failures, cunit_total_tests}; + return result; +} diff --git a/tests/test_runners.h b/tests/test_runners.h new file mode 100644 index 0000000..12ff171 --- /dev/null +++ b/tests/test_runners.h @@ -0,0 +1,28 @@ +/** + * @file test_runners.h + * @brief Per-module unit test runner declarations + * + * One runner per source file under src/, each reporting its own tally to the + * unit_tests.c entry point. + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef TEST_RUNNERS_H +#define TEST_RUNNERS_H + +/** @brief Outcome tally of a module's test suite. */ +typedef struct +{ + int passed; /**< Number of tests that passed. */ + int total; /**< Number of tests run. */ +} test_result_t; + +/* Per-module test runners. Each runs all of its module's tests and returns the + * passed/total tally. */ +test_result_t test_cfdp_pdu_run_all(void); +test_result_t test_cfdp_directive_run_all(void); +test_result_t test_cfdp_tlv_run_all(void); +test_result_t test_cfdp_checksum_run_all(void); + +#endif /* TEST_RUNNERS_H */ diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 8ee320b..e99d2f1 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,21 +1,45 @@ -#include "cunit.h" +/** + * @file unit_tests.c + * @brief Unit test entry point: runs each module's suite and reports the tally + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "test_runners.h" + #include -#include -#include -static int test_case_0(void) { +/** @brief Print one module's tally in the summary layout. */ +#define REPORT(label, r) printf(" %-18s Passed %d/%d\n\n", label ":", (r).passed, (r).total) - return 0; -} +int main(void) +{ + test_result_t r; + int total_passed = 0; + int total_tests = 0; + + r = test_cfdp_pdu_run_all(); + REPORT("cfdp_pdu", r); + total_passed += r.passed; + total_tests += r.total; + + r = test_cfdp_directive_run_all(); + REPORT("cfdp_directive", r); + total_passed += r.passed; + total_tests += r.total; -int main(void) { - RUN_TEST(test_case_0); - - if (cunit_overall_failures == 0) { - printf("ALL TESTS PASSED\n"); - return 0; - } else { - printf("%d TEST(S) FAILED\n", cunit_overall_failures); - return 1; - } -} \ No newline at end of file + r = test_cfdp_tlv_run_all(); + REPORT("cfdp_tlv", r); + total_passed += r.passed; + total_tests += r.total; + + r = test_cfdp_checksum_run_all(); + REPORT("cfdp_checksum", r); + total_passed += r.passed; + total_tests += r.total; + + printf(" ------------------------------\n"); + printf(" %-18s Passed %d/%d\n", "All UT:", total_passed, total_tests); + + return (total_passed == total_tests) ? 0 : 1; +} diff --git a/tools/coverage-html.sh b/tools/coverage-html.sh index 49e354f..f38fabd 100644 --- a/tools/coverage-html.sh +++ b/tools/coverage-html.sh @@ -8,7 +8,9 @@ if [[ "${OUT_FILE}" != /* ]]; then OUT_FILE="${ROOT_DIR}/${OUT_FILE}" fi -COVERAGE_CFLAGS='-O0 -g --coverage -std=c11 -Iinclude -Itests -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef' +# Instrumentation is the ONLY thing that differs from the normal build; the C standard, +# include paths and warning set all come from the Makefile so they cannot drift apart. +COVERAGE_OPT='-O0 -g --coverage' cd "${ROOT_DIR}" @@ -19,14 +21,21 @@ if ! command -v gcovr >/dev/null 2>&1; then fi make clean >/dev/null -make build/tests/ctest CFLAGS="${COVERAGE_CFLAGS}" >/dev/null -./build/tests/ctest +make build/tests/ctest OPT="${COVERAGE_OPT}" >/dev/null +./build/tests/ctest >/dev/null mkdir -p "$(dirname "${OUT_FILE}")" + +# Emit the HTML report and a text summary (line + branch) in a single gcovr pass, so +# the console output is not duplicated. gcovr's chatty "(INFO)" progress lines are +# filtered from stderr; warnings and errors still pass through and preserve the exit code. +echo "Coverage:" gcovr -r "${ROOT_DIR}" \ --filter "${ROOT_DIR}/src" \ - --html \ --html-details \ - --output "${OUT_FILE}" + --output "${OUT_FILE}" \ + --txt - \ + --txt-summary \ + 2> >(grep -v '^(INFO)' >&2) -echo "Coverage HTML report written to: ${OUT_FILE}" +echo "Coverage HTML report written to: ${OUT_FILE}" \ No newline at end of file