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..926df00 --- /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 library, example and run unit tests + run: make CC=${{ matrix.cc }} + - name: Run example + run: ./build/examples/spacepacket_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 example under ASan and UBSan + run: make sanitize diff --git a/.gitignore b/.gitignore index aee5896..27604c5 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,6 @@ dkms.conf # project build directories build/ -.github/ .vscode/ .idea/ diff --git a/Makefile b/Makefile index 0fdde65..9cc2031 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,51 @@ CC ?= cc -# Stronger warnings for code quality -CFLAGS ?= -O2 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow \ +# Optimisation / instrumentation flags; overridden 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=c99 AR ?= ar -PREFIX ?= /usr/local - LIBNAME = libspacepacket.a BUILD_DIR = build LIB_PATH = $(BUILD_DIR)/$(LIBNAME) OBJ_PATH = $(BUILD_DIR)/src/space_packet.o EXAMPLE_PATH = $(BUILD_DIR)/examples/spacepacket_example CTEST_PATH = $(BUILD_DIR)/tests/ctest -COVERAGE_MIN ?= 95 +SANITIZE_DIR = $(BUILD_DIR)/sanitize + +PUBLIC_HEADERS = include/space_packet.h +TEST_SOURCES = tests/unit_tests.c tests/test_space_packet.c +TEST_HEADERS = tests/cunit.h tests/test_runners.h all: lib example test lib: $(LIB_PATH) -$(OBJ_PATH): src/space_packet.c +$(OBJ_PATH): src/space_packet.c $(PUBLIC_HEADERS) mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c src/space_packet.c -o $(OBJ_PATH) + $(CC) $(CFLAGS) -Iinclude -c src/space_packet.c -o $(OBJ_PATH) $(LIB_PATH): $(OBJ_PATH) mkdir -p $(dir $@) $(AR) rcs $(LIB_PATH) $(OBJ_PATH) example: $(EXAMPLE_PATH) + ./$(EXAMPLE_PATH) -$(EXAMPLE_PATH): lib examples/main.c +$(EXAMPLE_PATH): $(LIB_PATH) examples/main.c $(PUBLIC_HEADERS) mkdir -p $(dir $@) $(CC) $(CFLAGS) -Iinclude examples/main.c $(LIB_PATH) -o $(EXAMPLE_PATH) - ctest: $(CTEST_PATH) -$(CTEST_PATH): lib tests/unit_tests.c tests/test_space_packet.c +$(CTEST_PATH): $(LIB_PATH) $(TEST_SOURCES) $(TEST_HEADERS) $(PUBLIC_HEADERS) mkdir -p $(dir $@) - $(CC) $(CFLAGS) -Iinclude tests/unit_tests.c tests/test_space_packet.c $(LIB_PATH) -o $(CTEST_PATH) + $(CC) $(CFLAGS) -Iinclude $(TEST_SOURCES) $(LIB_PATH) -o $(CTEST_PATH) test: ctest ./$(CTEST_PATH) @@ -47,7 +53,27 @@ test: ctest coverage-html: bash tools/coverage_html.sh +# Rebuild everything with ASan + UBSan and run the unit tests and the example. +# 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 example ctest OPT="$(SANITIZE_OPT)" >/dev/null \ + || { $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @mkdir -p $(SANITIZE_DIR) + @echo "Sanitizers (ASan + UBSan):" + @./$(CTEST_PATH) >$(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; } + @./$(EXAMPLE_PATH) >$(SANITIZE_DIR)/example.log \ + && echo " library via example : no errors detected" \ + || { cat $(SANITIZE_DIR)/example.log; echo " library via example : FAILED"; \ + $(MAKE) --no-print-directory clean >/dev/null; exit 1; } + @$(MAKE) --no-print-directory clean >/dev/null + @echo "Result: PASS" + clean: rm -rf $(BUILD_DIR) -.PHONY: all lib example test ctest coverage-html ci clean install +.PHONY: all lib example test ctest coverage-html sanitize clean diff --git a/README.md b/README.md index d031389..387cb1e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ the source end user"). - **Primary Header serializer / parser** — big-endian, bit-exact per CCSDS §4.1.3 - **Sequence Flags** — wire values match the standard (`UNSEGMENTED = 0b11 = 3`) -- **Version enforcement** — always serializes Packet Version Number as `000` (§4.1.3.2) +- **Version enforcement** — always serializes Packet Version Number as `000` and rejects any other value on parse (§4.1.3.2) - **Zero allocation** — no dynamic memory inside the library; caller supplies all buffers - **Minimal footprint** — single header + single source file, no external dependencies - **Pure C99** — no OS primitives, suitable for bare-metal targets @@ -62,6 +62,9 @@ appended to the Packet Data Field before calling `sp_packet_serialize`. See ``` EmbeddedSpacePacket/ +├── .github/ +│ └── workflows/ +│ └── ci.yml # CI: unit tests, coverage gate, sanitizers ├── include/ │ └── space_packet.h # Public API and types ├── src/ @@ -76,7 +79,6 @@ EmbeddedSpacePacket/ ├── tools/ │ └── coverage_html.sh # gcovr HTML coverage report ├── build/ # Build artifacts (git-ignored) -├── docs/ # CCSDS reference PDFs ├── Makefile └── README.md ``` @@ -90,6 +92,9 @@ make example # example binary → build/examples/spacepacket_example make test # build and run tests ``` +Warnings are treated as errors (`-Werror`). The build is checked with both `gcc` and +`clang`; select the compiler with `make CC=clang`. + ### Coverage (requires `gcovr`) ```bash @@ -97,21 +102,75 @@ sudo apt install gcovr make coverage-html # → build/coverage/index.html ``` +### Sanitizers (ASan + UBSan) + +```bash +make sanitize # rebuild with ASan + UBSan, run tests and example, then clean up +``` + ### Clean ```bash make clean ``` +### Continuous Integration + +Every push to `main` and every pull request runs three jobs +([.github/workflows/ci.yml](.github/workflows/ci.yml)): + +| Job | What it checks | +| ------------ | ------------------------------------------------------------------- | +| Unit tests | `make` with `gcc` and with `clang`, then the example binary | +| Coverage | `make coverage-html`, then requires 100% line and branch coverage | +| ASan + UBSan | `make sanitize` | + ## Quick Start -Look at the example/. +Minimal encode / decode round trip, condensed from [examples/main.c](examples/main.c): + +```c +#include "space_packet.h" + +const uint8_t payload[] = {'H', 'e', 'l', 'l', 'o', ' ', 'S', 'P'}; + +/* Encode */ +sp_packet_t pkt; +sp_packet_init(&pkt); +sp_set_primary_header(&pkt, + SP_PACKET_TYPE_TM, + 0, /* no secondary header */ + 0x100, /* APID */ + SP_SEQ_FLAG_UNSEGMENTED, + 1 /* sequence count */); +sp_set_data(&pkt, payload, sizeof(payload)); + +uint8_t buf[256]; +uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); +if (n == 0) +{ + /* NULL args, empty data, or buffer too small */ +} + +/* Decode */ +sp_packet_t parsed; +if (!sp_packet_parse(&parsed, buf, n)) +{ + /* non-zero version, or buffer shorter than declared data length */ +} +/* parsed.ph.apid, parsed.ph.seq_count, parsed.data, parsed.data_len + * parsed.data points into buf (zero-copy) — keep buf alive. */ +``` + +The full example in [examples/main.c](examples/main.c) additionally shows a +mission-defined secondary header and an application-level CRC-16-CCITT +(`make example`). ## Memory Footprint (estimated, 64-bit host) | Item | Size | | ----------------------- | -------------------- | -| `sp_packet_t` struct | ~28 bytes | +| `sp_packet_t` struct | 24 bytes | | Library code (stripped) | < 1 KB | | Serialization buffer | 6 + `data_len` bytes | | Heap usage | none | diff --git a/docs/130x3g1.pdf b/docs/130x3g1.pdf deleted file mode 100644 index d506d45..0000000 Binary files a/docs/130x3g1.pdf and /dev/null differ diff --git a/docs/133x0b2e2.pdf b/docs/133x0b2e2.pdf deleted file mode 100644 index ebb0c1a..0000000 Binary files a/docs/133x0b2e2.pdf and /dev/null differ diff --git a/examples/main.c b/examples/main.c index 4e75209..058d664 100644 --- a/examples/main.c +++ b/examples/main.c @@ -1,4 +1,4 @@ -#include "../include/space_packet.h" +#include "space_packet.h" #include #include @@ -16,7 +16,9 @@ static uint16_t crc16_ccitt(const uint8_t *data, size_t len) { crc ^= (uint16_t)((uint16_t)data[i] << 8); for (int k = 0; k < 8; ++k) - crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1); + { + crc = (uint16_t)(((unsigned)crc << 1) ^ ((crc & 0x8000u) ? 0x1021u : 0u)); + } } return crc; } @@ -53,19 +55,21 @@ int main(void) 0x100, /* APID */ SP_SEQ_FLAG_UNSEGMENTED, 1 /* sequence count */); - sp_set_data(&pkt, pkt_data, (uint16_t)off); + sp_set_data(&pkt, pkt_data, (uint32_t)off); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) { printf("serialize failed\n"); return 1; } - printf("Serialized %zu bytes:\n", n); + printf("Serialized %lu bytes:\n", (unsigned long)n); for (size_t i = 0; i < n; ++i) + { printf("%02X ", buf[i]); + } printf("\n"); sp_packet_t parsed; @@ -82,7 +86,8 @@ int main(void) return 3; } size_t crc_area = parsed.data_len - 2; - uint16_t crc_recv = ((uint16_t)parsed.data[crc_area] << 8) | parsed.data[crc_area + 1]; + uint16_t crc_recv = + (uint16_t)(((unsigned)parsed.data[crc_area] << 8) | parsed.data[crc_area + 1]); uint16_t crc_calc = crc16_ccitt(parsed.data, crc_area); if (crc_recv != crc_calc) { @@ -95,10 +100,10 @@ int main(void) const uint8_t *pay = parsed.data + sec_total; size_t pay_len = crc_area - sec_total; - printf("APID=0x%03X seq_count=%u data_len=%u CRC OK\n", + printf("APID=0x%03X seq_count=%u data_len=%lu CRC OK\n", parsed.ph.apid, parsed.ph.seq_count, - parsed.data_len); + (unsigned long)parsed.data_len); printf("Payload: "); fwrite(pay, 1, pay_len, stdout); printf("\n"); diff --git a/include/space_packet.h b/include/space_packet.h index 9cce603..5040185 100644 --- a/include/space_packet.h +++ b/include/space_packet.h @@ -16,6 +16,20 @@ extern "C" { #endif +/** @brief Primary Header length in bytes (CCSDS 133.0-B-2 §4.1.3.1). */ +#define SP_PRIMARY_HEADER_LEN 6U + +/** + * @brief Maximum Packet Data Field length in bytes (CCSDS 133.0-B-2 §4.1.3.5.3). + * + * The 16-bit length count is C = (Total Number of Octets in the Packet Data Field) – 1, + * so C = 0xFFFF denotes 65536 octets. + */ +#define SP_PDF_MAX_LEN 65536UL + +/** @brief Packet Version Number of a Space Packet (CCSDS 133.0-B-2 §4.1.3.2). */ +#define SP_PACKET_VERSION 0U + /** * @brief Sequence Flags wire encoding (CCSDS 133.0-B-2 §4.1.3.4.2). * @@ -39,16 +53,18 @@ typedef enum * @brief Decoded CCSDS Space Packet primary header fields (CCSDS 133.0-B-2 §4.1.3). * * @note Do not serialise this struct directly; use sp_packet_serialize(). + * @note Bit-fields are plain @c unsigned: enum-typed bit-fields are implementation-defined in C99. + * @p type and @p seq_flags hold ::sp_packet_type_t and ::sp_seq_flag_t values. */ typedef struct { - unsigned version : 3; /**< Packet Version Number — always 0 on transmit (§4.1.3.2). */ - sp_packet_type_t type : 1; /**< Packet Type (see ::sp_packet_type_t). */ - unsigned sec_hdr_flag : 1; /**< Secondary Header Flag: 1 if a secondary header is present. */ - unsigned apid : 11; /**< Application Process Identifier (11 bits). */ - sp_seq_flag_t seq_flags : 2; /**< Sequence Flags (see ::sp_seq_flag_t). */ - unsigned seq_count : 14; /**< Packet Sequence Count, modulo-16384 per APID (§4.1.3.4.3). */ - uint16_t packet_length; /**< Raw Packet Data Length field: (data octets) − 1 (§4.1.3.5). */ + unsigned version : 3; /**< Packet Version Number — always 0 (§4.1.3.2). */ + unsigned type : 1; /**< Packet Type (see ::sp_packet_type_t). */ + unsigned sec_hdr_flag : 1; /**< Secondary Header Flag: 1 if a secondary header is present. */ + unsigned apid : 11; /**< Application Process Identifier (11 bits). */ + unsigned seq_flags : 2; /**< Sequence Flags (see ::sp_seq_flag_t). */ + unsigned seq_count : 14; /**< Packet Sequence Count, modulo-16384 per APID (§4.1.3.4.3). */ + uint16_t packet_length; /**< Raw Packet Data Length field: (data octets) − 1 (§4.1.3.5). */ } sp_primary_header_t; /** @@ -61,7 +77,7 @@ typedef struct sp_primary_header_t ph; /**< Decoded primary header fields. */ const uint8_t *data; /**< Packet Data Field — mission-defined layout (secondary header + user data). */ - uint16_t data_len; /**< Packet Data Field length in bytes. */ + uint32_t data_len; /**< Packet Data Field length in bytes (1 to ::SP_PDF_MAX_LEN). */ } sp_packet_t; /** @@ -96,17 +112,18 @@ void sp_set_primary_header(sp_packet_t *pkt, * * @param[out] pkt Target packet. No-op if NULL. * @param[in] data Packet Data Field (not copied; caller keeps memory alive). - * @param[in] data_len Length of @p data in bytes. + * @param[in] data_len Length of @p data in bytes (1 to ::SP_PDF_MAX_LEN). */ -void sp_set_data(sp_packet_t *pkt, const uint8_t *data, uint16_t data_len); +void sp_set_data(sp_packet_t *pkt, const uint8_t *data, uint32_t data_len); /** * @brief Return the serialised size of a packet. * * @param[in] pkt Packet to measure. - * @return 6 + data_len, or 0 if @p pkt is NULL or data_len is 0. + * @return ::SP_PRIMARY_HEADER_LEN + data_len, or 0 if @p pkt is NULL or data_len is outside + * 1 to ::SP_PDF_MAX_LEN. */ -size_t sp_packet_serialize_size(const sp_packet_t *pkt); +uint32_t sp_packet_serialize_size(const sp_packet_t *pkt); /** * @brief Serialise a Space Packet into a caller-supplied buffer. @@ -114,22 +131,24 @@ size_t sp_packet_serialize_size(const sp_packet_t *pkt); * @param[in] pkt Packet to serialise. * @param[out] buf Output buffer. * @param[in] buf_len Buffer capacity in bytes. - * @return Bytes written, or 0 on error (NULL args, empty data, or buffer too small). + * @return Bytes written, or 0 on error (NULL args, data length outside 1 to ::SP_PDF_MAX_LEN, + * or buffer too small). */ -size_t sp_packet_serialize(const sp_packet_t *pkt, uint8_t *buf, size_t buf_len); +uint32_t sp_packet_serialize(const sp_packet_t *pkt, uint8_t *buf, uint32_t buf_len); /** * @brief Parse a wire-format Space Packet. * * On success, @p out->data points into @p buf (zero-copy). Keep @p buf alive - * as long as the parsed packet is in use. + * as long as the parsed packet is in use. On failure, @p out is left unchanged. * * @param[out] out Decoded packet. * @param[in] buf Wire buffer to parse. * @param[in] buf_len Buffer length in bytes. - * @return 1 on success, 0 on failure (NULL args or buffer shorter than declared data length). + * @return 1 on success, 0 on failure (NULL args, non-zero Packet Version Number, or buffer + * shorter than declared data length). */ -int sp_packet_parse(sp_packet_t *out, const uint8_t *buf, size_t buf_len); +int sp_packet_parse(sp_packet_t *out, const uint8_t *buf, uint32_t buf_len); #ifdef __cplusplus } diff --git a/src/space_packet.c b/src/space_packet.c index b339d43..1287bde 100644 --- a/src/space_packet.c +++ b/src/space_packet.c @@ -2,14 +2,16 @@ * @file space_packet.c * @brief CCSDS Space Packet Protocol — serialiser, parser and header helpers. */ -#include "../include/space_packet.h" +#include "space_packet.h" #include void sp_packet_init(sp_packet_t *pkt) { if (!pkt) + { return; + } memset(pkt, 0, sizeof(*pkt)); } @@ -21,45 +23,59 @@ void sp_set_primary_header(sp_packet_t *pkt, uint16_t seq_count) { if (!pkt) + { return; - pkt->ph.version = 0; - pkt->ph.type = (sp_packet_type_t)((unsigned)(type) & 0x1u); + } + pkt->ph.version = SP_PACKET_VERSION; + pkt->ph.type = (unsigned)(type) & 0x1u; pkt->ph.sec_hdr_flag = (unsigned)(sec_hdr_flag ? 1u : 0u); pkt->ph.apid = (unsigned)(apid & 0x07FFu); - pkt->ph.seq_flags = (sp_seq_flag_t)((unsigned)(seq_flags) & 0x3u); + pkt->ph.seq_flags = (unsigned)(seq_flags) & 0x3u; pkt->ph.seq_count = (unsigned)(seq_count & 0x3FFFu); } -void sp_set_data(sp_packet_t *pkt, const uint8_t *data, uint16_t data_len) +void sp_set_data(sp_packet_t *pkt, const uint8_t *data, uint32_t data_len) { if (!pkt) + { return; + } pkt->data = data; pkt->data_len = data_len; } -size_t sp_packet_serialize_size(const sp_packet_t *pkt) +uint32_t sp_packet_serialize_size(const sp_packet_t *pkt) { - if (!pkt || pkt->data_len == 0) + if ((!pkt) || (pkt->data_len == 0) || (pkt->data_len > SP_PDF_MAX_LEN)) + { return 0; - return (size_t)6 + pkt->data_len; + } + return (uint32_t)SP_PRIMARY_HEADER_LEN + pkt->data_len; } -size_t sp_packet_serialize(const sp_packet_t *pkt, uint8_t *buf, size_t buf_len) +uint32_t sp_packet_serialize(const sp_packet_t *pkt, uint8_t *buf, uint32_t buf_len) { - if (!pkt || !buf || !pkt->data || pkt->data_len == 0) + if ((!pkt) || (!buf) || (!pkt->data)) + { return 0; + } + if ((pkt->data_len == 0) || (pkt->data_len > SP_PDF_MAX_LEN)) + { + return 0; + } - const size_t need = (size_t)6 + pkt->data_len; + const uint32_t need = (uint32_t)SP_PRIMARY_HEADER_LEN + pkt->data_len; if (buf_len < need) + { return 0; + } /* Version bits are always 000 (CCSDS 133.0-B-2 §4.1.3.2). */ const uint16_t first = (uint16_t)(((pkt->ph.type & 0x1u) << 12) | ((pkt->ph.sec_hdr_flag & 0x1u) << 11) | (pkt->ph.apid & 0x07FFu)); const uint16_t second = - (uint16_t)(((unsigned)(pkt->ph.seq_flags & 0x3u) << 14) | (pkt->ph.seq_count & 0x3FFFu)); + (uint16_t)(((pkt->ph.seq_flags & 0x3u) << 14) | (pkt->ph.seq_count & 0x3FFFu)); const uint16_t length = (uint16_t)(pkt->data_len - 1u); buf[0] = (uint8_t)(first >> 8); @@ -69,36 +85,49 @@ size_t sp_packet_serialize(const sp_packet_t *pkt, uint8_t *buf, size_t buf_len) buf[4] = (uint8_t)(length >> 8); buf[5] = (uint8_t)(length & 0xFFu); - memcpy(&buf[6], pkt->data, pkt->data_len); + memcpy(&buf[SP_PRIMARY_HEADER_LEN], pkt->data, pkt->data_len); return need; } -int sp_packet_parse(sp_packet_t *out, const uint8_t *buf, size_t buf_len) +int sp_packet_parse(sp_packet_t *out, const uint8_t *buf, uint32_t buf_len) { if (!out || !buf) + { return 0; - if (buf_len < 6) + } + if (buf_len < SP_PRIMARY_HEADER_LEN) + { return 0; + } - const uint16_t first = ((uint16_t)buf[0] << 8) | buf[1]; - const uint16_t second = ((uint16_t)buf[2] << 8) | buf[3]; - const uint16_t length_field = ((uint16_t)buf[4] << 8) | buf[5]; + const uint16_t first = (uint16_t)(((unsigned)buf[0] << 8) | buf[1]); + const uint16_t second = (uint16_t)(((unsigned)buf[2] << 8) | buf[3]); + const uint16_t length_field = (uint16_t)(((unsigned)buf[4] << 8) | buf[5]); - out->ph.version = (unsigned)((first >> 13) & 0x7u); - out->ph.type = (sp_packet_type_t)((first >> 12) & 0x1u); - out->ph.sec_hdr_flag = (unsigned)((first >> 11) & 0x1u); - out->ph.apid = (unsigned)(first & 0x07FFu); - out->ph.seq_flags = (sp_seq_flag_t)((second >> 14) & 0x3u); - out->ph.seq_count = (unsigned)(second & 0x3FFFu); - out->ph.packet_length = length_field; + /* Only version 000 identifies a Space Packet (CCSDS 133.0-B-2 §4.1.3.2). */ + if (((first >> 13) & 0x7u) != SP_PACKET_VERSION) + { + return 0; + } - const uint16_t data_len = (uint16_t)(length_field + 1u); + /* 32-bit: a length count of 0xFFFF denotes 65536 octets (CCSDS 133.0-B-2 §4.1.3.5.3). */ + const uint32_t data_len = (uint32_t)length_field + 1u; - if (buf_len < (size_t)6 + data_len) + if (buf_len < ((uint32_t)SP_PRIMARY_HEADER_LEN + data_len)) + { return 0; + } - out->data = &buf[6]; + /* All checks passed: out is written only from here on, so a failed parse leaves it intact. */ + out->ph.version = SP_PACKET_VERSION; + out->ph.type = (unsigned)((first >> 12) & 0x1u); + out->ph.sec_hdr_flag = (unsigned)((first >> 11) & 0x1u); + out->ph.apid = (unsigned)(first & 0x07FFu); + out->ph.seq_flags = (unsigned)((second >> 14) & 0x3u); + out->ph.seq_count = (unsigned)(second & 0x3FFFu); + out->ph.packet_length = length_field; + out->data = &buf[SP_PRIMARY_HEADER_LEN]; out->data_len = data_len; return 1; diff --git a/tests/cunit.h b/tests/cunit.h index 76fcac3..ca2aba1 100644 --- a/tests/cunit.h +++ b/tests/cunit.h @@ -52,7 +52,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_space_packet.c b/tests/test_space_packet.c index fecf535..27afce0 100644 --- a/tests/test_space_packet.c +++ b/tests/test_space_packet.c @@ -1,5 +1,5 @@ -#include "../include/space_packet.h" #include "cunit.h" +#include "space_packet.h" #include "test_runners.h" #include @@ -13,11 +13,13 @@ static int test_roundtrip_basic(void) pkt.ph.seq_count = 0x2; sp_set_data(&pkt, data, sizeof(data)); - size_t buf_len = sp_packet_serialize_size(&pkt); + uint32_t buf_len = sp_packet_serialize_size(&pkt); uint8_t *buf = (uint8_t *)malloc(buf_len); if (!buf) + { return 1; - size_t n = sp_packet_serialize(&pkt, buf, buf_len); + } + uint32_t n = sp_packet_serialize(&pkt, buf, buf_len); if (n == 0) { free(buf); @@ -55,11 +57,13 @@ static int test_roundtrip_with_secheader_flag(void) pkt.ph.seq_count = 0x3; sp_set_data(&pkt, data, sizeof(data)); - size_t buf_len = sp_packet_serialize_size(&pkt); + uint32_t buf_len = sp_packet_serialize_size(&pkt); uint8_t *buf = (uint8_t *)malloc(buf_len); if (!buf) + { return 1; - size_t n = sp_packet_serialize(&pkt, buf, buf_len); + } + uint32_t n = sp_packet_serialize(&pkt, buf, buf_len); if (n == 0) { free(buf); @@ -102,14 +106,18 @@ static int test_highlevel_api(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) + { return 1; + } sp_packet_t parsed; int ok = sp_packet_parse(&parsed, buf, n); if (!ok) + { return 1; + } ASSERT_EQ_INT(parsed.ph.apid, 0x456); ASSERT_EQ_INT(parsed.ph.seq_count, 42); @@ -126,15 +134,25 @@ static int test_null_pointers(void) uint8_t buf[64]; if (sp_packet_serialize_size(NULL) != 0) + { return 1; + } if (sp_packet_serialize(NULL, buf, sizeof(buf)) != 0) + { return 1; + } if (sp_packet_serialize(&pkt, NULL, sizeof(buf)) != 0) + { return 1; + } if (sp_packet_parse(NULL, buf, sizeof(buf))) + { return 1; + } if (sp_packet_parse(&pkt, NULL, sizeof(buf))) + { return 1; + } sp_packet_init(NULL); sp_set_primary_header(NULL, SP_PACKET_TYPE_TM, 0, 0, SP_SEQ_FLAG_UNSEGMENTED, 0); @@ -159,19 +177,25 @@ static int test_sequence_flags(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) + { return 1; + } /* Verify wire bits directly: seq flags are bits 16-17, i.e. top 2 bits of buf[2]. */ unsigned wire_flags = (unsigned)(buf[2] >> 6) & 0x3u; if (wire_flags != (unsigned)flags[i]) + { return 1; + } sp_packet_t parsed; int ok = sp_packet_parse(&parsed, buf, n); if (!ok) + { return 1; + } ASSERT_EQ_INT(parsed.ph.seq_flags, flags[i]); } @@ -187,9 +211,11 @@ static int test_version_is_zero(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[32]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) + { return 1; + } /* Version is bits 0-2 of byte 0, i.e. the top 3 bits. */ unsigned version_bits = (unsigned)(buf[0] >> 5) & 0x7u; @@ -198,7 +224,9 @@ static int test_version_is_zero(void) sp_packet_t parsed; int ok = sp_packet_parse(&parsed, buf, n); if (!ok) + { return 1; + } ASSERT_EQ_INT(parsed.ph.version, 0); return 0; @@ -213,14 +241,18 @@ static int test_type_and_fields(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) + { return 1; + } sp_packet_t parsed; int ok = sp_packet_parse(&parsed, buf, n); if (!ok) + { return 1; + } ASSERT_EQ_INT(parsed.ph.version, 0); ASSERT_EQ_INT(parsed.ph.type, SP_PACKET_TYPE_TC); @@ -240,7 +272,7 @@ static int test_buffer_too_small(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[8]; /* too small: need 6 + 10 = 16 bytes */ - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); return (n == 0) ? 0 : 1; } @@ -252,10 +284,45 @@ static int test_empty_data(void) sp_set_data(&pkt, NULL, 0); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); return (n == 0) ? 0 : 1; /* serializer must reject empty Packet Data Field */ } +static int test_zero_length_with_data_pointer(void) +{ + sp_packet_t pkt; + sp_packet_init(&pkt); + const uint8_t data[] = {0x01}; + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, 0); + + uint8_t buf[32]; + ASSERT_EQ_INT(0, sp_packet_serialize_size(&pkt)); + ASSERT_EQ_INT(0, sp_packet_serialize(&pkt, buf, sizeof(buf))); + return 0; +} + +static int test_secondary_header_flag_on_wire(void) +{ + sp_packet_t pkt; + sp_packet_init(&pkt); + const uint8_t data[] = {0x01}; + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 1, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, sizeof(data)); + + uint8_t buf[32]; + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + ASSERT_TRUE(n != 0); + + /* Secondary Header Flag is bit 4 of the header, i.e. bit 3 of byte 0. */ + ASSERT_EQ_INT(1, (buf[0] >> 3) & 0x1u); + + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, n)); + ASSERT_EQ_INT(1, parsed.ph.sec_hdr_flag); + return 0; +} + static int test_bitfield_masking(void) { sp_packet_t pkt; @@ -267,14 +334,18 @@ static int test_bitfield_masking(void) sp_set_data(&pkt, data, sizeof(data)); uint8_t buf[256]; - size_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); if (n == 0) + { return 1; + } sp_packet_t parsed; int ok = sp_packet_parse(&parsed, buf, n); if (!ok) + { return 1; + } ASSERT_EQ_INT(parsed.ph.version, 0); /* always 0, not masked from input */ ASSERT_EQ_INT(parsed.ph.type, SP_PACKET_TYPE_TC); /* TC = 1 */ @@ -313,6 +384,273 @@ static int test_parse_data_far_too_short(void) return ok ? 1 : 0; } +static int test_parse_rejects_nonzero_version(void) +{ + /* Valid 7-byte packet except for the version bits (top 3 bits of byte 0). */ + for (unsigned version = 1; version <= 7u; version++) + { + uint8_t buf[7] = {0x08, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xAA}; + buf[0] = (uint8_t)(buf[0] | (version << 5)); + sp_packet_t parsed; + if (sp_packet_parse(&parsed, buf, sizeof(buf))) + { + return 1; + } + } + return 0; +} + +static int test_parse_max_length_field(void) +{ + /* length_field=0xFFFF → 65536 octets, the largest valid Packet Data Field (§4.1.3.5.3). + * It must never be mistaken for a zero-length packet. */ + static uint8_t buf[SP_PRIMARY_HEADER_LEN + SP_PDF_MAX_LEN] = + {0x08, 0x00, 0x00, 0x00, 0xFF, 0xFF}; + sp_packet_t parsed; + + /* Bare header and one-octet-short buffers are truncated packets. */ + ASSERT_TRUE(!sp_packet_parse(&parsed, buf, SP_PRIMARY_HEADER_LEN)); + ASSERT_TRUE(!sp_packet_parse(&parsed, buf, sizeof(buf) - 1u)); + + ASSERT_TRUE(sp_packet_parse(&parsed, buf, sizeof(buf))); + ASSERT_EQ_INT(0xFFFF, parsed.ph.packet_length); + ASSERT_TRUE(parsed.data_len == SP_PDF_MAX_LEN); + ASSERT_TRUE(parsed.data == &buf[SP_PRIMARY_HEADER_LEN]); + return 0; +} + +static int test_serialize_rejects_oversized_data(void) +{ + /* One octet above the maximum cannot be expressed in the 16-bit length field. */ + static const uint8_t data[SP_PDF_MAX_LEN + 1u] = {0}; + static uint8_t buf[SP_PRIMARY_HEADER_LEN + SP_PDF_MAX_LEN + 1u]; + + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, SP_PDF_MAX_LEN + 1u); + + ASSERT_TRUE(sp_packet_serialize_size(&pkt) == 0); + ASSERT_TRUE(sp_packet_serialize(&pkt, buf, sizeof(buf)) == 0); + return 0; +} + +static int test_roundtrip_max_length(void) +{ + static uint8_t data[SP_PDF_MAX_LEN]; + static uint8_t buf[SP_PRIMARY_HEADER_LEN + SP_PDF_MAX_LEN]; + for (size_t i = 0; i < sizeof(data); i++) + { + data[i] = (uint8_t)(i * 31u); + } + + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, SP_PDF_MAX_LEN); + + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + ASSERT_TRUE(n == sizeof(buf)); + + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, n)); + ASSERT_EQ_INT(0xFFFF, parsed.ph.packet_length); + ASSERT_TRUE(parsed.data_len == SP_PDF_MAX_LEN); + ASSERT_EQ_MEM(parsed.data, data, sizeof(data)); + return 0; +} + +static int test_parse_failure_leaves_out_untouched(void) +{ + /* Each buffer has header fields that differ from the sentinel, and fails a different check. */ + const uint8_t bad_version[] = {0x3F, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xAA}; + const uint8_t max_length_truncated[] = {0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAA}; + const uint8_t truncated[] = {0x1F, 0xFF, 0xFF, 0xFF, 0x00, 0x05, 0xAA}; + const uint8_t *const cases[] = {bad_version, max_length_truncated, truncated}; + const uint8_t sentinel_data[] = {0x11, 0x22}; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) + { + sp_packet_t out; + sp_packet_init(&out); + sp_set_primary_header(&out, SP_PACKET_TYPE_TM, 0, 0x123, SP_SEQ_FLAG_FIRST_SEGMENT, 7); + sp_set_data(&out, sentinel_data, sizeof(sentinel_data)); + out.ph.packet_length = 0x55; + + ASSERT_TRUE(!sp_packet_parse(&out, cases[i], sizeof(bad_version))); + + ASSERT_EQ_INT(0, out.ph.version); + ASSERT_EQ_INT(SP_PACKET_TYPE_TM, out.ph.type); + ASSERT_EQ_INT(0, out.ph.sec_hdr_flag); + ASSERT_EQ_INT(0x123, out.ph.apid); + ASSERT_EQ_INT(SP_SEQ_FLAG_FIRST_SEGMENT, out.ph.seq_flags); + ASSERT_EQ_INT(7, out.ph.seq_count); + ASSERT_EQ_INT(0x55, out.ph.packet_length); + ASSERT_TRUE(out.data == sentinel_data); + ASSERT_EQ_INT(sizeof(sentinel_data), out.data_len); + } + return 0; +} + +/* --- Boundary tests ------------------------------------------------------- */ + +static int test_serialize_buffer_size_boundary(void) +{ + const uint8_t data[] = {1, 2, 3, 4}; + const uint32_t need = SP_PRIMARY_HEADER_LEN + sizeof(data); + + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, sizeof(data)); + + /* Heap buffers of the exact size, so a one-byte overrun is visible to ASan. */ + uint8_t *one_short = (uint8_t *)malloc(need - 1u); + uint8_t *exact = (uint8_t *)malloc(need); + int rc = 1; + + if (one_short && exact) + { + rc = !((sp_packet_serialize(&pkt, one_short, need - 1u) == 0) && + (sp_packet_serialize(&pkt, exact, need) == need) && + (memcmp(&exact[SP_PRIMARY_HEADER_LEN], data, sizeof(data)) == 0)); + } + + free(one_short); + free(exact); + return rc; +} + +static int test_serialize_size_boundaries(void) +{ + static const uint8_t data[SP_PDF_MAX_LEN] = {0}; + sp_packet_t pkt; + sp_packet_init(&pkt); + + sp_set_data(&pkt, data, 1); + ASSERT_TRUE(sp_packet_serialize_size(&pkt) == (SP_PRIMARY_HEADER_LEN + 1u)); + + sp_set_data(&pkt, data, SP_PDF_MAX_LEN); + ASSERT_TRUE(sp_packet_serialize_size(&pkt) == (SP_PRIMARY_HEADER_LEN + SP_PDF_MAX_LEN)); + return 0; +} + +static int test_roundtrip_min_length(void) +{ + const uint8_t data[] = {0x5A}; + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x100, SP_SEQ_FLAG_UNSEGMENTED, 1); + sp_set_data(&pkt, data, sizeof(data)); + + uint8_t buf[SP_PRIMARY_HEADER_LEN + 1u]; + uint32_t n = sp_packet_serialize(&pkt, buf, sizeof(buf)); + ASSERT_TRUE(n == sizeof(buf)); + + /* One data octet → length count C = 0 (§4.1.3.5.3). */ + ASSERT_EQ_INT(0x00, buf[4]); + ASSERT_EQ_INT(0x00, buf[5]); + + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, n)); + ASSERT_EQ_INT(0, parsed.ph.packet_length); + ASSERT_EQ_INT(1, parsed.data_len); + ASSERT_EQ_INT(0x5A, parsed.data[0]); + return 0; +} + +static int test_parse_buffer_length_boundaries(void) +{ + /* Smallest valid packet: 6-byte header (length_field=0) + 1 data octet. */ + const uint8_t wire[] = {0x08, 0x00, 0xC0, 0x00, 0x00, 0x00, 0xAA}; + sp_packet_t parsed; + + /* Heap copies of the exact size, so a one-byte over-read is visible to ASan. */ + for (uint32_t len = 0; len <= sizeof(wire); len++) + { + uint8_t *buf = (uint8_t *)malloc(len ? len : 1u); + if (!buf) + { + return 1; + } + memcpy(buf, wire, len); + int ok = sp_packet_parse(&parsed, buf, len); + int data_ok = ok && (parsed.data_len == 1) && (parsed.data[0] == 0xAA); + free(buf); + + /* Header-only (6) and anything shorter must fail; only the full 7 bytes parse. */ + if (len < sizeof(wire)) + { + ASSERT_TRUE(!ok); + } + else + { + ASSERT_TRUE(data_ok); + } + } + return 0; +} + +static int test_parse_ignores_trailing_bytes(void) +{ + /* length_field=1 → data_len=2; two extra bytes follow the packet. */ + const uint8_t buf[] = {0x08, 0x00, 0xC0, 0x00, 0x00, 0x01, 0x11, 0x22, 0x33, 0x44}; + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, sizeof(buf))); + ASSERT_EQ_INT(2, parsed.data_len); + ASSERT_TRUE(parsed.data == &buf[SP_PRIMARY_HEADER_LEN]); + return 0; +} + +static int test_header_fields_all_zero(void) +{ + const uint8_t data[] = {0x00}; + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TM, 0, 0x000, SP_SEQ_FLAG_CONTINUING_SEGMENT, 0); + sp_set_data(&pkt, data, sizeof(data)); + + uint8_t buf[SP_PRIMARY_HEADER_LEN + 1u]; + ASSERT_TRUE(sp_packet_serialize(&pkt, buf, sizeof(buf)) == sizeof(buf)); + + const uint8_t expected[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + ASSERT_EQ_MEM(buf, expected, sizeof(expected)); + + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, sizeof(buf))); + ASSERT_EQ_INT(SP_PACKET_TYPE_TM, parsed.ph.type); + ASSERT_EQ_INT(0, parsed.ph.sec_hdr_flag); + ASSERT_EQ_INT(0, parsed.ph.apid); + ASSERT_EQ_INT(SP_SEQ_FLAG_CONTINUING_SEGMENT, parsed.ph.seq_flags); + ASSERT_EQ_INT(0, parsed.ph.seq_count); + return 0; +} + +static int test_header_fields_all_max(void) +{ + const uint8_t data[] = {0x00}; + sp_packet_t pkt; + sp_packet_init(&pkt); + sp_set_primary_header(&pkt, SP_PACKET_TYPE_TC, 1, 0x7FF, SP_SEQ_FLAG_UNSEGMENTED, 0x3FFF); + sp_set_data(&pkt, data, sizeof(data)); + + uint8_t buf[SP_PRIMARY_HEADER_LEN + 1u]; + ASSERT_TRUE(sp_packet_serialize(&pkt, buf, sizeof(buf)) == sizeof(buf)); + + /* Version stays 000, so the first octet is 0x1F, not 0xFF. */ + const uint8_t expected[] = {0x1F, 0xFF, 0xFF, 0xFF, 0x00, 0x00}; + ASSERT_EQ_MEM(buf, expected, sizeof(expected)); + + sp_packet_t parsed; + ASSERT_TRUE(sp_packet_parse(&parsed, buf, sizeof(buf))); + ASSERT_EQ_INT(SP_PACKET_TYPE_TC, parsed.ph.type); + ASSERT_EQ_INT(1, parsed.ph.sec_hdr_flag); + ASSERT_EQ_INT(0x7FF, parsed.ph.apid); + ASSERT_EQ_INT(SP_SEQ_FLAG_UNSEGMENTED, parsed.ph.seq_flags); + ASSERT_EQ_INT(0x3FFF, parsed.ph.seq_count); + return 0; +} + test_result_t test_space_packet_run_all(void) { RUN_TEST(test_roundtrip_basic); @@ -325,10 +663,24 @@ test_result_t test_space_packet_run_all(void) RUN_TEST(test_type_and_fields); RUN_TEST(test_buffer_too_small); RUN_TEST(test_empty_data); + RUN_TEST(test_zero_length_with_data_pointer); + RUN_TEST(test_secondary_header_flag_on_wire); RUN_TEST(test_bitfield_masking); RUN_TEST(test_parse_data_just_short); RUN_TEST(test_parse_data_truncated); RUN_TEST(test_parse_data_far_too_short); + RUN_TEST(test_parse_rejects_nonzero_version); + RUN_TEST(test_parse_max_length_field); + RUN_TEST(test_serialize_rejects_oversized_data); + RUN_TEST(test_roundtrip_max_length); + RUN_TEST(test_parse_failure_leaves_out_untouched); + RUN_TEST(test_serialize_buffer_size_boundary); + RUN_TEST(test_serialize_size_boundaries); + RUN_TEST(test_roundtrip_min_length); + RUN_TEST(test_parse_buffer_length_boundaries); + RUN_TEST(test_parse_ignores_trailing_bytes); + RUN_TEST(test_header_fields_all_zero); + RUN_TEST(test_header_fields_all_max); test_result_t r; r.total = cunit_total_tests; diff --git a/tools/coverage_html.sh b/tools/coverage_html.sh index ae62b7e..80c71f7 100644 --- a/tools/coverage_html.sh +++ b/tools/coverage_html.sh @@ -8,7 +8,9 @@ if [[ "${OUT_FILE}" != /* ]]; then OUT_FILE="${ROOT_DIR}/${OUT_FILE}" fi -COVERAGE_CFLAGS='-O0 -g --coverage -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef -std=c99' +# Instrumentation is the ONLY thing that differs from the normal build; the C standard, +# include paths and warning set all come from the Makefile so they cannot drift apart. +COVERAGE_OPT='-O0 -g --coverage' cd "${ROOT_DIR}" @@ -19,14 +21,23 @@ if ! command -v gcovr >/dev/null 2>&1; then fi make clean >/dev/null -make ctest CFLAGS="${COVERAGE_CFLAGS}" >/dev/null +make build/tests/ctest OPT="${COVERAGE_OPT}" >/dev/null ./build/tests/ctest >/dev/null mkdir -p "$(dirname "${OUT_FILE}")" + +# Emit the HTML report and a text summary (line + branch) in a single gcovr pass, so +# the console output is not duplicated. gcovr's chatty "(INFO)" progress lines are +# filtered from stderr; warnings and errors still pass through and preserve the exit code. +echo "Coverage:" gcovr -r "${ROOT_DIR}" \ --filter "${ROOT_DIR}/src" \ - --html \ --html-details \ - --output "${OUT_FILE}" - -echo "Coverage HTML report written to: ${OUT_FILE}" + --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