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..15fa331 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,65 @@ -# 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/ccsds_cfdp.md`](docs/ccsds_cfdp.md) for implementation notes. ## 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. +- **File Directive PDUs** (§5.2, §5.4) — EOF, Finished, ACK, Metadata, NAK, + Prompt and Keep Alive. +- **Modular file checksum** (§4.2.2) — streaming, segment-order independent. ### Design Principles +- **No heap usage** — every buffer is caller-supplied. +- **No external dependencies** — C11, 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 ├── src/ -│ └── +│ ├── cfdp_checksum.c +│ ├── cfdp_pdu.c +│ └── cfdp_directive.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 +│ └── unit_tests.c # Test entry point +├── docs/ +│ └── ccsds_cfdp.md # Implementation notes +├── tools/ +│ └── coverage-html.sh # Coverage report +├── build/ # Build artifacts ├── Makefile └── README.md ``` @@ -45,14 +74,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 +99,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 +124,115 @@ make clean ## Quick Start -### Step 1 +### Step 1 — Serialise a header and an EOF PDU ```c +#include "cfdp.h" -``` +uint8_t buf[64]; -### Step 2 +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; -```c +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 X +### 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; +size_t payload_len = hdr.data_field_length; + +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, &fd); + /* write fd.file_data_len octets at fd.offset */ +} +``` ## API Reference -### Lifecycle +### 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); ``` -### Building a Packet +### 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, + 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_file_data_pdu_t *fd); ``` -### Utilities - -```c +### Directives (`cfdp_directive.h`) -``` +`cfdp_{eof,finished,ack,metadata,nak,prompt,keep_alive}_{serialize,deserialize}()` +— each returns the number of octets written or consumed, or `0` on error. -### 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. +- File Data segment metadata is not supported (the flag must be absent). +- The 16-bit CRC is not computed or checked; the CRC flag is preserved on the + wire but the trailer 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..2a00d9d --- /dev/null +++ b/examples/example.c @@ -0,0 +1,140 @@ +/** + * @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 + +#include "cfdp.h" + +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, 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); + cfdp_file_data_pdu_t fd; + cfdp_file_data_deserialize(&pdu[hlen], hdr.data_field_length, hdr.large_file_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..cb5e1f9 --- /dev/null +++ b/include/cfdp.h @@ -0,0 +1,21 @@ +/** + * @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 and the file checksum. + * Implements a basic subset of CCSDS 727.0-B-5 (CCSDS File Delivery Protocol). + * See also: docs/ccsds_cfdp.md + * + * 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" + +#endif /* CFDP_H */ diff --git a/include/cfdp_checksum.h b/include/cfdp_checksum.h new file mode 100644 index 0000000..87785f7 --- /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/ccsds_cfdp.md + * + * 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..037afea --- /dev/null +++ b/include/cfdp_common.h @@ -0,0 +1,233 @@ +/** + * @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/ccsds_cfdp.md + * + * 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) + +/* ------------------------------------------------------------------------- + * 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 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..89c1349 --- /dev/null +++ b/include/cfdp_directive.h @@ -0,0 +1,292 @@ +/** + * @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 Optional Type-Length-Value fields (fault location, filestore + * responses, filestore requests and message-to-user options) are not + * encoded or decoded by this basic implementation. + * See also: docs/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_DIRECTIVE_H +#define CFDP_DIRECTIVE_H + +#include +#include + +#include "cfdp_common.h" + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +/** @brief Maximum number of segment requests decoded from a NAK PDU. */ +#define CFDP_NAK_MAX_SEGMENT_REQUESTS 32U + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief End-of-File PDU contents (CCSDS 727.0-B-5 §5.2.2). + * + * @note The fault location TLV, present on a non-nominal condition code, is + * not encoded by this basic implementation. + */ +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. */ +} cfdp_eof_pdu_t; + +/** + * @brief Finished PDU contents (CCSDS 727.0-B-5 §5.2.3). + * + * @note Filestore responses and the fault location TLV are not encoded by + * this basic implementation. + */ +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. */ +} cfdp_finished_pdu_t; + +/** + * @brief Acknowledgement PDU contents (CCSDS 727.0-B-5 §5.2.4). + */ +typedef struct +{ + cfdp_directive_code_t ack_directive_code; /**< Directive being acknowledged (EOF/Finished). */ + uint8_t directive_subtype; /**< Directive subtype code (4-bit field). */ + 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 and @p destination_filename point into + * caller-owned memory; the library neither copies nor frees them. + * Option TLVs are not encoded or decoded by this basic implementation. + */ +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. */ +} 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 On decode, at most ::CFDP_NAK_MAX_SEGMENT_REQUESTS requests are + * stored; @p segment_request_count reflects how many were kept. + */ +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 or buffer too small). + */ +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. + * + * @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. + * @return Bytes consumed, or 0 on error (NULL args, wrong directive code, or + * truncated input). + */ +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. + */ +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. + * + * @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. + * @return Bytes consumed, or 0 on error. + */ +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. + * + * @param[in] ack ACK contents to serialise. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error. + */ +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. + * @return Bytes consumed, or 0 on error. + */ +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 pointers index into @p buf. + * @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. + * + * @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 32- or 64-bit offset fields. + * @param[out] nak Decoded contents. + * @return Bytes consumed, or 0 on error. + */ +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..c843d04 --- /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/ccsds_cfdp.md + * + * 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..88c4aca --- /dev/null +++ b/include/cfdp_pdu.h @@ -0,0 +1,142 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CFDP_PDU_H +#define CFDP_PDU_H + +#include +#include + +#include "cfdp_common.h" + +/* ------------------------------------------------------------------------- + * 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; /**< Protocol version (3-bit field). */ + 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). + * + * @note @p file_data points into caller-owned memory; the library neither + * copies nor frees it. Segment metadata is not supported: the header's + * segment metadata flag must be CFDP_SEG_METADATA_ABSENT. + */ +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_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, 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. + * @return Header size in octets consumed, or 0 on error (NULL args or + * truncated header). + */ +size_t cfdp_pdu_header_deserialize(const uint8_t *buf, size_t buf_len, cfdp_pdu_header_t *hdr); + +/** + * @brief Serialise a File Data PDU payload (offset plus file data). + * + * Writes the data field only; serialise the fixed header separately. + * + * @param[in] fd File Data payload to serialise. + * @param[in] large_file_flag Selects a 32- or 64-bit offset field. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return Bytes written, or 0 on error (NULL args or buffer too small). + */ +size_t cfdp_file_data_serialize(const cfdp_file_data_pdu_t *fd, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len); + +/** + * @brief Deserialise a File Data PDU payload from a data-field slice. + * + * @param[in] buf Data field, positioned at the segment offset. + * @param[in] buf_len Length of the data field in octets. + * @param[in] large_file_flag Selects a 32- or 64-bit offset field. + * @param[out] fd Decoded payload; @p fd->file_data points into @p buf. + * @return Bytes consumed (equal to @p buf_len), or 0 on error. + */ +size_t cfdp_file_data_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_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/src/cfdp_checksum.c b/src/cfdp_checksum.c new file mode 100644 index 0000000..a0ecffb --- /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/ccsds_cfdp.md + * + * 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..dff9916 --- /dev/null +++ b/src/cfdp_directive.c @@ -0,0 +1,405 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_directive.h" + +#include + +#include "cfdp_endian.h" + +/** + * @brief Write a Length-Value field (1-octet length prefix plus value). + * + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[in] value Value octets; may be NULL only when @p value_len is 0. + * @param[in] value_len Value length in octets. + * @return Bytes written, or 0 on error. + */ +static size_t cfdp_write_lv(uint8_t *buf, size_t buf_len, const char *value, uint8_t value_len) +{ + if ((value_len > 0) && (!value)) + { + return 0; + } + /* Unreachable from cfdp_metadata_serialize, which sizes the buffer for both + * LV fields up front; kept as a bounds check for any future call site, and + * excluded from coverage because no input can reach it. */ + if (buf_len < (size_t)value_len + 1U) /* GCOVR_EXCL_BR_LINE */ + { + return 0; /* GCOVR_EXCL_LINE */ + } + buf[0] = value_len; + if (value_len > 0) + { + memcpy(&buf[1], value, value_len); + } + return (size_t)value_len + 1U; +} + +/** + * @brief Read a Length-Value field, 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. + */ +static size_t cfdp_read_lv(const uint8_t *buf, size_t buf_len, const char **value, + uint8_t *value_len) +{ + if (buf_len < 1U) + { + return 0; + } + uint8_t len = buf[0]; + if (buf_len < (size_t)len + 1U) + { + return 0; + } + *value = (len > 0) ? (const char *)&buf[1] : NULL; + *value_len = len; + return (size_t)len + 1U; +} + +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; + } + + 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); + + return size; +} + +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); + + return size; +} + +size_t cfdp_finished_serialize(const cfdp_finished_pdu_t *fin, uint8_t *buf, size_t buf_len) +{ + if ((!fin) || (!buf) || (buf_len < 2U)) + { + 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)); + + return 2; +} + +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); + + return 2; +} + +size_t cfdp_ack_serialize(const cfdp_ack_pdu_t *ack, uint8_t *buf, size_t buf_len) +{ + if ((!ack) || (!buf) || (buf_len < 3U)) + { + return 0; + } + + buf[0] = (uint8_t)CFDP_DIRECTIVE_ACK; + buf[1] = (uint8_t)((((uint8_t)ack->ack_directive_code & 0xFU) << 4) | + (ack->directive_subtype & 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; + } + + ack->ack_directive_code = (cfdp_directive_code_t)((buf[1] >> 4) & 0xFU); + ack->directive_subtype = (uint8_t)(buf[1] & 0xFU); + 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)) + { + 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 + 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_write_lv(&buf[pos], buf_len - pos, md->source_filename, + md->source_filename_len); + if (n == 0) + { + return 0; + } + pos += n; + + n = cfdp_write_lv(&buf[pos], buf_len - pos, md->destination_filename, + md->destination_filename_len); + if (n == 0) + { + return 0; + } + return pos + n; +} + +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_read_lv(&buf[pos], buf_len - pos, &md->source_filename, + &md->source_filename_len); + if (n == 0) + { + return 0; + } + pos += n; + + n = cfdp_read_lv(&buf[pos], buf_len - pos, &md->destination_filename, + &md->destination_filename_len); + if (n == 0) + { + return 0; + } + return pos + n; +} + +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; +} + +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 count = (buf_len - pos) / pair; + nak->segment_request_count = 0; + for (size_t i = 0; (i < count) && (i < CFDP_NAK_MAX_SEGMENT_REQUESTS); 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_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..2318e60 --- /dev/null +++ b/src/cfdp_pdu.c @@ -0,0 +1,211 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cfdp_pdu.h" + +#include + +#include "cfdp_endian.h" + +/** + * @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)) + { + 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); + + /* 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_file_data_serialize(const cfdp_file_data_pdu_t *fd, + cfdp_large_file_flag_t large_file_flag, + uint8_t *buf, + size_t buf_len) +{ + if ((!fd) || (!buf) || ((!fd->file_data) && (fd->file_data_len > 0))) + { + return 0; + } + + uint8_t offset_octets = cfdp_file_size_octets(large_file_flag); + size_t size = (size_t)offset_octets + fd->file_data_len; + if (buf_len < size) + { + return 0; + } + + cfdp_write_uint(buf, fd->offset, offset_octets); + if (fd->file_data_len > 0) + { + memcpy(&buf[offset_octets], fd->file_data, fd->file_data_len); + } + + return size; +} + +size_t cfdp_file_data_deserialize(const uint8_t *buf, + size_t buf_len, + cfdp_large_file_flag_t large_file_flag, + cfdp_file_data_pdu_t *fd) +{ + if ((!buf) || (!fd)) + { + return 0; + } + + uint8_t offset_octets = cfdp_file_size_octets(large_file_flag); + if (buf_len < offset_octets) + { + return 0; + } + + fd->offset = cfdp_read_uint(buf, offset_octets); + fd->file_data = (buf_len > offset_octets) ? &buf[offset_octets] : NULL; + fd->file_data_len = buf_len - offset_octets; + + 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/tests/test_cfdp_checksum.c b/tests/test_cfdp_checksum.c new file mode 100644 index 0000000..1600feb --- /dev/null +++ b/tests/test_cfdp_checksum.c @@ -0,0 +1,64 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cunit.h" +#include "test_runners.h" + +#include "cfdp.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..a966ad4 --- /dev/null +++ b/tests/test_cfdp_directive.c @@ -0,0 +1,444 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cunit.h" +#include "test_runners.h" + +#include "cfdp.h" + +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}; + fin.condition_code = CFDP_COND_FILE_CHECKSUM_FAILURE; + 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_FILE_CHECKSUM_FAILURE, out.condition_code); + ASSERT_EQ_INT(CFDP_DELIVERY_INCOMPLETE, out.delivery_code); + ASSERT_EQ_INT(CFDP_FILE_STATUS_RETAINED, out.file_status); + return 0; +} + +static int test_ack_roundtrip(void) +{ + cfdp_ack_pdu_t ack = {0}; + ack.ack_directive_code = CFDP_DIRECTIVE_FINISHED; + ack.directive_subtype = 1; + 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(1, out.directive_subtype); + ASSERT_EQ_INT(CFDP_TXN_STATUS_ACTIVE, 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_FILE_SIZE_ERROR; + eof.file_checksum = 0xAABBCCDDU; + eof.file_size = 0x0000000100000000ULL; + + uint8_t buf[16]; + 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_EQ_INT(CFDP_COND_FILE_SIZE_ERROR, out.condition_code); + ASSERT_TRUE(out.file_checksum == 0xAABBCCDDU); + ASSERT_TRUE(out.file_size == eof.file_size); + 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) +{ + cfdp_ack_pdu_t ack = {0}; + uint8_t buf[3] = {0}; + buf[0] = (uint8_t)CFDP_DIRECTIVE_ACK; + + 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_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))); + 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_caps_segment_requests(void) +{ + /* One segment request more than the decoder can store. */ + 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}; + size_t n = cfdp_nak_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, &out); + ASSERT_EQ_INT(CFDP_NAK_MAX_SEGMENT_REQUESTS, out.segment_request_count); + ASSERT_EQ_INT(9 + CFDP_NAK_MAX_SEGMENT_REQUESTS * 8U, n); + 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_serialize_invalid_args); + RUN_TEST(test_eof_deserialize_invalid_args); + RUN_TEST(test_finished_roundtrip); + RUN_TEST(test_finished_invalid_args); + RUN_TEST(test_ack_roundtrip); + RUN_TEST(test_ack_invalid_args); + RUN_TEST(test_metadata_roundtrip); + RUN_TEST(test_metadata_empty_filenames); + 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_caps_segment_requests); + 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..aa6cf5e --- /dev/null +++ b/tests/test_cfdp_pdu.c @@ -0,0 +1,270 @@ +/** + * @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/ccsds_cfdp.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cunit.h" +#include "test_runners.h" + +#include + +#include "cfdp.h" + +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, 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, &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, 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, &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, 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, &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}; + + 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_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, buf, sizeof(buf))); + ASSERT_EQ_INT(0, cfdp_file_data_serialize(&fd, CFDP_FILE_SIZE_SMALL, 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, 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, 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, &fd)); + ASSERT_EQ_INT(0, cfdp_file_data_deserialize(buf, sizeof(buf), CFDP_FILE_SIZE_SMALL, NULL)); + ASSERT_EQ_INT(0, cfdp_file_data_deserialize(buf, 3, CFDP_FILE_SIZE_SMALL, &fd)); + 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_file_data_serialize_invalid_args); + RUN_TEST(test_file_data_deserialize_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_runners.h b/tests/test_runners.h new file mode 100644 index 0000000..0efb4ed --- /dev/null +++ b/tests/test_runners.h @@ -0,0 +1,27 @@ +/** + * @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_checksum_run_all(void); + +#endif /* TEST_RUNNERS_H */ diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 8ee320b..6a6124e 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,21 +1,40 @@ -#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; -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_directive_run_all(); + REPORT("cfdp_directive", 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