diff --git a/.clang-format b/.clang-format index 0e49e25..57a2da2 100644 --- a/.clang-format +++ b/.clang-format @@ -1,5 +1,5 @@ --- -Language: C +Language: Cpp BasedOnStyle: LLVM # Indentation @@ -13,7 +13,7 @@ ColumnLimit: 100 # Braces BreakBeforeBraces: Allman -InsertBraces: false +InsertBraces: true # Spaces SpaceAfterCStyleCast: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0ef9e20 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + unit-tests: + name: Unit tests (${{ matrix.cc }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + steps: + - uses: actions/checkout@v4 + - name: Build library, examples and run unit tests + run: make all test CC=${{ matrix.cc }} + - name: Run examples + run: | + ./build/bin/tm_example + ./build/bin/tc_example + ./build/segment_header/tm_example + ./build/segment_header/tc_example + + coverage: + name: Coverage (100% line and branch) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install gcovr + run: sudo apt-get update && sudo apt-get install -y gcovr + - name: Build instrumented tests and generate report + run: make coverage-html + - name: Enforce 100% line and branch coverage + run: > + gcovr -r . --filter src + --fail-under-line 100 + --fail-under-branch 100 + - name: Upload HTML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-html + path: build/coverage/ + if-no-files-found: ignore + + sanitizers: + name: ASan + UBSan + runs-on: ubuntu-latest + env: + ASAN_OPTIONS: detect_leaks=1:abort_on_error=1 + UBSAN_OPTIONS: print_stacktrace=1 + steps: + - uses: actions/checkout@v4 + - name: Build and run unit tests and examples under ASan and UBSan + run: make sanitize diff --git a/.gitignore b/.gitignore index 4748466..e9d4071 100644 --- a/.gitignore +++ b/.gitignore @@ -58,5 +58,5 @@ dkms.conf build/ obj/ bin/ -.github/ .vscode/ +TODO.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..83a59aa 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 OpenSpaceCode contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile index 584de0c..36d5223 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,14 @@ -CC = gcc -CFLAGS ?= -O2 -Iinclude -Wall -Wextra -Wpedantic -Wshadow \ +CC ?= cc +# Optimisation / instrumentation flags; overridden by the sanitize target and by +# tools/coverage_html.sh +OPT ?= -O2 +SANITIZE_OPT = -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined \ + -fno-sanitize-recover=all +# Stronger warnings for code quality; any warning fails the build +CFLAGS ?= $(OPT) -Iinclude -Werror -Wall -Wextra -Wpedantic -Wconversion -Wshadow \ -Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 \ -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef \ - -std=c11 + -std=c99 LDFLAGS = SRC_DIR = src @@ -12,18 +18,31 @@ TEST_DIR = tests BUILD_DIR = build OBJ_DIR = $(BUILD_DIR)/obj BIN_DIR = $(BUILD_DIR)/bin +SANITIZE_DIR = $(BUILD_DIR)/sanitize SRCS = $(wildcard $(SRC_DIR)/*.c) OBJS = $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRCS)) +# Headers are prerequisites of everything that includes them, so editing one rebuilds +# the objects, examples and tests that depend on it. +PUBLIC_HEADERS = $(wildcard $(INC_DIR)/*.h) +EXAMPLE_HEADERS = $(wildcard $(EXAMPLES_DIR)/*.h) +TEST_HEADERS = $(wildcard $(TEST_DIR)/*.h) + EXAMPLES = $(wildcard $(EXAMPLES_DIR)/*.c) EXAMPLE_BINS = $(patsubst $(EXAMPLES_DIR)/%.c,$(BIN_DIR)/%,$(EXAMPLES)) TEST_SRCS = $(wildcard $(TEST_DIR)/*.c) TEST_BIN = $(BIN_DIR)/unit_tests +# The unit tests and the examples are built a second time with the optional TC Segment +# Header compiled in. That build has its own directory, so its coverage data stays apart +# from the default one. +SEGMENT_DIR = $(BUILD_DIR)/segment_header +SEGMENT_TEST_BIN = $(SEGMENT_DIR)/unit_tests +SEGMENT_EXAMPLE_BINS = $(patsubst $(EXAMPLES_DIR)/%.c,$(SEGMENT_DIR)/%,$(EXAMPLES)) LIB = $(BUILD_DIR)/libsdlp.a -.PHONY: all clean examples lib unit-tests test coverage-html +.PHONY: all clean example examples lib unit-tests test coverage-html sanitize all: lib examples @@ -32,20 +51,33 @@ lib: $(LIB) $(LIB): $(OBJS) ar rcs $@ $^ -$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(PUBLIC_HEADERS) | $(OBJ_DIR) $(CC) $(CFLAGS) -c $< -o $@ -examples: $(EXAMPLE_BINS) +examples: $(EXAMPLE_BINS) $(SEGMENT_EXAMPLE_BINS) + +# Build the examples and run the ones linked against the library as `make lib` builds it. +example: examples + @for example in $(EXAMPLE_BINS); do ./$$example || exit 1; done -unit-tests: $(TEST_BIN) +unit-tests: $(TEST_BIN) $(SEGMENT_TEST_BIN) -$(BIN_DIR)/%: $(EXAMPLES_DIR)/%.c $(LIB) | $(BIN_DIR) +$(BIN_DIR)/%: $(EXAMPLES_DIR)/%.c $(EXAMPLE_HEADERS) $(PUBLIC_HEADERS) $(LIB) | $(BIN_DIR) $(CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS) -# Compile the sources and tests together with TC_SEGMENT_HEADER_ENABLED so the -# segment-header code paths are built and exercised. -$(TEST_BIN): $(SRCS) $(TEST_SRCS) | $(BIN_DIR) - $(CC) $(CFLAGS) -DTC_SEGMENT_HEADER_ENABLED $(SRCS) $(TEST_SRCS) -o $@ $(LDFLAGS) +# Segment-header configuration: the library is built without it, so its sources are +# compiled together with each example, all with SDLP_TC_SEGMENT_HEADER_ENABLED. +$(SEGMENT_DIR)/%: $(EXAMPLES_DIR)/%.c $(EXAMPLE_HEADERS) $(PUBLIC_HEADERS) $(SRCS) | $(SEGMENT_DIR) + $(CC) $(CFLAGS) -DSDLP_TC_SEGMENT_HEADER_ENABLED $< $(SRCS) -o $@ $(LDFLAGS) + +# Default configuration: the tests link the library exactly as `make lib` builds it. +$(TEST_BIN): $(TEST_SRCS) $(TEST_HEADERS) $(PUBLIC_HEADERS) $(LIB) | $(BIN_DIR) + $(CC) $(CFLAGS) $(TEST_SRCS) $(LIB) -o $@ $(LDFLAGS) + +# Segment-header configuration: the library is built without it, so the sources are +# compiled together with the tests, all with SDLP_TC_SEGMENT_HEADER_ENABLED. +$(SEGMENT_TEST_BIN): $(SRCS) $(TEST_SRCS) $(TEST_HEADERS) $(PUBLIC_HEADERS) | $(SEGMENT_DIR) + $(CC) $(CFLAGS) -DSDLP_TC_SEGMENT_HEADER_ENABLED $(SRCS) $(TEST_SRCS) -o $@ $(LDFLAGS) $(BUILD_DIR): mkdir -p $(BUILD_DIR) @@ -57,12 +89,48 @@ $(OBJ_DIR): | $(BUILD_DIR) $(BIN_DIR): | $(BUILD_DIR) mkdir -p $(BIN_DIR) +$(SEGMENT_DIR): | $(BUILD_DIR) + mkdir -p $(SEGMENT_DIR) + clean: - rm -rf $(BUILD_DIR) obj bin libsdlp.a + rm -rf $(BUILD_DIR) coverage-html: bash tools/coverage_html.sh +# Rebuild everything with ASan + UBSan and run the unit tests and the examples. +# Program output is shown only on failure; sanitizer reports go to stderr and are always +# visible. The instrumented build is removed afterwards, on success and on failure. +sanitize: + @$(MAKE) --no-print-directory clean >/dev/null + @$(MAKE) --no-print-directory lib examples unit-tests OPT="$(SANITIZE_OPT)" >/dev/null \ + || { $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @mkdir -p $(SANITIZE_DIR) + @echo "Sanitizers (ASan + UBSan):" + @./$(TEST_BIN) >$(SANITIZE_DIR)/unit_tests.log \ + && echo " library via unit tests : no errors detected" \ + || { cat $(SANITIZE_DIR)/unit_tests.log; \ + echo " library via unit tests : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @./$(SEGMENT_TEST_BIN) >$(SANITIZE_DIR)/unit_tests_segment_header.log \ + && echo " library via unit tests (segment header) : no errors detected" \ + || { cat $(SANITIZE_DIR)/unit_tests_segment_header.log; \ + echo " library via unit tests (segment header) : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @for example in $(EXAMPLE_BINS) $(SEGMENT_EXAMPLE_BINS); do \ + name=$$(basename $$example); \ + case $$example in $(SEGMENT_DIR)/*) name="$$name (segment header)";; esac; \ + log=$(SANITIZE_DIR)/$$(echo $$example | tr '/' '_').log; \ + ./$$example >$$log \ + && printf " library via %-27s : no errors detected\n" "$$name" \ + || { cat $$log; printf " library via %-27s : FAILED\n" "$$name"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; }; \ + done + @$(MAKE) --no-print-directory clean >/dev/null + @echo "Result: PASS" + test: unit-tests - @echo "Running unit tests (TC_SEGMENT_HEADER_ENABLED)..." + @echo "Running unit tests (default configuration)..." @./$(TEST_BIN) + @echo "Running unit tests (SDLP_TC_SEGMENT_HEADER_ENABLED)..." + @./$(SEGMENT_TEST_BIN) diff --git a/README.md b/README.md index b8c62f4..3f54213 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,22 @@ Minimal, embedded-optimized implementation of **CCSDS Space Data Link Protocol ( - **TM Secondary Header**: Optional Transfer Frame Secondary Header (1–63 data octets) via `sdlp_tm_set_secondary_header`, signaled by the Secondary Header Flag and parsed automatically on decode - **TM Operational Control Field**: Optional 4-octet OCF carried verbatim via `sdlp_tm_set_ocf` (the caller decides whether to set the OCF Flag); its content, e.g. a CLCW, is mission-specific and left to the application - **TC Frame Types**: Type-AD/BD/BC frames via `sdlp_tc_set_frame_type`, plus Unlock and Set V(R) control-command helpers (`sdlp_tc_create_unlock_frame`, `sdlp_tc_create_set_vr_frame`) -- **TC Segment Header**: Optional MAP-based segmentation support (enabled with `TC_SEGMENT_HEADER_ENABLED`) +- **TC Segment Header**: Optional MAP-based segmentation support (enabled with `SDLP_TC_SEGMENT_HEADER_ENABLED`) ### Design Principles - **Minimal footprint**: Small library size (stripped) - **Zero allocation**: Stack-based, no dynamic memory -- **Embedded-optimized**: Pure C11, no external dependencies -- **Portable**: Standard C11, big-endian network byte order +- **Embedded-optimized**: Pure C99, no external dependencies +- **Portable**: Standard C99, big-endian network byte order ## Project Structure ``` EmbeddedSDLP/ +├── .github/ +│ └── workflows/ +│ └── ci.yml # CI: unit tests, coverage gate, sanitizers ├── include/ │ ├── sdlp_common.h # Common definitions and error codes │ ├── sdlp_tm.h # TM frame definitions @@ -50,15 +53,15 @@ EmbeddedSDLP/ │ └── unit_tests.c # Test entry point (aggregates per-module results) ├── tools/ │ └── coverage_html.sh # Coverage (HTML) report generator -├── docs/ -│ ├── 132x0b3_TM_SDLP.pdf # CCSDS 132.0-B-3 standard -│ └── 232x0b4e1c1_TC_SDLP.pdf # CCSDS 232.0-B-4 standard ├── Makefile └── README.md ``` ## Building +Warnings are treated as errors (`-Werror`). The build is checked with both `gcc` and +`clang`; select the compiler with `make CC=clang`. + ### Build Everything ```bash @@ -69,6 +72,8 @@ This will create: - `build/libsdlp.a` - Static library - `build/bin/tm_example` - TM frame example - `build/bin/tc_example` - TC frame example +- `build/segment_header/tm_example` and `build/segment_header/tc_example` - the same + examples built with the optional TC Segment Header (`SDLP_TC_SEGMENT_HEADER_ENABLED`) ### Build Library Only @@ -80,7 +85,8 @@ make lib ### Build Examples ```bash -make examples +make examples # build only +make example # build, then run tm_example and tc_example ``` ### Run Tests @@ -89,6 +95,9 @@ make examples make test ``` +The unit tests are built and run twice: against the library as `make lib` builds it, and +again with the optional TC Segment Header compiled in (`SDLP_TC_SEGMENT_HEADER_ENABLED`). + ### Coverage (HTML) Requires `gcovr` installed in your system: @@ -103,12 +112,25 @@ Generate coverage report: make coverage-html ``` +The command fails unless line and branch coverage of `src/` are both 100% in each of the +two build configurations (without and with the TC Segment Header). It rebuilds the unit +tests from scratch with the Makefile's own flags plus coverage instrumentation. + Output report: ```text build/coverage/index.html ``` +### Sanitizers (ASan + UBSan) + +```bash +make sanitize # rebuild with ASan + UBSan, run tests and examples, then clean up +``` + +The target starts from a clean build and removes `build/` again when it finishes, so a +coverage report generated earlier is deleted with it. + ### Clean ```bash @@ -122,9 +144,9 @@ Look at the examples/ ## Memory Usage (Estimated) - **Library (stripped)**: < 5 KB -- **Per TM frame buffer**: `TM_PRIMARY_HEADER_SIZE` (6) + optional secondary header (≤ 64) + data + optional OCF (4) + 2 bytes FECF -- **Per TC frame buffer**: `TC_PRIMARY_HEADER_SIZE` (5) + optional segment header (1) + data + 2 bytes FECF -- **Maximum data per frame**: `TM_MAX_DATA_SIZE` = 1024 bytes (TM); `TC_MAX_DATA_SIZE` = 1017 bytes (TC — the whole frame is capped at 1024 octets per CCSDS 232.0-B-4; one less when the TC segment header is enabled) +- **Per TM frame buffer**: `SDLP_TM_PRIMARY_HEADER_SIZE` (6) + optional secondary header (≤ 64) + data + optional OCF (4) + 2 bytes FECF +- **Per TC frame buffer**: `SDLP_TC_PRIMARY_HEADER_SIZE` (5) + optional segment header (1) + data + 2 bytes FECF +- **Maximum data per frame**: `SDLP_TM_MAX_DATA_SIZE` = 1024 bytes (TM); `SDLP_TC_MAX_DATA_SIZE` = 1017 bytes (TC — the whole frame is capped at 1024 octets per CCSDS 232.0-B-4; one less when the TC segment header is enabled) ## Limitations and Extensions @@ -136,14 +158,17 @@ Current implementation focuses on core protocol features: - No SDLS (Space Data Link Security) option - No TM Only-Idle-Data (OID) frame generation or PN randomization - TM Transfer Frames are variable length; the mission-fixed frame length must be enforced by the caller -- TM frame counts are kept per Master Channel / Virtual Channel in fixed static state (up to `TM_MAX_MASTER_CHANNELS` Master Channels; not thread-safe) +- The 2-octet FECF is always emitted, although both standards make it optional +- The TC Segment Header is a build-time switch for all Virtual Channels, not per Virtual Channel; it changes `sdlp_tc_frame_t`, so build the library and the application with the same setting +- Decoders do not check the Transfer Frame Version Number +- TM frame counts are kept per Master Channel / Virtual Channel in fixed static state (not thread-safe). At most `SDLP_TM_MAX_MASTER_CHANNELS` (default 8) Spacecraft IDs are tracked; `sdlp_tm_create_frame` returns `SDLP_ERROR_NO_RESOURCE` for one more, and `sdlp_tm_reset_frame_counts` empties the table These can be extended as needed for specific mission requirements. ## References -- CCSDS 132.0-B-3: TM Space Data Link Protocol ([docs/132x0b3_TM_SDLP.pdf](docs/132x0b3_TM_SDLP.pdf)) -- CCSDS 232.0-B-4: TC Space Data Link Protocol ([docs/232x0b4e1c1_TC_SDLP.pdf](docs/232x0b4e1c1_TC_SDLP.pdf)) +- [CCSDS 132.0-B-3: TM Space Data Link Protocol](https://public.ccsds.org/Pubs/132x0b3.pdf) +- [CCSDS 232.0-B-4: TC Space Data Link Protocol](https://public.ccsds.org/Pubs/232x0b4e1c1.pdf) ## License diff --git a/docs/132x0b3_TM_SDLP.pdf b/docs/132x0b3_TM_SDLP.pdf deleted file mode 100644 index b59c09d..0000000 Binary files a/docs/132x0b3_TM_SDLP.pdf and /dev/null differ diff --git a/docs/232x0b4e1c1_TC_SDLP.pdf b/docs/232x0b4e1c1_TC_SDLP.pdf deleted file mode 100644 index 98f4e66..0000000 Binary files a/docs/232x0b4e1c1_TC_SDLP.pdf and /dev/null differ diff --git a/examples/example_crc.h b/examples/example_crc.h index 12d4300..f7d08b0 100644 --- a/examples/example_crc.h +++ b/examples/example_crc.h @@ -1,11 +1,16 @@ /** - * @file example_crc.h - * @brief CRC-16-CCITT helper used only by the examples. + * @file example_crc.h + * @brief CRC-16-CCITT helper used only by the examples. * * The SDLP library does not compute or validate the Frame Error Control Field; * error control is the application's responsibility. This helper lets the * examples populate and verify the FECF, and is intentionally kept out of the * library's public API. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef EXAMPLE_CRC_H @@ -14,8 +19,18 @@ #include #include +/** @brief CRC-16-CCITT generator polynomial: x^16 + x^12 + x^5 + 1. */ +#define EXAMPLE_CRC16_POLYNOMIAL 0x1021u + +/** @brief CRC-16-CCITT shift register preset: all ones. */ +#define EXAMPLE_CRC16_INITIAL_VALUE 0xFFFFu + +/** @brief Bits shifted through the register for each input octet. */ +#define EXAMPLE_CRC16_BITS_PER_OCTET 8 + /** - * @brief Compute a CRC-16-CCITT (polynomial 0x1021, initial value 0xFFFF). + * @brief Compute a CRC-16-CCITT (::EXAMPLE_CRC16_POLYNOMIAL, preset to + * ::EXAMPLE_CRC16_INITIAL_VALUE). * * @param[in] data Input octets. * @param[in] length Number of octets. @@ -23,21 +38,21 @@ */ static inline uint16_t example_crc16(const uint8_t *data, size_t length) { - uint16_t crc = 0xFFFFu; + uint16_t crc = EXAMPLE_CRC16_INITIAL_VALUE; for (size_t i = 0; i < length; i++) { - crc ^= (uint16_t)data[i] << 8; + crc = (uint16_t)(crc ^ ((unsigned)data[i] << 8)); - for (int j = 0; j < 8; j++) + for (int j = 0; j < EXAMPLE_CRC16_BITS_PER_OCTET; j++) { if (crc & 0x8000u) { - crc = (crc << 1) ^ 0x1021u; + crc = (uint16_t)(((unsigned)crc << 1) ^ EXAMPLE_CRC16_POLYNOMIAL); } else { - crc = crc << 1; + crc = (uint16_t)((unsigned)crc << 1); } } } @@ -45,4 +60,4 @@ static inline uint16_t example_crc16(const uint8_t *data, size_t length) return crc; } -#endif +#endif /* EXAMPLE_CRC_H */ diff --git a/examples/tc_example.c b/examples/tc_example.c index 649317e..0cb4868 100644 --- a/examples/tc_example.c +++ b/examples/tc_example.c @@ -1,9 +1,14 @@ /** - * @file tc_example.c - * @brief Worked example: build, encode, and decode TC Transfer Frames. + * @file tc_example.c + * @brief Worked example: build, encode, and decode TC Transfer Frames. * * Demonstrates a Type-AD data frame with application-side FECF computation and a * Type-BC Unlock control-command frame. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "example_crc.h" @@ -11,13 +16,16 @@ #include -/* Example telecommand identifiers */ +/** @brief Example telecommand identifier: switch to safe mode. */ #define TC_CMD_SET_MODE_SAFE 0x01U +/** @brief Number of leading frame octets shown in the hex dump. */ +#define EXAMPLE_HEX_DUMP_OCTETS 20u + int main(void) { sdlp_tc_frame_t frame; - uint8_t buffer[1500]; + uint8_t buffer[SDLP_TC_MAX_FRAME_SIZE]; size_t encoded_size; sdlp_status_t result; @@ -47,9 +55,9 @@ int main(void) return 1; } -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED /* Attach a segment header: no segmentation, MAP ID = 0 (CCSDS 232.0-B-4 4.1.3.2.2) */ - result = sdlp_tc_set_segment_header(&frame, TC_SEQ_FLAG_NO_SEG, 0); + result = sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_NO_SEG, 0); if (result != SDLP_SUCCESS) { printf("Error setting segment header: %d\n", result); @@ -72,13 +80,14 @@ int main(void) /* The library leaves the Frame Error Control Field to the application. * Compute a CRC-16-CCITT over the encoded frame (excluding the trailing * 2-byte FECF) and write it into the FECF, mirroring CCSDS error control. */ - uint16_t fecf = example_crc16(buffer, encoded_size - TC_FRAME_ERROR_CONTROL_SIZE); - buffer[encoded_size - 2] = (uint8_t)((fecf >> 8) & 0xFFu); - buffer[encoded_size - 1] = (uint8_t)(fecf & 0xFFu); + size_t fecf_offset = encoded_size - SDLP_TC_FRAME_ERROR_CONTROL_SIZE; + uint16_t fecf = example_crc16(buffer, fecf_offset); + buffer[fecf_offset] = (uint8_t)((fecf >> 8) & 0xFFu); + buffer[fecf_offset + 1] = (uint8_t)(fecf & 0xFFu); printf("Encoded frame size: %zu bytes\n", encoded_size); printf("Frame bytes: "); - for (size_t i = 0; i < encoded_size && i < 20; i++) + for (size_t i = 0; (i < encoded_size) && (i < EXAMPLE_HEX_DUMP_OCTETS); i++) { printf("%02X ", buffer[i]); } @@ -98,7 +107,7 @@ int main(void) printf("Spacecraft ID: 0x%03X\n", decoded_frame.header.spacecraft_id); printf("Virtual Channel: %d\n", decoded_frame.header.virtual_channel_id); printf("Frame Length: %d\n", decoded_frame.header.frame_length); -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED printf("Segment Header: sequence_flags=0x%X map_id=%d\n", decoded_frame.segment_header.sequence_flags, decoded_frame.segment_header.map_id); @@ -106,7 +115,7 @@ int main(void) printf("Command ID: 0x%02X\n", decoded_frame.data[0]); /* Verify the FECF the same way it was produced (application-side). */ - uint16_t expected_fecf = example_crc16(buffer, encoded_size - TC_FRAME_ERROR_CONTROL_SIZE); + uint16_t expected_fecf = example_crc16(buffer, encoded_size - SDLP_TC_FRAME_ERROR_CONTROL_SIZE); printf("FECF: 0x%04X (%s)\n", decoded_frame.fecf, decoded_frame.fecf == expected_fecf ? "valid" : "invalid"); @@ -130,9 +139,10 @@ int main(void) return 1; } - uint16_t unlock_fecf = example_crc16(buffer, encoded_size - TC_FRAME_ERROR_CONTROL_SIZE); - buffer[encoded_size - 2] = (uint8_t)((unlock_fecf >> 8) & 0xFFu); - buffer[encoded_size - 1] = (uint8_t)(unlock_fecf & 0xFFu); + fecf_offset = encoded_size - SDLP_TC_FRAME_ERROR_CONTROL_SIZE; + uint16_t unlock_fecf = example_crc16(buffer, fecf_offset); + buffer[fecf_offset] = (uint8_t)((unlock_fecf >> 8) & 0xFFu); + buffer[fecf_offset + 1] = (uint8_t)(unlock_fecf & 0xFFu); printf("Encoded frame size: %zu bytes\n", encoded_size); @@ -149,7 +159,7 @@ int main(void) decoded_frame.header.control_command_flag); printf("Control Command: 0x%02X (%s)\n", decoded_frame.data[0], - decoded_frame.data[0] == TC_CONTROL_CMD_UNLOCK ? "Unlock" : "unknown"); + decoded_frame.data[0] == SDLP_TC_CONTROL_CMD_UNLOCK ? "Unlock" : "unknown"); printf("\n=== TC Frame Example Complete ===\n"); diff --git a/examples/tm_example.c b/examples/tm_example.c index d48f87d..ec6e77d 100644 --- a/examples/tm_example.c +++ b/examples/tm_example.c @@ -1,9 +1,14 @@ /** - * @file tm_example.c - * @brief Worked example: build, encode, and decode a TM Transfer Frame. + * @file tm_example.c + * @brief Worked example: build, encode, and decode a TM Transfer Frame. * * Demonstrates a plain data frame, application-side FECF computation, and an * optional Operational Control Field. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "example_crc.h" @@ -12,10 +17,14 @@ #include #include +/** @brief Number of leading frame octets shown in the hex dump. */ +#define EXAMPLE_HEX_DUMP_OCTETS 20u + int main(void) { sdlp_tm_frame_t frame; - uint8_t buffer[1500]; + uint8_t buffer[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + SDLP_TM_OCF_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size; sdlp_status_t result; @@ -54,13 +63,14 @@ int main(void) /* The library leaves the Frame Error Control Field to the application. * Compute a CRC-16-CCITT over the encoded frame (excluding the trailing * 2-byte FECF) and write it into the FECF, mirroring CCSDS error control. */ - uint16_t fecf = example_crc16(buffer, encoded_size - TM_FRAME_ERROR_CONTROL_SIZE); - buffer[encoded_size - 2] = (uint8_t)((fecf >> 8) & 0xFFu); - buffer[encoded_size - 1] = (uint8_t)(fecf & 0xFFu); + size_t fecf_offset = encoded_size - SDLP_TM_FRAME_ERROR_CONTROL_SIZE; + uint16_t fecf = example_crc16(buffer, fecf_offset); + buffer[fecf_offset] = (uint8_t)((fecf >> 8) & 0xFFu); + buffer[fecf_offset + 1] = (uint8_t)(fecf & 0xFFu); printf("Encoded frame size: %zu bytes\n", encoded_size); printf("Frame bytes: "); - for (size_t i = 0; i < encoded_size && i < 20; i++) + for (size_t i = 0; (i < encoded_size) && (i < EXAMPLE_HEX_DUMP_OCTETS); i++) { printf("%02X ", buffer[i]); } @@ -83,7 +93,7 @@ int main(void) printf("Data: %.*s\n", (int)decoded_frame.data_length, decoded_frame.data); /* Verify the FECF the same way it was produced (application-side). */ - uint16_t expected_fecf = example_crc16(buffer, encoded_size - TM_FRAME_ERROR_CONTROL_SIZE); + uint16_t expected_fecf = example_crc16(buffer, encoded_size - SDLP_TM_FRAME_ERROR_CONTROL_SIZE); printf("FECF: 0x%04X (%s)\n", decoded_frame.fecf, decoded_frame.fecf == expected_fecf ? "valid" : "invalid"); @@ -92,7 +102,7 @@ int main(void) * Its content is mission-specific (e.g. a CLCW); here it is opaque. Setting it * raises the OCF Flag; leaving it unset emits a frame with no OCF. */ printf("\nEncoding the same frame with an Operational Control Field...\n"); - const uint8_t clcw[4] = {0x00u, 0x00u, 0x00u, 0x00u}; + const uint8_t clcw[SDLP_TM_OCF_SIZE] = {0}; result = sdlp_tm_set_ocf(&frame, clcw); if (result != SDLP_SUCCESS) { @@ -115,12 +125,12 @@ int main(void) } printf("Encoded frame size: %zu bytes\n", encoded_size); - printf("OCF Flag: %d, OCF: %02X %02X %02X %02X\n", - decoded_frame.header.ocf_flag, - decoded_frame.ocf[0], - decoded_frame.ocf[1], - decoded_frame.ocf[2], - decoded_frame.ocf[3]); + printf("OCF Flag: %d, OCF:", decoded_frame.header.ocf_flag); + for (size_t i = 0; i < SDLP_TM_OCF_SIZE; i++) + { + printf(" %02X", decoded_frame.ocf[i]); + } + printf("\n"); printf("\n=== TM Frame Example Complete ===\n"); diff --git a/include/sdlp_common.h b/include/sdlp_common.h index 5fdb45b..b4d920d 100644 --- a/include/sdlp_common.h +++ b/include/sdlp_common.h @@ -1,9 +1,14 @@ /** - * @file sdlp_common.h - * @brief Shared definitions for the CCSDS Space Data Link Protocols. + * @file sdlp_common.h + * @brief Shared definitions for the CCSDS Space Data Link Protocols. * * Protocol version and the library-wide return codes used by the TM * (CCSDS 132.0-B-3) and TC (CCSDS 232.0-B-4) frame handlers. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef SDLP_COMMON_H @@ -17,7 +22,8 @@ extern "C" { #endif -#define SDLP_VERSION 0 /**< Transfer Frame Version Number — '00' for both TM and TC. */ +/** @brief Transfer Frame Version Number: '00' for both TM and TC. */ +#define SDLP_VERSION 0 /** * @brief Library-wide return codes for the TM and TC frame handlers. @@ -27,7 +33,8 @@ typedef enum SDLP_SUCCESS = 0, /**< Operation completed successfully. */ SDLP_ERROR_INVALID_PARAM = -1, /**< NULL pointer or out-of-range argument. */ SDLP_ERROR_BUFFER_TOO_SMALL = -2, /**< Output buffer smaller than the encoded frame. */ - SDLP_ERROR_INVALID_FRAME = -3 /**< Malformed or inconsistent frame on decode. */ + SDLP_ERROR_INVALID_FRAME = -3, /**< Malformed or inconsistent frame on decode. */ + SDLP_ERROR_NO_RESOURCE = -4 /**< A fixed-size internal table is full. */ } sdlp_status_t; #ifdef __cplusplus diff --git a/include/sdlp_tc.h b/include/sdlp_tc.h index 5b66188..4f83b3a 100644 --- a/include/sdlp_tc.h +++ b/include/sdlp_tc.h @@ -1,12 +1,17 @@ /** - * @file sdlp_tc.h - * @brief CCSDS TC Space Data Link Protocol frame handling (CCSDS 232.0-B-4). + * @file sdlp_tc.h + * @brief CCSDS TC Space Data Link Protocol frame handling (CCSDS 232.0-B-4). * * Builds, encodes, and decodes TC Transfer Frames: the 5-octet primary header, * an optional Segment Header, the Data Field, and the 2-octet Frame Error Control * Field. Supports the AD/BD/BC frame types and the Unlock / Set V(R) control * commands. The Data Field and FECF content are treated as opaque, application- * owned octet sequences. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef SDLP_TC_H @@ -19,11 +24,14 @@ extern "C" { #endif -#define TC_PRIMARY_HEADER_SIZE 5 /**< Transfer Frame Primary Header size (§4.1.2). */ -#define TC_FRAME_ERROR_CONTROL_SIZE 2 /**< Frame Error Control Field size (§4.1.4). */ +/** @brief Transfer Frame Primary Header size in octets (§4.1.2). */ +#define SDLP_TC_PRIMARY_HEADER_SIZE 5 + +/** @brief Frame Error Control Field size in octets (§4.1.4). */ +#define SDLP_TC_FRAME_ERROR_CONTROL_SIZE 2 -/** Maximum whole Transfer Frame size in octets (CCSDS 232.0-B-4 §4.1.2.7.2). */ -#define TC_MAX_FRAME_SIZE 1024 +/** @brief Maximum Transfer Frame size in octets (§4.1.2.7). */ +#define SDLP_TC_MAX_FRAME_SIZE 1024 /** * @brief Maximum Transfer Frame Data Field length (CCSDS 232.0-B-4 §4.1.3.1.2). @@ -32,7 +40,11 @@ extern "C" * Segment Header is present it consumes one further octet of the Data Field * (enforced by sdlp_tc_create_frame()). */ -#define TC_MAX_DATA_SIZE (TC_MAX_FRAME_SIZE - TC_PRIMARY_HEADER_SIZE - TC_FRAME_ERROR_CONTROL_SIZE) +#define SDLP_TC_MAX_DATA_SIZE \ + (SDLP_TC_MAX_FRAME_SIZE - SDLP_TC_PRIMARY_HEADER_SIZE - SDLP_TC_FRAME_ERROR_CONTROL_SIZE) + +/** @brief Frame Length field = octets in the Transfer Frame minus this (§4.1.2.7.2). */ +#define SDLP_TC_FRAME_LENGTH_OFFSET 1u /** * @brief TC Transfer Frame types — Bypass/Control Command Flag combinations (§table 4-1). @@ -51,16 +63,22 @@ typedef enum * @brief Control Commands carried by Type-BC frames (CCSDS 232.0-B-4 §4.1.3.3). * @{ */ -#define TC_CONTROL_CMD_UNLOCK 0x00u /**< Unlock: a single 'all zeroes' octet (§4.1.3.3.2). */ -#define TC_CONTROL_CMD_UNLOCK_LENGTH 1u /**< Unlock command length in octets. */ -#define TC_CONTROL_CMD_SET_VR_OCTET0 0x82u /**< Set V(R) octet 0: '10000010' (§4.1.3.3.3). */ -#define TC_CONTROL_CMD_SET_VR_OCTET1 0x00u /**< Set V(R) octet 1: '00000000'. */ -#define TC_CONTROL_CMD_SET_VR_LENGTH 3u /**< Set V(R) command length in octets. */ +/** @brief Unlock: one 'all zeroes' octet (§4.1.3.3.2). */ +#define SDLP_TC_CONTROL_CMD_UNLOCK 0x00u +/** @brief Unlock command length in octets. */ +#define SDLP_TC_CONTROL_CMD_UNLOCK_LENGTH 1u +/** @brief Set V(R) octet 0: '10000010' (§4.1.3.3.3). */ +#define SDLP_TC_CONTROL_CMD_SET_VR_OCTET0 0x82u +/** @brief Set V(R) octet 1: '00000000'. */ +#define SDLP_TC_CONTROL_CMD_SET_VR_OCTET1 0x00u +/** @brief Set V(R) command length in octets. */ +#define SDLP_TC_CONTROL_CMD_SET_VR_LENGTH 3u /** @} */ -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED -# define TC_SEGMENT_HEADER_SIZE 1 /**< Segment Header size in octets (§4.1.3.2.2). */ +/** @brief Segment Header size in octets (§4.1.3.2.2). */ +# define SDLP_TC_SEGMENT_HEADER_SIZE 1 /** * @brief TC Segment Header Sequence Flags (CCSDS 232.0-B-4 §table 4-2). @@ -69,10 +87,11 @@ typedef enum */ typedef enum { - TC_SEQ_FLAG_CONTINUE = 0x00u, /**< '00': continuing portion of an SDU on one MAP. */ - TC_SEQ_FLAG_FIRST = 0x01u, /**< '01': first portion of an SDU on one MAP. */ - TC_SEQ_FLAG_LAST = 0x02u, /**< '10': last portion of an SDU on one MAP. */ - TC_SEQ_FLAG_NO_SEG = 0x03u /**< '11': no segmentation (complete SDU or multiple packets). */ + SDLP_TC_SEQ_FLAG_CONTINUE = 0x00u, /**< '00': continuing portion of an SDU on one MAP. */ + SDLP_TC_SEQ_FLAG_FIRST = 0x01u, /**< '01': first portion of an SDU on one MAP. */ + SDLP_TC_SEQ_FLAG_LAST = 0x02u, /**< '10': last portion of an SDU on one MAP. */ + SDLP_TC_SEQ_FLAG_NO_SEG = + 0x03u /**< '11': no segmentation (complete SDU or multiple packets). */ } sdlp_tc_seq_flag_t; /** @@ -84,7 +103,7 @@ typedef struct uint8_t map_id : 6; /**< Multiplexer Access Point (MAP) Identifier (0-63). */ } sdlp_tc_segment_header_t; -#endif /* TC_SEGMENT_HEADER_ENABLED */ +#endif /* SDLP_TC_SEGMENT_HEADER_ENABLED */ /** * @brief TC Transfer Frame Primary Header fields (CCSDS 232.0-B-4 §4.1.2). @@ -111,12 +130,12 @@ typedef struct typedef struct { sdlp_tc_header_t header; /**< Primary header fields. */ -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED sdlp_tc_segment_header_t segment_header; /**< Segment Header (Type-D frames only). */ #endif - uint8_t data[TC_MAX_DATA_SIZE]; /**< Transfer Frame Data Field. */ - uint16_t data_length; /**< Data Field length in octets. */ - uint16_t fecf; /**< Frame Error Control Field (application-managed). */ + uint8_t data[SDLP_TC_MAX_DATA_SIZE]; /**< Transfer Frame Data Field. */ + uint16_t data_length; /**< Data Field length in octets. */ + uint16_t fecf; /**< Frame Error Control Field (application-managed). */ } sdlp_tc_frame_t; /** @@ -127,7 +146,7 @@ typedef struct * @param[in] virtual_channel_id Virtual Channel Identifier — masked to 6 bits. * @param[in] frame_seq_num Frame Sequence Number N(S). * @param[in] data Data Field content (copied into the frame). - * @param[in] data_length Data Field length (1..::TC_MAX_DATA_SIZE, one less with a + * @param[in] data_length Data Field length (1..::SDLP_TC_MAX_DATA_SIZE, one less with a * Segment Header). * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on NULL args or bad length. */ @@ -144,8 +163,8 @@ sdlp_status_t sdlp_tc_create_frame(sdlp_tc_frame_t *frame, * Also recomputes the Frame Length, since Type-BC frames carry no Segment Header * (§4.1.3.2.2.1.3). sdlp_tc_create_frame() produces a Type-AD frame. * - * @param[out] frame Target frame. - * @param[in] type Desired frame type. + * @param[in,out] frame Target frame. + * @param[in] type Desired frame type. * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on a NULL frame or unknown type. */ sdlp_status_t sdlp_tc_set_frame_type(sdlp_tc_frame_t *frame, sdlp_tc_frame_type_t type); @@ -183,13 +202,16 @@ sdlp_status_t sdlp_tc_create_set_vr_frame(sdlp_tc_frame_t *frame, * * Emits the primary header, the Segment Header (when compiled in and the frame is * not Type-BC), the Data Field, then the FECF (verbatim from @p frame->fecf). The - * wire Frame Length is derived from the emitted octet count. + * wire Frame Length is derived from the emitted octet count. On failure, @p buffer + * and @p encoded_size are left unchanged. * * @param[in] frame Frame to serialise. * @param[out] buffer Output buffer. * @param[in] buffer_size Buffer capacity in octets. * @param[out] encoded_size Bytes written on success. - * @return ::SDLP_SUCCESS, ::SDLP_ERROR_INVALID_PARAM, or ::SDLP_ERROR_BUFFER_TOO_SMALL. + * @return ::SDLP_SUCCESS; ::SDLP_ERROR_INVALID_PARAM on NULL args or a frame that would + * exceed ::SDLP_TC_MAX_FRAME_SIZE octets; ::SDLP_ERROR_BUFFER_TOO_SMALL if @p buffer + * cannot hold the frame. */ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, uint8_t *buffer, @@ -201,6 +223,7 @@ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, * * Validates that the Frame Length matches the octet count and rejects the reserved * Bypass=0/Control Command=1 combination. The FECF is surfaced without validation. + * On failure, @p frame is left unchanged. * * @param[in] buffer Wire buffer to parse. * @param[in] buffer_size Buffer length in octets. @@ -211,16 +234,19 @@ sdlp_status_t sdlp_tc_decode_frame(const uint8_t *buffer, size_t buffer_size, sdlp_tc_frame_t *frame); -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED /** * @brief Set the Segment Header fields on a TC frame (CCSDS 232.0-B-4 §4.1.3.2.2). * - * Must not be used on frames with the Control Command Flag set (§4.1.3.2.2.1.3). + * The Segment Header must not be present in frames carrying Control Commands + * (§4.1.3.2.2.1.3), so a frame with the Control Command Flag set is rejected. On failure, + * @p frame is left unchanged. * - * @param[out] frame Target frame. - * @param[in] sequence_flags One of the ::sdlp_tc_seq_flag_t values. - * @param[in] map_id Multiplexer Access Point Identifier (0-63). - * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on a NULL frame. + * @param[in,out] frame Target frame. + * @param[in] sequence_flags One of the ::sdlp_tc_seq_flag_t values. + * @param[in] map_id Multiplexer Access Point Identifier (0-63). + * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on a NULL frame or a frame with the + * Control Command Flag set. */ sdlp_status_t sdlp_tc_set_segment_header(sdlp_tc_frame_t *frame, sdlp_tc_seq_flag_t sequence_flags, diff --git a/include/sdlp_tm.h b/include/sdlp_tm.h index c5c23a8..e6718e3 100644 --- a/include/sdlp_tm.h +++ b/include/sdlp_tm.h @@ -1,12 +1,17 @@ /** - * @file sdlp_tm.h - * @brief CCSDS TM Space Data Link Protocol frame handling (CCSDS 132.0-B-3). + * @file sdlp_tm.h + * @brief CCSDS TM Space Data Link Protocol frame handling (CCSDS 132.0-B-3). * * Builds, encodes, and decodes TM Transfer Frames: the 6-octet primary header, * the optional Transfer Frame Secondary Header, the Data Field, the optional * Operational Control Field, and the 2-octet Frame Error Control Field. Data * Field, Secondary Header, OCF, and FECF content are treated as opaque, * application-owned octet sequences. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef SDLP_TM_H @@ -19,9 +24,14 @@ extern "C" { #endif -#define TM_PRIMARY_HEADER_SIZE 6 /**< Transfer Frame Primary Header size (§4.1.2). */ -#define TM_FRAME_ERROR_CONTROL_SIZE 2 /**< Frame Error Control Field size (§4.1.6). */ -#define TM_MAX_DATA_SIZE 1024 /**< Maximum Transfer Frame Data Field carried. */ +/** @brief Transfer Frame Primary Header size in octets (§4.1.2). */ +#define SDLP_TM_PRIMARY_HEADER_SIZE 6 + +/** @brief Frame Error Control Field size in octets (§4.1.6). */ +#define SDLP_TM_FRAME_ERROR_CONTROL_SIZE 2 + +/** @brief Maximum Transfer Frame Data Field length the library carries, in octets. */ +#define SDLP_TM_MAX_DATA_SIZE 1024 /** * @brief Transfer Frame Secondary Header sizing (CCSDS 132.0-B-3 §4.1.3). @@ -30,22 +40,41 @@ extern "C" * i.e. up to 64 octets in total. * @{ */ -#define TM_SECONDARY_HEADER_ID_SIZE 1 /**< Identification Field size in octets. */ -#define TM_SECONDARY_HEADER_MAX_DATA 63 /**< Maximum Secondary Header Data Field length. */ +/** @brief Identification Field size in octets. */ +#define SDLP_TM_SECONDARY_HEADER_ID_SIZE 1 +/** @brief Minimum Secondary Header Data Field length in octets. */ +#define SDLP_TM_SECONDARY_HEADER_MIN_DATA 1 +/** @brief Maximum Secondary Header Data Field length in octets. */ +#define SDLP_TM_SECONDARY_HEADER_MAX_DATA 63 /** @} */ -#define TM_OCF_SIZE 4 /**< Operational Control Field size in octets (§4.1.5). */ +/** @brief Operational Control Field size in octets (§4.1.5). */ +#define SDLP_TM_OCF_SIZE 4 + +/** + * @brief Number of distinct Master Channels whose frame counts are tracked concurrently. + * + * TM Master/Virtual Channel Frame Counts are kept per Master Channel (identified by + * Spacecraft ID, as the Transfer Frame Version Number is fixed at 0) and, within each, + * per Virtual Channel (CCSDS 132.0-B-3 §4.1.2.5 and §4.1.2.6). The state lives in a + * fixed-size table (no dynamic allocation), so sdlp_tm_create_frame() returns + * ::SDLP_ERROR_NO_RESOURCE for a Spacecraft ID beyond this many. Define it when building + * the library to change the limit. + */ +#ifndef SDLP_TM_MAX_MASTER_CHANNELS +# define SDLP_TM_MAX_MASTER_CHANNELS 8 +#endif /** * @brief Transfer Frame Data Field Status sub-field values (CCSDS 132.0-B-3 §4.1.2.7). * @{ */ -/** '11'; mandatory Segment Length Identifier when Sync Flag = 0 (§4.1.2.7.5.2). */ -#define TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION 0x03u -/** First Header Pointer: no Packet starts in the Data Field (§4.1.2.7.6.4). */ -#define TM_FIRST_HEADER_POINTER_NO_PACKET 0x07FFu -/** First Header Pointer: Only Idle Data (OID) Transfer Frame (§4.1.2.7.6.5). */ -#define TM_FIRST_HEADER_POINTER_ONLY_IDLE 0x07FEu +/** @brief Segment Length Identifier '11', mandatory when Sync Flag = 0 (§4.1.2.7.5.2). */ +#define SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION 0x03u +/** @brief First Header Pointer: no Packet starts in the Data Field (§4.1.2.7.6.4). */ +#define SDLP_TM_FIRST_HEADER_POINTER_NO_PACKET 0x07FFu +/** @brief First Header Pointer: Only Idle Data (OID) Transfer Frame (§4.1.2.7.6.5). */ +#define SDLP_TM_FIRST_HEADER_POINTER_ONLY_IDLE 0x07FEu /** @} */ /** @@ -62,8 +91,7 @@ typedef struct uint16_t sync_flag : 1; /**< 0 = Packets/Idle Data, 1 = VCA_SDU. */ uint16_t packet_order_flag : 1; /**< Reserved ('0') when sync_flag = 0. */ uint16_t segment_length_id : 2; /**< 2 bits; '11' when sync_flag = 0. */ - uint16_t first_header_pointer - : 11; /**< 11 bits; offset of the first Packet or a special value. */ + uint16_t first_header_pointer : 11; /**< Offset of the first Packet, or a special value. */ } sdlp_tm_data_field_status_t; /** @@ -92,9 +120,9 @@ typedef struct */ typedef struct { - uint8_t version : 2; /**< Secondary Header Version Number ('00'). */ - uint8_t length : 6; /**< Data Field length (1..63); 0 = absent. */ - uint8_t data[TM_SECONDARY_HEADER_MAX_DATA]; /**< Secondary Header Data Field. */ + uint8_t version : 2; /**< Secondary Header Version Number ('00'). */ + uint8_t length : 6; /**< Data Field length (1..63); 0 = absent. */ + uint8_t data[SDLP_TM_SECONDARY_HEADER_MAX_DATA]; /**< Secondary Header Data Field. */ } sdlp_tm_secondary_header_t; /** @@ -106,9 +134,9 @@ typedef struct { sdlp_tm_header_t header; /**< Primary header fields. */ sdlp_tm_secondary_header_t secondary_header; /**< Secondary Header (when its flag is set). */ - uint8_t data[TM_MAX_DATA_SIZE]; /**< Transfer Frame Data Field. */ + uint8_t data[SDLP_TM_MAX_DATA_SIZE]; /**< Transfer Frame Data Field. */ uint16_t data_length; /**< Data Field length in octets. */ - uint8_t ocf[TM_OCF_SIZE]; /**< OCF (valid when ocf_flag is set). */ + uint8_t ocf[SDLP_TM_OCF_SIZE]; /**< OCF (valid when ocf_flag is set). */ uint16_t fecf; /**< Frame Error Control Field (app-managed). */ } sdlp_tm_frame_t; @@ -132,14 +160,17 @@ void sdlp_tm_unpack_data_field_status(uint16_t raw, sdlp_tm_data_field_status_t * @brief Build a TM Transfer Frame carrying a Data Field. * * Sets a standards-valid default Data Field Status (Sync Flag = 0, Segment Length - * Identifier '11') and advances the per-Master/Virtual-Channel frame counts. + * Identifier '11') and advances the per-Master/Virtual-Channel frame counts. On failure, + * @p frame is left unchanged and no frame count advances. * * @param[out] frame Target frame. * @param[in] spacecraft_id Spacecraft Identifier — masked to 10 bits. * @param[in] virtual_channel_id Virtual Channel Identifier — masked to 3 bits. * @param[in] data Data Field content (copied into the frame). - * @param[in] data_length Data Field length (0..::TM_MAX_DATA_SIZE). - * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on NULL args or oversized data. + * @param[in] data_length Data Field length (0..::SDLP_TM_MAX_DATA_SIZE). + * @return ::SDLP_SUCCESS; ::SDLP_ERROR_INVALID_PARAM on NULL args or oversized data; + * ::SDLP_ERROR_NO_RESOURCE if @p spacecraft_id is a new Master Channel and + * ::SDLP_TM_MAX_MASTER_CHANNELS of them are already tracked. */ sdlp_status_t sdlp_tm_create_frame(sdlp_tm_frame_t *frame, uint16_t spacecraft_id, @@ -147,15 +178,25 @@ sdlp_status_t sdlp_tm_create_frame(sdlp_tm_frame_t *frame, const uint8_t *data, uint16_t data_length); +/** + * @brief Forget every Master Channel and restart all frame counts from zero. + * + * Empties the table sdlp_tm_create_frame() uses to number frames, freeing all + * ::SDLP_TM_MAX_MASTER_CHANNELS slots. Meant for (re-)initialisation and for tests: in normal + * operation a count must not be reset unless it is unavoidable (CCSDS 132.0-B-3 + * §4.1.2.5.3 and §4.1.2.6.3). Not thread-safe. + */ +void sdlp_tm_reset_frame_counts(void); + /** * @brief Attach a Transfer Frame Secondary Header (CCSDS 132.0-B-3 §4.1.3). * * Raises the Secondary Header Flag and copies @p length octets of Data Field * content; the Version Number is set to '00'. * - * @param[out] frame Target frame. - * @param[in] data Secondary Header Data Field content (copied). - * @param[in] length Data Field length (1..::TM_SECONDARY_HEADER_MAX_DATA). + * @param[in,out] frame Target frame. + * @param[in] data Secondary Header Data Field content (copied). + * @param[in] length Data Field length (1..::SDLP_TM_SECONDARY_HEADER_MAX_DATA). * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on NULL args or bad length. */ sdlp_status_t sdlp_tm_set_secondary_header(sdlp_tm_frame_t *frame, @@ -169,23 +210,26 @@ sdlp_status_t sdlp_tm_set_secondary_header(sdlp_tm_frame_t *frame, * CLCW or an SDLS report) is mission-specific. Leave this unset to emit a frame * with no OCF. * - * @param[out] frame Target frame. - * @param[in] ocf Four octets of OCF content (copied). + * @param[in,out] frame Target frame. + * @param[in] ocf Four octets of OCF content (copied). * @return ::SDLP_SUCCESS, or ::SDLP_ERROR_INVALID_PARAM on NULL args. */ -sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[TM_OCF_SIZE]); +sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[SDLP_TM_OCF_SIZE]); /** * @brief Serialise a TM Transfer Frame into a caller-supplied buffer. * * Emits, in order: primary header, Secondary Header (if flagged), Data Field, - * OCF (if flagged), then the FECF (written verbatim from @p frame->fecf). + * OCF (if flagged), then the FECF (written verbatim from @p frame->fecf). On failure, + * @p buffer and @p encoded_size are left unchanged. * * @param[in] frame Frame to serialise. * @param[out] buffer Output buffer. * @param[in] buffer_size Buffer capacity in octets. * @param[out] encoded_size Bytes written on success. - * @return ::SDLP_SUCCESS, ::SDLP_ERROR_INVALID_PARAM, or ::SDLP_ERROR_BUFFER_TOO_SMALL. + * @return ::SDLP_SUCCESS; ::SDLP_ERROR_INVALID_PARAM on NULL args, a Data Field longer + * than ::SDLP_TM_MAX_DATA_SIZE, or the Secondary Header Flag set with an empty Secondary + * Header; ::SDLP_ERROR_BUFFER_TOO_SMALL if @p buffer cannot hold the frame. */ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, uint8_t *buffer, @@ -196,7 +240,8 @@ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, * @brief Parse a wire-format TM Transfer Frame. * * Recovers the header, any Secondary Header / OCF signalled by their flags, the - * Data Field, and the FECF (surfaced as-is, without validation). + * Data Field, and the FECF (surfaced as-is, without validation). On failure, + * @p frame is left unchanged. * * @param[in] buffer Wire buffer to parse. * @param[in] buffer_size Buffer length in octets. diff --git a/src/sdlp_tc.c b/src/sdlp_tc.c index ec237a3..2a232f7 100644 --- a/src/sdlp_tc.c +++ b/src/sdlp_tc.c @@ -1,10 +1,15 @@ /** - * @file sdlp_tc.c - * @brief TC Space Data Link Protocol frame handling (CCSDS 232.0-B-4). + * @file sdlp_tc.c + * @brief TC Space Data Link Protocol frame handling (CCSDS 232.0-B-4). * * Implements the API declared in sdlp_tc.h: create/encode/decode of TC Transfer * Frames, the AD/BD/BC frame-type selector, and the Unlock / Set V(R) control- * command builders. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "sdlp_tc.h" @@ -19,16 +24,37 @@ * @param[in] frame Frame whose current fields determine the emitted size. * @return The 10-bit Frame Length value. */ -static uint16_t tc_frame_length(const sdlp_tc_frame_t *frame) +static uint16_t sdlp_tc_frame_length(const sdlp_tc_frame_t *frame) { - size_t frame_octets = TC_PRIMARY_HEADER_SIZE + frame->data_length + TC_FRAME_ERROR_CONTROL_SIZE; -#ifdef TC_SEGMENT_HEADER_ENABLED + size_t frame_octets = + (size_t)SDLP_TC_PRIMARY_HEADER_SIZE + frame->data_length + SDLP_TC_FRAME_ERROR_CONTROL_SIZE; +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED if (!frame->header.control_command_flag) { - frame_octets += TC_SEGMENT_HEADER_SIZE; + frame_octets += SDLP_TC_SEGMENT_HEADER_SIZE; } #endif - return (uint16_t)(frame_octets - 1u); + return (uint16_t)(frame_octets - SDLP_TC_FRAME_LENGTH_OFFSET); +} + +/** + * @brief Decode the 5-octet Transfer Frame Primary Header (CCSDS 232.0-B-4 §4.1.2). + * + * @param[in] buffer Wire buffer holding at least ::SDLP_TC_PRIMARY_HEADER_SIZE octets. + * @param[out] header Decoded primary header fields. + */ +static void sdlp_tc_decode_primary_header(const uint8_t *buffer, sdlp_tc_header_t *header) +{ + memset(header, 0, sizeof(sdlp_tc_header_t)); + + header->transfer_frame_version = (uint16_t)((buffer[0] >> 6) & 0x03u); + header->bypass_flag = (uint16_t)((buffer[0] >> 5) & 0x01u); + header->control_command_flag = (uint16_t)((buffer[0] >> 4) & 0x01u); + header->reserved = (uint16_t)((buffer[0] >> 2) & 0x03u); + header->spacecraft_id = (uint16_t)((((unsigned)buffer[0] << 8) | buffer[1]) & 0x03FFu); + header->virtual_channel_id = (uint16_t)((buffer[2] >> 2) & 0x3Fu); + header->frame_length = (uint16_t)((((unsigned)buffer[2] << 8) | buffer[3]) & 0x03FFu); + header->frame_sequence_number = buffer[4]; } sdlp_status_t sdlp_tc_create_frame(sdlp_tc_frame_t *frame, @@ -40,12 +66,12 @@ sdlp_status_t sdlp_tc_create_frame(sdlp_tc_frame_t *frame, { /* A Type-D Frame Data Unit carries a Segment Header (when configured), which * consumes one octet of the Data Field budget (CCSDS 232.0-B-4, 4.1.3.2.1). */ - size_t max_data = TC_MAX_DATA_SIZE; -#ifdef TC_SEGMENT_HEADER_ENABLED - max_data -= TC_SEGMENT_HEADER_SIZE; + size_t max_data = SDLP_TC_MAX_DATA_SIZE; +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + max_data -= SDLP_TC_SEGMENT_HEADER_SIZE; #endif - if (!frame || !data || data_length == 0u || data_length > max_data) + if ((!frame) || (!data) || (data_length == 0u) || (data_length > max_data)) { return SDLP_ERROR_INVALID_PARAM; } @@ -65,7 +91,7 @@ sdlp_status_t sdlp_tc_create_frame(sdlp_tc_frame_t *frame, /* sdlp_tc_encode_frame recomputes the Frame Length from the bytes it actually * emits; it is set here so the struct is self-consistent. */ - frame->header.frame_length = tc_frame_length(frame); + frame->header.frame_length = (uint16_t)(sdlp_tc_frame_length(frame) & 0x03FFu); return SDLP_SUCCESS; } @@ -97,7 +123,7 @@ sdlp_status_t sdlp_tc_set_frame_type(sdlp_tc_frame_t *frame, sdlp_tc_frame_type_ /* The Segment Header is absent from Type-BC frames, so the frame type affects * the total frame size when segment headers are compiled in. */ - frame->header.frame_length = tc_frame_length(frame); + frame->header.frame_length = (uint16_t)(sdlp_tc_frame_length(frame) & 0x03FFu); return SDLP_SUCCESS; } @@ -106,7 +132,7 @@ sdlp_status_t sdlp_tc_create_unlock_frame(sdlp_tc_frame_t *frame, uint16_t spacecraft_id, uint8_t virtual_channel_id) { - const uint8_t cmd[TC_CONTROL_CMD_UNLOCK_LENGTH] = {TC_CONTROL_CMD_UNLOCK}; + const uint8_t cmd[SDLP_TC_CONTROL_CMD_UNLOCK_LENGTH] = {SDLP_TC_CONTROL_CMD_UNLOCK}; /* COP does not use the Frame Sequence Number of Type-B frames; it is set to * 'all zeroes' (CCSDS 232.0-B-4, 4.1.2.8 note 3). */ @@ -129,9 +155,9 @@ sdlp_status_t sdlp_tc_create_set_vr_frame(sdlp_tc_frame_t *frame, uint8_t virtual_channel_id, uint8_t vr) { - const uint8_t cmd[TC_CONTROL_CMD_SET_VR_LENGTH] = {TC_CONTROL_CMD_SET_VR_OCTET0, - TC_CONTROL_CMD_SET_VR_OCTET1, - vr}; + const uint8_t cmd[SDLP_TC_CONTROL_CMD_SET_VR_LENGTH] = {SDLP_TC_CONTROL_CMD_SET_VR_OCTET0, + SDLP_TC_CONTROL_CMD_SET_VR_OCTET1, + vr}; /* COP does not use the Frame Sequence Number of Type-B frames; it is set to * 'all zeroes' (CCSDS 232.0-B-4, 4.1.2.8 note 3). */ @@ -154,21 +180,29 @@ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, size_t buffer_size, size_t *encoded_size) { - if (!frame || !buffer || !encoded_size) + if ((!frame) || (!buffer) || (!encoded_size)) { return SDLP_ERROR_INVALID_PARAM; } size_t required_size = - TC_PRIMARY_HEADER_SIZE + frame->data_length + TC_FRAME_ERROR_CONTROL_SIZE; + (size_t)SDLP_TC_PRIMARY_HEADER_SIZE + frame->data_length + SDLP_TC_FRAME_ERROR_CONTROL_SIZE; -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED if (!frame->header.control_command_flag) { - required_size += TC_SEGMENT_HEADER_SIZE; + required_size += SDLP_TC_SEGMENT_HEADER_SIZE; } #endif + /* A Transfer Frame is at most SDLP_TC_MAX_FRAME_SIZE octets, the most the 10-bit Frame + * Length can express (CCSDS 232.0-B-4, 4.1.2.7). The same limit keeps data_length + * within frame->data, so the copy below cannot read past the array. */ + if (required_size > SDLP_TC_MAX_FRAME_SIZE) + { + return SDLP_ERROR_INVALID_PARAM; + } + if (buffer_size < required_size) { return SDLP_ERROR_BUFFER_TOO_SMALL; @@ -176,11 +210,11 @@ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, /* Frame Length = total octets in the emitted Transfer Frame - 1 (CCSDS 232.0-B-4, * 4.1.2.7.2), derived from the actual encoded size so it always matches the wire. */ - uint16_t frame_length = (uint16_t)(required_size - 1u); + uint16_t frame_length = (uint16_t)(required_size - SDLP_TC_FRAME_LENGTH_OFFSET); size_t offset = 0; - buffer[offset++] = (uint8_t)((frame->header.transfer_frame_version << 6) | + buffer[offset++] = (uint8_t)(((frame->header.transfer_frame_version & 0x03u) << 6) | ((frame->header.bypass_flag & 0x01u) << 5) | ((frame->header.control_command_flag & 0x01u) << 4) | ((frame->header.reserved & 0x03u) << 2) | @@ -191,7 +225,7 @@ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, buffer[offset++] = (uint8_t)(frame_length & 0xFFu); buffer[offset++] = frame->header.frame_sequence_number; -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED if (!frame->header.control_command_flag) { buffer[offset++] = (uint8_t)(((frame->segment_header.sequence_flags & 0x03u) << 6) | @@ -216,85 +250,76 @@ sdlp_status_t sdlp_tc_decode_frame(const uint8_t *buffer, size_t buffer_size, sdlp_tc_frame_t *frame) { - if (!buffer || !frame || buffer_size < TC_PRIMARY_HEADER_SIZE + TC_FRAME_ERROR_CONTROL_SIZE) + if ((!buffer) || (!frame) || + (buffer_size < (SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE))) { return SDLP_ERROR_INVALID_PARAM; } - memset(frame, 0, sizeof(sdlp_tc_frame_t)); - - size_t offset = 0; - - frame->header.transfer_frame_version = (uint8_t)((buffer[offset] >> 6) & 0x03u); - frame->header.bypass_flag = (uint8_t)((buffer[offset] >> 5) & 0x01u); - frame->header.control_command_flag = (uint8_t)((buffer[offset] >> 4) & 0x01u); - frame->header.reserved = (uint8_t)((buffer[offset] >> 2) & 0x03u); - frame->header.spacecraft_id = - (uint16_t)(((uint16_t)(buffer[offset] & 0x03u) << 8) | buffer[offset + 1]); - offset += 2; + sdlp_tc_header_t header; + sdlp_tc_decode_primary_header(buffer, &header); /* Bypass=0 with Control Command=1 is reserved for future application * (CCSDS 232.0-B-4, table 4-1). */ - if (!frame->header.bypass_flag && frame->header.control_command_flag) + if ((!header.bypass_flag) && (header.control_command_flag)) { return SDLP_ERROR_INVALID_FRAME; } - frame->header.virtual_channel_id = (uint8_t)((buffer[offset] >> 2) & 0x3Fu); - frame->header.frame_length = - (uint16_t)((((uint16_t)buffer[offset] & 0x03u) << 8) | (uint16_t)buffer[offset + 1]); - offset += 2; - frame->header.frame_sequence_number = buffer[offset++]; - /* Frame Validation: the Frame Length must equal the actual octet count minus one * (CCSDS 232.0-B-4, 4.1.2.7.2). */ - if ((size_t)frame->header.frame_length + 1u != buffer_size) + if (((size_t)header.frame_length + SDLP_TC_FRAME_LENGTH_OFFSET) != buffer_size) { return SDLP_ERROR_INVALID_FRAME; } -#ifdef TC_SEGMENT_HEADER_ENABLED - if (!frame->header.control_command_flag) + size_t data_offset = SDLP_TC_PRIMARY_HEADER_SIZE; + +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + sdlp_tc_segment_header_t segment_header = {0}; + if (!header.control_command_flag) { - if (buffer_size < - TC_PRIMARY_HEADER_SIZE + TC_SEGMENT_HEADER_SIZE + TC_FRAME_ERROR_CONTROL_SIZE) + if (buffer_size < (SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_SEGMENT_HEADER_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE)) { return SDLP_ERROR_INVALID_FRAME; } - frame->segment_header.sequence_flags = (uint8_t)((buffer[offset] >> 6) & 0x03u); - frame->segment_header.map_id = (uint8_t)(buffer[offset] & 0x3Fu); - offset++; - frame->data_length = (uint16_t)(buffer_size - TC_PRIMARY_HEADER_SIZE - - TC_SEGMENT_HEADER_SIZE - TC_FRAME_ERROR_CONTROL_SIZE); + segment_header.sequence_flags = (buffer[data_offset] >> 6) & 0x03u; + segment_header.map_id = buffer[data_offset] & 0x3Fu; + data_offset += SDLP_TC_SEGMENT_HEADER_SIZE; } - else - { - frame->data_length = - (uint16_t)(buffer_size - TC_PRIMARY_HEADER_SIZE - TC_FRAME_ERROR_CONTROL_SIZE); - } -#else - frame->data_length = - (uint16_t)(buffer_size - TC_PRIMARY_HEADER_SIZE - TC_FRAME_ERROR_CONTROL_SIZE); #endif /* The Frame Length validation above pins buffer_size to frame_length + 1 (<= 1024), - * so data_length is inherently <= TC_MAX_DATA_SIZE and the Data Field fits. */ - memcpy(frame->data, &buffer[offset], frame->data_length); - offset += frame->data_length; + * so data_length is inherently <= SDLP_TC_MAX_DATA_SIZE and the Data Field fits. */ + const size_t data_length = buffer_size - data_offset - SDLP_TC_FRAME_ERROR_CONTROL_SIZE; + const size_t fecf_offset = data_offset + data_length; + + /* Every check has passed. The output frame is written only from here on, so a + * rejected buffer leaves it exactly as the caller passed it. */ + memset(frame, 0, sizeof(sdlp_tc_frame_t)); + frame->header = header; +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + frame->segment_header = segment_header; +#endif + memcpy(frame->data, &buffer[data_offset], data_length); + frame->data_length = (uint16_t)data_length; /* The Frame Error Control Field is surfaced as-is; validating it (e.g. via * CRC-16) is left to the application. */ - frame->fecf = (uint16_t)(((uint16_t)buffer[offset] << 8) | buffer[offset + 1]); + frame->fecf = (uint16_t)(((uint16_t)buffer[fecf_offset] << 8) | buffer[fecf_offset + 1u]); return SDLP_SUCCESS; } -#ifdef TC_SEGMENT_HEADER_ENABLED +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED sdlp_status_t sdlp_tc_set_segment_header(sdlp_tc_frame_t *frame, sdlp_tc_seq_flag_t sequence_flags, uint8_t map_id) { - if (!frame) + /* The Segment Header must not be present in Transfer Frames carrying Control Commands + * (CCSDS 232.0-B-4, 4.1.3.2.2.1.3). */ + if ((!frame) || (frame->header.control_command_flag)) { return SDLP_ERROR_INVALID_PARAM; } diff --git a/src/sdlp_tm.c b/src/sdlp_tm.c index 7e14426..8ddc245 100644 --- a/src/sdlp_tm.c +++ b/src/sdlp_tm.c @@ -1,43 +1,41 @@ /** - * @file sdlp_tm.c - * @brief TM Space Data Link Protocol frame handling (CCSDS 132.0-B-3). + * @file sdlp_tm.c + * @brief TM Space Data Link Protocol frame handling (CCSDS 132.0-B-3). * * Implements the API declared in sdlp_tm.h: create/encode/decode of TM Transfer * Frames, the Data Field Status codec, the Secondary Header and OCF setters, and * the per-Master/Virtual-Channel frame counters. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "sdlp_tm.h" #include -/** - * @brief Number of distinct Master Channels whose frame counts are tracked concurrently. - * - * TM Master/Virtual Channel Frame Counts are kept per Master Channel (identified by - * Spacecraft ID, as the Transfer Frame Version Number is fixed at 0) and, within each, - * per Virtual Channel (CCSDS 132.0-B-3, 4.1.2.5 and 4.1.2.6). Both counts are - * free-running modulo-256. State lives in a fixed-size table (no dynamic allocation); - * frames for further Master Channels are emitted with zeroed counts. Not thread-safe. - */ -#ifndef TM_MAX_MASTER_CHANNELS -# define TM_MAX_MASTER_CHANNELS 8 -#endif - -#define TM_VC_PER_MC 8 /**< TM Virtual Channels per Master Channel (VCID is 3 bits). */ +/** @brief Virtual Channels per Master Channel: the VCID is 3 bits. */ +#define SDLP_TM_VC_PER_MC 8 /** * @brief Per-Master-Channel frame-count state. */ typedef struct { - uint8_t in_use; /**< Slot occupied. */ - uint16_t spacecraft_id; /**< Master Channel ID (Spacecraft Identifier). */ - uint8_t mc_frame_count; /**< Master Channel Frame Count (modulo-256). */ - uint8_t vc_frame_count[TM_VC_PER_MC]; /**< Virtual Channel Frame Counts, indexed by VCID. */ -} tm_master_channel_t; + uint8_t in_use; /**< Slot occupied. */ + uint16_t spacecraft_id; /**< Master Channel ID (Spacecraft Identifier). */ + uint8_t mc_frame_count; /**< Master Channel Frame Count (modulo-256). */ + uint8_t + vc_frame_count[SDLP_TM_VC_PER_MC]; /**< Virtual Channel Frame Counts, indexed by VCID. */ +} sdlp_tm_master_channel_t; -/** @brief Fixed-size table of per-Master-Channel counter state. */ -static tm_master_channel_t tm_master_channels[TM_MAX_MASTER_CHANNELS]; +/** + * @brief Fixed-size table of per-Master-Channel counter state (see ::SDLP_TM_MAX_MASTER_CHANNELS). + * + * Both counts are free-running modulo-256. Not thread-safe. + */ +static sdlp_tm_master_channel_t sdlp_tm_master_channels[SDLP_TM_MAX_MASTER_CHANNELS]; /** * @brief Return the counter state for a Master Channel, allocating a slot on first use. @@ -46,31 +44,145 @@ static tm_master_channel_t tm_master_channels[TM_MAX_MASTER_CHANNELS]; * @return Pointer to the channel's counters, or NULL if the table is already full of * other Master Channels. */ -static tm_master_channel_t *tm_get_master_channel(uint16_t spacecraft_id) +static sdlp_tm_master_channel_t *sdlp_tm_get_master_channel(uint16_t spacecraft_id) { - for (size_t i = 0; i < TM_MAX_MASTER_CHANNELS; i++) + for (size_t i = 0; i < SDLP_TM_MAX_MASTER_CHANNELS; i++) { - if (tm_master_channels[i].in_use && tm_master_channels[i].spacecraft_id == spacecraft_id) + if ((sdlp_tm_master_channels[i].in_use) && + (sdlp_tm_master_channels[i].spacecraft_id == spacecraft_id)) { - return &tm_master_channels[i]; + return &sdlp_tm_master_channels[i]; } } - for (size_t i = 0; i < TM_MAX_MASTER_CHANNELS; i++) + for (size_t i = 0; i < SDLP_TM_MAX_MASTER_CHANNELS; i++) { - if (!tm_master_channels[i].in_use) + if (!sdlp_tm_master_channels[i].in_use) { - tm_master_channels[i].in_use = 1; - tm_master_channels[i].spacecraft_id = spacecraft_id; - tm_master_channels[i].mc_frame_count = 0; - memset(tm_master_channels[i].vc_frame_count, + sdlp_tm_master_channels[i].in_use = 1; + sdlp_tm_master_channels[i].spacecraft_id = spacecraft_id; + sdlp_tm_master_channels[i].mc_frame_count = 0; + memset(sdlp_tm_master_channels[i].vc_frame_count, 0, - sizeof(tm_master_channels[i].vc_frame_count)); - return &tm_master_channels[i]; + sizeof(sdlp_tm_master_channels[i].vc_frame_count)); + return &sdlp_tm_master_channels[i]; } } return NULL; } +/** + * @brief Decode the 6-octet Transfer Frame Primary Header (CCSDS 132.0-B-3 §4.1.2). + * + * @param[in] buffer Wire buffer holding at least ::SDLP_TM_PRIMARY_HEADER_SIZE octets. + * @param[out] header Decoded primary header fields. + */ +static void sdlp_tm_decode_primary_header(const uint8_t *buffer, sdlp_tm_header_t *header) +{ + const uint16_t data_field_status = (uint16_t)(((uint16_t)buffer[4] << 8) | buffer[5]); + + memset(header, 0, sizeof(sdlp_tm_header_t)); + + header->transfer_frame_version = (uint16_t)((buffer[0] >> 6) & 0x03u); + header->spacecraft_id = (uint16_t)(((buffer[0] & 0x3Fu) << 4) | ((buffer[1] >> 4) & 0x0Fu)); + header->virtual_channel_id = (uint16_t)((buffer[1] >> 1) & 0x07u); + header->ocf_flag = (uint16_t)(buffer[1] & 0x01u); + header->master_channel_frame_count = buffer[2]; + header->virtual_channel_frame_count = buffer[3]; + sdlp_tm_unpack_data_field_status(data_field_status, &header->transfer_frame_data_field_status); +} + +/** + * @brief Validate the Transfer Frame Secondary Header of a wire frame and return its size. + * + * Reads nothing beyond the Identification Field, so it is safe on any buffer that holds + * at least a primary header and a Frame Error Control Field. + * + * @param[in] buffer Wire buffer holding the whole Transfer Frame. + * @param[in] buffer_size Buffer length in octets. + * @return Secondary Header size in octets (Identification Field plus Data Field), or 0 if + * the header is malformed or does not fit in the frame. + */ +static size_t sdlp_tm_secondary_header_wire_size(const uint8_t *buffer, size_t buffer_size) +{ + /* Need the Identification Field plus at least one Data Field octet (4.1.3.1.3). */ + if (buffer_size < (SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + SDLP_TM_SECONDARY_HEADER_MIN_DATA + SDLP_TM_FRAME_ERROR_CONTROL_SIZE)) + { + return 0; + } + + /* The wire Length is the total Secondary Header size minus one, which is exactly the + * Data Field length (4.1.3.2.3.2). */ + const size_t data_length = buffer[SDLP_TM_PRIMARY_HEADER_SIZE] & 0x3Fu; + const size_t header_size = SDLP_TM_SECONDARY_HEADER_ID_SIZE + data_length; + + if ((data_length == 0u) || (buffer_size < (SDLP_TM_PRIMARY_HEADER_SIZE + header_size + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE))) + { + return 0; + } + + return header_size; +} + +/** + * @brief Decode a wire-format Transfer Frame Secondary Header (CCSDS 132.0-B-3 §4.1.3). + * + * @param[in] wire Secondary Header octets, starting at the Identification Field + * and already validated by sdlp_tm_secondary_header_wire_size(). + * @param[out] secondary_header Decoded Secondary Header. + */ +static void sdlp_tm_decode_secondary_header(const uint8_t *wire, + sdlp_tm_secondary_header_t *secondary_header) +{ + secondary_header->version = (wire[0] >> 6) & 0x03u; + secondary_header->length = wire[0] & 0x3Fu; + memcpy(secondary_header->data, + &wire[SDLP_TM_SECONDARY_HEADER_ID_SIZE], + secondary_header->length); +} + +/** + * @brief Compute the encoded size of a TM Transfer Frame, validating the lengths it depends on. + * + * A frame filled in by hand, rather than through sdlp_tm_create_frame() and + * sdlp_tm_set_secondary_header(), can carry lengths that cannot be encoded. + * + * @param[in] frame Frame to measure. + * @return Encoded size in octets, or 0 if the Data Field is longer than ::SDLP_TM_MAX_DATA_SIZE + * or the Secondary Header Flag is set with an empty Secondary Header. + */ +static size_t sdlp_tm_encoded_size(const sdlp_tm_frame_t *frame) +{ + size_t size = + (size_t)SDLP_TM_PRIMARY_HEADER_SIZE + frame->data_length + SDLP_TM_FRAME_ERROR_CONTROL_SIZE; + + /* A data_length beyond the Data Field array would make the encoder read past it. */ + if (frame->data_length > SDLP_TM_MAX_DATA_SIZE) + { + return 0; + } + + if (frame->header.transfer_frame_data_field_status.secondary_header_flag) + { + /* The wire Length is the total Secondary Header size minus one (CCSDS 132.0-B-3, + * 4.1.3.2.3.2). A Length of 0 would be an Identification Field with no Data Field behind + * it, which is mandatory */ + if (frame->secondary_header.length == 0u) + { + return 0; + } + size += SDLP_TM_SECONDARY_HEADER_ID_SIZE + frame->secondary_header.length; + } + + if (frame->header.ocf_flag) + { + size += SDLP_TM_OCF_SIZE; + } + + return size; +} + uint16_t sdlp_tm_pack_data_field_status(const sdlp_tm_data_field_status_t *status) { if (!status) @@ -97,37 +209,39 @@ void sdlp_tm_unpack_data_field_status(uint16_t raw, sdlp_tm_data_field_status_t status->first_header_pointer = (uint16_t)(raw & 0x07FFu); } +void sdlp_tm_reset_frame_counts(void) +{ + memset(sdlp_tm_master_channels, 0, sizeof(sdlp_tm_master_channels)); +} + sdlp_status_t sdlp_tm_create_frame(sdlp_tm_frame_t *frame, uint16_t spacecraft_id, uint8_t virtual_channel_id, const uint8_t *data, uint16_t data_length) { - if (!frame || !data || data_length > TM_MAX_DATA_SIZE) + if ((!frame) || (!data) || (data_length > SDLP_TM_MAX_DATA_SIZE)) { return SDLP_ERROR_INVALID_PARAM; } - memset(frame, 0, sizeof(sdlp_tm_frame_t)); + /* The lookup is the last check and claims a table slot only when it succeeds, so a + * rejected call leaves the frame and every frame count as they were. */ + sdlp_tm_master_channel_t *mc = sdlp_tm_get_master_channel((uint16_t)(spacecraft_id & 0x3FFu)); + if (!mc) + { + return SDLP_ERROR_NO_RESOURCE; + } - uint16_t scid = (uint16_t)(spacecraft_id & 0x3FFu); - uint8_t vcid = (uint8_t)(virtual_channel_id & 0x07u); - tm_master_channel_t *mc = tm_get_master_channel(scid); + memset(frame, 0, sizeof(sdlp_tm_frame_t)); frame->header.transfer_frame_version = SDLP_VERSION; - frame->header.spacecraft_id = scid; - frame->header.virtual_channel_id = vcid; + frame->header.spacecraft_id = (uint16_t)(spacecraft_id & 0x3FFu); + frame->header.virtual_channel_id = (uint16_t)(virtual_channel_id & 0x07u); frame->header.ocf_flag = 0; - if (mc != NULL) - { - frame->header.master_channel_frame_count = mc->mc_frame_count++; - frame->header.virtual_channel_frame_count = mc->vc_frame_count[vcid]++; - } - else - { - frame->header.master_channel_frame_count = 0; - frame->header.virtual_channel_frame_count = 0; - } + frame->header.master_channel_frame_count = mc->mc_frame_count++; + frame->header.virtual_channel_frame_count = + mc->vc_frame_count[frame->header.virtual_channel_id]++; /* Default to a valid "Packets, no segmentation" Data Field Status: Sync Flag = 0 * requires the Segment Length Identifier to be '11' (CCSDS 132.0-B-3, 4.1.2.7.5.2), @@ -136,7 +250,7 @@ sdlp_status_t sdlp_tm_create_frame(sdlp_tm_frame_t *frame, frame->header.transfer_frame_data_field_status.sync_flag = 0; frame->header.transfer_frame_data_field_status.packet_order_flag = 0; frame->header.transfer_frame_data_field_status.segment_length_id = - TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION; + SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION; frame->header.transfer_frame_data_field_status.first_header_pointer = 0; memcpy(frame->data, data, data_length); @@ -149,28 +263,28 @@ sdlp_status_t sdlp_tm_set_secondary_header(sdlp_tm_frame_t *frame, const uint8_t *data, uint8_t length) { - if (!frame || !data || length == 0u || length > TM_SECONDARY_HEADER_MAX_DATA) + if ((!frame) || (!data) || (length == 0u) || (length > SDLP_TM_SECONDARY_HEADER_MAX_DATA)) { return SDLP_ERROR_INVALID_PARAM; } frame->header.transfer_frame_data_field_status.secondary_header_flag = 1; frame->secondary_header.version = 0; /* CCSDS 132.0-B-3, 4.1.3.2.2.2 */ - frame->secondary_header.length = length; + frame->secondary_header.length = (uint8_t)(length & 0x3Fu); memcpy(frame->secondary_header.data, data, length); return SDLP_SUCCESS; } -sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[TM_OCF_SIZE]) +sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[SDLP_TM_OCF_SIZE]) { - if (!frame || !ocf) + if ((!frame) || (!ocf)) { return SDLP_ERROR_INVALID_PARAM; } frame->header.ocf_flag = 1; - memcpy(frame->ocf, ocf, TM_OCF_SIZE); + memcpy(frame->ocf, ocf, SDLP_TM_OCF_SIZE); return SDLP_SUCCESS; } @@ -180,25 +294,15 @@ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, size_t buffer_size, size_t *encoded_size) { - if (!frame || !buffer || !encoded_size) + if ((!frame) || (!buffer) || (!encoded_size)) { return SDLP_ERROR_INVALID_PARAM; } - int secondary_header_present = - frame->header.transfer_frame_data_field_status.secondary_header_flag ? 1 : 0; - int ocf_present = frame->header.ocf_flag ? 1 : 0; - - size_t required_size = - TM_PRIMARY_HEADER_SIZE + frame->data_length + TM_FRAME_ERROR_CONTROL_SIZE; - - if (secondary_header_present) - { - required_size += TM_SECONDARY_HEADER_ID_SIZE + frame->secondary_header.length; - } - if (ocf_present) + const size_t required_size = sdlp_tm_encoded_size(frame); + if (required_size == 0u) { - required_size += TM_OCF_SIZE; + return SDLP_ERROR_INVALID_PARAM; } if (buffer_size < required_size) @@ -208,7 +312,7 @@ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, size_t offset = 0; - buffer[offset++] = (uint8_t)((frame->header.transfer_frame_version << 6) | + buffer[offset++] = (uint8_t)(((frame->header.transfer_frame_version & 0x03u) << 6) | ((frame->header.spacecraft_id >> 4) & 0x3Fu)); buffer[offset++] = (uint8_t)(((frame->header.spacecraft_id & 0x0Fu) << 4) | ((frame->header.virtual_channel_id & 0x07u) << 1) | @@ -223,7 +327,7 @@ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, /* Transfer Frame Secondary Header (CCSDS 132.0-B-3, 4.1.3): Identification Field * (Version '00' | Length = total size - 1 = Data Field length) then the Data Field. */ - if (secondary_header_present) + if (frame->header.transfer_frame_data_field_status.secondary_header_flag) { buffer[offset++] = (uint8_t)(((frame->secondary_header.version & 0x03u) << 6) | (frame->secondary_header.length & 0x3Fu)); @@ -236,10 +340,10 @@ sdlp_status_t sdlp_tm_encode_frame(const sdlp_tm_frame_t *frame, /* Operational Control Field (CCSDS 132.0-B-3, 4.1.5): four octets following the * Data Field, present when the OCF Flag is set. The content is caller-supplied. */ - if (ocf_present) + if (frame->header.ocf_flag) { - memcpy(&buffer[offset], frame->ocf, TM_OCF_SIZE); - offset += TM_OCF_SIZE; + memcpy(&buffer[offset], frame->ocf, SDLP_TM_OCF_SIZE); + offset += SDLP_TM_OCF_SIZE; } /* The Frame Error Control Field is passed through verbatim; computing an @@ -256,92 +360,61 @@ sdlp_status_t sdlp_tm_decode_frame(const uint8_t *buffer, size_t buffer_size, sdlp_tm_frame_t *frame) { - if (!buffer || !frame || buffer_size < TM_PRIMARY_HEADER_SIZE + TM_FRAME_ERROR_CONTROL_SIZE) + if ((!buffer) || (!frame) || + (buffer_size < (SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE))) { return SDLP_ERROR_INVALID_PARAM; } - memset(frame, 0, sizeof(sdlp_tm_frame_t)); - - size_t offset = 0; - - frame->header.transfer_frame_version = (uint8_t)((buffer[offset] >> 6) & 0x03u); - frame->header.spacecraft_id = - (uint16_t)(((buffer[offset] & 0x3Fu) << 4) | ((buffer[offset + 1] >> 4) & 0x0Fu)); - offset++; - - frame->header.virtual_channel_id = (uint8_t)((buffer[offset] >> 1) & 0x07u); - frame->header.ocf_flag = (uint8_t)(buffer[offset] & 0x01u); - offset++; - - frame->header.master_channel_frame_count = buffer[offset++]; - frame->header.virtual_channel_frame_count = buffer[offset++]; - - uint16_t data_field_status = (uint16_t)(((uint16_t)buffer[offset] << 8) | buffer[offset + 1]); - sdlp_tm_unpack_data_field_status(data_field_status, - &frame->header.transfer_frame_data_field_status); - offset += 2; + sdlp_tm_header_t header; + sdlp_tm_decode_primary_header(buffer, &header); /* Transfer Frame Secondary Header (CCSDS 132.0-B-3, 4.1.3), present when the * Secondary Header Flag is set. Its size is signaled in the Identification Field. */ size_t secondary_header_size = 0; - if (frame->header.transfer_frame_data_field_status.secondary_header_flag) + if (header.transfer_frame_data_field_status.secondary_header_flag) { - uint8_t sh_length; - - /* Need the Identification Field plus at least one Data Field octet (4.1.3.1.3). */ - if (buffer_size < - TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + 1u + TM_FRAME_ERROR_CONTROL_SIZE) + secondary_header_size = sdlp_tm_secondary_header_wire_size(buffer, buffer_size); + if (secondary_header_size == 0u) { return SDLP_ERROR_INVALID_FRAME; } - - frame->secondary_header.version = (uint8_t)((buffer[offset] >> 6) & 0x03u); - sh_length = (uint8_t)(buffer[offset] & 0x3Fu); /* total size - 1 = Data Field length */ - if (sh_length == 0u) - { - return SDLP_ERROR_INVALID_FRAME; - } - secondary_header_size = TM_SECONDARY_HEADER_ID_SIZE + sh_length; - - if (buffer_size < - TM_PRIMARY_HEADER_SIZE + secondary_header_size + TM_FRAME_ERROR_CONTROL_SIZE) - { - return SDLP_ERROR_INVALID_FRAME; - } - - frame->secondary_header.length = sh_length; - memcpy(frame->secondary_header.data, - &buffer[offset + TM_SECONDARY_HEADER_ID_SIZE], - sh_length); - offset += secondary_header_size; } /* Operational Control Field (CCSDS 132.0-B-3, 4.1.5): four octets between the * Data Field and the Frame Error Control Field, present when the OCF Flag is set. */ - size_t ocf_size = frame->header.ocf_flag ? TM_OCF_SIZE : 0; + const size_t ocf_size = header.ocf_flag ? SDLP_TM_OCF_SIZE : 0; + const size_t overhead = SDLP_TM_PRIMARY_HEADER_SIZE + secondary_header_size + ocf_size + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE; - size_t overhead = - TM_PRIMARY_HEADER_SIZE + secondary_header_size + ocf_size + TM_FRAME_ERROR_CONTROL_SIZE; - if (buffer_size < overhead) + /* The Data Field length is range-checked at full width: narrowing it to the 16-bit + * data_length first would let an oversized buffer wrap round to a small, valid length. */ + if ((buffer_size < overhead) || ((buffer_size - overhead) > SDLP_TM_MAX_DATA_SIZE)) { return SDLP_ERROR_INVALID_FRAME; } + const size_t data_length = buffer_size - overhead; - frame->data_length = (uint16_t)(buffer_size - overhead); + /* Every check has passed. The output frame is written only from here on, so a + * rejected buffer leaves it exactly as the caller passed it. */ + memset(frame, 0, sizeof(sdlp_tm_frame_t)); + frame->header = header; - if (frame->data_length > TM_MAX_DATA_SIZE) + size_t offset = SDLP_TM_PRIMARY_HEADER_SIZE; + if (secondary_header_size > 0u) { - return SDLP_ERROR_INVALID_FRAME; + sdlp_tm_decode_secondary_header(&buffer[offset], &frame->secondary_header); + offset += secondary_header_size; } - memcpy(frame->data, &buffer[offset], frame->data_length); - offset += frame->data_length; + memcpy(frame->data, &buffer[offset], data_length); + frame->data_length = (uint16_t)data_length; + offset += data_length; - if (frame->header.ocf_flag) + if (header.ocf_flag) { - memcpy(frame->ocf, &buffer[offset], TM_OCF_SIZE); - offset += TM_OCF_SIZE; + memcpy(frame->ocf, &buffer[offset], SDLP_TM_OCF_SIZE); + offset += SDLP_TM_OCF_SIZE; } /* The Frame Error Control Field is surfaced as-is; validating it (e.g. via diff --git a/tests/cunit.h b/tests/cunit.h index 76fcac3..628faf8 100644 --- a/tests/cunit.h +++ b/tests/cunit.h @@ -1,6 +1,14 @@ -/* Tiny C unit test helpers. Suitable for embedding in small projects. - * Usage: include this header in a single C test file and implement test - * functions returning 0 on success, non-zero on failure. Use RUN_TEST(fn). +/** + * @file cunit.h + * @brief Tiny C unit test helpers + * + * Include this header in a test file and implement test functions returning 0 on + * success, non-zero on failure. Use RUN_TEST(fn). + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef CUNIT_H #define CUNIT_H @@ -14,13 +22,15 @@ static int cunit_total_tests = 0; #define ASSERT_EQ_INT(expected, actual) \ do \ { \ - if ((int)(expected) != (int)(actual)) \ + const int cunit_expected = (int)(expected); \ + const int cunit_actual = (int)(actual); \ + if (cunit_expected != cunit_actual) \ { \ printf("ASSERT_EQ_INT failed: %s:%d: expected %d got %d\n", \ __FILE__, \ __LINE__, \ - (int)(expected), \ - (int)(actual)); \ + cunit_expected, \ + cunit_actual); \ return 1; \ } \ } while (0) @@ -52,7 +62,9 @@ static int cunit_total_tests = 0; cunit_total_tests++; \ int r = fn(); \ if (r == 0) \ + { \ printf("PASS %s\n", #fn); \ + } \ else \ { \ printf("FAIL %s\n", #fn); \ diff --git a/tests/test_runners.h b/tests/test_runners.h index 321764e..906ce21 100644 --- a/tests/test_runners.h +++ b/tests/test_runners.h @@ -1,10 +1,21 @@ +/** + * @file test_runners.h + * @brief Per-module test runner interface + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + #ifndef TEST_RUNNERS_H #define TEST_RUNNERS_H +/** @brief Pass/total tally returned by a module's test runner. */ typedef struct { - int passed; - int total; + int passed; /**< Tests that passed. */ + int total; /**< Tests that ran. */ } test_result_t; /* Per-module test runners. Each runs all of its module's tests and returns the diff --git a/tests/test_tc.c b/tests/test_tc.c index bd03e79..e014f93 100644 --- a/tests/test_tc.c +++ b/tests/test_tc.c @@ -1,6 +1,11 @@ /** - * @file test_tc.c - * @brief Unit tests for the TC Space Data Link Protocol frame handler. + * @file test_tc.c + * @brief Unit tests for the TC Space Data Link Protocol frame handler. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "cunit.h" #include "sdlp_common.h" @@ -9,22 +14,59 @@ #include #include +#include +#include -/* The TC unit tests always exercise the segment-header configuration. */ -#ifndef TC_SEGMENT_HEADER_ENABLED -#error "test_tc.c must be built with -DTC_SEGMENT_HEADER_ENABLED" +/** + * @brief Octets the Segment Header adds to a Type-D frame in this build configuration. + * + * The suite is built twice, without and with SDLP_TC_SEGMENT_HEADER_ENABLED, so every size that + * depends on the Segment Header is written in terms of this value. + */ +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED +# define TEST_TC_SEGMENT_OCTETS SDLP_TC_SEGMENT_HEADER_SIZE +#else +# define TEST_TC_SEGMENT_OCTETS 0 #endif -static int test_tc_create_frame_invalid_params(void) +/** @brief Scratch buffer size for rejected-encode checks: holds a frame just over the limit. */ +#define TEST_TC_SCRATCH_SIZE (2 * SDLP_TC_MAX_FRAME_SIZE) + +/** + * @brief Build a frame the library must reject and check the output frame is untouched. + * + * @param[in] data Data Field content (may be NULL). + * @param[in] data_length Data Field length in octets. + * @return 0 if sdlp_tc_create_frame() returned ::SDLP_ERROR_INVALID_PARAM and left the frame + * unchanged, 1 otherwise. + */ +static int test_tc_expect_create_rejected(const uint8_t *data, uint16_t data_length) { sdlp_tc_frame_t frame; - uint8_t payload[1] = {0x55u}; + sdlp_tc_frame_t untouched; + + memset(&frame, 0xA5, sizeof(frame)); + memset(&untouched, 0xA5, sizeof(untouched)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_create_frame(NULL, 1, 1, 1, payload, 1)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_create_frame(&frame, 1, 1, 1, NULL, 1)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tc_create_frame(&frame, 1, 1, 1, payload, TC_MAX_DATA_SIZE + 1)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_create_frame(&frame, 1, 1, 1, payload, 0)); + sdlp_tc_create_frame(&frame, 1, 1, 1, data, data_length)); + ASSERT_EQ_MEM(&untouched, &frame, sizeof(frame)); + + return 0; +} + +static int test_tc_create_frame_invalid_params(void) +{ + static const uint8_t payload[SDLP_TC_MAX_DATA_SIZE + 1] = {0x55u}; + /* The largest Data Field is the array, less the Segment Header octet when compiled in. */ + const uint16_t max_data = SDLP_TC_MAX_DATA_SIZE - TEST_TC_SEGMENT_OCTETS; + + ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_create_frame(NULL, 1, 1, 1, payload, 1)); + ASSERT_EQ_INT(0, test_tc_expect_create_rejected(NULL, 1)); + ASSERT_EQ_INT(0, test_tc_expect_create_rejected(payload, 0)); + /* The first rejected length is one octet past the largest Data Field. */ + ASSERT_EQ_INT(0, test_tc_expect_create_rejected(payload, (uint16_t)(max_data + 1))); + ASSERT_EQ_INT(0, test_tc_expect_create_rejected(payload, SDLP_TC_MAX_DATA_SIZE + 1)); return 0; } @@ -34,9 +76,10 @@ static int test_tc_encode_decode_roundtrip(void) sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0x01u, 0x23u, 0x45u, 0x67u}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; - const int seg = TC_SEGMENT_HEADER_SIZE; /* a Type-D frame carries a Segment Header */ + const int seg = TEST_TC_SEGMENT_OCTETS; ASSERT_EQ_INT( SDLP_SUCCESS, @@ -44,7 +87,8 @@ static int test_tc_encode_decode_roundtrip(void) ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); - ASSERT_EQ_INT(TC_PRIMARY_HEADER_SIZE + seg + (int)sizeof(payload) + TC_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TC_PRIMARY_HEADER_SIZE + seg + (int)sizeof(payload) + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); @@ -53,8 +97,8 @@ static int test_tc_encode_decode_roundtrip(void) ASSERT_EQ_INT((int)(0x7Fu & 0x3Fu), decoded.header.virtual_channel_id); ASSERT_EQ_INT(0x9Au, decoded.header.frame_sequence_number); /* Frame Length = total octets in the frame - 1 (CCSDS 232.0-B-4, 4.1.2.7.2). */ - ASSERT_EQ_INT(TC_PRIMARY_HEADER_SIZE + seg + (int)sizeof(payload) + - TC_FRAME_ERROR_CONTROL_SIZE - 1, + ASSERT_EQ_INT(SDLP_TC_PRIMARY_HEADER_SIZE + seg + (int)sizeof(payload) + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1, decoded.header.frame_length); ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); @@ -66,7 +110,9 @@ static int test_tc_encode_buffer_too_small(void) { sdlp_tc_frame_t frame; const uint8_t payload[] = {0xABu, 0xCDu, 0xEFu}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + 2 + TC_FRAME_ERROR_CONTROL_SIZE]; + /* One octet short of the encoded frame. */ + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + TEST_TC_SEGMENT_OCTETS + sizeof(payload) - 1 + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -82,7 +128,8 @@ static int test_tc_fecf_passthrough(void) sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0x11u, 0x22u, 0x33u, 0x44u}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT( @@ -96,8 +143,7 @@ static int test_tc_fecf_passthrough(void) ASSERT_EQ_INT(0x12u, encoded[encoded_size - 2]); ASSERT_EQ_INT(0x34u, encoded[encoded_size - 1]); - /* Decode no longer validates the FECF: it always succeeds and surfaces the - * field as-is. */ + /* Decode does not validate the FECF: it surfaces the field as-is. */ ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); ASSERT_EQ_INT(0x1234u, decoded.fecf); @@ -109,7 +155,8 @@ static int test_tc_decode_invalid_frame_length(void) sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0x01u, 0x02u, 0x03u, 0x04u}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT( @@ -131,7 +178,8 @@ static int test_tc_frame_type_bd_roundtrip(void) sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0x5Au, 0xA5u}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT( @@ -163,12 +211,16 @@ static int test_tc_frame_type_bd_roundtrip(void) static int test_tc_set_frame_type_invalid(void) { sdlp_tc_frame_t frame; + sdlp_tc_frame_t before; const uint8_t payload[1] = {0x01u}; ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 1, 1, 1, payload, 1)); + memcpy(&before, &frame, sizeof(frame)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_set_frame_type(NULL, SDLP_TC_FRAME_TYPE_BD)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_set_frame_type(&frame, (sdlp_tc_frame_type_t)99)); + /* A rejected type leaves the whole frame as it was, flags and Frame Length included. */ + ASSERT_EQ_MEM(&before, &frame, sizeof(frame)); return 0; } @@ -177,7 +229,8 @@ static int test_tc_unlock_command(void) { sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_unlock_frame(&frame, 0x30u, 0x01u)); @@ -187,21 +240,21 @@ static int test_tc_unlock_command(void) /* Frame Sequence Number is 'all zeroes' for Type-B frames. */ ASSERT_EQ_INT(0, frame.header.frame_sequence_number); /* Unlock: a single 'all zeroes' octet (4.1.3.3.2). */ - ASSERT_EQ_INT(TC_CONTROL_CMD_UNLOCK_LENGTH, frame.data_length); - ASSERT_EQ_INT(TC_CONTROL_CMD_UNLOCK, frame.data[0]); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_UNLOCK_LENGTH, frame.data_length); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_UNLOCK, frame.data[0]); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); /* Type-BC frames never carry a Segment Header (4.1.3.2.2.1.3). */ - ASSERT_EQ_INT(TC_PRIMARY_HEADER_SIZE + (int)TC_CONTROL_CMD_UNLOCK_LENGTH + - TC_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TC_PRIMARY_HEADER_SIZE + (int)SDLP_TC_CONTROL_CMD_UNLOCK_LENGTH + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); ASSERT_EQ_INT(1, decoded.header.bypass_flag); ASSERT_EQ_INT(1, decoded.header.control_command_flag); - ASSERT_EQ_INT(TC_CONTROL_CMD_UNLOCK_LENGTH, decoded.data_length); - ASSERT_EQ_INT(TC_CONTROL_CMD_UNLOCK, decoded.data[0]); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_UNLOCK_LENGTH, decoded.data_length); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_UNLOCK, decoded.data[0]); return 0; } @@ -210,7 +263,8 @@ static int test_tc_set_vr_command(void) { sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_set_vr_frame(&frame, 0x30u, 0x01u, 0x7Cu)); @@ -218,15 +272,15 @@ static int test_tc_set_vr_command(void) ASSERT_EQ_INT(1, frame.header.control_command_flag); ASSERT_EQ_INT(0, frame.header.frame_sequence_number); /* Set V(R): '10000010 00000000 XXXXXXXX' (4.1.3.3.3). */ - ASSERT_EQ_INT(TC_CONTROL_CMD_SET_VR_LENGTH, frame.data_length); - ASSERT_EQ_INT(TC_CONTROL_CMD_SET_VR_OCTET0, frame.data[0]); - ASSERT_EQ_INT(TC_CONTROL_CMD_SET_VR_OCTET1, frame.data[1]); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_SET_VR_LENGTH, frame.data_length); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_SET_VR_OCTET0, frame.data[0]); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_SET_VR_OCTET1, frame.data[1]); ASSERT_EQ_INT(0x7Cu, frame.data[2]); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); - ASSERT_EQ_INT(TC_CONTROL_CMD_SET_VR_LENGTH, decoded.data_length); + ASSERT_EQ_INT(SDLP_TC_CONTROL_CMD_SET_VR_LENGTH, decoded.data_length); ASSERT_EQ_INT(0x7Cu, decoded.data[2]); return 0; @@ -237,7 +291,8 @@ static int test_tc_decode_reserved_frame_type(void) sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0x01u, 0x02u}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_MAX_DATA_SIZE + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT( @@ -279,21 +334,23 @@ static int test_tc_null_params(void) /* decode rejects a NULL buffer, a NULL frame, and a buffer shorter than the header + FECF. */ ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_decode_frame(NULL, sizeof(buffer), &decoded)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tc_decode_frame(buffer, sizeof(buffer), NULL)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tc_decode_frame(buffer, - TC_PRIMARY_HEADER_SIZE + TC_FRAME_ERROR_CONTROL_SIZE - 1, - &decoded)); + ASSERT_EQ_INT( + SDLP_ERROR_INVALID_PARAM, + sdlp_tc_decode_frame(buffer, + SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1, + &decoded)); return 0; } +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED static int test_tc_segment_header_roundtrip(void) { sdlp_tc_frame_t frame; sdlp_tc_frame_t decoded; const uint8_t payload[] = {0xC0u, 0xDEu}; - uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_SEGMENT_HEADER_SIZE + TC_MAX_DATA_SIZE + - TC_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_SEGMENT_HEADER_SIZE + + SDLP_TC_MAX_DATA_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT( @@ -301,20 +358,22 @@ static int test_tc_segment_header_roundtrip(void) sdlp_tc_create_frame(&frame, 0x2Au, 0x03u, 0x08u, payload, (uint16_t)sizeof(payload))); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tc_set_segment_header(NULL, TC_SEQ_FLAG_NO_SEG, 0)); - ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, TC_SEQ_FLAG_FIRST, 0x2Fu)); - ASSERT_EQ_INT(TC_SEQ_FLAG_FIRST, frame.segment_header.sequence_flags); + sdlp_tc_set_segment_header(NULL, SDLP_TC_SEQ_FLAG_NO_SEG, 0)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_FIRST, 0x2Fu)); + ASSERT_EQ_INT(SDLP_TC_SEQ_FLAG_FIRST, frame.segment_header.sequence_flags); ASSERT_EQ_INT(0x2Fu, frame.segment_header.map_id); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + /* On the wire: Sequence Flags '01' in the top two bits, then the MAP ID. */ + ASSERT_EQ_INT(0x6Fu, encoded[SDLP_TC_PRIMARY_HEADER_SIZE]); /* primary(5) + segment(1) + payload(2) + FECF(2) */ - ASSERT_EQ_INT(TC_PRIMARY_HEADER_SIZE + TC_SEGMENT_HEADER_SIZE + (int)sizeof(payload) + - TC_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_SEGMENT_HEADER_SIZE + (int)sizeof(payload) + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); - ASSERT_EQ_INT(TC_SEQ_FLAG_FIRST, decoded.segment_header.sequence_flags); + ASSERT_EQ_INT(SDLP_TC_SEQ_FLAG_FIRST, decoded.segment_header.sequence_flags); ASSERT_EQ_INT(0x2Fu, decoded.segment_header.map_id); ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); @@ -334,6 +393,399 @@ static int test_tc_decode_segment_too_small(void) return 0; } +#endif /* SDLP_TC_SEGMENT_HEADER_ENABLED */ + +/** + * @brief Decode a buffer the decoder must reject and check the output frame is untouched. + * + * The frame is pre-filled with a sentinel pattern, so any write by the decoder shows up + * as a difference, whichever field it lands in. + * + * @param[in] buffer Wire buffer to decode (may be NULL). + * @param[in] buffer_size Buffer length in octets. + * @param[in] expected Status the decoder must return. + * @return 0 if the decoder returned @p expected and left the frame unchanged, 1 otherwise. + */ +static int test_tc_expect_decode_rejected(const uint8_t *buffer, + size_t buffer_size, + sdlp_status_t expected) +{ + sdlp_tc_frame_t frame; + sdlp_tc_frame_t untouched; + uint8_t *exact = NULL; + sdlp_status_t status; + + memset(&frame, 0xA5, sizeof(frame)); + memset(&untouched, 0xA5, sizeof(untouched)); + + /* The decoder reads a heap copy of exactly buffer_size octets, so ASan reports any + * read past the end of the input. */ + if (buffer) + { + exact = malloc((buffer_size > 0u) ? buffer_size : 1u); + ASSERT_TRUE(exact); + memcpy(exact, buffer, buffer_size); + } + status = sdlp_tc_decode_frame(exact, buffer_size, &frame); + free(exact); + + ASSERT_EQ_INT(expected, status); + ASSERT_EQ_MEM(&untouched, &frame, sizeof(frame)); + + return 0; +} + +static int test_tc_decode_failure_leaves_frame_unchanged(void) +{ + const uint8_t too_short[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1] = { + 0}; + /* Bypass=0, Control Command=1 is reserved (table 4-1); the Frame Length is consistent. */ + const uint8_t reserved_type[8] = {0x10u, 0, 0, 7}; + /* Frame Length = 6 announces 7 octets, but 8 are present. */ + const uint8_t wrong_length[8] = {0, 0, 0, 6}; +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + /* A consistent 7-octet Type-D frame has no room for the Segment Header and the FECF. */ + const uint8_t no_segment_room[7] = {0, 0, 0, 6}; +#endif + + ASSERT_EQ_INT( + 0, + test_tc_expect_decode_rejected(NULL, sizeof(wrong_length), SDLP_ERROR_INVALID_PARAM)); + ASSERT_EQ_INT( + 0, + test_tc_expect_decode_rejected(too_short, sizeof(too_short), SDLP_ERROR_INVALID_PARAM)); + ASSERT_EQ_INT(0, + test_tc_expect_decode_rejected(reserved_type, + sizeof(reserved_type), + SDLP_ERROR_INVALID_FRAME)); + ASSERT_EQ_INT(0, + test_tc_expect_decode_rejected(wrong_length, + sizeof(wrong_length), + SDLP_ERROR_INVALID_FRAME)); +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + ASSERT_EQ_INT(0, + test_tc_expect_decode_rejected(no_segment_room, + sizeof(no_segment_room), + SDLP_ERROR_INVALID_FRAME)); +#endif + + return 0; +} + +/** + * @brief Encode a frame the encoder must reject and check both outputs are untouched. + * + * The buffer and the encoded size are pre-filled with a sentinel pattern, so any write + * by the encoder shows up as a difference. + * + * @param[in] frame Frame to encode. + * @param[in] buffer_size Capacity to offer the encoder, at most ::TEST_TC_SCRATCH_SIZE. + * @param[in] expected Status the encoder must return. + * @return 0 if the encoder returned @p expected and left its outputs unchanged, 1 otherwise. + */ +static int test_tc_expect_encode_rejected(const sdlp_tc_frame_t *frame, + size_t buffer_size, + sdlp_status_t expected) +{ + uint8_t buffer[TEST_TC_SCRATCH_SIZE]; + uint8_t untouched[TEST_TC_SCRATCH_SIZE]; + size_t encoded_size = 0xA5A5u; + + memset(buffer, 0xA5, sizeof(buffer)); + memset(untouched, 0xA5, sizeof(untouched)); + + ASSERT_EQ_INT(expected, sdlp_tc_encode_frame(frame, buffer, buffer_size, &encoded_size)); + ASSERT_EQ_INT(0xA5A5u, encoded_size); + ASSERT_EQ_MEM(untouched, buffer, sizeof(buffer)); + + return 0; +} + +/** + * @brief Round-trip the largest legal Type-D frame through an exact-size buffer. + * + * @param[out] encoded Buffer of exactly ::SDLP_TC_MAX_FRAME_SIZE octets. + * @return 0 if every check passed, 1 otherwise. + */ +static int test_tc_check_max_frame_roundtrip(uint8_t *encoded) +{ + sdlp_tc_frame_t frame; + sdlp_tc_frame_t decoded; + uint8_t payload[SDLP_TC_MAX_DATA_SIZE - TEST_TC_SEGMENT_OCTETS]; + size_t encoded_size = 0; + + for (size_t i = 0; i < sizeof(payload); i++) + { + payload[i] = (uint8_t)i; + } + + ASSERT_EQ_INT( + SDLP_SUCCESS, + sdlp_tc_create_frame(&frame, 0x3FFu, 0x3Fu, 0xFFu, payload, (uint16_t)sizeof(payload))); + ASSERT_EQ_INT(0, + test_tc_expect_encode_rejected(&frame, + SDLP_TC_MAX_FRAME_SIZE - 1, + SDLP_ERROR_BUFFER_TOO_SMALL)); + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, SDLP_TC_MAX_FRAME_SIZE, &encoded_size)); + ASSERT_EQ_INT(SDLP_TC_MAX_FRAME_SIZE, encoded_size); + /* Frame Length = 1023 sets all ten bits: the low two of octet 2 and all of octet 3. */ + ASSERT_EQ_INT(0x03u, encoded[2] & 0x03u); + ASSERT_EQ_INT(0xFFu, encoded[3]); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); + ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); + ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); + + return 0; +} + +static int test_tc_encode_max_frame_exact_buffer(void) +{ + uint8_t *encoded = malloc(SDLP_TC_MAX_FRAME_SIZE); + int result; + + ASSERT_TRUE(encoded); + result = test_tc_check_max_frame_roundtrip(encoded); + free(encoded); + ASSERT_EQ_INT(0, result); + + return 0; +} + +static int test_tc_encode_rejects_oversized_frame(void) +{ + sdlp_tc_frame_t frame; + const uint8_t payload[1] = {0x01u}; + uint8_t encoded[SDLP_TC_MAX_FRAME_SIZE]; + size_t encoded_size = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 1, 1, 1, payload, 1)); + + /* Type-D: one octet more than the largest Data Field, which is the array less the + * Segment Header octet when that is compiled in. The frame would be 1025 octets, one + * more than the 10-bit Frame Length can express. */ + frame.data_length = SDLP_TC_MAX_DATA_SIZE - TEST_TC_SEGMENT_OCTETS + 1; + ASSERT_EQ_INT( + 0, + test_tc_expect_encode_rejected(&frame, TEST_TC_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + /* A length far beyond the Data Field array must not be read out of the frame. */ + frame.data_length = UINT16_MAX; + ASSERT_EQ_INT( + 0, + test_tc_expect_encode_rejected(&frame, TEST_TC_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + /* Type-BC carries no Segment Header: a full Data Field array is exactly the largest + * frame, and one octet more is rejected. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_frame_type(&frame, SDLP_TC_FRAME_TYPE_BC)); + frame.data_length = SDLP_TC_MAX_DATA_SIZE; + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT(SDLP_TC_MAX_FRAME_SIZE, encoded_size); + + frame.data_length = SDLP_TC_MAX_DATA_SIZE + 1; + ASSERT_EQ_INT( + 0, + test_tc_expect_encode_rejected(&frame, TEST_TC_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + return 0; +} + +/** + * @brief A complete Type-BD frame with a distinct value in every field, as sent on the wire. + */ +static const uint8_t test_tc_golden[] = { + 0x22u, /* Version 00, Bypass 1, Control Command 0, Reserved 00, Spacecraft ID bits 9-8 */ + 0xA5u, /* Spacecraft ID bits 7-0: the ID is 0x2A5 */ + 0x54u, /* Virtual Channel ID 0x15, Frame Length bits 9-8 */ + SDLP_TC_PRIMARY_HEADER_SIZE + TEST_TC_SEGMENT_OCTETS + 2 + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1, + 0x9Cu, /* Frame Sequence Number */ +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + 0xAFu, /* Sequence Flags '10' (last portion), MAP ID 0x2F */ +#endif + 0xD1u, + 0xD2u, /* Data Field */ + 0xFEu, + 0xC5u /* FECF */ +}; + +static int test_tc_golden_frame_encode(void) +{ + sdlp_tc_frame_t frame; + const uint8_t payload[] = {0xD1u, 0xD2u}; + uint8_t encoded[sizeof(test_tc_golden)]; + size_t encoded_size = 0; + + ASSERT_EQ_INT( + SDLP_SUCCESS, + sdlp_tc_create_frame(&frame, 0x2A5u, 0x15u, 0x9Cu, payload, (uint16_t)sizeof(payload))); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_frame_type(&frame, SDLP_TC_FRAME_TYPE_BD)); +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_LAST, 0x2Fu)); +#endif + frame.fecf = 0xFEC5u; + + /* The buffer is exactly as long as the frame, and every octet is compared. */ + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT((int)sizeof(test_tc_golden), (int)encoded_size); + ASSERT_EQ_MEM(test_tc_golden, encoded, sizeof(test_tc_golden)); + + return 0; +} + +static int test_tc_golden_frame_decode(void) +{ + sdlp_tc_frame_t decoded; + const uint8_t payload[] = {0xD1u, 0xD2u}; + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_decode_frame(test_tc_golden, sizeof(test_tc_golden), &decoded)); + ASSERT_EQ_INT(0, decoded.header.transfer_frame_version); + ASSERT_EQ_INT(1, decoded.header.bypass_flag); + ASSERT_EQ_INT(0, decoded.header.control_command_flag); + ASSERT_EQ_INT(0, decoded.header.reserved); + ASSERT_EQ_INT(0x2A5u, decoded.header.spacecraft_id); + ASSERT_EQ_INT(0x15u, decoded.header.virtual_channel_id); + ASSERT_EQ_INT((int)sizeof(test_tc_golden) - 1, decoded.header.frame_length); + ASSERT_EQ_INT(0x9Cu, decoded.header.frame_sequence_number); +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + ASSERT_EQ_INT(SDLP_TC_SEQ_FLAG_LAST, decoded.segment_header.sequence_flags); + ASSERT_EQ_INT(0x2Fu, decoded.segment_header.map_id); +#endif + ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); + ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); + ASSERT_EQ_INT(0xFEC5u, decoded.fecf); + + return 0; +} + +static int test_tc_primary_header_wire_limits(void) +{ + sdlp_tc_frame_t frame; + sdlp_tc_frame_t decoded; + const uint8_t payload[1] = {0x00u}; + uint8_t encoded[SDLP_TC_MAX_FRAME_SIZE]; + size_t encoded_size = 0; + /* Every field the caller sets is zero; only the Frame Length (total octets - 1) is not. */ + const uint8_t all_zero[SDLP_TC_PRIMARY_HEADER_SIZE] = {0x00u, + 0x00u, + 0x00u, + SDLP_TC_PRIMARY_HEADER_SIZE + + TEST_TC_SEGMENT_OCTETS + 1 + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1, + 0x00u}; + const uint8_t all_max[SDLP_TC_PRIMARY_HEADER_SIZE] = {0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu}; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 0, 0, 0, payload, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_MEM(all_zero, encoded, sizeof(all_zero)); + + /* Every field at its maximum. A Type-BC frame carries no Segment Header, so a full Data + * Field array makes the largest frame and the Frame Length reaches 1023 as well. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 0x3FFu, 0x3Fu, 0xFFu, payload, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_frame_type(&frame, SDLP_TC_FRAME_TYPE_BC)); + frame.header.transfer_frame_version = 0x03u; + frame.header.reserved = 0x03u; + frame.data_length = SDLP_TC_MAX_DATA_SIZE; + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_MEM(all_max, encoded, sizeof(all_max)); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_decode_frame(encoded, encoded_size, &decoded)); + ASSERT_EQ_INT(0x03u, decoded.header.transfer_frame_version); + ASSERT_EQ_INT(1, decoded.header.bypass_flag); + ASSERT_EQ_INT(1, decoded.header.control_command_flag); + ASSERT_EQ_INT(0x03u, decoded.header.reserved); + ASSERT_EQ_INT(0x3FFu, decoded.header.spacecraft_id); + ASSERT_EQ_INT(0x3Fu, decoded.header.virtual_channel_id); + ASSERT_EQ_INT(SDLP_TC_MAX_FRAME_SIZE - 1, decoded.header.frame_length); + ASSERT_EQ_INT(0xFFu, decoded.header.frame_sequence_number); + + return 0; +} + +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED +static int test_tc_segment_header_wire_limits(void) +{ + sdlp_tc_frame_t frame; + const uint8_t payload[1] = {0x00u}; + uint8_t encoded[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_SEGMENT_HEADER_SIZE + 1 + + SDLP_TC_FRAME_ERROR_CONTROL_SIZE]; + size_t encoded_size = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 1, 1, 1, payload, 1)); + + /* Both fields zero, then both at their maximum: the octet right after the primary header. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_CONTINUE, 0)); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT(0x00u, encoded[SDLP_TC_PRIMARY_HEADER_SIZE]); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_NO_SEG, 0x3Fu)); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT(0xFFu, encoded[SDLP_TC_PRIMARY_HEADER_SIZE]); + + return 0; +} + +static int test_tc_segment_header_rejected_on_control_command(void) +{ + sdlp_tc_frame_t frame; + sdlp_tc_frame_t before; + const uint8_t payload[1] = {0x01u}; + + /* A frame carrying a Control Command must not have a Segment Header (4.1.3.2.2.1.3): + * the call is refused and the frame is left as it was. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_unlock_frame(&frame, 1, 1)); + memcpy(&before, &frame, sizeof(frame)); + ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, + sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_NO_SEG, 0x2Fu)); + ASSERT_EQ_MEM(&before, &frame, sizeof(frame)); + + /* The other Bypass frame type, Type-BD, carries data and still accepts one. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_create_frame(&frame, 1, 1, 1, payload, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_frame_type(&frame, SDLP_TC_FRAME_TYPE_BD)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, SDLP_TC_SEQ_FLAG_NO_SEG, 0x2Fu)); + ASSERT_EQ_INT(0x2Fu, frame.segment_header.map_id); + + return 0; +} +#endif /* SDLP_TC_SEGMENT_HEADER_ENABLED */ + +static int test_tc_decode_short_inputs(void) +{ + static const uint8_t zeros[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE] = { + 0}; + /* The shortest frame: a Type-BC header (so no Segment Header), no data, and the FECF. */ + const uint8_t shortest[SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE] = + {0x30u, 0x00u, 0x00u, SDLP_TC_PRIMARY_HEADER_SIZE + SDLP_TC_FRAME_ERROR_CONTROL_SIZE - 1}; + sdlp_tc_frame_t decoded; + uint8_t *exact; + sdlp_status_t status; + + /* Every length below a primary header plus the FECF is rejected. */ + for (size_t length = 0; length < sizeof(zeros); length++) + { + ASSERT_EQ_INT(0, test_tc_expect_decode_rejected(zeros, length, SDLP_ERROR_INVALID_PARAM)); + } + + /* Exactly that length is the first one accepted, read from an exact-size heap buffer. */ + exact = malloc(sizeof(shortest)); + ASSERT_TRUE(exact); + memcpy(exact, shortest, sizeof(shortest)); + status = sdlp_tc_decode_frame(exact, sizeof(shortest), &decoded); + free(exact); + ASSERT_EQ_INT(SDLP_SUCCESS, status); + ASSERT_EQ_INT(1, decoded.header.control_command_flag); + ASSERT_EQ_INT(0, decoded.data_length); + + return 0; +} test_result_t test_tc_run_all(void) { @@ -350,8 +802,21 @@ test_result_t test_tc_run_all(void) RUN_TEST(test_tc_unlock_command); RUN_TEST(test_tc_set_vr_command); RUN_TEST(test_tc_decode_reserved_frame_type); +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED RUN_TEST(test_tc_segment_header_roundtrip); RUN_TEST(test_tc_decode_segment_too_small); +#endif + RUN_TEST(test_tc_decode_failure_leaves_frame_unchanged); + RUN_TEST(test_tc_encode_max_frame_exact_buffer); + RUN_TEST(test_tc_encode_rejects_oversized_frame); + RUN_TEST(test_tc_golden_frame_encode); + RUN_TEST(test_tc_golden_frame_decode); + RUN_TEST(test_tc_primary_header_wire_limits); +#ifdef SDLP_TC_SEGMENT_HEADER_ENABLED + RUN_TEST(test_tc_segment_header_wire_limits); + RUN_TEST(test_tc_segment_header_rejected_on_control_command); +#endif + RUN_TEST(test_tc_decode_short_inputs); /* cunit's counters have internal linkage, so this translation unit tallies * only its own tests. */ diff --git a/tests/test_tm.c b/tests/test_tm.c index ffd7452..b794e2e 100644 --- a/tests/test_tm.c +++ b/tests/test_tm.c @@ -1,6 +1,11 @@ /** - * @file test_tm.c - * @brief Unit tests for the TM Space Data Link Protocol frame handler. + * @file test_tm.c + * @brief Unit tests for the TM Space Data Link Protocol frame handler. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "cunit.h" #include "sdlp_common.h" @@ -9,6 +14,26 @@ #include #include +#include +#include + +/** @brief Scratch buffer size for rejected-encode checks: holds a frame just over the limit. */ +#define TEST_TM_SCRATCH_SIZE (2 * SDLP_TM_MAX_DATA_SIZE) + +/** @brief Size of a frame with a full Data Field and neither Secondary Header nor OCF. */ +#define TEST_TM_MAX_PLAIN_FRAME_SIZE \ + (SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE) + +/** @brief Byte pre-filled into outputs that a rejected call must leave untouched. */ +#define TEST_TM_SENTINEL 0xA5 + +/** @brief Run one TM test from an empty frame-count table, so no test depends on another. */ +#define RUN_TM_TEST(fn) \ + do \ + { \ + sdlp_tm_reset_frame_counts(); \ + RUN_TEST(fn); \ + } while (0) static int test_tm_create_frame_invalid_params(void) { @@ -18,7 +43,7 @@ static int test_tm_create_frame_invalid_params(void) ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_create_frame(NULL, 1, 1, payload, 1)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_create_frame(&frame, 1, 1, NULL, 1)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tm_create_frame(&frame, 1, 1, payload, TM_MAX_DATA_SIZE + 1)); + sdlp_tm_create_frame(&frame, 1, 1, payload, SDLP_TM_MAX_DATA_SIZE + 1)); return 0; } @@ -28,7 +53,8 @@ static int test_tm_encode_decode_roundtrip(void) sdlp_tm_frame_t frame; sdlp_tm_frame_t decoded; const uint8_t payload[] = {0x10u, 0x20u, 0x30u, 0x40u, 0x50u}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + TM_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -36,7 +62,8 @@ static int test_tm_encode_decode_roundtrip(void) ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); - ASSERT_EQ_INT(TM_PRIMARY_HEADER_SIZE + (int)sizeof(payload) + TM_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TM_PRIMARY_HEADER_SIZE + (int)sizeof(payload) + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); @@ -50,7 +77,7 @@ static int test_tm_encode_decode_roundtrip(void) /* The default Data Field Status round-trips: Sync Flag = 0 with a '11' Segment * Length Identifier (CCSDS 132.0-B-3, 4.1.2.7.5.2). */ ASSERT_EQ_INT(0, decoded.header.transfer_frame_data_field_status.sync_flag); - ASSERT_EQ_INT(TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION, + ASSERT_EQ_INT(SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION, decoded.header.transfer_frame_data_field_status.segment_length_id); return 0; @@ -65,7 +92,7 @@ static int test_tm_data_field_status_codec(void) status.secondary_header_flag = 1; status.sync_flag = 0; status.packet_order_flag = 0; - status.segment_length_id = TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION; + status.segment_length_id = SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION; status.first_header_pointer = 0x123u; /* MSB-first layout: shf<<15 | slid<<11 | fhp = 0x8000 | 0x1800 | 0x123. */ @@ -76,7 +103,7 @@ static int test_tm_data_field_status_codec(void) ASSERT_EQ_INT(1, parsed.secondary_header_flag); ASSERT_EQ_INT(0, parsed.sync_flag); ASSERT_EQ_INT(0, parsed.packet_order_flag); - ASSERT_EQ_INT(TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION, parsed.segment_length_id); + ASSERT_EQ_INT(SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION, parsed.segment_length_id); ASSERT_EQ_INT(0x123u, parsed.first_header_pointer); return 0; @@ -86,7 +113,7 @@ static int test_tm_encode_buffer_too_small(void) { sdlp_tm_frame_t frame; const uint8_t payload[] = {0x01u, 0x02u, 0x03u}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + 2 + TM_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + 2 + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -102,7 +129,8 @@ static int test_tm_fecf_passthrough(void) sdlp_tm_frame_t frame; sdlp_tm_frame_t decoded; const uint8_t payload[] = {0xDEu, 0xADu, 0xBEu, 0xEFu}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + TM_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -115,8 +143,7 @@ static int test_tm_fecf_passthrough(void) ASSERT_EQ_INT(0xABu, encoded[encoded_size - 2]); ASSERT_EQ_INT(0xCDu, encoded[encoded_size - 1]); - /* Decode no longer validates the FECF: it always succeeds and surfaces the - * field as-is. */ + /* Decode does not validate the FECF: it surfaces the field as-is. */ ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); ASSERT_EQ_INT(0xABCDu, decoded.fecf); @@ -127,7 +154,7 @@ static int test_tm_frame_counts_per_channel(void) { sdlp_tm_frame_t f; const uint8_t payload[1] = {0xA5u}; - const uint16_t scid_a = 0x055u; /* SCIDs not used by other tests => fresh counters */ + const uint16_t scid_a = 0x055u; const uint16_t scid_b = 0x056u; /* First frame on (SCID A, VC 0): both counts start at 0. */ @@ -160,8 +187,9 @@ static int test_tm_secondary_header_roundtrip(void) sdlp_tm_frame_t decoded; const uint8_t payload[] = {0x10u, 0x20u, 0x30u}; const uint8_t sh_data[] = {0xAAu, 0xBBu, 0xCCu, 0xDDu}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + - TM_SECONDARY_HEADER_MAX_DATA + TM_MAX_DATA_SIZE + TM_FRAME_ERROR_CONTROL_SIZE]; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + SDLP_TM_SECONDARY_HEADER_MAX_DATA + SDLP_TM_MAX_DATA_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -173,8 +201,9 @@ static int test_tm_secondary_header_roundtrip(void) ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); /* primary(6) + ID(1) + secondary data(4) + payload(3) + FECF(2) */ - ASSERT_EQ_INT(TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + (int)sizeof(sh_data) + - (int)sizeof(payload) + TM_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + (int)sizeof(sh_data) + (int)sizeof(payload) + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); @@ -191,17 +220,20 @@ static int test_tm_secondary_header_roundtrip(void) static int test_tm_set_secondary_header_invalid(void) { sdlp_tm_frame_t frame; + sdlp_tm_frame_t before; const uint8_t payload[1] = {0x01u}; - const uint8_t sh_data[1] = {0xFFu}; + const uint8_t sh_data[SDLP_TM_SECONDARY_HEADER_MAX_DATA + 1] = {0xFFu}; ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, payload, 1)); + memcpy(&before, &frame, sizeof(frame)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_set_secondary_header(NULL, sh_data, 1)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_set_secondary_header(&frame, NULL, 1)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_set_secondary_header(&frame, sh_data, 0)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tm_set_secondary_header(&frame, sh_data, TM_SECONDARY_HEADER_MAX_DATA + 1)); - /* A rejected call must leave the Secondary Header Flag clear. */ - ASSERT_EQ_INT(0, frame.header.transfer_frame_data_field_status.secondary_header_flag); + ASSERT_EQ_INT( + SDLP_ERROR_INVALID_PARAM, + sdlp_tm_set_secondary_header(&frame, sh_data, SDLP_TM_SECONDARY_HEADER_MAX_DATA + 1)); + /* A rejected call leaves the whole frame as it was, the Secondary Header Flag included. */ + ASSERT_EQ_MEM(&before, &frame, sizeof(frame)); return 0; } @@ -211,9 +243,9 @@ static int test_tm_ocf_roundtrip(void) sdlp_tm_frame_t frame; sdlp_tm_frame_t decoded; const uint8_t payload[] = {0x10u, 0x20u, 0x30u}; - const uint8_t ocf[TM_OCF_SIZE] = {0x01u, 0x02u, 0x03u, 0x04u}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + TM_OCF_SIZE + - TM_FRAME_ERROR_CONTROL_SIZE]; + const uint8_t ocf[SDLP_TM_OCF_SIZE] = {0x01u, 0x02u, 0x03u, 0x04u}; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + SDLP_TM_OCF_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -227,13 +259,13 @@ static int test_tm_ocf_roundtrip(void) ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); /* primary(6) + payload(3) + OCF(4) + FECF(2) */ - ASSERT_EQ_INT(TM_PRIMARY_HEADER_SIZE + (int)sizeof(payload) + TM_OCF_SIZE + - TM_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TM_PRIMARY_HEADER_SIZE + (int)sizeof(payload) + SDLP_TM_OCF_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); ASSERT_EQ_INT(1, decoded.header.ocf_flag); - ASSERT_EQ_MEM(ocf, decoded.ocf, TM_OCF_SIZE); + ASSERT_EQ_MEM(ocf, decoded.ocf, SDLP_TM_OCF_SIZE); /* The OCF sits between the Data Field and the FECF; the data must be intact. */ ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); @@ -247,10 +279,10 @@ static int test_tm_secondary_header_and_ocf_roundtrip(void) sdlp_tm_frame_t decoded; const uint8_t payload[] = {0xAAu, 0xBBu}; const uint8_t sh_data[] = {0x11u, 0x22u, 0x33u}; - const uint8_t ocf[TM_OCF_SIZE] = {0xDEu, 0xADu, 0xBEu, 0xEFu}; - uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + - TM_SECONDARY_HEADER_MAX_DATA + TM_MAX_DATA_SIZE + TM_OCF_SIZE + - TM_FRAME_ERROR_CONTROL_SIZE]; + const uint8_t ocf[SDLP_TM_OCF_SIZE] = {0xDEu, 0xADu, 0xBEu, 0xEFu}; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + SDLP_TM_SECONDARY_HEADER_MAX_DATA + SDLP_TM_MAX_DATA_SIZE + SDLP_TM_OCF_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -262,15 +294,16 @@ static int test_tm_secondary_header_and_ocf_roundtrip(void) ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); /* primary(6) + ID(1) + secondary(3) + payload(2) + OCF(4) + FECF(2) */ - ASSERT_EQ_INT(TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + (int)sizeof(sh_data) + - (int)sizeof(payload) + TM_OCF_SIZE + TM_FRAME_ERROR_CONTROL_SIZE, + ASSERT_EQ_INT(SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + (int)sizeof(sh_data) + (int)sizeof(payload) + SDLP_TM_OCF_SIZE + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE, (int)encoded_size); ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); ASSERT_EQ_INT(1, decoded.header.transfer_frame_data_field_status.secondary_header_flag); ASSERT_EQ_INT(1, decoded.header.ocf_flag); ASSERT_EQ_MEM(sh_data, decoded.secondary_header.data, sizeof(sh_data)); - ASSERT_EQ_MEM(ocf, decoded.ocf, TM_OCF_SIZE); + ASSERT_EQ_MEM(ocf, decoded.ocf, SDLP_TM_OCF_SIZE); ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); @@ -280,14 +313,16 @@ static int test_tm_secondary_header_and_ocf_roundtrip(void) static int test_tm_set_ocf_invalid(void) { sdlp_tm_frame_t frame; + sdlp_tm_frame_t before; const uint8_t payload[1] = {0x01u}; - const uint8_t ocf[TM_OCF_SIZE] = {0}; + const uint8_t ocf[SDLP_TM_OCF_SIZE] = {0}; ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, payload, 1)); + memcpy(&before, &frame, sizeof(frame)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_set_ocf(NULL, ocf)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_set_ocf(&frame, NULL)); - /* A rejected call must leave the OCF Flag clear. */ - ASSERT_EQ_INT(0, frame.header.ocf_flag); + /* A rejected call leaves the whole frame as it was, the OCF Flag included. */ + ASSERT_EQ_MEM(&before, &frame, sizeof(frame)); return 0; } @@ -317,10 +352,11 @@ static int test_tm_null_params(void) /* decode rejects a NULL buffer, a NULL frame, and a buffer shorter than the header + FECF. */ ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_decode_frame(NULL, sizeof(buffer), &decoded)); ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, sdlp_tm_decode_frame(buffer, sizeof(buffer), NULL)); - ASSERT_EQ_INT(SDLP_ERROR_INVALID_PARAM, - sdlp_tm_decode_frame(buffer, - TM_PRIMARY_HEADER_SIZE + TM_FRAME_ERROR_CONTROL_SIZE - 1, - &decoded)); + ASSERT_EQ_INT( + SDLP_ERROR_INVALID_PARAM, + sdlp_tm_decode_frame(buffer, + SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE - 1, + &decoded)); return 0; } @@ -350,33 +386,666 @@ static int test_tm_decode_malformed(void) uint8_t buf[8] = {0, 0x01u}; ASSERT_EQ_INT(SDLP_ERROR_INVALID_FRAME, sdlp_tm_decode_frame(buf, sizeof(buf), &decoded)); } - /* Data Field larger than TM_MAX_DATA_SIZE. */ + /* Data Field larger than SDLP_TM_MAX_DATA_SIZE. */ { - static uint8_t - big[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + 1 + TM_FRAME_ERROR_CONTROL_SIZE] = {0}; + static uint8_t big[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + 1 + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE] = {0}; ASSERT_EQ_INT(SDLP_ERROR_INVALID_FRAME, sdlp_tm_decode_frame(big, sizeof(big), &decoded)); } return 0; } +/** + * @brief Decode a buffer the decoder must reject and check the output frame is untouched. + * + * The frame is pre-filled with a sentinel pattern, so any write by the decoder shows up + * as a difference, whichever field it lands in. + * + * @param[in] buffer Wire buffer to decode (may be NULL). + * @param[in] buffer_size Buffer length in octets. + * @param[in] expected Status the decoder must return. + * @return 0 if the decoder returned @p expected and left the frame unchanged, 1 otherwise. + */ +static int test_tm_expect_decode_rejected(const uint8_t *buffer, + size_t buffer_size, + sdlp_status_t expected) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t untouched; + uint8_t *exact = NULL; + sdlp_status_t status; + + memset(&frame, 0xA5, sizeof(frame)); + memset(&untouched, 0xA5, sizeof(untouched)); + + /* The decoder reads a heap copy of exactly buffer_size octets, so ASan reports any + * read past the end of the input. */ + if (buffer) + { + exact = malloc((buffer_size > 0u) ? buffer_size : 1u); + ASSERT_TRUE(exact); + memcpy(exact, buffer, buffer_size); + } + status = sdlp_tm_decode_frame(exact, buffer_size, &frame); + free(exact); + + ASSERT_EQ_INT(expected, status); + ASSERT_EQ_MEM(&untouched, &frame, sizeof(frame)); + + return 0; +} + +static int test_tm_decode_failure_leaves_frame_unchanged(void) +{ + const uint8_t too_short[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE - 1] = { + 0}; + /* Secondary Header Flag set (byte 4 bit 7), no room for its Identification Field plus + * one Data Field octet. */ + const uint8_t no_secondary_room[9] = {0, 0, 0, 0, 0x80u}; + /* Secondary Header Length of zero, i.e. an empty Data Field. */ + const uint8_t empty_secondary[10] = {0, 0, 0, 0, 0x80u}; + /* Secondary Header Length overruns the remaining buffer. */ + const uint8_t secondary_overrun[10] = {0, 0, 0, 0, 0x80u, 0, 0x0Au}; + /* OCF Flag set (byte 1 bit 0), no room for the 4-octet OCF. */ + const uint8_t no_ocf_room[8] = {0, 0x01u}; + /* Data Field one octet longer than SDLP_TM_MAX_DATA_SIZE. */ + static const uint8_t oversized[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_MAX_DATA_SIZE + 1 + + SDLP_TM_FRAME_ERROR_CONTROL_SIZE] = {0}; + + ASSERT_EQ_INT( + 0, + test_tm_expect_decode_rejected(NULL, sizeof(no_ocf_room), SDLP_ERROR_INVALID_PARAM)); + ASSERT_EQ_INT( + 0, + test_tm_expect_decode_rejected(too_short, sizeof(too_short), SDLP_ERROR_INVALID_PARAM)); + ASSERT_EQ_INT(0, + test_tm_expect_decode_rejected(no_secondary_room, + sizeof(no_secondary_room), + SDLP_ERROR_INVALID_FRAME)); + ASSERT_EQ_INT(0, + test_tm_expect_decode_rejected(empty_secondary, + sizeof(empty_secondary), + SDLP_ERROR_INVALID_FRAME)); + ASSERT_EQ_INT(0, + test_tm_expect_decode_rejected(secondary_overrun, + sizeof(secondary_overrun), + SDLP_ERROR_INVALID_FRAME)); + ASSERT_EQ_INT( + 0, + test_tm_expect_decode_rejected(no_ocf_room, sizeof(no_ocf_room), SDLP_ERROR_INVALID_FRAME)); + ASSERT_EQ_INT( + 0, + test_tm_expect_decode_rejected(oversized, sizeof(oversized), SDLP_ERROR_INVALID_FRAME)); + + return 0; +} + +static int test_tm_decode_data_length_beyond_16_bits(void) +{ + /* A Data Field of 65546 octets reads as 10 once narrowed to 16 bits, so the decoder + * must range-check the length before narrowing it. */ + const size_t data_length = (size_t)UINT16_MAX + 1u + 10u; + const size_t buffer_size = + SDLP_TM_PRIMARY_HEADER_SIZE + data_length + SDLP_TM_FRAME_ERROR_CONTROL_SIZE; + uint8_t *buffer = calloc(buffer_size, 1); + int result; + + ASSERT_TRUE(buffer); + result = test_tm_expect_decode_rejected(buffer, buffer_size, SDLP_ERROR_INVALID_FRAME); + free(buffer); + ASSERT_EQ_INT(0, result); + + return 0; +} + +/** + * @brief Encode a frame the encoder must reject and check both outputs are untouched. + * + * The buffer and the encoded size are pre-filled with a sentinel pattern, so any write + * by the encoder shows up as a difference. + * + * @param[in] frame Frame to encode. + * @param[in] buffer_size Capacity to offer the encoder, at most ::TEST_TM_SCRATCH_SIZE. + * @param[in] expected Status the encoder must return. + * @return 0 if the encoder returned @p expected and left its outputs unchanged, 1 otherwise. + */ +static int test_tm_expect_encode_rejected(const sdlp_tm_frame_t *frame, + size_t buffer_size, + sdlp_status_t expected) +{ + uint8_t buffer[TEST_TM_SCRATCH_SIZE]; + uint8_t untouched[TEST_TM_SCRATCH_SIZE]; + size_t encoded_size = 0xA5A5u; + + memset(buffer, 0xA5, sizeof(buffer)); + memset(untouched, 0xA5, sizeof(untouched)); + + ASSERT_EQ_INT(expected, sdlp_tm_encode_frame(frame, buffer, buffer_size, &encoded_size)); + ASSERT_EQ_INT(0xA5A5u, encoded_size); + ASSERT_EQ_MEM(untouched, buffer, sizeof(buffer)); + + return 0; +} + +/** + * @brief Round-trip a frame with the largest Data Field through an exact-size buffer. + * + * @param[out] encoded Buffer of exactly ::TEST_TM_MAX_PLAIN_FRAME_SIZE octets. + * @return 0 if every check passed, 1 otherwise. + */ +static int test_tm_check_max_data_roundtrip(uint8_t *encoded) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t decoded; + uint8_t payload[SDLP_TM_MAX_DATA_SIZE]; + size_t encoded_size = 0; + + for (size_t i = 0; i < sizeof(payload); i++) + { + payload[i] = (uint8_t)i; + } + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, 1, 0, payload, (uint16_t)sizeof(payload))); + ASSERT_EQ_INT(0, + test_tm_expect_encode_rejected(&frame, + TEST_TM_MAX_PLAIN_FRAME_SIZE - 1, + SDLP_ERROR_BUFFER_TOO_SMALL)); + + ASSERT_EQ_INT( + SDLP_SUCCESS, + sdlp_tm_encode_frame(&frame, encoded, TEST_TM_MAX_PLAIN_FRAME_SIZE, &encoded_size)); + ASSERT_EQ_INT(TEST_TM_MAX_PLAIN_FRAME_SIZE, encoded_size); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); + ASSERT_EQ_INT(SDLP_TM_MAX_DATA_SIZE, decoded.data_length); + ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); + + return 0; +} + +static int test_tm_encode_max_data_exact_buffer(void) +{ + uint8_t *encoded = malloc(TEST_TM_MAX_PLAIN_FRAME_SIZE); + int result; + + ASSERT_TRUE(encoded); + result = test_tm_check_max_data_roundtrip(encoded); + free(encoded); + ASSERT_EQ_INT(0, result); + + return 0; +} + +static int test_tm_encode_rejects_oversized_data(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x01u}; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, payload, 1)); + + /* One octet more than the Data Field array holds. */ + frame.data_length = SDLP_TM_MAX_DATA_SIZE + 1; + ASSERT_EQ_INT( + 0, + test_tm_expect_encode_rejected(&frame, TEST_TM_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + /* A length far beyond the Data Field array must not be read out of the frame. */ + frame.data_length = UINT16_MAX; + ASSERT_EQ_INT( + 0, + test_tm_expect_encode_rejected(&frame, TEST_TM_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + return 0; +} + +static int test_tm_encode_rejects_empty_secondary_header(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x01u}; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, payload, 1)); + + /* The flag is raised by hand, without sdlp_tm_set_secondary_header(), so the Secondary + * Header Data Field is empty; 4.1.3.1.3 requires 1 to 63 octets. */ + frame.header.transfer_frame_data_field_status.secondary_header_flag = 1; + ASSERT_EQ_INT( + 0, + test_tm_expect_encode_rejected(&frame, TEST_TM_SCRATCH_SIZE, SDLP_ERROR_INVALID_PARAM)); + + return 0; +} + +/** + * @brief Round-trip a frame carrying a Secondary Header of a given Data Field length. + * + * @param[in] length Secondary Header Data Field length in octets. + * @param[out] encoded Buffer of exactly @p frame_size octets. + * @param[in] frame_size Encoded size of the frame: one Data Field octet plus the headers + * and the FECF. + * @return 0 if every check passed, 1 otherwise. + */ +static int test_tm_check_secondary_header_roundtrip(uint8_t length, + uint8_t *encoded, + size_t frame_size) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t decoded; + const uint8_t payload[1] = {0x5Au}; + uint8_t sh_data[SDLP_TM_SECONDARY_HEADER_MAX_DATA]; + size_t encoded_size = 0; + + for (size_t i = 0; i < sizeof(sh_data); i++) + { + sh_data[i] = (uint8_t)(i + 1u); + } + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, payload, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_set_secondary_header(&frame, sh_data, length)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_encode_frame(&frame, encoded, frame_size, &encoded_size)); + ASSERT_EQ_INT((int)frame_size, (int)encoded_size); + /* Identification Field: Version '00' in the top two bits, then the Length, which + * equals the Data Field length (4.1.3.2). */ + ASSERT_EQ_INT(length, encoded[SDLP_TM_PRIMARY_HEADER_SIZE]); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); + ASSERT_EQ_INT(length, decoded.secondary_header.length); + ASSERT_EQ_MEM(sh_data, decoded.secondary_header.data, length); + ASSERT_EQ_INT(1, decoded.data_length); + ASSERT_EQ_INT(payload[0], decoded.data[0]); + + return 0; +} + +/** + * @brief Run the Secondary Header round trip through an exact-size heap buffer. + * + * @param[in] length Secondary Header Data Field length in octets. + * @return 0 if every check passed, 1 otherwise. + */ +static int test_tm_secondary_header_roundtrip_at(uint8_t length) +{ + const size_t frame_size = SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + + length + 1u + SDLP_TM_FRAME_ERROR_CONTROL_SIZE; + uint8_t *encoded = malloc(frame_size); + int result; + + ASSERT_TRUE(encoded); + result = test_tm_check_secondary_header_roundtrip(length, encoded, frame_size); + free(encoded); + + return result; +} + +static int test_tm_secondary_header_length_limits(void) +{ + ASSERT_EQ_INT(0, test_tm_secondary_header_roundtrip_at(1)); + ASSERT_EQ_INT(0, test_tm_secondary_header_roundtrip_at(SDLP_TM_SECONDARY_HEADER_MAX_DATA)); + + return 0; +} + +/** + * @brief Build a frame the library must reject and check the output frame is untouched. + * + * @param[in] spacecraft_id Spacecraft Identifier to request. + * @param[in] data Data Field content (may be NULL). + * @param[in] data_length Data Field length in octets. + * @param[in] expected Status sdlp_tm_create_frame() must return. + * @return 0 if the call returned @p expected and left the frame unchanged, 1 otherwise. + */ +static int test_tm_expect_create_rejected(uint16_t spacecraft_id, + const uint8_t *data, + uint16_t data_length, + sdlp_status_t expected) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t untouched; + const uint8_t vcid = 0; + + memset(&frame, TEST_TM_SENTINEL, sizeof(frame)); + memset(&untouched, TEST_TM_SENTINEL, sizeof(untouched)); + + ASSERT_EQ_INT(expected, sdlp_tm_create_frame(&frame, spacecraft_id, vcid, data, data_length)); + ASSERT_EQ_MEM(&untouched, &frame, sizeof(frame)); + + return 0; +} + +static int test_tm_create_failure_leaves_state_unchanged(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x5Au}; + const uint16_t payload_size = (uint16_t)sizeof(payload); + const uint16_t scid = 0x155u; + const uint8_t vcid = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(0, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(0, frame.header.virtual_channel_frame_count); + + ASSERT_EQ_INT( + 0, + test_tm_expect_create_rejected(scid, NULL, payload_size, SDLP_ERROR_INVALID_PARAM)); + ASSERT_EQ_INT(0, + test_tm_expect_create_rejected(scid, + payload, + SDLP_TM_MAX_DATA_SIZE + 1, + SDLP_ERROR_INVALID_PARAM)); + + /* Neither rejected call consumed a count: the next frame carries the next number. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(1, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(1, frame.header.virtual_channel_frame_count); + + return 0; +} + +static int test_tm_master_channel_table_full(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x5Au}; + const uint16_t payload_size = (uint16_t)sizeof(payload); + const uint16_t first_scid = 0x200u; + const uint16_t extra_scid = (uint16_t)(first_scid + SDLP_TM_MAX_MASTER_CHANNELS); + const uint8_t vcid = 0; + + /* A call rejected for its arguments claims no slot, however many Spacecraft IDs + * are tried. */ + for (uint16_t i = 0; i <= SDLP_TM_MAX_MASTER_CHANNELS; i++) + { + ASSERT_EQ_INT(0, + test_tm_expect_create_rejected((uint16_t)(first_scid + i), + NULL, + payload_size, + SDLP_ERROR_INVALID_PARAM)); + } + + /* Every slot can be claimed, the last one included. */ + for (uint16_t i = 0; i < SDLP_TM_MAX_MASTER_CHANNELS; i++) + { + ASSERT_EQ_INT( + SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, (uint16_t)(first_scid + i), vcid, payload, payload_size)); + ASSERT_EQ_INT(0, frame.header.master_channel_frame_count); + } + + /* One Master Channel more is refused, not numbered with zeroes. */ + ASSERT_EQ_INT( + 0, + test_tm_expect_create_rejected(extra_scid, payload, payload_size, SDLP_ERROR_NO_RESOURCE)); + + /* The Master Channels already tracked keep counting. */ + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, first_scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(1, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(1, frame.header.virtual_channel_frame_count); + + /* A reset empties the table: the refused Master Channel is accepted, and one that was + * tracked before starts again from zero. */ + sdlp_tm_reset_frame_counts(); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, extra_scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(0, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, first_scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(0, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(0, frame.header.virtual_channel_frame_count); + + return 0; +} + +static int test_tm_frame_counts_wrap(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x5Au}; + const uint16_t payload_size = (uint16_t)sizeof(payload); + const uint16_t scid = 0x155u; + const uint8_t vcid = 3; + + /* Both counts are modulo-256 (4.1.2.5.2, 4.1.2.6.2): they run from 0 to 255... */ + for (unsigned expected = 0; expected <= UINT8_MAX; expected++) + { + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(expected, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(expected, frame.header.virtual_channel_frame_count); + } + + /* ...and the frame after 255 is numbered 0 again. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, scid, vcid, payload, payload_size)); + ASSERT_EQ_INT(0, frame.header.master_channel_frame_count); + ASSERT_EQ_INT(0, frame.header.virtual_channel_frame_count); + + return 0; +} + +/** + * @brief A complete TM frame with a distinct value in every field, as sent on the wire. + */ +static const uint8_t test_tm_golden[] = { + 0x2Au, /* Version 00, Spacecraft ID bits 9-4: the ID is 0x2A5 */ + 0x5Bu, /* Spacecraft ID bits 3-0, Virtual Channel ID 5, OCF Flag 1 */ + 0x12u, /* Master Channel Frame Count */ + 0x34u, /* Virtual Channel Frame Count */ + 0x99u, /* Secondary Header Flag 1, Segment Length ID '11', First Header Pointer bits 10-8 */ + 0x23u, /* First Header Pointer bits 7-0: the pointer is 0x123 */ + 0x02u, /* Secondary Header: Version 00, Length 2 */ + 0x51u, + 0x52u, /* Secondary Header Data Field */ + 0xD1u, + 0xD2u, /* Data Field */ + 0xC1u, + 0xC2u, + 0xC3u, + 0xC4u, /* Operational Control Field */ + 0xFEu, + 0xC5u /* FECF */ +}; + +static int test_tm_golden_frame_encode(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[] = {0xD1u, 0xD2u}; + const uint8_t sh_data[] = {0x51u, 0x52u}; + const uint8_t ocf[SDLP_TM_OCF_SIZE] = {0xC1u, 0xC2u, 0xC3u, 0xC4u}; + uint8_t encoded[sizeof(test_tm_golden)]; + size_t encoded_size = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_create_frame(&frame, 0x2A5u, 5, payload, (uint16_t)sizeof(payload))); + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_set_secondary_header(&frame, sh_data, (uint8_t)sizeof(sh_data))); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_set_ocf(&frame, ocf)); + frame.header.master_channel_frame_count = 0x12u; + frame.header.virtual_channel_frame_count = 0x34u; + frame.header.transfer_frame_data_field_status.first_header_pointer = 0x123u; + frame.fecf = 0xFEC5u; + + /* The buffer is exactly as long as the frame, and every octet is compared. */ + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT((int)sizeof(test_tm_golden), (int)encoded_size); + ASSERT_EQ_MEM(test_tm_golden, encoded, sizeof(test_tm_golden)); + + return 0; +} + +static int test_tm_golden_frame_decode(void) +{ + sdlp_tm_frame_t decoded; + const sdlp_tm_data_field_status_t *status = &decoded.header.transfer_frame_data_field_status; + const uint8_t payload[] = {0xD1u, 0xD2u}; + const uint8_t sh_data[] = {0x51u, 0x52u}; + const uint8_t ocf[SDLP_TM_OCF_SIZE] = {0xC1u, 0xC2u, 0xC3u, 0xC4u}; + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_decode_frame(test_tm_golden, sizeof(test_tm_golden), &decoded)); + ASSERT_EQ_INT(0, decoded.header.transfer_frame_version); + ASSERT_EQ_INT(0x2A5u, decoded.header.spacecraft_id); + ASSERT_EQ_INT(5, decoded.header.virtual_channel_id); + ASSERT_EQ_INT(1, decoded.header.ocf_flag); + ASSERT_EQ_INT(0x12u, decoded.header.master_channel_frame_count); + ASSERT_EQ_INT(0x34u, decoded.header.virtual_channel_frame_count); + ASSERT_EQ_INT(1, status->secondary_header_flag); + ASSERT_EQ_INT(0, status->sync_flag); + ASSERT_EQ_INT(0, status->packet_order_flag); + ASSERT_EQ_INT(SDLP_TM_SEGMENT_LENGTH_ID_NO_SEGMENTATION, status->segment_length_id); + ASSERT_EQ_INT(0x123u, status->first_header_pointer); + ASSERT_EQ_INT((int)sizeof(sh_data), decoded.secondary_header.length); + ASSERT_EQ_MEM(sh_data, decoded.secondary_header.data, sizeof(sh_data)); + ASSERT_EQ_INT((int)sizeof(payload), decoded.data_length); + ASSERT_EQ_MEM(payload, decoded.data, sizeof(payload)); + ASSERT_EQ_MEM(ocf, decoded.ocf, sizeof(ocf)); + ASSERT_EQ_INT(0xFEC5u, decoded.fecf); + + return 0; +} + +static int test_tm_primary_header_all_zero(void) +{ + sdlp_tm_frame_t frame; + const uint8_t payload[1] = {0x00u}; + const uint8_t all_zero[SDLP_TM_PRIMARY_HEADER_SIZE] = {0}; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + 1 + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; + size_t encoded_size = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 0, 0, payload, 1)); + /* The default Segment Length Identifier is '11'; it is cleared to put every bit at zero. */ + frame.header.transfer_frame_data_field_status.segment_length_id = 0; + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_MEM(all_zero, encoded, sizeof(all_zero)); + + return 0; +} + +static int test_tm_primary_header_all_max(void) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t decoded; + sdlp_tm_data_field_status_t *status = &frame.header.transfer_frame_data_field_status; + const uint8_t octets[SDLP_TM_OCF_SIZE] = {0}; + const uint8_t all_max[SDLP_TM_PRIMARY_HEADER_SIZE] = {0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu}; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_SECONDARY_HEADER_ID_SIZE + 1 + 1 + + SDLP_TM_OCF_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; + size_t encoded_size = 0; + + /* Both optional fields are attached, which raises their flags; the rest is set by hand. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 0x3FFu, 7, octets, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_set_secondary_header(&frame, octets, 1)); + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_set_ocf(&frame, octets)); + frame.header.transfer_frame_version = 0x03u; + frame.header.master_channel_frame_count = 0xFFu; + frame.header.virtual_channel_frame_count = 0xFFu; + status->sync_flag = 1; + status->packet_order_flag = 1; + status->first_header_pointer = 0x7FFu; + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT((int)sizeof(encoded), (int)encoded_size); + ASSERT_EQ_MEM(all_max, encoded, sizeof(all_max)); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); + status = &decoded.header.transfer_frame_data_field_status; + ASSERT_EQ_INT(0x03u, decoded.header.transfer_frame_version); + ASSERT_EQ_INT(0x3FFu, decoded.header.spacecraft_id); + ASSERT_EQ_INT(7, decoded.header.virtual_channel_id); + ASSERT_EQ_INT(1, decoded.header.ocf_flag); + ASSERT_EQ_INT(0xFFu, decoded.header.master_channel_frame_count); + ASSERT_EQ_INT(0xFFu, decoded.header.virtual_channel_frame_count); + ASSERT_EQ_INT(1, status->secondary_header_flag); + ASSERT_EQ_INT(1, status->sync_flag); + ASSERT_EQ_INT(1, status->packet_order_flag); + ASSERT_EQ_INT(0x03u, status->segment_length_id); + ASSERT_EQ_INT(0x7FFu, status->first_header_pointer); + + return 0; +} + +static int test_tm_empty_data_field_roundtrip(void) +{ + sdlp_tm_frame_t frame; + sdlp_tm_frame_t decoded; + const uint8_t unused[1] = {0x00u}; + uint8_t encoded[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE]; + size_t encoded_size = 0; + + /* A Data Field of zero octets is the smallest length sdlp_tm_create_frame() accepts. */ + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_create_frame(&frame, 1, 0, unused, 0)); + ASSERT_EQ_INT(0, frame.data_length); + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tm_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT((int)sizeof(encoded), (int)encoded_size); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tm_decode_frame(encoded, encoded_size, &decoded)); + ASSERT_EQ_INT(0, decoded.data_length); + + return 0; +} + +static int test_tm_decode_short_inputs(void) +{ + static const uint8_t zeros[SDLP_TM_PRIMARY_HEADER_SIZE + SDLP_TM_FRAME_ERROR_CONTROL_SIZE] = { + 0}; + sdlp_tm_frame_t decoded; + uint8_t *exact; + sdlp_status_t status; + + /* Every length below a primary header plus the FECF is rejected. */ + for (size_t length = 0; length < sizeof(zeros); length++) + { + ASSERT_EQ_INT(0, test_tm_expect_decode_rejected(zeros, length, SDLP_ERROR_INVALID_PARAM)); + } + + /* Exactly that length is the first one accepted, read from an exact-size heap buffer. */ + exact = malloc(sizeof(zeros)); + ASSERT_TRUE(exact); + memcpy(exact, zeros, sizeof(zeros)); + status = sdlp_tm_decode_frame(exact, sizeof(zeros), &decoded); + free(exact); + ASSERT_EQ_INT(SDLP_SUCCESS, status); + ASSERT_EQ_INT(0, decoded.data_length); + + return 0; +} + test_result_t test_tm_run_all(void) { test_result_t result; - RUN_TEST(test_tm_create_frame_invalid_params); - RUN_TEST(test_tm_encode_decode_roundtrip); - RUN_TEST(test_tm_data_field_status_codec); - RUN_TEST(test_tm_encode_buffer_too_small); - RUN_TEST(test_tm_fecf_passthrough); - RUN_TEST(test_tm_frame_counts_per_channel); - RUN_TEST(test_tm_secondary_header_roundtrip); - RUN_TEST(test_tm_set_secondary_header_invalid); - RUN_TEST(test_tm_ocf_roundtrip); - RUN_TEST(test_tm_secondary_header_and_ocf_roundtrip); - RUN_TEST(test_tm_set_ocf_invalid); - RUN_TEST(test_tm_null_params); - RUN_TEST(test_tm_decode_malformed); + RUN_TM_TEST(test_tm_create_frame_invalid_params); + RUN_TM_TEST(test_tm_encode_decode_roundtrip); + RUN_TM_TEST(test_tm_data_field_status_codec); + RUN_TM_TEST(test_tm_encode_buffer_too_small); + RUN_TM_TEST(test_tm_fecf_passthrough); + RUN_TM_TEST(test_tm_frame_counts_per_channel); + RUN_TM_TEST(test_tm_secondary_header_roundtrip); + RUN_TM_TEST(test_tm_set_secondary_header_invalid); + RUN_TM_TEST(test_tm_ocf_roundtrip); + RUN_TM_TEST(test_tm_secondary_header_and_ocf_roundtrip); + RUN_TM_TEST(test_tm_set_ocf_invalid); + RUN_TM_TEST(test_tm_null_params); + RUN_TM_TEST(test_tm_decode_malformed); + RUN_TM_TEST(test_tm_decode_failure_leaves_frame_unchanged); + RUN_TM_TEST(test_tm_decode_data_length_beyond_16_bits); + RUN_TM_TEST(test_tm_encode_max_data_exact_buffer); + RUN_TM_TEST(test_tm_encode_rejects_oversized_data); + RUN_TM_TEST(test_tm_encode_rejects_empty_secondary_header); + RUN_TM_TEST(test_tm_secondary_header_length_limits); + RUN_TM_TEST(test_tm_create_failure_leaves_state_unchanged); + RUN_TM_TEST(test_tm_master_channel_table_full); + RUN_TM_TEST(test_tm_frame_counts_wrap); + RUN_TM_TEST(test_tm_golden_frame_encode); + RUN_TM_TEST(test_tm_golden_frame_decode); + RUN_TM_TEST(test_tm_primary_header_all_zero); + RUN_TM_TEST(test_tm_primary_header_all_max); + RUN_TM_TEST(test_tm_empty_data_field_roundtrip); + RUN_TM_TEST(test_tm_decode_short_inputs); /* cunit's counters have internal linkage, so this translation unit tallies * only its own tests. */ diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 9ffd8a4..fda6a32 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,6 +1,11 @@ /** - * @file unit_tests.c - * @brief Unit test entry point: runs each module's suite and reports the tally. + * @file unit_tests.c + * @brief Unit test entry point: runs each module's suite and reports the tally. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "test_runners.h" diff --git a/tools/coverage_html.sh b/tools/coverage_html.sh index 6755c00..3898bd0 100644 --- a/tools/coverage_html.sh +++ b/tools/coverage_html.sh @@ -8,10 +8,10 @@ if [[ "${OUT_FILE}" != /* ]]; then OUT_FILE="${ROOT_DIR}/${OUT_FILE}" fi -COVERAGE_CFLAGS='-O0 -g --coverage -Iinclude -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef -std=c11' - -SRCS="src/sdlp_tm.c src/sdlp_tc.c" -TESTS="tests/test_tm.c tests/test_tc.c tests/unit_tests.c" +# Instrumentation is the ONLY thing that differs from the normal build; the C standard, +# include paths, warning set and the list of sources all come from the Makefile so they +# cannot drift apart. +COVERAGE_OPT='-O0 -g --coverage' cd "${ROOT_DIR}" @@ -21,32 +21,45 @@ if ! command -v gcovr >/dev/null 2>&1; then exit 1 fi -COV_DIR="${ROOT_DIR}/build/coverage" -rm -rf "${COV_DIR}" -mkdir -p "$(dirname "${OUT_FILE}")" "${COV_DIR}" - -# Build the unit tests with coverage instrumentation. Sources and tests are compiled -# together with TC_SEGMENT_HEADER_ENABLED so the segment-header code paths are built -# and exercised (matching how `make test` runs them). -objs="" -for f in ${SRCS} ${TESTS}; do - obj="${COV_DIR}/$(basename "${f%.c}").o" - # shellcheck disable=SC2086 - gcc ${COVERAGE_CFLAGS} -DTC_SEGMENT_HEADER_ENABLED -c "${f}" -o "${obj}" - objs="${objs} ${obj}" -done -# shellcheck disable=SC2086 -gcc --coverage ${objs} -o "${COV_DIR}/unit_tests" -"${COV_DIR}/unit_tests" >/dev/null - -# 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 (TC_SEGMENT_HEADER_ENABLED):" -gcovr --root "${ROOT_DIR}" --filter "${ROOT_DIR}/src" \ - --gcov-object-directory "${COV_DIR}" \ - --html-details --output "${OUT_FILE}" \ - --txt - --txt-summary \ +# gcovr reads gcov data, so the instrumented build uses gcc whatever CC is set to elsewhere. +# The unit tests are built and run once per build configuration (see the Makefile): the +# default one, and one with the TC Segment Header compiled in. +make clean >/dev/null +make unit-tests CC=gcc OPT="${COVERAGE_OPT}" >/dev/null +./build/bin/unit_tests >/dev/null +./build/segment_header/unit_tests >/dev/null + +mkdir -p "$(dirname "${OUT_FILE}")" + +# Print the text summary (line + branch) of one build configuration and require 100% of +# both. Each configuration is gated on its own, so code that only one of them compiles +# cannot hide behind the other's tests. gcovr's chatty "(INFO)" progress lines are filtered +# from stderr; warnings and errors still pass through. +# $1 label printed above the summary +# $2 directory holding that configuration's coverage data +status=0 +check_configuration() { + echo "Coverage ($1):" + gcovr -r "${ROOT_DIR}" \ + --filter "${ROOT_DIR}/src" \ + --txt - \ + --txt-summary \ + --fail-under-line 100 \ + --fail-under-branch 100 \ + "$2" \ + 2> >(grep -v '^(INFO)' >&2) || status=1 +} + +check_configuration "default configuration" "${ROOT_DIR}/build/obj" +check_configuration "SDLP_TC_SEGMENT_HEADER_ENABLED" "${ROOT_DIR}/build/segment_header" + +# One HTML report for both configurations together, so it also shows the lines that only +# the segment-header build compiles. +gcovr -r "${ROOT_DIR}" \ + --filter "${ROOT_DIR}/src" \ + --html-details \ + --output "${OUT_FILE}" \ 2> >(grep -v '^(INFO)' >&2) echo "Coverage HTML report written to: ${OUT_FILE}" +exit "${status}"