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/Makefile b/Makefile index 584de0c..7b1d7a6 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,19 +51,32 @@ 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) +# Segment-header configuration: the library is built without it, so its sources are +# compiled together with each example, all with TC_SEGMENT_HEADER_ENABLED. +$(SEGMENT_DIR)/%: $(EXAMPLES_DIR)/%.c $(EXAMPLE_HEADERS) $(PUBLIC_HEADERS) $(SRCS) | $(SEGMENT_DIR) + $(CC) $(CFLAGS) -DTC_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 TC_SEGMENT_HEADER_ENABLED. +$(SEGMENT_TEST_BIN): $(SRCS) $(TEST_SRCS) $(TEST_HEADERS) $(PUBLIC_HEADERS) | $(SEGMENT_DIR) $(CC) $(CFLAGS) -DTC_SEGMENT_HEADER_ENABLED $(SRCS) $(TEST_SRCS) -o $@ $(LDFLAGS) $(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 (TC_SEGMENT_HEADER_ENABLED)..." + @./$(SEGMENT_TEST_BIN) diff --git a/README.md b/README.md index b8c62f4..06438b8 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,16 @@ Minimal, embedded-optimized implementation of **CCSDS Space Data Link Protocol ( - **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 (`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 (`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 @@ -136,14 +158,14 @@ 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) +- TM frame counts are kept per Master Channel / Virtual Channel in fixed static state (not thread-safe). At most `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..e7cf8bf 100644 --- a/examples/example_crc.h +++ b/examples/example_crc.h @@ -14,8 +14,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 +33,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); } } } diff --git a/examples/tc_example.c b/examples/tc_example.c index 649317e..2fb6e6d 100644 --- a/examples/tc_example.c +++ b/examples/tc_example.c @@ -11,13 +11,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[TC_MAX_FRAME_SIZE]; size_t encoded_size; sdlp_status_t result; @@ -72,13 +75,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 - 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]); } @@ -130,9 +134,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 - 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); diff --git a/examples/tm_example.c b/examples/tm_example.c index d48f87d..b4e9e69 100644 --- a/examples/tm_example.c +++ b/examples/tm_example.c @@ -12,10 +12,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[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + TM_OCF_SIZE + + TM_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size; sdlp_status_t result; @@ -54,13 +58,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 - 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]); } @@ -92,7 +97,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[TM_OCF_SIZE] = {0}; result = sdlp_tm_set_ocf(&frame, clcw); if (result != SDLP_SUCCESS) { @@ -115,12 +120,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 < 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..798d9ae 100644 --- a/include/sdlp_common.h +++ b/include/sdlp_common.h @@ -17,7 +17,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 +28,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..7f3d737 100644 --- a/include/sdlp_tc.h +++ b/include/sdlp_tc.h @@ -19,10 +19,13 @@ 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 TC_PRIMARY_HEADER_SIZE 5 -/** Maximum whole Transfer Frame size in octets (CCSDS 232.0-B-4 §4.1.2.7.2). */ +/** @brief Frame Error Control Field size in octets (§4.1.4). */ +#define TC_FRAME_ERROR_CONTROL_SIZE 2 + +/** @brief Maximum Transfer Frame size in octets (§4.1.2.7). */ #define TC_MAX_FRAME_SIZE 1024 /** @@ -34,6 +37,9 @@ extern "C" */ #define TC_MAX_DATA_SIZE (TC_MAX_FRAME_SIZE - TC_PRIMARY_HEADER_SIZE - TC_FRAME_ERROR_CONTROL_SIZE) +/** @brief Frame Length field = octets in the Transfer Frame minus this (§4.1.2.7.2). */ +#define TC_FRAME_LENGTH_OFFSET 1u + /** * @brief TC Transfer Frame types — Bypass/Control Command Flag combinations (§table 4-1). * @@ -51,16 +57,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 TC_CONTROL_CMD_UNLOCK 0x00u +/** @brief Unlock command length in octets. */ +#define TC_CONTROL_CMD_UNLOCK_LENGTH 1u +/** @brief Set V(R) octet 0: '10000010' (§4.1.3.3.3). */ +#define TC_CONTROL_CMD_SET_VR_OCTET0 0x82u +/** @brief Set V(R) octet 1: '00000000'. */ +#define TC_CONTROL_CMD_SET_VR_OCTET1 0x00u +/** @brief Set V(R) command length in octets. */ +#define TC_CONTROL_CMD_SET_VR_LENGTH 3u /** @} */ #ifdef 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 TC_SEGMENT_HEADER_SIZE 1 /** * @brief TC Segment Header Sequence Flags (CCSDS 232.0-B-4 §table 4-2). @@ -144,8 +156,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 +195,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 ::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 +216,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. @@ -215,12 +231,15 @@ sdlp_status_t sdlp_tc_decode_frame(const uint8_t *buffer, /** * @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..2950ad9 100644 --- a/include/sdlp_tm.h +++ b/include/sdlp_tm.h @@ -19,9 +19,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 TM_PRIMARY_HEADER_SIZE 6 + +/** @brief Frame Error Control Field size in octets (§4.1.6). */ +#define TM_FRAME_ERROR_CONTROL_SIZE 2 + +/** @brief Maximum Transfer Frame Data Field length the library carries, in octets. */ +#define TM_MAX_DATA_SIZE 1024 /** * @brief Transfer Frame Secondary Header sizing (CCSDS 132.0-B-3 §4.1.3). @@ -30,21 +35,40 @@ 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 TM_SECONDARY_HEADER_ID_SIZE 1 +/** @brief Minimum Secondary Header Data Field length in octets. */ +#define TM_SECONDARY_HEADER_MIN_DATA 1 +/** @brief Maximum Secondary Header Data Field length in octets. */ +#define 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 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 TM_MAX_MASTER_CHANNELS +# define 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). */ +/** @brief Segment Length Identifier '11', mandatory 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). */ +/** @brief 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). */ +/** @brief First Header Pointer: Only Idle Data (OID) Transfer Frame (§4.1.2.7.6.5). */ #define TM_FIRST_HEADER_POINTER_ONLY_IDLE 0x07FEu /** @} */ @@ -62,8 +86,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; /** @@ -132,14 +155,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. + * @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 + * ::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 +173,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 + * ::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..::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,8 +205,8 @@ 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]); @@ -179,13 +215,16 @@ sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[TM_OCF_S * @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 ::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 +235,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..8c11b81 100644 --- a/src/sdlp_tc.c +++ b/src/sdlp_tc.c @@ -19,16 +19,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; + size_t frame_octets = + (size_t)TC_PRIMARY_HEADER_SIZE + frame->data_length + TC_FRAME_ERROR_CONTROL_SIZE; #ifdef TC_SEGMENT_HEADER_ENABLED if (!frame->header.control_command_flag) { frame_octets += TC_SEGMENT_HEADER_SIZE; } #endif - return (uint16_t)(frame_octets - 1u); + return (uint16_t)(frame_octets - 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 ::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, @@ -45,7 +66,7 @@ sdlp_status_t sdlp_tc_create_frame(sdlp_tc_frame_t *frame, max_data -= 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 +86,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 +118,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; } @@ -154,13 +175,13 @@ 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)TC_PRIMARY_HEADER_SIZE + frame->data_length + TC_FRAME_ERROR_CONTROL_SIZE; #ifdef TC_SEGMENT_HEADER_ENABLED if (!frame->header.control_command_flag) @@ -169,6 +190,14 @@ sdlp_status_t sdlp_tc_encode_frame(const sdlp_tc_frame_t *frame, } #endif + /* A Transfer Frame is at most 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 > TC_MAX_FRAME_SIZE) + { + return SDLP_ERROR_INVALID_PARAM; + } + if (buffer_size < required_size) { return SDLP_ERROR_BUFFER_TOO_SMALL; @@ -176,11 +205,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 - 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) | @@ -216,75 +245,64 @@ 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 < (TC_PRIMARY_HEADER_SIZE + 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 + TC_FRAME_LENGTH_OFFSET) != buffer_size) { return SDLP_ERROR_INVALID_FRAME; } + size_t data_offset = TC_PRIMARY_HEADER_SIZE; + #ifdef TC_SEGMENT_HEADER_ENABLED - if (!frame->header.control_command_flag) + 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) + (TC_PRIMARY_HEADER_SIZE + TC_SEGMENT_HEADER_SIZE + 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); - } - else - { - frame->data_length = - (uint16_t)(buffer_size - TC_PRIMARY_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 += TC_SEGMENT_HEADER_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; + const size_t data_length = buffer_size - data_offset - 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 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; } @@ -294,7 +312,9 @@ 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..5647330 100644 --- a/src/sdlp_tm.c +++ b/src/sdlp_tm.c @@ -10,20 +10,8 @@ #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 TM_VC_PER_MC 8 /** * @brief Per-Master-Channel frame-count state. @@ -34,10 +22,14 @@ typedef struct 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; +} 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 ::TM_MAX_MASTER_CHANNELS). + * + * Both counts are free-running modulo-256. Not thread-safe. + */ +static sdlp_tm_master_channel_t sdlp_tm_master_channels[TM_MAX_MASTER_CHANNELS]; /** * @brief Return the counter state for a Master Channel, allocating a slot on first use. @@ -46,31 +38,142 @@ 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++) { - 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++) { - 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 ::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 < (TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + + TM_SECONDARY_HEADER_MIN_DATA + 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[TM_PRIMARY_HEADER_SIZE] & 0x3Fu; + const size_t header_size = TM_SECONDARY_HEADER_ID_SIZE + data_length; + + if ((data_length == 0u) || + (buffer_size < (TM_PRIMARY_HEADER_SIZE + header_size + 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[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 ::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)TM_PRIMARY_HEADER_SIZE + frame->data_length + TM_FRAME_ERROR_CONTROL_SIZE; + + /* A data_length beyond the Data Field array would make the encoder read past it. */ + if (frame->data_length > 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 += TM_SECONDARY_HEADER_ID_SIZE + frame->secondary_header.length; + } + + if (frame->header.ocf_flag) + { + size += 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 +200,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 > 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), @@ -149,14 +254,14 @@ 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 > 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; @@ -164,7 +269,7 @@ sdlp_status_t sdlp_tm_set_secondary_header(sdlp_tm_frame_t *frame, sdlp_status_t sdlp_tm_set_ocf(sdlp_tm_frame_t *frame, const uint8_t ocf[TM_OCF_SIZE]) { - if (!frame || !ocf) + if ((!frame) || (!ocf)) { return SDLP_ERROR_INVALID_PARAM; } @@ -180,25 +285,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) + const size_t required_size = sdlp_tm_encoded_size(frame); + if (required_size == 0u) { - required_size += TM_SECONDARY_HEADER_ID_SIZE + frame->secondary_header.length; - } - if (ocf_present) - { - required_size += TM_OCF_SIZE; + return SDLP_ERROR_INVALID_PARAM; } if (buffer_size < required_size) @@ -208,7 +303,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 +318,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,7 +331,7 @@ 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; @@ -256,89 +351,58 @@ 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 < (TM_PRIMARY_HEADER_SIZE + 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) - { - 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) + 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.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; - - size_t overhead = + const size_t ocf_size = header.ocf_flag ? TM_OCF_SIZE : 0; + const 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) > 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 = 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; diff --git a/tests/cunit.h b/tests/cunit.h index 76fcac3..8634b45 100644 --- a/tests/cunit.h +++ b/tests/cunit.h @@ -14,13 +14,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 +54,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_tc.c b/tests/test_tc.c index bd03e79..bf6a2c0 100644 --- a/tests/test_tc.c +++ b/tests/test_tc.c @@ -9,22 +9,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 TC_SEGMENT_HEADER_ENABLED, so every size that + * depends on the Segment Header is written in terms of this value. + */ +#ifdef TC_SEGMENT_HEADER_ENABLED +# define TEST_TC_SEGMENT_OCTETS 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 * 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[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 = 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, TC_MAX_DATA_SIZE + 1)); return 0; } @@ -36,7 +73,7 @@ static int test_tc_encode_decode_roundtrip(void) const uint8_t payload[] = {0x01u, 0x23u, 0x45u, 0x67u}; uint8_t encoded[TC_PRIMARY_HEADER_SIZE + TC_MAX_DATA_SIZE + 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, @@ -66,7 +103,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[TC_PRIMARY_HEADER_SIZE + TEST_TC_SEGMENT_OCTETS + sizeof(payload) - 1 + + TC_FRAME_ERROR_CONTROL_SIZE]; size_t encoded_size = 0; ASSERT_EQ_INT(SDLP_SUCCESS, @@ -96,8 +135,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); @@ -163,12 +201,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; } @@ -287,6 +329,7 @@ static int test_tc_null_params(void) return 0; } +#ifdef TC_SEGMENT_HEADER_ENABLED static int test_tc_segment_header_roundtrip(void) { sdlp_tc_frame_t frame; @@ -308,6 +351,8 @@ static int test_tc_segment_header_roundtrip(void) 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[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, @@ -334,6 +379,395 @@ static int test_tc_decode_segment_too_small(void) return 0; } +#endif /* 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[TC_PRIMARY_HEADER_SIZE + 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 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 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 ::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[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, TC_MAX_FRAME_SIZE - 1, SDLP_ERROR_BUFFER_TOO_SMALL)); + + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, TC_MAX_FRAME_SIZE, &encoded_size)); + ASSERT_EQ_INT(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(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[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 = 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 = TC_MAX_DATA_SIZE; + ASSERT_EQ_INT(SDLP_SUCCESS, + sdlp_tc_encode_frame(&frame, encoded, sizeof(encoded), &encoded_size)); + ASSERT_EQ_INT(TC_MAX_FRAME_SIZE, encoded_size); + + frame.data_length = 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 */ + TC_PRIMARY_HEADER_SIZE + TEST_TC_SEGMENT_OCTETS + 2 + TC_FRAME_ERROR_CONTROL_SIZE - 1, + 0x9Cu, /* Frame Sequence Number */ +#ifdef 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 TC_SEGMENT_HEADER_ENABLED + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, 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 TC_SEGMENT_HEADER_ENABLED + ASSERT_EQ_INT(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[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[TC_PRIMARY_HEADER_SIZE] = { + 0x00u, + 0x00u, + 0x00u, + TC_PRIMARY_HEADER_SIZE + TEST_TC_SEGMENT_OCTETS + 1 + TC_FRAME_ERROR_CONTROL_SIZE - 1, + 0x00u}; + const uint8_t all_max[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 = 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(TC_MAX_FRAME_SIZE - 1, decoded.header.frame_length); + ASSERT_EQ_INT(0xFFu, decoded.header.frame_sequence_number); + + return 0; +} + +#ifdef 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[TC_PRIMARY_HEADER_SIZE + TC_SEGMENT_HEADER_SIZE + 1 + 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, 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[TC_PRIMARY_HEADER_SIZE]); + + ASSERT_EQ_INT(SDLP_SUCCESS, sdlp_tc_set_segment_header(&frame, 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[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, 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, TC_SEQ_FLAG_NO_SEG, 0x2Fu)); + ASSERT_EQ_INT(0x2Fu, frame.segment_header.map_id); + + return 0; +} +#endif /* TC_SEGMENT_HEADER_ENABLED */ + +static int test_tc_decode_short_inputs(void) +{ + static const uint8_t zeros[TC_PRIMARY_HEADER_SIZE + 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[TC_PRIMARY_HEADER_SIZE + TC_FRAME_ERROR_CONTROL_SIZE] = + {0x30u, 0x00u, 0x00u, TC_PRIMARY_HEADER_SIZE + 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 +784,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 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 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..b8e1e2f 100644 --- a/tests/test_tm.c +++ b/tests/test_tm.c @@ -9,6 +9,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 * 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 \ + (TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + 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) { @@ -115,8 +135,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 +146,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. */ @@ -191,17 +210,19 @@ 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[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); + /* A rejected call leaves the whole frame as it was, the Secondary Header Flag included. */ + ASSERT_EQ_MEM(&before, &frame, sizeof(frame)); return 0; } @@ -280,14 +301,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}; 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; } @@ -360,23 +383,653 @@ static int test_tm_decode_malformed(void) 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[TM_PRIMARY_HEADER_SIZE + 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 TM_MAX_DATA_SIZE. */ + static const uint8_t oversized[TM_PRIMARY_HEADER_SIZE + TM_MAX_DATA_SIZE + 1 + + 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 = TM_PRIMARY_HEADER_SIZE + data_length + 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[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(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 = 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[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[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 = TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + length + 1u + + 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(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, + 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 + 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 <= 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 < 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[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[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(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[TM_PRIMARY_HEADER_SIZE] = {0}; + uint8_t encoded[TM_PRIMARY_HEADER_SIZE + 1 + 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[TM_OCF_SIZE] = {0}; + const uint8_t all_max[TM_PRIMARY_HEADER_SIZE] = {0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu, 0xFFu}; + uint8_t encoded[TM_PRIMARY_HEADER_SIZE + TM_SECONDARY_HEADER_ID_SIZE + 1 + 1 + TM_OCF_SIZE + + 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[TM_PRIMARY_HEADER_SIZE + 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[TM_PRIMARY_HEADER_SIZE + 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/tools/coverage_html.sh b/tools/coverage_html.sh index 6755c00..f597022 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 "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}"