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..fae7944 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +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 libraries, examples and run unit tests + run: make CC=${{ matrix.cc }} && make test CC=${{ matrix.cc }} + - name: Run examples + run: make example CC=${{ matrix.cc }} + + 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 6f63836..4fddc2b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,4 @@ dkms.conf # project build directories build/ -.github/ .vscode/ diff --git a/LICENSE b/LICENSE index 261eeb9..83a59aa 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 OpenSpaceCode contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile index 863696c..1aead80 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,14 @@ CC ?= cc AR ?= ar OPT ?= -O2 -CFLAGS ?= -std=c99 -Wall -Wextra -Iinclude +SANITIZE_OPT = -O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined \ + -fno-sanitize-recover=all +CFLAGS ?= -std=c99 -Iinclude BUILD_DIR = build +SANITIZE_DIR = $(BUILD_DIR)/sanitize + +WARNINGS = -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror +ALL_CFLAGS = $(CFLAGS) $(WARNINGS) CUC_LIB = $(BUILD_DIR)/libcuc.a CUC_OBJ = $(BUILD_DIR)/src/cuc.o @@ -31,7 +37,7 @@ lib: $(LIBS) $(CUC_OBJ): $(CUC_SRC) $(CUC_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude -c $(CUC_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude -c $(CUC_SRC) -o $@ $(CUC_LIB): $(CUC_OBJ) mkdir -p $(dir $@) @@ -39,7 +45,7 @@ $(CUC_LIB): $(CUC_OBJ) $(CDS_OBJ): $(CDS_SRC) $(CDS_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude -c $(CDS_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude -c $(CDS_SRC) -o $@ $(CDS_LIB): $(CDS_OBJ) mkdir -p $(dir $@) @@ -47,7 +53,7 @@ $(CDS_LIB): $(CDS_OBJ) $(CCS_OBJ): $(CCS_SRC) $(CCS_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude -c $(CCS_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude -c $(CCS_SRC) -o $@ $(CCS_LIB): $(CCS_OBJ) mkdir -p $(dir $@) @@ -58,31 +64,61 @@ ctest: $(CTEST) $(CTEST): tests/unit_tests.c tests/test_cuc.c tests/test_cds.c tests/test_ccs.c tests/cunit.h \ tests/test_runners.h $(CUC_SRC) $(CUC_HDR) $(CDS_SRC) $(CDS_HDR) $(CCS_SRC) $(CCS_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude -Itests \ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude -Itests \ tests/unit_tests.c tests/test_cuc.c tests/test_cds.c tests/test_ccs.c \ $(CUC_SRC) $(CDS_SRC) $(CCS_SRC) -o $@ example: $(EXAMPLES) + $(CUC_EXAMPLE) + $(CDS_EXAMPLE) + $(CCS_EXAMPLE) $(CUC_EXAMPLE): examples/cuc_example.c $(CUC_SRC) $(CUC_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude examples/cuc_example.c $(CUC_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude examples/cuc_example.c $(CUC_SRC) -o $@ $(CDS_EXAMPLE): examples/cds_example.c $(CDS_SRC) $(CDS_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude examples/cds_example.c $(CDS_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude examples/cds_example.c $(CDS_SRC) -o $@ $(CCS_EXAMPLE): examples/ccs_example.c $(CCS_SRC) $(CCS_HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(OPT) -Iinclude examples/ccs_example.c $(CCS_SRC) -o $@ + $(CC) $(ALL_CFLAGS) $(OPT) -Iinclude examples/ccs_example.c $(CCS_SRC) -o $@ -run: $(CTEST) +test: $(CTEST) $(CTEST) +# Instrumented rebuild; program output is shown only on failure and the build is removed +# afterwards, on success and on failure. +sanitize: + @$(MAKE) --no-print-directory clean >/dev/null + @$(MAKE) --no-print-directory ctest example OPT="$(SANITIZE_OPT)" >/dev/null \ + || { $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @mkdir -p $(SANITIZE_DIR) + @echo "Sanitizers (ASan + UBSan):" + @./$(CTEST) >$(SANITIZE_DIR)/unit_tests.log \ + && echo " libraries via unit tests : no errors detected" \ + || { cat $(SANITIZE_DIR)/unit_tests.log; echo " libraries via unit tests : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @./$(CUC_EXAMPLE) >$(SANITIZE_DIR)/cuc_example.log \ + && echo " cuc via example : no errors detected" \ + || { cat $(SANITIZE_DIR)/cuc_example.log; echo " cuc via example : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @./$(CDS_EXAMPLE) >$(SANITIZE_DIR)/cds_example.log \ + && echo " cds via example : no errors detected" \ + || { cat $(SANITIZE_DIR)/cds_example.log; echo " cds via example : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @./$(CCS_EXAMPLE) >$(SANITIZE_DIR)/ccs_example.log \ + && echo " ccs via example : no errors detected" \ + || { cat $(SANITIZE_DIR)/ccs_example.log; echo " ccs via example : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @$(MAKE) --no-print-directory clean >/dev/null + @echo "Result: PASS" + coverage-html: bash tools/coverage-html.sh clean: rm -rf $(BUILD_DIR) -.PHONY: all lib ctest example run coverage-html clean +.PHONY: all lib ctest example test sanitize coverage-html clean diff --git a/README.md b/README.md index abd1c41..d292214 100644 --- a/README.md +++ b/README.md @@ -106,20 +106,26 @@ make lib # produces build/libcuc.a, build/libcds.a and build/libccs.a ```bash make example -./build/examples/cuc_example -./build/examples/cds_example -./build/examples/ccs_example ``` ### Run the tests ```bash -make run # or: make ctest && ./build/tests/ctest +make test # or: make ctest && ./build/tests/ctest +``` + +### Sanitizers + +Runs the test suite and the examples under AddressSanitizer and +UndefinedBehaviorSanitizer: + +```bash +make sanitize ``` ### Coverage (HTML) -Requires `gcovr` (`pip install gcovr`): +Requires `gcovr` (`pip install gcovr`). Fails below 100% line or branch coverage: ```bash make coverage-html # writes build/coverage/index.html diff --git a/examples/ccs_example.c b/examples/ccs_example.c index 561eabd..f46c273 100644 --- a/examples/ccs_example.c +++ b/examples/ccs_example.c @@ -5,6 +5,9 @@ * Encodes a calendar date and time of day into a self-identified CCS code and * decodes it back, printing the octets. Demonstrates CCSDS 301.0-B-4, 3.4. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ diff --git a/examples/cds_example.c b/examples/cds_example.c index cf8bf93..8ae0761 100644 --- a/examples/cds_example.c +++ b/examples/cds_example.c @@ -5,6 +5,9 @@ * Encodes a day/millisecond time value into a self-identified CDS code and * decodes it back, printing the octets. Demonstrates CCSDS 301.0-B-4, 3.3. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ diff --git a/examples/cuc_example.c b/examples/cuc_example.c index d283c5f..fb7cb20 100644 --- a/examples/cuc_example.c +++ b/examples/cuc_example.c @@ -10,6 +10,9 @@ * cuc_time_to_seconds() / cuc_time_from_seconds() helpers are deliberately * unused here. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ diff --git a/include/ccs.h b/include/ccs.h index ad506fa..45f212f 100644 --- a/include/ccs.h +++ b/include/ccs.h @@ -13,6 +13,9 @@ * is carried in a TIME SPECIFICATION FIELD (T-field) preceded by a one-octet * TIME CODE PREAMBLE FIELD (P-field) describing its structure. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -219,6 +222,8 @@ ccs_status_t ccs_pfield_encode(const ccs_format_t *fmt, /** * @brief Decode a P-field into a format. * + * @note On failure @p fmt and @p consumed are left unchanged. + * * @param[in] buf Input buffer positioned at the P-field. * @param[in] buf_len Number of octets available in @p buf. * @param[out] fmt Receives the decoded format. @@ -282,10 +287,9 @@ ccs_status_t ccs_tfield_decode(const uint8_t *buf, /** * @brief Encode a self-identified CCS code: P-field followed by T-field. * - * @note On failure the contents of @p buf are unspecified, since the P-field may - * already be written when the T-field stage rejects the value. Only a - * #CCS_OK return sets @p written, so a caller that checks the status never - * transmits a partial code. + * @note On failure @p buf and @p written are left unchanged: the code is assembled in a + * local buffer and copied out only once every stage has succeeded and the total + * length is known to fit. * * @param[in] time Time value to encode. * @param[in] fmt Format to encode. @@ -306,6 +310,9 @@ ccs_status_t ccs_encode(const ccs_time_t *time, /** * @brief Decode a self-identified CCS code: parse the P-field, then the T-field. * + * @note On failure @p fmt, @p time and @p consumed are left unchanged; the code is + * decoded into locals and committed only once every check has passed. + * * @param[in] buf Input buffer positioned at the P-field. * @param[in] buf_len Number of octets available in @p buf. * @param[out] fmt Receives the recovered format. diff --git a/include/cds.h b/include/cds.h index 439a8aa..0d9992d 100644 --- a/include/cds.h +++ b/include/cds.h @@ -11,6 +11,9 @@ * carried in a TIME SPECIFICATION FIELD (T-field) preceded by a one-octet * TIME CODE PREAMBLE FIELD (P-field) describing its structure. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -200,6 +203,8 @@ cds_status_t cds_pfield_encode(const cds_format_t *fmt, /** * @brief Decode a P-field into a format. * + * @note On failure @p fmt and @p consumed are left unchanged. + * * @param[in] buf Input buffer positioned at the P-field. * @param[in] buf_len Number of octets available in @p buf. * @param[out] fmt Receives the decoded format. @@ -256,10 +261,9 @@ cds_status_t cds_tfield_decode(const uint8_t *buf, /** * @brief Encode a self-identified CDS code: P-field followed by T-field. * - * @note On failure the contents of @p buf are unspecified, since the P-field may - * already be written when the T-field stage rejects the value. Only a - * #CDS_OK return sets @p written, so a caller that checks the status never - * transmits a partial code. + * @note On failure @p buf and @p written are left unchanged: the code is assembled in a + * local buffer and copied out only once every stage has succeeded and the total + * length is known to fit. * * @param[in] time Time value to encode. * @param[in] fmt Format to encode. @@ -280,6 +284,9 @@ cds_status_t cds_encode(const cds_time_t *time, /** * @brief Decode a self-identified CDS code: parse the P-field, then the T-field. * + * @note On failure @p fmt, @p time and @p consumed are left unchanged; the code is + * decoded into locals and committed only once every check has passed. + * * @param[in] buf Input buffer positioned at the P-field. * @param[in] buf_len Number of octets available in @p buf. * @param[out] fmt Receives the recovered format. diff --git a/include/cuc.h b/include/cuc.h index 1d7e0ec..c7ef627 100644 --- a/include/cuc.h +++ b/include/cuc.h @@ -10,6 +10,9 @@ * carried in a TIME SPECIFICATION FIELD (T-field) that may be preceded by an * explicit TIME CODE PREAMBLE FIELD (P-field) describing its structure. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -187,6 +190,8 @@ cuc_status_t cuc_pfield_encode(const cuc_format_t *fmt, /** * @brief Decode a P-field into a format. * + * @note On failure @p fmt and @p consumed are left unchanged. + * * @param[in] buf Input buffer positioned at the P-field. * @param[in] buf_len Number of octets available in @p buf. * @param[out] fmt Receives the decoded format. @@ -242,10 +247,9 @@ cuc_status_t cuc_tfield_decode(const uint8_t *buf, /** * @brief Encode a self-identified CUC code: P-field followed by T-field. * - * @note On failure the contents of @p buf are unspecified, since the P-field may - * already be written when the T-field stage rejects the value. Only a - * #CUC_OK return sets @p written, so a caller that checks the status never - * transmits a partial code. + * @note On failure @p buf and @p written are left unchanged: the code is assembled in a + * local buffer and copied out only once every stage has succeeded and the total + * length is known to fit. * * @param[in] time Time value to encode. * @param[in] fmt Format to encode. @@ -265,6 +269,9 @@ cuc_status_t cuc_encode(const cuc_time_t *time, /** * @brief Decode a self-identified CUC code: parse the P-field, then the T-field. * + * @note On failure @p fmt, @p time and @p consumed are left unchanged; the code is + * decoded into locals and committed only once every check has passed. + * * @note @p consumed is the authoritative length of the code just read, and is what a * caller must advance by when codes are concatenated. It can exceed * cuc_size(@p fmt), which reports the shortest encoding of the recovered format diff --git a/src/ccs.c b/src/ccs.c index 327c992..a064c36 100644 --- a/src/ccs.c +++ b/src/ccs.c @@ -5,6 +5,9 @@ * Implements the CCSDS Calendar Segmented Time Code (CCS) as per * CCSDS 301.0-B-4 (Time Code Formats), Section 3.4. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -501,7 +504,7 @@ ccs_status_t ccs_tfield_decode(const uint8_t *buf, } /* Decode into a local so a malformed segment cannot leave *time half written. */ - ccs_time_t decoded = {0}; + ccs_time_t decoded = {0, 0, 0, 0, 0, 0, 0, {0, 0, 0, 0, 0, 0}}; status = ccs_decode_date(buf, fmt, &decoded); if (status != CCS_OK) { @@ -529,26 +532,39 @@ ccs_status_t ccs_encode(const ccs_time_t *time, size_t buf_len, size_t *written) { - if (!written) + if ((!buf) || (!written)) { return CCS_ERR_NULL; } + /* Assemble the code in a local buffer so the caller's buffer is written only once + every stage has succeeded and the total length is known to fit. */ + uint8_t staging[CCS_OCTETS_MAX]; size_t p_len = 0; - ccs_status_t status = ccs_pfield_encode(fmt, buf, buf_len, &p_len); + ccs_status_t status = ccs_pfield_encode(fmt, staging, sizeof(staging), &p_len); if (status != CCS_OK) { return status; } size_t t_len = 0; - status = ccs_tfield_encode(time, fmt, buf + p_len, buf_len - p_len, &t_len); + status = ccs_tfield_encode(time, fmt, staging + p_len, sizeof(staging) - p_len, &t_len); if (status != CCS_OK) { return status; } - *written = p_len + t_len; + size_t size = p_len + t_len; + if (buf_len < size) + { + return CCS_ERR_BUFFER; + } + + for (size_t i = 0; i < size; i++) + { + buf[i] = staging[i]; + } + *written = size; return CCS_OK; } @@ -558,25 +574,29 @@ ccs_status_t ccs_decode(const uint8_t *buf, ccs_time_t *time, size_t *consumed) { - if (!consumed) + if ((!fmt) || (!time) || (!consumed)) { return CCS_ERR_NULL; } + ccs_format_t decoded_fmt; size_t p_len = 0; - ccs_status_t status = ccs_pfield_decode(buf, buf_len, fmt, &p_len); + ccs_status_t status = ccs_pfield_decode(buf, buf_len, &decoded_fmt, &p_len); if (status != CCS_OK) { return status; } + ccs_time_t decoded_time; size_t t_len = 0; - status = ccs_tfield_decode(buf + p_len, buf_len - p_len, fmt, time, &t_len); + status = ccs_tfield_decode(buf + p_len, buf_len - p_len, &decoded_fmt, &decoded_time, &t_len); if (status != CCS_OK) { return status; } + *fmt = decoded_fmt; + *time = decoded_time; *consumed = p_len + t_len; return CCS_OK; } diff --git a/src/cds.c b/src/cds.c index 546adde..73ecf3e 100644 --- a/src/cds.c +++ b/src/cds.c @@ -5,6 +5,9 @@ * Implements the CCSDS Day Segmented Time Code (CDS) as per * CCSDS 301.0-B-4 (Time Code Formats), Section 3.3. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -200,17 +203,18 @@ cds_status_t cds_pfield_decode(const uint8_t *buf, return CDS_ERR_PFIELD_ID; } - fmt->epoch = (cds_epoch_t)((octet >> CDS_P_EPOCH_SHIFT) & CDS_P_EPOCH_MASK); - fmt->day_length = (cds_day_length_t)((octet >> CDS_P_DAY_SHIFT) & CDS_P_DAY_MASK); - fmt->submillisecond = (cds_subms_t)(octet & CDS_P_SUBMS_MASK); + cds_subms_t submillisecond = (cds_subms_t)(octet & CDS_P_SUBMS_MASK); /* Bits 6-7 = '11' is reserved for future use (CCSDS 301.0-B-4, 3.3.2). */ - if ((fmt->submillisecond != CDS_SUBMS_NONE) && (fmt->submillisecond != CDS_SUBMS_US) && - (fmt->submillisecond != CDS_SUBMS_PS)) + if ((submillisecond != CDS_SUBMS_NONE) && (submillisecond != CDS_SUBMS_US) && + (submillisecond != CDS_SUBMS_PS)) { return CDS_ERR_FORMAT; } + fmt->epoch = (cds_epoch_t)((octet >> CDS_P_EPOCH_SHIFT) & CDS_P_EPOCH_MASK); + fmt->day_length = (cds_day_length_t)((octet >> CDS_P_DAY_SHIFT) & CDS_P_DAY_MASK); + fmt->submillisecond = submillisecond; *consumed = CDS_PFIELD_OCTETS; return CDS_OK; } @@ -319,26 +323,39 @@ cds_status_t cds_encode(const cds_time_t *time, size_t buf_len, size_t *written) { - if (!written) + if ((!buf) || (!written)) { return CDS_ERR_NULL; } + /* Assemble the code in a local buffer so the caller's buffer is written only once + every stage has succeeded and the total length is known to fit. */ + uint8_t staging[CDS_OCTETS_MAX]; size_t p_len = 0; - cds_status_t status = cds_pfield_encode(fmt, buf, buf_len, &p_len); + cds_status_t status = cds_pfield_encode(fmt, staging, sizeof(staging), &p_len); if (status != CDS_OK) { return status; } size_t t_len = 0; - status = cds_tfield_encode(time, fmt, buf + p_len, buf_len - p_len, &t_len); + status = cds_tfield_encode(time, fmt, staging + p_len, sizeof(staging) - p_len, &t_len); if (status != CDS_OK) { return status; } - *written = p_len + t_len; + size_t size = p_len + t_len; + if (buf_len < size) + { + return CDS_ERR_BUFFER; + } + + for (size_t i = 0; i < size; i++) + { + buf[i] = staging[i]; + } + *written = size; return CDS_OK; } @@ -348,25 +365,29 @@ cds_status_t cds_decode(const uint8_t *buf, cds_time_t *time, size_t *consumed) { - if (!consumed) + if ((!fmt) || (!time) || (!consumed)) { return CDS_ERR_NULL; } + cds_format_t decoded_fmt; size_t p_len = 0; - cds_status_t status = cds_pfield_decode(buf, buf_len, fmt, &p_len); + cds_status_t status = cds_pfield_decode(buf, buf_len, &decoded_fmt, &p_len); if (status != CDS_OK) { return status; } + cds_time_t decoded_time; size_t t_len = 0; - status = cds_tfield_decode(buf + p_len, buf_len - p_len, fmt, time, &t_len); + status = cds_tfield_decode(buf + p_len, buf_len - p_len, &decoded_fmt, &decoded_time, &t_len); if (status != CDS_OK) { return status; } + *fmt = decoded_fmt; + *time = decoded_time; *consumed = p_len + t_len; return CDS_OK; } diff --git a/src/cuc.c b/src/cuc.c index 19ed23c..d07a597 100644 --- a/src/cuc.c +++ b/src/cuc.c @@ -5,6 +5,9 @@ * Implements the CCSDS Unsegmented Time Code (CUC) as per * CCSDS 301.0-B-4 (Time Code Formats), Section 3.2. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -183,32 +186,34 @@ cuc_status_t cuc_pfield_decode(const uint8_t *buf, return CUC_ERR_PFIELD_ID; } - fmt->epoch = (cuc_epoch_t)id; - fmt->basic_octets = (uint8_t)(((oct1 >> CUC_P1_BASIC_SHIFT) & CUC_P1_BASIC_MASK) + 1u); - fmt->fraction_octets = (uint8_t)(oct1 & CUC_P1_FRAC_MASK); + cuc_format_t decoded; + decoded.epoch = (cuc_epoch_t)id; + decoded.basic_octets = (uint8_t)(((oct1 >> CUC_P1_BASIC_SHIFT) & CUC_P1_BASIC_MASK) + 1u); + decoded.fraction_octets = (uint8_t)(oct1 & CUC_P1_FRAC_MASK); + size_t size = 1u; - if ((oct1 & CUC_P1_EXTENSION) == 0u) + if ((oct1 & CUC_P1_EXTENSION) != 0u) { - *consumed = 1u; - - return CUC_OK; - } + if (buf_len < 2u) + { + return CUC_ERR_BUFFER; + } - if (buf_len < 2u) - { - return CUC_ERR_BUFFER; - } + uint8_t oct2 = buf[1]; + /* A third P-field octet would be signalled here; this library defines only two. */ + if ((oct2 & CUC_P2_EXTENSION) != 0u) + { + return CUC_ERR_UNSUPPORTED; + } - uint8_t oct2 = buf[1]; - /* A third P-field octet would be signalled here; this library defines only two. */ - if ((oct2 & CUC_P2_EXTENSION) != 0u) - { - return CUC_ERR_UNSUPPORTED; + decoded.basic_octets += (uint8_t)((oct2 >> CUC_P2_ADD_BASIC_SHIFT) & CUC_P2_ADD_BASIC_MASK); + decoded.fraction_octets += + (uint8_t)((oct2 >> CUC_P2_ADD_FRAC_SHIFT) & CUC_P2_ADD_FRAC_MASK); + size = 2u; } - fmt->basic_octets += (uint8_t)((oct2 >> CUC_P2_ADD_BASIC_SHIFT) & CUC_P2_ADD_BASIC_MASK); - fmt->fraction_octets += (uint8_t)((oct2 >> CUC_P2_ADD_FRAC_SHIFT) & CUC_P2_ADD_FRAC_MASK); - *consumed = 2u; + *fmt = decoded; + *consumed = size; return CUC_OK; } @@ -309,26 +314,39 @@ cuc_status_t cuc_encode(const cuc_time_t *time, size_t buf_len, size_t *written) { - if (!written) + if ((!buf) || (!written)) { return CUC_ERR_NULL; } + /* Assemble the code in a local buffer so the caller's buffer is written only once + every stage has succeeded and the total length is known to fit. */ + uint8_t staging[CUC_OCTETS_MAX]; size_t p_len = 0; - cuc_status_t status = cuc_pfield_encode(fmt, buf, buf_len, &p_len); + cuc_status_t status = cuc_pfield_encode(fmt, staging, sizeof(staging), &p_len); if (status != CUC_OK) { return status; } size_t t_len = 0; - status = cuc_tfield_encode(time, fmt, buf + p_len, buf_len - p_len, &t_len); + status = cuc_tfield_encode(time, fmt, staging + p_len, sizeof(staging) - p_len, &t_len); if (status != CUC_OK) { return status; } - *written = p_len + t_len; + size_t size = p_len + t_len; + if (buf_len < size) + { + return CUC_ERR_BUFFER; + } + + for (size_t i = 0; i < size; i++) + { + buf[i] = staging[i]; + } + *written = size; return CUC_OK; } @@ -339,25 +357,29 @@ cuc_status_t cuc_decode(const uint8_t *buf, cuc_time_t *time, size_t *consumed) { - if (!consumed) + if ((!fmt) || (!time) || (!consumed)) { return CUC_ERR_NULL; } + cuc_format_t decoded_fmt; size_t p_len = 0; - cuc_status_t status = cuc_pfield_decode(buf, buf_len, fmt, &p_len); + cuc_status_t status = cuc_pfield_decode(buf, buf_len, &decoded_fmt, &p_len); if (status != CUC_OK) { return status; } + cuc_time_t decoded_time; size_t t_len = 0; - status = cuc_tfield_decode(buf + p_len, buf_len - p_len, fmt, time, &t_len); + status = cuc_tfield_decode(buf + p_len, buf_len - p_len, &decoded_fmt, &decoded_time, &t_len); if (status != CUC_OK) { return status; } + *fmt = decoded_fmt; + *time = decoded_time; *consumed = p_len + t_len; return CUC_OK; diff --git a/tests/cunit.h b/tests/cunit.h index 76fcac3..05bfe9f 100644 --- a/tests/cunit.h +++ b/tests/cunit.h @@ -1,6 +1,9 @@ /* Tiny C unit test helpers. Suitable for embedding in small projects. * Usage: include this header in a single C test file and implement test * functions returning 0 on success, non-zero on failure. Use RUN_TEST(fn). + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 */ #ifndef CUNIT_H #define CUNIT_H diff --git a/tests/test_ccs.c b/tests/test_ccs.c index c45a2fa..f35e069 100644 --- a/tests/test_ccs.c +++ b/tests/test_ccs.c @@ -5,6 +5,9 @@ * Exercises the P-field / T-field codecs against the encodings defined in * CCSDS 301.0-B-4 (Time Code Formats), Section 3.4. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -13,6 +16,7 @@ #include "test_runners.h" #include +#include #include /* 2024-02-29T12:34:56.78, month/day variation with one sub-second segment. Every @@ -435,6 +439,143 @@ static int test_encode_decode_errors(void) return 0; } +/* Validate First, Write After: a rejected call leaves every output exactly as the caller + * passed it. The 0x5A fill makes any stray write visible. */ +static int test_outputs_untouched_on_failure(void) +{ + ccs_format_t fmt = {(ccs_variation_t)0x5A, 0x5Au}; + ccs_time_t time = {0x5A5Au, + 0x5Au, + 0x5Au, + 0x5A5Au, + 0x5Au, + 0x5Au, + 0x5Au, + {0x5Au, 0x5Au, 0x5Au, 0x5Au, 0x5Au, 0x5Au}}; + size_t len = 0x5A5Au; + + /* Resolution bits '111' are not used, so the whole P-field is rejected. */ + uint8_t unused_resolution[1] = {0x57}; + ASSERT_EQ_INT(CCS_ERR_FORMAT, ccs_pfield_decode(unused_resolution, 1, &fmt, &len)); + /* The extension flag announces a second P-field octet, which CCS does not define. */ + uint8_t extended[1] = {0xD1}; + ASSERT_EQ_INT(CCS_ERR_UNSUPPORTED, ccs_pfield_decode(extended, 1, &fmt, &len)); + ASSERT_EQ_INT(0x5A, (int)fmt.variation); + ASSERT_EQ_INT(0x5A, fmt.subsecond_segments); + ASSERT_TRUE(len == 0x5A5Au); + + /* Valid P-field, but the T-field it announces is not present. */ + uint8_t pfield_only[1] = {0x51}; + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_decode(pfield_only, 1, &fmt, &time, &len)); + /* A full code whose seconds segment is not valid BCD. */ + uint8_t bad_bcd[9] = {0x51, 0x20, 0x24, 0x02, 0x29, 0x12, 0x34, 0x5F, 0x78}; + ASSERT_EQ_INT(CCS_ERR_BCD, ccs_decode(bad_bcd, sizeof(bad_bcd), &fmt, &time, &len)); + ASSERT_EQ_INT(0x5A, (int)fmt.variation); + ASSERT_TRUE(time.year == 0x5A5Au); + ASSERT_TRUE(time.second == 0x5Au); + ASSERT_TRUE(time.subseconds[0] == 0x5Au); + ASSERT_TRUE(len == 0x5A5Au); + + ccs_format_t valid = {CCS_VARIATION_MONTH_DAY, 1u}; + ccs_format_t invalid = {CCS_VARIATION_MONTH_DAY, CCS_SUBSECOND_SEGMENTS_MAX + 1u}; + ccs_time_t out_of_range = leap_day; + out_of_range.day = 30u; /* February never has 30 days */ + uint8_t buf[CCS_OCTETS_MAX]; + uint8_t untouched[CCS_OCTETS_MAX]; + memset(buf, 0x5A, sizeof(buf)); + memset(untouched, 0x5A, sizeof(untouched)); + size_t written = 0x5A5Au; + + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_encode(&leap_day, &valid, buf, 2, &written)); + ASSERT_EQ_INT(CCS_ERR_FORMAT, ccs_encode(&leap_day, &invalid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CCS_ERR_FORMAT, ccs_encode(&out_of_range, &valid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CCS_ERR_NULL, ccs_encode(NULL, &valid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CCS_ERR_NULL, ccs_encode(&leap_day, &valid, NULL, sizeof(buf), &written)); + ASSERT_EQ_INT(CCS_ERR_NULL, ccs_encode(&leap_day, &valid, buf, sizeof(buf), NULL)); + ASSERT_EQ_MEM(untouched, buf, sizeof(buf)); + ASSERT_TRUE(written == 0x5A5Au); + + ASSERT_EQ_INT(CCS_ERR_NULL, ccs_decode(pfield_only, 1, NULL, &time, &len)); + ASSERT_EQ_INT(CCS_ERR_NULL, ccs_decode(pfield_only, 1, &fmt, NULL, &len)); + return 0; +} + +/* Buffer-size boundaries. Exact-size heap allocations make a one-octet overrun or + * over-read visible to ASan instead of landing in slack space, and the sizes are derived + * from ccs_size()/ccs_tfield_size() rather than hard-coded. */ +static int test_buffer_size_boundaries(void) +{ + ccs_format_t fmt = {CCS_VARIATION_MONTH_DAY, 1u}; + ccs_time_t in = leap_day; + size_t need = ccs_size(&fmt); + size_t t_need = ccs_tfield_size(&fmt); + ASSERT_EQ_INT(9, (int)need); + ASSERT_EQ_INT(8, (int)t_need); + + /* Exactly the required size succeeds. */ + uint8_t *exact = malloc(need); + size_t written = 0; + ASSERT_TRUE(exact); + ASSERT_EQ_INT(CCS_OK, ccs_encode(&in, &fmt, exact, need, &written)); + ASSERT_TRUE(written == need); + + /* Every length shorter than the requirement is rejected with nothing written. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *shortbuf = malloc(alloc); + uint8_t *ref = malloc(alloc); + ASSERT_TRUE(shortbuf); + ASSERT_TRUE(ref); + memset(shortbuf, 0x5A, alloc); + memset(ref, 0x5A, alloc); + size_t w = 0x5A5Au; + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_encode(&in, &fmt, shortbuf, len, &w)); + ASSERT_EQ_MEM(ref, shortbuf, alloc); + ASSERT_TRUE(w == 0x5A5Au); + free(shortbuf); + free(ref); + } + + /* Decoding the exact-size code succeeds and consumes all of it. */ + ccs_format_t out_fmt; + ccs_time_t out; + size_t consumed = 0; + ASSERT_EQ_INT(CCS_OK, ccs_decode(exact, need, &out_fmt, &out, &consumed)); + ASSERT_TRUE(consumed == need); + + /* Every truncation of that code is rejected; the input buffer is sized to the + * truncated length so any over-read is caught. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *truncated = malloc(alloc); + ASSERT_TRUE(truncated); + memcpy(truncated, exact, len); + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_decode(truncated, len, &out_fmt, &out, &consumed)); + free(truncated); + } + + /* The same boundary on the T-field codec, which carries its own length check. */ + uint8_t *t_exact = malloc(t_need); + uint8_t *t_short = malloc(t_need - 1u); + ASSERT_TRUE(t_exact); + ASSERT_TRUE(t_short); + written = 0; + ASSERT_EQ_INT(CCS_OK, ccs_tfield_encode(&in, &fmt, t_exact, t_need, &written)); + ASSERT_TRUE(written == t_need); + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_tfield_encode(&in, &fmt, t_short, t_need - 1u, &written)); + consumed = 0; + ASSERT_EQ_INT(CCS_OK, ccs_tfield_decode(t_exact, t_need, &fmt, &out, &consumed)); + ASSERT_TRUE(consumed == t_need); + memcpy(t_short, t_exact, t_need - 1u); + ASSERT_EQ_INT(CCS_ERR_BUFFER, ccs_tfield_decode(t_short, t_need - 1u, &fmt, &out, &consumed)); + free(t_exact); + free(t_short); + free(exact); + return 0; +} + test_result_t test_ccs_run_all(void) { RUN_TEST(test_pfield_month_day); @@ -454,6 +595,8 @@ test_result_t test_ccs_run_all(void) RUN_TEST(test_tfield_decode_errors); RUN_TEST(test_tfield_decode_bcd_errors); RUN_TEST(test_encode_decode_errors); + RUN_TEST(test_outputs_untouched_on_failure); + RUN_TEST(test_buffer_size_boundaries); test_result_t r; r.total = cunit_total_tests; diff --git a/tests/test_cds.c b/tests/test_cds.c index 4d72006..f913862 100644 --- a/tests/test_cds.c +++ b/tests/test_cds.c @@ -5,6 +5,9 @@ * Exercises the P-field / T-field codecs against the encodings defined in * CCSDS 301.0-B-4 (Time Code Formats), Section 3.3. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -13,6 +16,8 @@ #include "test_runners.h" #include +#include +#include /* {CCSDS epoch, 16-bit day, microsecond sub-ms}: * P-field = ext(0) id(100) epoch(0) day(0) subms(01) = 0100 0001 = 0x41. */ @@ -277,6 +282,130 @@ static int test_encode_decode_errors(void) return 0; } +/* Validate First, Write After: a rejected call leaves every output exactly as the caller + * passed it. The 0x5A fill makes any stray write visible. */ +static int test_outputs_untouched_on_failure(void) +{ + cds_format_t fmt = {(cds_epoch_t)0x5A, (cds_day_length_t)0x5A, (cds_subms_t)0x5A}; + cds_time_t time = {0x5A5A5A5Au, 0x5A5A5A5Au, 0xA5A5A5A5u}; + size_t len = 0x5A5Au; + + /* Sub-millisecond bits '11' are reserved, so the whole P-field is rejected. */ + uint8_t reserved[1] = {0x43}; + ASSERT_EQ_INT(CDS_ERR_FORMAT, cds_pfield_decode(reserved, 1, &fmt, &len)); + ASSERT_EQ_INT(0x5A, (int)fmt.epoch); + ASSERT_EQ_INT(0x5A, (int)fmt.day_length); + ASSERT_EQ_INT(0x5A, (int)fmt.submillisecond); + ASSERT_TRUE(len == 0x5A5Au); + + /* Valid P-field, but the T-field it announces is not present. */ + uint8_t pfield_only[1] = {0x41}; + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_decode(pfield_only, 1, &fmt, &time, &len)); + ASSERT_EQ_INT(0x5A, (int)fmt.epoch); + ASSERT_TRUE(time.days == 0x5A5A5A5Au); + ASSERT_TRUE(time.submilliseconds == 0xA5A5A5A5u); + ASSERT_TRUE(len == 0x5A5Au); + + cds_format_t valid = {CDS_EPOCH_CCSDS, CDS_DAY_16BIT, CDS_SUBMS_US}; + cds_format_t invalid = {CDS_EPOCH_CCSDS, CDS_DAY_16BIT, (cds_subms_t)3}; + cds_time_t t = {1u, 0u, 0u}; + cds_time_t out_of_range = {0x10000u, 0u, 0u}; /* beyond the 16-bit day segment */ + uint8_t buf[CDS_OCTETS_MAX]; + uint8_t untouched[CDS_OCTETS_MAX]; + memset(buf, 0x5A, sizeof(buf)); + memset(untouched, 0x5A, sizeof(untouched)); + size_t written = 0x5A5Au; + + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_encode(&t, &valid, buf, 2, &written)); + ASSERT_EQ_INT(CDS_ERR_FORMAT, cds_encode(&t, &invalid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CDS_ERR_FORMAT, cds_encode(&out_of_range, &valid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CDS_ERR_NULL, cds_encode(NULL, &valid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CDS_ERR_NULL, cds_encode(&t, &valid, NULL, sizeof(buf), &written)); + ASSERT_EQ_INT(CDS_ERR_NULL, cds_encode(&t, &valid, buf, sizeof(buf), NULL)); + ASSERT_EQ_MEM(untouched, buf, sizeof(buf)); + ASSERT_TRUE(written == 0x5A5Au); + + ASSERT_EQ_INT(CDS_ERR_NULL, cds_decode(pfield_only, 1, NULL, &time, &len)); + ASSERT_EQ_INT(CDS_ERR_NULL, cds_decode(pfield_only, 1, &fmt, NULL, &len)); + return 0; +} + +/* Buffer-size boundaries. Exact-size heap allocations make a one-octet overrun or + * over-read visible to ASan instead of landing in slack space, and the sizes are derived + * from cds_size()/cds_tfield_size() rather than hard-coded. */ +static int test_buffer_size_boundaries(void) +{ + cds_format_t fmt = {CDS_EPOCH_CCSDS, CDS_DAY_16BIT, CDS_SUBMS_US}; + cds_time_t in = {20000u, 45296789u, 123u}; + size_t need = cds_size(&fmt); + size_t t_need = cds_tfield_size(&fmt); + ASSERT_EQ_INT(9, (int)need); + ASSERT_EQ_INT(8, (int)t_need); + + /* Exactly the required size succeeds. */ + uint8_t *exact = malloc(need); + size_t written = 0; + ASSERT_TRUE(exact); + ASSERT_EQ_INT(CDS_OK, cds_encode(&in, &fmt, exact, need, &written)); + ASSERT_TRUE(written == need); + + /* Every length shorter than the requirement is rejected with nothing written. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *shortbuf = malloc(alloc); + uint8_t *ref = malloc(alloc); + ASSERT_TRUE(shortbuf); + ASSERT_TRUE(ref); + memset(shortbuf, 0x5A, alloc); + memset(ref, 0x5A, alloc); + size_t w = 0x5A5Au; + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_encode(&in, &fmt, shortbuf, len, &w)); + ASSERT_EQ_MEM(ref, shortbuf, alloc); + ASSERT_TRUE(w == 0x5A5Au); + free(shortbuf); + free(ref); + } + + /* Decoding the exact-size code succeeds and consumes all of it. */ + cds_format_t out_fmt; + cds_time_t out; + size_t consumed = 0; + ASSERT_EQ_INT(CDS_OK, cds_decode(exact, need, &out_fmt, &out, &consumed)); + ASSERT_TRUE(consumed == need); + + /* Every truncation of that code is rejected; the input buffer is sized to the + * truncated length so any over-read is caught. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *truncated = malloc(alloc); + ASSERT_TRUE(truncated); + memcpy(truncated, exact, len); + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_decode(truncated, len, &out_fmt, &out, &consumed)); + free(truncated); + } + + /* The same boundary on the T-field codec, which carries its own length check. */ + uint8_t *t_exact = malloc(t_need); + uint8_t *t_short = malloc(t_need - 1u); + ASSERT_TRUE(t_exact); + ASSERT_TRUE(t_short); + written = 0; + ASSERT_EQ_INT(CDS_OK, cds_tfield_encode(&in, &fmt, t_exact, t_need, &written)); + ASSERT_TRUE(written == t_need); + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_tfield_encode(&in, &fmt, t_short, t_need - 1u, &written)); + consumed = 0; + ASSERT_EQ_INT(CDS_OK, cds_tfield_decode(t_exact, t_need, &fmt, &out, &consumed)); + ASSERT_TRUE(consumed == t_need); + memcpy(t_short, t_exact, t_need - 1u); + ASSERT_EQ_INT(CDS_ERR_BUFFER, cds_tfield_decode(t_short, t_need - 1u, &fmt, &out, &consumed)); + free(t_exact); + free(t_short); + free(exact); + return 0; +} + test_result_t test_cds_run_all(void) { RUN_TEST(test_pfield_microsecond); @@ -291,6 +420,8 @@ test_result_t test_cds_run_all(void) RUN_TEST(test_tfield_encode_errors); RUN_TEST(test_tfield_decode_errors); RUN_TEST(test_encode_decode_errors); + RUN_TEST(test_outputs_untouched_on_failure); + RUN_TEST(test_buffer_size_boundaries); test_result_t r; r.total = cunit_total_tests; diff --git a/tests/test_cuc.c b/tests/test_cuc.c index 9c8d73f..e41327e 100644 --- a/tests/test_cuc.c +++ b/tests/test_cuc.c @@ -5,6 +5,9 @@ * Exercises the P-field / T-field codecs against the encodings defined in * CCSDS 301.0-B-4 (Time Code Formats), Section 3.2. * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * * OpenSpaceCode — https://github.com/OpenSpaceCode */ @@ -13,6 +16,8 @@ #include "test_runners.h" #include +#include +#include /* A common configuration: 4 basic octets + 2 fractional octets, CCSDS epoch. * P-field octet 1 = ext(0) id(001) basic-1(011) frac(10) = 0001 1110 = 0x1E. */ @@ -379,6 +384,133 @@ static int test_seconds_conversion_unrepresentable(void) } #endif +/* Validate First, Write After: a rejected call leaves every output exactly as the caller + * passed it, so a partially decoded format or a half-written code never reaches the caller. + * The 0x5A fill makes any stray write visible. */ +static int test_outputs_untouched_on_failure(void) +{ + cuc_format_t fmt = {(cuc_epoch_t)0x5A, 0x5Au, 0x5Au}; + cuc_time_t time = {0x5A5A5A5A5A5A5A5Au, 0xA5A5A5A5A5A5A5A5u}; + size_t len = 0x5A5Au; + + /* Extension flag set in octet 1, but octet 2 is missing. */ + uint8_t truncated[1] = {0x90}; + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_pfield_decode(truncated, 1, &fmt, &len)); + /* Octet 2 requests a third octet, which this library does not define. */ + uint8_t third_octet[2] = {0x90, 0x80}; + ASSERT_EQ_INT(CUC_ERR_UNSUPPORTED, cuc_pfield_decode(third_octet, 2, &fmt, &len)); + ASSERT_EQ_INT(0x5A, (int)fmt.epoch); + ASSERT_EQ_INT(0x5A, fmt.basic_octets); + ASSERT_EQ_INT(0x5A, fmt.fraction_octets); + ASSERT_TRUE(len == 0x5A5Au); + + /* Valid P-field, but the T-field it announces is not present. */ + uint8_t pfield_only[1] = {0x1E}; + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_decode(pfield_only, 1, &fmt, &time, &len)); + ASSERT_EQ_INT(0x5A, fmt.basic_octets); + ASSERT_TRUE(time.seconds == 0x5A5A5A5A5A5A5A5Au); + ASSERT_TRUE(time.fraction == 0xA5A5A5A5A5A5A5A5u); + ASSERT_TRUE(len == 0x5A5Au); + + /* Encode with room for the P-field but not the whole code: no octet is written. */ + cuc_format_t valid = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_format_t invalid = {CUC_EPOCH_CCSDS, CUC_BASIC_OCTETS_MAX + 1, 0}; + cuc_time_t t = {1u, 0u}; + uint8_t buf[CUC_OCTETS_MAX]; + uint8_t untouched[CUC_OCTETS_MAX]; + memset(buf, 0x5A, sizeof(buf)); + memset(untouched, 0x5A, sizeof(untouched)); + size_t written = 0x5A5Au; + + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_encode(&t, &valid, buf, 2, &written)); + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_encode(&t, &invalid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_encode(NULL, &valid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_encode(&t, &valid, NULL, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_encode(&t, &valid, buf, sizeof(buf), NULL)); + ASSERT_EQ_MEM(untouched, buf, sizeof(buf)); + ASSERT_TRUE(written == 0x5A5Au); + + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_decode(pfield_only, 1, NULL, &time, &len)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_decode(pfield_only, 1, &fmt, NULL, &len)); + return 0; +} + +/* Buffer-size boundaries. Exact-size heap allocations make a one-octet overrun or + * over-read visible to ASan instead of landing in slack space, and the sizes are derived + * from cuc_size()/cuc_tfield_size() rather than hard-coded. */ +static int test_buffer_size_boundaries(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t in = {0x12345678u, UINT64_C(1) << 62}; + size_t need = cuc_size(&fmt); + size_t t_need = cuc_tfield_size(&fmt); + ASSERT_EQ_INT(7, (int)need); + ASSERT_EQ_INT(6, (int)t_need); + + /* Exactly the required size succeeds. */ + uint8_t *exact = malloc(need); + size_t written = 0; + ASSERT_TRUE(exact); + ASSERT_EQ_INT(CUC_OK, cuc_encode(&in, &fmt, exact, need, &written)); + ASSERT_TRUE(written == need); + + /* Every length shorter than the requirement is rejected with nothing written. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *shortbuf = malloc(alloc); + uint8_t *ref = malloc(alloc); + ASSERT_TRUE(shortbuf); + ASSERT_TRUE(ref); + memset(shortbuf, 0x5A, alloc); + memset(ref, 0x5A, alloc); + size_t w = 0x5A5Au; + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_encode(&in, &fmt, shortbuf, len, &w)); + ASSERT_EQ_MEM(ref, shortbuf, alloc); + ASSERT_TRUE(w == 0x5A5Au); + free(shortbuf); + free(ref); + } + + /* Decoding the exact-size code succeeds and consumes all of it. */ + cuc_format_t out_fmt; + cuc_time_t out; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_decode(exact, need, &out_fmt, &out, &consumed)); + ASSERT_TRUE(consumed == need); + + /* Every truncation of that code is rejected; the input buffer is sized to the + * truncated length so any over-read is caught. */ + for (size_t len = 0; len < need; len++) + { + size_t alloc = (len > 0u) ? len : 1u; + uint8_t *truncated = malloc(alloc); + ASSERT_TRUE(truncated); + memcpy(truncated, exact, len); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_decode(truncated, len, &out_fmt, &out, &consumed)); + free(truncated); + } + + /* The same boundary on the T-field codec, which carries its own length check. */ + uint8_t *t_exact = malloc(t_need); + uint8_t *t_short = malloc(t_need - 1u); + ASSERT_TRUE(t_exact); + ASSERT_TRUE(t_short); + written = 0; + ASSERT_EQ_INT(CUC_OK, cuc_tfield_encode(&in, &fmt, t_exact, t_need, &written)); + ASSERT_TRUE(written == t_need); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_tfield_encode(&in, &fmt, t_short, t_need - 1u, &written)); + consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_tfield_decode(t_exact, t_need, &fmt, &out, &consumed)); + ASSERT_TRUE(consumed == t_need); + memcpy(t_short, t_exact, t_need - 1u); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_tfield_decode(t_short, t_need - 1u, &fmt, &out, &consumed)); + free(t_exact); + free(t_short); + free(exact); + return 0; +} + test_result_t test_cuc_run_all(void) { RUN_TEST(test_pfield_single_octet); @@ -394,6 +526,8 @@ test_result_t test_cuc_run_all(void) RUN_TEST(test_tfield_decode_errors); RUN_TEST(test_wide_fraction_roundtrip); RUN_TEST(test_encode_decode_errors); + RUN_TEST(test_outputs_untouched_on_failure); + RUN_TEST(test_buffer_size_boundaries); RUN_TEST(test_redundant_pfield_consumes_more_than_size); #ifndef CUC_NO_FLOAT RUN_TEST(test_seconds_conversion); diff --git a/tests/test_runners.h b/tests/test_runners.h index 5af3f9a..e3de965 100644 --- a/tests/test_runners.h +++ b/tests/test_runners.h @@ -1,3 +1,7 @@ +/* Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + */ + #ifndef TEST_RUNNERS_H #define TEST_RUNNERS_H diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 488dbf7..d172a19 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,3 +1,7 @@ +/* Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + */ + #include "test_runners.h" #include diff --git a/tools/coverage-html.sh b/tools/coverage-html.sh index f38fabd..80c71f7 100644 --- a/tools/coverage-html.sh +++ b/tools/coverage-html.sh @@ -36,6 +36,8 @@ gcovr -r "${ROOT_DIR}" \ --output "${OUT_FILE}" \ --txt - \ --txt-summary \ + --fail-under-line 100 \ + --fail-under-branch 100 \ 2> >(grep -v '^(INFO)' >&2) echo "Coverage HTML report written to: ${OUT_FILE}" \ No newline at end of file