diff --git a/Makefile b/Makefile index b52ec12..906216c 100644 --- a/Makefile +++ b/Makefile @@ -1,24 +1,48 @@ CC ?= cc -# Minimal Makefile: only build and run the unit test binary. -CFLAGS ?= -O2 -Iinclude -Wall -Wextra -std=c11 +AR ?= ar +OPT ?= -O2 +CFLAGS ?= -std=c99 -Wall -Wextra -Iinclude BUILD_DIR = build -CTEST_PATH = $(BUILD_DIR)/tests/ctest -all: ctest +LIB = $(BUILD_DIR)/libcuc.a +OBJ = $(BUILD_DIR)/src/cuc.o +CTEST = $(BUILD_DIR)/tests/ctest +EXAMPLE = $(BUILD_DIR)/examples/cuc_example -ctest: $(CTEST_PATH) +SRC = src/cuc.c +HDR = include/cuc.h -$(CTEST_PATH): tests/unit_tests.c +all: $(LIB) $(CTEST) $(EXAMPLE) + +lib: $(LIB) + +$(OBJ): $(SRC) $(HDR) mkdir -p $(dir $@) - $(CC) $(CFLAGS) -Iinclude tests/unit_tests.c -o $(CTEST_PATH) + $(CC) $(CFLAGS) $(OPT) -Iinclude -c $(SRC) -o $@ -run: ctest - $(CTEST_PATH) +$(LIB): $(OBJ) + mkdir -p $(dir $@) + $(AR) rcs $@ $(OBJ) -clean: - rm -rf $(BUILD_DIR) +ctest: $(CTEST) + +$(CTEST): tests/unit_tests.c tests/test_cuc.c tests/cunit.h tests/test_runners.h $(SRC) $(HDR) + mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(OPT) -Iinclude -Itests tests/unit_tests.c tests/test_cuc.c $(SRC) -o $@ + +example: $(EXAMPLE) + +$(EXAMPLE): examples/cuc_example.c $(SRC) $(HDR) + mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(OPT) -Iinclude examples/cuc_example.c $(SRC) -o $@ + +run: $(CTEST) + $(CTEST) coverage-html: - bash tools/coverage_html.sh + bash tools/coverage-html.sh + +clean: + rm -rf $(BUILD_DIR) -.PHONY: all ctest run clean +.PHONY: all lib ctest example run coverage-html clean diff --git a/README.md b/README.md index 329fa00..ba05a99 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,84 @@ -# ProjectName -Project description. -Template repo for minimal embedded C implementations of CCSDS / ECSS standards. +# EmbeddedCUCTime +A minimal, embedded-friendly C library implementing the **CCSDS Unsegmented +Time Code (CUC)** — a pure binary count of seconds and binary fractions of a +second from a defined epoch. + +The library is deliberately small and dependency-free (only ``, +``, ``), uses caller-supplied buffers (no heap), and keeps +its core encode/decode path integer-only so it fits comfortably on +microcontrollers. ## Standards Compliance -- **CCSDS 000.0-X-Y**: +- **CCSDS 301.0-B-4** — *Time Code Formats*, Section 3.2 (CCSDS Unsegmented + Time Code). See [`docs/ccsds_cuc.md`](docs/ccsds_cuc.md) for a field-by-field + summary. ## Features -### Core Protocol Implementation - - -### Design Principles - +- Encode/decode the **P-field** (1 or 2 octets) and **T-field** separately, or + as a combined self-identified code. +- Full format range: 1–7 basic (second) octets, 0–10 fractional octets. +- Both CCSDS 1958 TAI epoch (Level 1) and agency-defined epoch (Level 2). +- Format-independent time value (`cuc_time_t`) using a Q0.64 binary fraction. +- Optional `double` conversion helpers, removable with `-DCUC_NO_FLOAT`. +- Explicit, checked error codes; no dynamic allocation. ## Project Structure ``` -EmbeddedSpacePacket/ +EmbeddedCUCTime/ ├── include/ -│ └── +│ └── cuc.h # Public API ├── src/ -│ └── +│ └── cuc.c # Implementation ├── examples/ -│ └── +│ └── cuc_example.c # Encode/decode demo ├── tests/ │ ├── cunit.h # Minimal test framework │ └── unit_tests.c # Unit tests -├── scripts/ -│ └── coverage_html.sh # Coverage report -├── build/ # Build artifacts +├── docs/ +│ └── ccsds_cuc.md # CUC format reference notes +├── tools/ +│ └── coverage-html.sh # Coverage report helper ├── Makefile └── README.md ``` ## Building -### Build Everything +### Build everything ```bash -make +make # static library, example and test binaries in build/ ``` -Builds the static library, the example binary and the test binary in `build/`. - -### Build Library Only +### Build the library only ```bash -make lib -# Produces: build/ +make lib # produces build/libcuc.a ``` -### Build Example +### Build and run the example ```bash make example -./build/examples/example +./build/examples/cuc_example ``` -### Run Tests +### Run the tests ```bash -make ctest -./build/tests/ctest +make run # or: make ctest && ./build/tests/ctest ``` ### Coverage (HTML) -Requires `gcovr` installed in your system: - -```bash -sudo apt install gcovr -``` - -Generate coverage report: +Requires `gcovr` (`pip install gcovr`): ```bash -make coverage-html -``` - -Output report: - -```bash -build/coverage/index.html +make coverage-html # writes build/coverage/index.html ``` ### Clean @@ -94,59 +89,108 @@ make clean ## Quick Start -### Step 1 - ```c +#include "cuc.h" -``` +/* 4 octets of seconds + 2 octets of fraction, CCSDS 1958 TAI epoch. */ +cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; +cuc_time_t t = cuc_time_from_seconds(1234567.25); -### Step 2 +uint8_t buf[CUC_OCTETS_MAX]; +size_t n = 0; -```c +if (cuc_encode(&t, &fmt, buf, sizeof(buf), &n) == CUC_OK) { + /* buf[0..n-1] holds P-field + T-field: 1E 00 12 D6 87 40 00 */ +} +cuc_format_t out_fmt; +cuc_time_t out; +size_t consumed = 0; +cuc_decode(buf, n, &out_fmt, &out, &consumed); /* out ≈ 1234567.25 s */ ``` -### Step X - +To work with an implicitly-conveyed P-field (structure known from external +metadata), use `cuc_tfield_encode` / `cuc_tfield_decode` with a caller-supplied +`cuc_format_t` and omit the P-field entirely. ## API Reference -### Lifecycle +### Types ```c +typedef enum { CUC_EPOCH_CCSDS = 1, CUC_EPOCH_AGENCY = 2 } cuc_epoch_t; -``` +typedef struct { + cuc_epoch_t epoch; + uint8_t basic_octets; /* 1..7 */ + uint8_t fraction_octets; /* 0..10 */ +} cuc_format_t; -### Building a Packet - -```c +typedef struct { + uint64_t seconds; /* integer seconds since epoch */ + uint64_t fraction; /* binary fraction of a second, Q0.64 */ +} cuc_time_t; +typedef enum { + CUC_OK = 0, CUC_ERR_NULL, CUC_ERR_BUFFER, + CUC_ERR_FORMAT, CUC_ERR_PFIELD_ID, CUC_ERR_UNSUPPORTED +} cuc_status_t; ``` -### Utilities +### Functions ```c - -``` - -### Types - -```c - -``` - -## Memory Usage (Estimated) - -- **Library (stripped)**: -- **Serialization buffer**: -- **No heap usage**: all allocations are caller-supplied - -## CCSDS XXX — Notes - -## Limitations +/* Sizing / validation */ +cuc_status_t cuc_format_validate(const cuc_format_t *fmt); +size_t cuc_pfield_size(const cuc_format_t *fmt); /* 1 or 2 */ +size_t cuc_tfield_size(const cuc_format_t *fmt); /* basic + frac */ +size_t cuc_size(const cuc_format_t *fmt); /* P + T */ + +/* P-field only */ +cuc_status_t cuc_pfield_encode(const cuc_format_t *fmt, uint8_t *buf, + size_t buf_len, size_t *written); +cuc_status_t cuc_pfield_decode(const uint8_t *buf, size_t buf_len, + cuc_format_t *fmt, size_t *consumed); + +/* T-field only (implicit P-field) */ +cuc_status_t cuc_tfield_encode(const cuc_time_t *time, const cuc_format_t *fmt, + uint8_t *buf, size_t buf_len, size_t *written); +cuc_status_t cuc_tfield_decode(const uint8_t *buf, size_t buf_len, + const cuc_format_t *fmt, cuc_time_t *time, + size_t *consumed); + +/* Combined self-identified code (P-field + T-field) */ +cuc_status_t cuc_encode(const cuc_time_t *time, const cuc_format_t *fmt, + uint8_t *buf, size_t buf_len, size_t *written); +cuc_status_t cuc_decode(const uint8_t *buf, size_t buf_len, cuc_format_t *fmt, + cuc_time_t *time, size_t *consumed); + +/* Convenience (omitted with -DCUC_NO_FLOAT) */ +double cuc_time_to_seconds(const cuc_time_t *time); +cuc_time_t cuc_time_from_seconds(double seconds); +``` + +## Memory Usage + +- **No heap usage**: all buffers are caller-supplied. +- **Largest CUC code**: `CUC_OCTETS_MAX` = 19 octets (2 P-field + 17 T-field). +- **State**: `cuc_time_t` and `cuc_format_t` are small plain structs. + +## Notes and Limitations + +- CUC is **not UTC-based**; leap-second corrections do not apply (use CDS/CCS + for UTC-based time — not yet implemented here). +- The fractional value is stored to 64 bits (Q0.64). Encodings that request + more than 8 fractional octets carry those extra low octets as zero. +- Integer seconds fit in `uint64_t`; a field configured for fewer octets rolls + over modulo 256 per octet, exactly as the standard specifies. +- This library implements the CUC format only. CDS, CCS and ASCII time codes + from CCSDS 301.0-B-4 are out of scope for this initial version. ## References +- CCSDS 301.0-B-4, *Time Code Formats*, Blue Book, Issue 4, November 2010. + ## License -See LICENSE file. \ No newline at end of file +See the [LICENSE](LICENSE) file. diff --git a/docs/301x0b4e1_time.pdf b/docs/301x0b4e1_time.pdf new file mode 100644 index 0000000..fb514c3 Binary files /dev/null and b/docs/301x0b4e1_time.pdf differ diff --git a/examples/cuc_example.c b/examples/cuc_example.c new file mode 100644 index 0000000..1bf1513 --- /dev/null +++ b/examples/cuc_example.c @@ -0,0 +1,50 @@ +/** + * @file cuc_example.c + * @brief Minimal usage example for the CUC time code library + * + * Encodes a time value into a self-identified CUC code and decodes it back, + * printing the intermediate octets. Demonstrates CCSDS 301.0-B-4, Section 3.2. + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cuc.h" + +#include + +int main(void) +{ + /* 4 octets of seconds, 2 octets of fraction, CCSDS 1958 TAI epoch. */ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t time = cuc_time_from_seconds(1234567.25); + + uint8_t buf[CUC_OCTETS_MAX]; + size_t written = 0; + if (cuc_encode(&time, &fmt, buf, sizeof(buf), &written) != CUC_OK) + { + fprintf(stderr, "encode failed\n"); + return 1; + } + + printf("encoded %zu octets:", written); + for (size_t i = 0; i < written; i++) + { + printf(" %02X", buf[i]); + } + printf("\n"); + + cuc_format_t decoded_fmt; + cuc_time_t decoded; + size_t consumed = 0; + if (cuc_decode(buf, written, &decoded_fmt, &decoded, &consumed) != CUC_OK) + { + fprintf(stderr, "decode failed\n"); + return 1; + } + + printf("decoded: %.3f s (%u basic + %u fractional octets)\n", + cuc_time_to_seconds(&decoded), + decoded_fmt.basic_octets, + decoded_fmt.fraction_octets); + return 0; +} diff --git a/include/cuc.h b/include/cuc.h new file mode 100644 index 0000000..13e5cbc --- /dev/null +++ b/include/cuc.h @@ -0,0 +1,295 @@ +/** + * @file cuc.h + * @brief CCSDS Unsegmented Time Code (CUC) encoder/decoder + * + * Implements the CCSDS Unsegmented Time Code (CUC) as per + * CCSDS 301.0-B-4 (Time Code Formats), Section 3.2. + * See also: docs/ccsds_cuc.md + * + * A CUC time code is a pure binary count of a basic time unit (the second) + * and a binary fraction of that unit, measured from a defined epoch. It is + * carried in a TIME SPECIFICATION FIELD (T-field) that may be preceded by an + * explicit TIME CODE PREAMBLE FIELD (P-field) describing its structure. + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef CUC_H +#define CUC_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* ------------------------------------------------------------------------- + * Constants + * + * The P-field can describe up to 4 octets of basic time in octet 1 plus 3 + * more in octet 2, and up to 3 octets of fractional time in octet 1 plus 7 + * more in octet 2 (CCSDS 301.0-B-4, 3.2.2). + * ---------------------------------------------------------------------- */ + +/** @brief Minimum number of basic-time (seconds) octets (CCSDS 301.0-B-4 §3.2.2). */ +#define CUC_BASIC_OCTETS_MIN 1 + +/** @brief Maximum number of basic-time octets: 4 (P-field octet 1) + 3 (P-field octet 2). */ +#define CUC_BASIC_OCTETS_MAX 7 + +/** @brief Minimum number of fractional-time octets (CCSDS 301.0-B-4 §3.2.2). */ +#define CUC_FRACTION_OCTETS_MIN 0 + +/** @brief Maximum number of fractional-time octets: 3 (P-field octet 1) + 7 (P-field octet 2). */ +#define CUC_FRACTION_OCTETS_MAX 10 + +/** @brief Maximum number of P-field (preamble) octets. */ +#define CUC_PFIELD_OCTETS_MAX 2 + +/** @brief Maximum number of T-field octets: basic + fractional (17). */ +#define CUC_TFIELD_OCTETS_MAX (CUC_BASIC_OCTETS_MAX + CUC_FRACTION_OCTETS_MAX) + +/** @brief Maximum octets of a self-identified code: P-field + T-field (19). */ +#define CUC_OCTETS_MAX (CUC_PFIELD_OCTETS_MAX + CUC_TFIELD_OCTETS_MAX) + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief Result codes returned by the CUC functions. + */ +typedef enum +{ + CUC_OK = 0, /**< Success. */ + CUC_ERR_NULL, /**< A required pointer argument was NULL. */ + CUC_ERR_BUFFER, /**< The supplied buffer was too small. */ + CUC_ERR_FORMAT, /**< The format has out-of-range octet counts. */ + CUC_ERR_PFIELD_ID, /**< The P-field time code identification is not a CUC id. */ + CUC_ERR_UNSUPPORTED, /**< The P-field requests more octets than this library supports. */ +} cuc_status_t; + +/** + * @brief Epoch / time code identification of a CUC code (CCSDS 301.0-B-4 §3.2.2). + * + * The enumerator values equal the P-field time code identification field + * (bits 1-3) so they map directly onto the encoded octet. + */ +typedef enum +{ + CUC_EPOCH_CCSDS = 1, /**< 1958 January 1 (TAI), Level 1 time code (id = 001). */ + CUC_EPOCH_AGENCY = 2, /**< Agency-defined epoch, Level 2 time code (id = 010). */ +} cuc_epoch_t; + +/** + * @brief Structure of a CUC time code as described by its P-field (CCSDS 301.0-B-4 §3.2.2). + * + * Captures how many octets encode the integer seconds and how many encode the + * binary fraction, plus which epoch is in use. + */ +typedef struct +{ + cuc_epoch_t epoch; /**< Epoch / time code identification. */ + uint8_t basic_octets; /**< Octets of basic time unit (seconds), 1..7. */ + uint8_t fraction_octets; /**< Octets of fractional time unit, 0..10. */ +} cuc_format_t; + +/** + * @brief A decoded CUC time value, independent of the encoded format. + * + * The fraction is stored as an unsigned Q0.64 binary fraction of a second, + * i.e. the fractional part in seconds equals fraction / 2^64. This keeps the + * core codec integer-only and lets the same value be encoded into any + * fractional resolution. Encodings using more than 8 fractional octets carry + * resolution below 2^-64 s, which is not representable here and is treated as + * zero in those low octets. + */ +typedef struct +{ + uint64_t seconds; /**< Integer basic time units (seconds) since the epoch. */ + uint64_t fraction; /**< Binary fraction of a second, Q0.64. */ +} cuc_time_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Validate that a format has octet counts within the ranges the standard allows. + * + * @param[in] fmt Format to validate. + * + * @return #CUC_OK when valid; #CUC_ERR_NULL if @p fmt is NULL; #CUC_ERR_FORMAT if the + * epoch or octet counts are out of range. + */ +cuc_status_t cuc_format_validate(const cuc_format_t *fmt); + +/** + * @brief Number of P-field octets a format needs (1 or 2). + * + * @param[in] fmt Format to size. + * + * @return 1 or 2 on success, or 0 if @p fmt is NULL. + */ +size_t cuc_pfield_size(const cuc_format_t *fmt); + +/** + * @brief Number of T-field octets a format needs (basic + fractional). + * + * @param[in] fmt Format to size. + * + * @return T-field length in octets, or 0 if @p fmt is NULL. + */ +size_t cuc_tfield_size(const cuc_format_t *fmt); + +/** + * @brief Total octets of a self-identified code (P-field + T-field). + * + * @param[in] fmt Format to size. + * + * @return Total length in octets, or 0 if @p fmt is NULL. + */ +size_t cuc_size(const cuc_format_t *fmt); + +/** + * @brief Encode the P-field (preamble) describing a format. + * + * @param[in] fmt Format to describe. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] written Receives the number of octets produced (1 or 2). + * + * @return #CUC_OK on success; #CUC_ERR_NULL if @p buf or @p written is NULL; + * #CUC_ERR_FORMAT for an invalid format; #CUC_ERR_BUFFER if @p buf_len is too small. + */ +cuc_status_t cuc_pfield_encode(const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written); + +/** + * @brief Decode a P-field into a format. + * + * @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. + * @param[out] consumed Receives the number of octets read (1 or 2). + * + * @return #CUC_OK on success; #CUC_ERR_NULL if any pointer is NULL; #CUC_ERR_BUFFER if + * @p buf_len is too small; #CUC_ERR_PFIELD_ID if the identification is not a CUC id; + * #CUC_ERR_UNSUPPORTED if the P-field requests a third octet. + */ +cuc_status_t cuc_pfield_decode(const uint8_t *buf, + size_t buf_len, + cuc_format_t *fmt, + size_t *consumed); + +/** + * @brief Encode only the T-field (time data) for a value using a given format. + * + * Use this when the P-field is conveyed implicitly (external metadata). + * + * @param[in] time Time value to encode. + * @param[in] fmt Format describing the octet layout. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] written Receives the number of octets produced. + * + * @return #CUC_OK on success; #CUC_ERR_NULL if @p time, @p buf or @p written is NULL; + * #CUC_ERR_FORMAT for an invalid format; #CUC_ERR_BUFFER if @p buf_len is too small. + */ +cuc_status_t cuc_tfield_encode(const cuc_time_t *time, + const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written); + +/** + * @brief Decode only the T-field using a caller-supplied format. + * + * @param[in] buf Input buffer positioned at the T-field. + * @param[in] buf_len Number of octets available in @p buf. + * @param[in] fmt Format describing the octet layout. + * @param[out] time Receives the decoded time value. + * @param[out] consumed Receives the number of octets read. + * + * @return #CUC_OK on success; #CUC_ERR_NULL if @p buf, @p time or @p consumed is NULL; + * #CUC_ERR_FORMAT for an invalid format; #CUC_ERR_BUFFER if @p buf_len is too small. + */ +cuc_status_t cuc_tfield_decode(const uint8_t *buf, + size_t buf_len, + const cuc_format_t *fmt, + cuc_time_t *time, + size_t *consumed); + +/** + * @brief Encode a self-identified CUC code: P-field followed by T-field. + * + * @param[in] time Time value to encode. + * @param[in] fmt Format to encode. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] written Receives the total number of octets produced. + * + * @return #CUC_OK on success; #CUC_ERR_NULL if @p written or another required pointer is NULL; + * #CUC_ERR_FORMAT for an invalid format; #CUC_ERR_BUFFER if @p buf_len is too small. + */ +cuc_status_t cuc_encode(const cuc_time_t *time, + const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written); + +/** + * @brief Decode a self-identified CUC code: parse the P-field, then the T-field. + * + * @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. + * @param[out] time Receives the decoded time value. + * @param[out] consumed Receives the total number of octets read. + * + * @return #CUC_OK on success; #CUC_ERR_NULL if @p consumed or another required pointer is NULL; + * #CUC_ERR_BUFFER if @p buf_len is too small; #CUC_ERR_PFIELD_ID or + * #CUC_ERR_UNSUPPORTED on an invalid P-field. + */ +cuc_status_t cuc_decode(const uint8_t *buf, + size_t buf_len, + cuc_format_t *fmt, + cuc_time_t *time, + size_t *consumed); + +#ifndef CUC_NO_FLOAT +/** + * @brief Convert a CUC time to seconds as a double. + * + * @note Compile with -DCUC_NO_FLOAT to omit this on targets without an FPU; + * the core codec does not use floating point. + * + * @param[in] time Time value to convert. + * + * @return Seconds since the epoch as a double, or 0.0 if @p time is NULL. + */ +double cuc_time_to_seconds(const cuc_time_t *time); + +/** + * @brief Convert seconds as a double to a CUC time. + * + * @note Compile with -DCUC_NO_FLOAT to omit this on targets without an FPU; + * the core codec does not use floating point. + * + * @param[in] seconds Seconds since the epoch; negative values yield a zero time. + * + * @return The equivalent CUC time value. + */ +cuc_time_t cuc_time_from_seconds(double seconds); +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* CUC_H */ diff --git a/src/cuc.c b/src/cuc.c new file mode 100644 index 0000000..4c77df6 --- /dev/null +++ b/src/cuc.c @@ -0,0 +1,397 @@ +/** + * @file cuc.c + * @brief CCSDS Unsegmented Time Code (CUC) encoder/decoder + * + * Implements the CCSDS Unsegmented Time Code (CUC) as per + * CCSDS 301.0-B-4 (Time Code Formats), Section 3.2. + * See also: docs/ccsds_cuc.md + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "cuc.h" + +/* Bit-0-is-MSB masks for P-field octet 1 (CCSDS 301.0-B-4, 3.2.2). */ +/** @brief P-field octet 1 extension flag (bit 0): another P-field octet follows. */ +#define CUC_P1_EXTENSION 0x80u + +/** @brief Left shift of the P-field octet 1 time code identification field (bits 1-3). */ +#define CUC_P1_ID_SHIFT 4 + +/** @brief Mask for the 3-bit time code identification field. */ +#define CUC_P1_ID_MASK 0x07u + +/** @brief Left shift of the P-field octet 1 basic-octet count (bits 4-5), holding (count - 1). */ +#define CUC_P1_BASIC_SHIFT 2 + +/** @brief Mask for the 2-bit basic-octet count field in P-field octet 1. */ +#define CUC_P1_BASIC_MASK 0x03u + +/** @brief Mask for the 2-bit fractional-octet count field in P-field octet 1 (bits 6-7). */ +#define CUC_P1_FRAC_MASK 0x03u + +/* Masks for P-field octet 2. */ +/** @brief P-field octet 2 extension flag (bit 0): a third P-field octet would follow. */ +#define CUC_P2_EXTENSION 0x80u + +/** @brief Left shift of the additional basic-octet count in P-field octet 2 (bits 1-2). */ +#define CUC_P2_ADD_BASIC_SHIFT 5 + +/** @brief Mask for the 2-bit additional basic-octet count field in P-field octet 2. */ +#define CUC_P2_ADD_BASIC_MASK 0x03u + +/** @brief Left shift of the additional fractional-octet count in P-field octet 2 (bits 3-5). */ +#define CUC_P2_ADD_FRAC_SHIFT 2 + +/** @brief Mask for the 3-bit additional fractional-octet count field in P-field octet 2. */ +#define CUC_P2_ADD_FRAC_MASK 0x07u + +/** @brief Maximum basic-time octets encodable in P-field octet 1 alone. */ +#define CUC_P1_BASIC_MAX 4 + +/** @brief Maximum fractional-time octets encodable in P-field octet 1 alone. */ +#define CUC_P1_FRAC_MAX 3 + +/** @brief Fractional octets stored in the Q0.64 representation; deeper octets fall below 2^-64 s. + */ +#define CUC_FRACTION_STORED_OCTETS 8 + +cuc_status_t cuc_format_validate(const cuc_format_t *fmt) +{ + if (!fmt) + { + return CUC_ERR_NULL; + } + + if ((fmt->epoch != CUC_EPOCH_CCSDS) && (fmt->epoch != CUC_EPOCH_AGENCY)) + { + return CUC_ERR_FORMAT; + } + + if ((fmt->basic_octets < CUC_BASIC_OCTETS_MIN) || (fmt->basic_octets > CUC_BASIC_OCTETS_MAX)) + { + return CUC_ERR_FORMAT; + } + + if (fmt->fraction_octets > CUC_FRACTION_OCTETS_MAX) + { + return CUC_ERR_FORMAT; + } + + return CUC_OK; +} + +/** + * @brief Test whether a format needs a second P-field octet. + * + * @param[in] fmt Format to inspect (assumed non-NULL and valid). + * + * @return true if the basic or fractional octet count exceeds what P-field octet 1 can hold. + */ +static bool cuc_format_is_extended(const cuc_format_t *fmt) +{ + return (fmt->basic_octets > CUC_P1_BASIC_MAX) || (fmt->fraction_octets > CUC_P1_FRAC_MAX); +} + +size_t cuc_pfield_size(const cuc_format_t *fmt) +{ + if (!fmt) + { + return 0; + } + + return cuc_format_is_extended(fmt) ? 2u : 1u; +} + +size_t cuc_tfield_size(const cuc_format_t *fmt) +{ + if (!fmt) + { + return 0; + } + + return (size_t)fmt->basic_octets + (size_t)fmt->fraction_octets; +} + +size_t cuc_size(const cuc_format_t *fmt) +{ + return cuc_pfield_size(fmt) + cuc_tfield_size(fmt); +} + +cuc_status_t cuc_pfield_encode(const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written) +{ + cuc_status_t status = cuc_format_validate(fmt); + if (status != CUC_OK) + { + return status; + } + + if ((!buf) || (!written)) + { + return CUC_ERR_NULL; + } + + bool extended = cuc_format_is_extended(fmt); + size_t size = extended ? 2u : 1u; + if (buf_len < size) + { + return CUC_ERR_BUFFER; + } + + uint8_t oct1_basic = extended ? CUC_P1_BASIC_MAX : fmt->basic_octets; + uint8_t oct1_frac = + fmt->fraction_octets > CUC_P1_FRAC_MAX ? CUC_P1_FRAC_MAX : fmt->fraction_octets; + buf[0] = (uint8_t)((extended ? CUC_P1_EXTENSION : 0u) | + (((uint8_t)fmt->epoch & CUC_P1_ID_MASK) << CUC_P1_ID_SHIFT) | + (((oct1_basic - 1u) & CUC_P1_BASIC_MASK) << CUC_P1_BASIC_SHIFT) | + (oct1_frac & CUC_P1_FRAC_MASK)); + + if (extended) + { + uint8_t add_basic = (uint8_t)(fmt->basic_octets - oct1_basic); + uint8_t add_frac = (uint8_t)(fmt->fraction_octets - oct1_frac); + buf[1] = (uint8_t)(((add_basic & CUC_P2_ADD_BASIC_MASK) << CUC_P2_ADD_BASIC_SHIFT) | + ((add_frac & CUC_P2_ADD_FRAC_MASK) << CUC_P2_ADD_FRAC_SHIFT)); + } + + *written = size; + + return CUC_OK; +} + +cuc_status_t cuc_pfield_decode(const uint8_t *buf, + size_t buf_len, + cuc_format_t *fmt, + size_t *consumed) +{ + if ((!buf) || (!fmt) || (!consumed)) + { + return CUC_ERR_NULL; + } + + if (buf_len < 1u) + { + return CUC_ERR_BUFFER; + } + + uint8_t oct1 = buf[0]; + uint8_t id = (oct1 >> CUC_P1_ID_SHIFT) & CUC_P1_ID_MASK; + if ((id != CUC_EPOCH_CCSDS) && (id != CUC_EPOCH_AGENCY)) + { + 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); + + if ((oct1 & CUC_P1_EXTENSION) == 0u) + { + *consumed = 1u; + + return CUC_OK; + } + + 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; + } + + 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; + + return CUC_OK; +} + +cuc_status_t cuc_tfield_encode(const cuc_time_t *time, + const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written) +{ + cuc_status_t status = cuc_format_validate(fmt); + if (status != CUC_OK) + { + return status; + } + + if ((!time) || (!buf) || (!written)) + { + return CUC_ERR_NULL; + } + + size_t size = cuc_tfield_size(fmt); + if (buf_len < size) + { + return CUC_ERR_BUFFER; + } + + /* Basic time: most significant octet first. Octets above bit 56 hold the + * high part of a 7-octet count; the field naturally rolls over mod 256^n. */ + for (uint8_t i = 0; i < fmt->basic_octets; i++) + { + unsigned shift = (unsigned)(fmt->basic_octets - 1u - i) * 8u; + buf[i] = (uint8_t)(time->seconds >> shift); + } + + /* Fractional time: most significant octet (weight 2^-8) first. */ + for (uint8_t j = 0; j < fmt->fraction_octets; j++) + { + uint8_t octet = 0; + if (j < CUC_FRACTION_STORED_OCTETS) + { + octet = (uint8_t)(time->fraction >> (56u - (unsigned)j * 8u)); + } + + buf[fmt->basic_octets + j] = octet; + } + + *written = size; + + return CUC_OK; +} + +cuc_status_t cuc_tfield_decode(const uint8_t *buf, + size_t buf_len, + const cuc_format_t *fmt, + cuc_time_t *time, + size_t *consumed) +{ + cuc_status_t status = cuc_format_validate(fmt); + if (status != CUC_OK) + { + return status; + } + + if ((!buf) || (!time) || (!consumed)) + { + return CUC_ERR_NULL; + } + + size_t size = cuc_tfield_size(fmt); + if (buf_len < size) + { + return CUC_ERR_BUFFER; + } + + uint64_t seconds = 0; + for (uint8_t i = 0; i < fmt->basic_octets; i++) + { + seconds = (seconds << 8) | buf[i]; + } + + uint64_t fraction = 0; + for (uint8_t j = 0; (j < fmt->fraction_octets) && (j < CUC_FRACTION_STORED_OCTETS); j++) + { + fraction |= (uint64_t)buf[fmt->basic_octets + j] << (56u - (unsigned)j * 8u); + } + + time->seconds = seconds; + time->fraction = fraction; + *consumed = size; + + return CUC_OK; +} + +cuc_status_t cuc_encode(const cuc_time_t *time, + const cuc_format_t *fmt, + uint8_t *buf, + size_t buf_len, + size_t *written) +{ + if (!written) + { + return CUC_ERR_NULL; + } + + size_t p_len = 0; + cuc_status_t status = cuc_pfield_encode(fmt, buf, buf_len, &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); + if (status != CUC_OK) + { + return status; + } + + *written = p_len + t_len; + + return CUC_OK; +} + +cuc_status_t cuc_decode(const uint8_t *buf, + size_t buf_len, + cuc_format_t *fmt, + cuc_time_t *time, + size_t *consumed) +{ + if (!consumed) + { + return CUC_ERR_NULL; + } + + size_t p_len = 0; + cuc_status_t status = cuc_pfield_decode(buf, buf_len, fmt, &p_len); + if (status != CUC_OK) + { + return status; + } + + size_t t_len = 0; + status = cuc_tfield_decode(buf + p_len, buf_len - p_len, fmt, time, &t_len); + if (status != CUC_OK) + { + return status; + } + + *consumed = p_len + t_len; + + return CUC_OK; +} + +#ifndef CUC_NO_FLOAT + +/** @brief 2^64 as a double, for converting the Q0.64 fraction to/from seconds. */ +# define CUC_TWO_POW_64 18446744073709551616.0 + +double cuc_time_to_seconds(const cuc_time_t *time) +{ + if (!time) + { + return 0.0; + } + + return (double)time->seconds + (double)time->fraction / CUC_TWO_POW_64; +} + +cuc_time_t cuc_time_from_seconds(double seconds) +{ + cuc_time_t time = {0, 0}; + if (seconds < 0.0) + { + return time; + } + + time.seconds = (uint64_t)seconds; + double frac = seconds - (double)time.seconds; + time.fraction = (uint64_t)(frac * CUC_TWO_POW_64); + + return time; +} + +#endif /* CUC_NO_FLOAT */ diff --git a/tests/test_cuc.c b/tests/test_cuc.c new file mode 100644 index 0000000..b4aa2fb --- /dev/null +++ b/tests/test_cuc.c @@ -0,0 +1,344 @@ +/** + * @file test_cuc.c + * @brief Unit tests for the CCSDS Unsegmented Time Code (CUC) library + * + * Exercises the P-field / T-field codecs against the encodings defined in + * CCSDS 301.0-B-4 (Time Code Formats), Section 3.2. + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "../include/cuc.h" +#include "cunit.h" +#include "test_runners.h" + +#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. */ +static int test_pfield_single_octet(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + uint8_t buf[2] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_OK, cuc_pfield_encode(&fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(1, (int)written); + ASSERT_EQ_INT(0x1E, buf[0]); + + cuc_format_t out = {0, 0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_pfield_decode(buf, written, &out, &consumed)); + ASSERT_EQ_INT(1, (int)consumed); + ASSERT_EQ_INT(CUC_EPOCH_CCSDS, out.epoch); + ASSERT_EQ_INT(4, out.basic_octets); + ASSERT_EQ_INT(2, out.fraction_octets); + return 0; +} + +/* 5 basic + 4 fractional octets forces a second P-field octet. + * Octet 1 = ext(1) id(001) basic-1(011) frac(11) = 1001 1111 = 0x9F. + * Octet 2 = ext(0) add_basic(01) add_frac(001) rsvd(00) = 0010 0100 = 0x24. */ +static int test_pfield_extended(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 5, 4}; + uint8_t buf[2] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_OK, cuc_pfield_encode(&fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(2, (int)written); + ASSERT_EQ_INT(0x9F, buf[0]); + ASSERT_EQ_INT(0x24, buf[1]); + + cuc_format_t out = {0, 0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_pfield_decode(buf, written, &out, &consumed)); + ASSERT_EQ_INT(2, (int)consumed); + ASSERT_EQ_INT(5, out.basic_octets); + ASSERT_EQ_INT(4, out.fraction_octets); + return 0; +} + +static int test_tfield_roundtrip(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t in = {0x12345678u, 0x8000000000000000u}; /* 0.5 s fraction */ + uint8_t buf[CUC_TFIELD_OCTETS_MAX] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_OK, cuc_tfield_encode(&in, &fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(6, (int)written); + + uint8_t expected[6] = {0x12, 0x34, 0x56, 0x78, 0x80, 0x00}; + ASSERT_EQ_MEM(expected, buf, 6); + + cuc_time_t out = {0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_tfield_decode(buf, written, &fmt, &out, &consumed)); + ASSERT_EQ_INT(6, (int)consumed); + ASSERT_TRUE(out.seconds == in.seconds); + ASSERT_TRUE(out.fraction == in.fraction); + return 0; +} + +static int test_full_roundtrip(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t in = {0x12345678u, 0x8000000000000000u}; + uint8_t buf[CUC_OCTETS_MAX] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_OK, cuc_encode(&in, &fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(7, (int)written); /* 1 P-field + 6 T-field */ + ASSERT_EQ_INT(0x1E, buf[0]); + + cuc_format_t out_fmt = {0, 0, 0}; + cuc_time_t out = {0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_decode(buf, written, &out_fmt, &out, &consumed)); + ASSERT_EQ_INT(7, (int)consumed); + ASSERT_EQ_INT(4, out_fmt.basic_octets); + ASSERT_EQ_INT(2, out_fmt.fraction_octets); + ASSERT_TRUE(out.seconds == in.seconds); + ASSERT_TRUE(out.fraction == in.fraction); + return 0; +} + +static int test_error_handling(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t t = {1, 0}; + uint8_t buf[3] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_encode(&t, &fmt, buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_encode(&t, &fmt, buf, sizeof(buf), &written)); + + cuc_format_t bad = {CUC_EPOCH_CCSDS, 0, 0}; /* basic_octets must be >= 1 */ + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_format_validate(&bad)); + + uint8_t bad_id[1] = {0x00}; /* id 000 is reserved, not CUC */ + cuc_format_t out = {0, 0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_ERR_PFIELD_ID, cuc_pfield_decode(bad_id, 1, &out, &consumed)); + return 0; +} + +/* Every rejection path of cuc_format_validate: NULL, each out-of-range field, and both + * accepted epochs (CCSDS and Agency). */ +static int test_format_validate_errors(void) +{ + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_format_validate(NULL)); + + cuc_format_t bad_epoch = {(cuc_epoch_t)0, 4, 2}; + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_format_validate(&bad_epoch)); + + cuc_format_t agency = {CUC_EPOCH_AGENCY, 4, 2}; + ASSERT_EQ_INT(CUC_OK, cuc_format_validate(&agency)); + + cuc_format_t basic_low = {CUC_EPOCH_CCSDS, 0, 2}; + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_format_validate(&basic_low)); + + cuc_format_t basic_high = {CUC_EPOCH_CCSDS, CUC_BASIC_OCTETS_MAX + 1, 2}; + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_format_validate(&basic_high)); + + cuc_format_t frac_high = {CUC_EPOCH_CCSDS, 4, CUC_FRACTION_OCTETS_MAX + 1}; + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_format_validate(&frac_high)); + + cuc_format_t good = {CUC_EPOCH_CCSDS, 4, 2}; + ASSERT_EQ_INT(CUC_OK, cuc_format_validate(&good)); + return 0; +} + +/* The stand-alone size helpers, including the NULL guards and both P-field widths. + * A format extended only by its fraction count exercises the second half of the + * cuc_format_is_extended predicate. */ +static int test_size_helpers(void) +{ + cuc_format_t single = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_format_t ext_basic = {CUC_EPOCH_CCSDS, 5, 4}; + cuc_format_t ext_frac = {CUC_EPOCH_CCSDS, 4, 5}; + + ASSERT_EQ_INT(0, (int)cuc_pfield_size(NULL)); + ASSERT_EQ_INT(1, (int)cuc_pfield_size(&single)); + ASSERT_EQ_INT(2, (int)cuc_pfield_size(&ext_basic)); + ASSERT_EQ_INT(2, (int)cuc_pfield_size(&ext_frac)); + + ASSERT_EQ_INT(0, (int)cuc_tfield_size(NULL)); + ASSERT_EQ_INT(6, (int)cuc_tfield_size(&single)); + + ASSERT_EQ_INT(0, (int)cuc_size(NULL)); + ASSERT_EQ_INT(7, (int)cuc_size(&single)); + return 0; +} + +static int test_pfield_encode_errors(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_format_t invalid = {CUC_EPOCH_CCSDS, 0, 0}; + uint8_t buf[CUC_PFIELD_OCTETS_MAX] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_pfield_encode(&invalid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_pfield_encode(&fmt, NULL, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_pfield_encode(&fmt, buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_pfield_encode(&fmt, buf, 0, &written)); + return 0; +} + +static int test_pfield_decode_errors(void) +{ + cuc_format_t out = {0, 0, 0}; + size_t consumed = 0; + uint8_t single[1] = {0x1E}; + + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_pfield_decode(NULL, 1, &out, &consumed)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_pfield_decode(single, 1, NULL, &consumed)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_pfield_decode(single, 1, &out, NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_pfield_decode(single, 0, &out, &consumed)); + + /* The Agency epoch id (010) is a valid CUC identification and must round-trip. */ + cuc_format_t agency = {CUC_EPOCH_AGENCY, 4, 2}; + uint8_t agency_pf[CUC_PFIELD_OCTETS_MAX] = {0}; + size_t written = 0; + ASSERT_EQ_INT(CUC_OK, cuc_pfield_encode(&agency, agency_pf, sizeof(agency_pf), &written)); + ASSERT_EQ_INT(CUC_OK, cuc_pfield_decode(agency_pf, written, &out, &consumed)); + ASSERT_EQ_INT(CUC_EPOCH_AGENCY, out.epoch); + + /* Extension bit set in octet 1, but only one octet is supplied. */ + uint8_t truncated_ext[1] = {0x90}; + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_pfield_decode(truncated_ext, 1, &out, &consumed)); + + /* Extension bit set in octet 2 would request a third octet, which is unsupported. */ + uint8_t third_octet[2] = {0x90, 0x80}; + ASSERT_EQ_INT(CUC_ERR_UNSUPPORTED, cuc_pfield_decode(third_octet, 2, &out, &consumed)); + return 0; +} + +static int test_tfield_encode_errors(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_format_t invalid = {CUC_EPOCH_CCSDS, 0, 0}; + cuc_time_t t = {1, 0}; + uint8_t buf[CUC_TFIELD_OCTETS_MAX] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_tfield_encode(&t, &invalid, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_encode(NULL, &fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_encode(&t, &fmt, NULL, sizeof(buf), &written)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_encode(&t, &fmt, buf, sizeof(buf), NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_tfield_encode(&t, &fmt, buf, 2, &written)); + return 0; +} + +static int test_tfield_decode_errors(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_format_t invalid = {CUC_EPOCH_CCSDS, 0, 0}; + cuc_time_t out = {0, 0}; + size_t consumed = 0; + uint8_t buf[CUC_TFIELD_OCTETS_MAX] = {0}; + + ASSERT_EQ_INT(CUC_ERR_FORMAT, cuc_tfield_decode(buf, sizeof(buf), &invalid, &out, &consumed)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_decode(NULL, sizeof(buf), &fmt, &out, &consumed)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_decode(buf, sizeof(buf), &fmt, NULL, &consumed)); + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_tfield_decode(buf, sizeof(buf), &fmt, &out, NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_tfield_decode(buf, 2, &fmt, &out, &consumed)); + return 0; +} + +/* Ten fractional octets exceed the 8 held in the Q0.64 representation, so the two low + * octets are transmitted as zero and dropped on decode while the value round-trips. */ +static int test_wide_fraction_roundtrip(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, CUC_FRACTION_OCTETS_MAX}; + cuc_time_t in = {0x11223344u, 0xABCDEF0123456789u}; + uint8_t buf[CUC_TFIELD_OCTETS_MAX] = {0}; + size_t written = 0; + + ASSERT_EQ_INT(CUC_OK, cuc_tfield_encode(&in, &fmt, buf, sizeof(buf), &written)); + ASSERT_EQ_INT(14, (int)written); + ASSERT_EQ_INT(0, buf[12]); + ASSERT_EQ_INT(0, buf[13]); + + cuc_time_t out = {0, 0}; + size_t consumed = 0; + ASSERT_EQ_INT(CUC_OK, cuc_tfield_decode(buf, written, &fmt, &out, &consumed)); + ASSERT_EQ_INT(14, (int)consumed); + ASSERT_TRUE(out.seconds == in.seconds); + ASSERT_TRUE(out.fraction == in.fraction); + return 0; +} + +static int test_encode_decode_errors(void) +{ + cuc_format_t fmt = {CUC_EPOCH_CCSDS, 4, 2}; + cuc_time_t t = {1, 0}; + uint8_t buf[CUC_OCTETS_MAX] = {0}; + size_t written = 0; + + /* No room even for the P-field preamble: the failure surfaces from the P-field stage. */ + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_encode(&t, &fmt, buf, 0, &written)); + + cuc_format_t out_fmt = {0, 0, 0}; + cuc_time_t out = {0, 0}; + size_t consumed = 0; + + ASSERT_EQ_INT(CUC_ERR_NULL, cuc_decode(buf, sizeof(buf), &out_fmt, &out, NULL)); + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_decode(buf, 0, &out_fmt, &out, &consumed)); + + /* A valid one-octet P-field for a 6-octet T-field, but no T-field bytes follow. */ + uint8_t pfield_only[1] = {0x1E}; + ASSERT_EQ_INT(CUC_ERR_BUFFER, cuc_decode(pfield_only, 1, &out_fmt, &out, &consumed)); + return 0; +} + +#ifndef CUC_NO_FLOAT +static int test_seconds_conversion(void) +{ + cuc_time_t t = cuc_time_from_seconds(1.5); + ASSERT_TRUE(t.seconds == 1u); + ASSERT_TRUE(t.fraction == 0x8000000000000000u); + + double s = cuc_time_to_seconds(&t); + ASSERT_TRUE((s > 1.4999) && (s < 1.5001)); + return 0; +} + +/* NULL input converts to zero seconds; a negative input yields a zero time value. */ +static int test_seconds_conversion_edge(void) +{ + ASSERT_TRUE(cuc_time_to_seconds(NULL) == 0.0); + + cuc_time_t z = cuc_time_from_seconds(-1.0); + ASSERT_TRUE(z.seconds == 0u); + ASSERT_TRUE(z.fraction == 0u); + return 0; +} +#endif + +test_result_t test_cuc_run_all(void) +{ + RUN_TEST(test_pfield_single_octet); + RUN_TEST(test_pfield_extended); + RUN_TEST(test_tfield_roundtrip); + RUN_TEST(test_full_roundtrip); + RUN_TEST(test_error_handling); + RUN_TEST(test_format_validate_errors); + RUN_TEST(test_size_helpers); + RUN_TEST(test_pfield_encode_errors); + RUN_TEST(test_pfield_decode_errors); + RUN_TEST(test_tfield_encode_errors); + RUN_TEST(test_tfield_decode_errors); + RUN_TEST(test_wide_fraction_roundtrip); + RUN_TEST(test_encode_decode_errors); +#ifndef CUC_NO_FLOAT + RUN_TEST(test_seconds_conversion); + RUN_TEST(test_seconds_conversion_edge); +#endif + + test_result_t r; + r.total = cunit_total_tests; + r.passed = cunit_total_tests - cunit_overall_failures; + return r; +} diff --git a/tests/test_runners.h b/tests/test_runners.h new file mode 100644 index 0000000..49ddebb --- /dev/null +++ b/tests/test_runners.h @@ -0,0 +1,12 @@ +#ifndef TEST_RUNNERS_H +#define TEST_RUNNERS_H + +typedef struct +{ + int passed; + int total; +} test_result_t; + +test_result_t test_cuc_run_all(void); + +#endif /* TEST_RUNNERS_H */ diff --git a/tests/unit_tests.c b/tests/unit_tests.c index 8ee320b..b67fe55 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,21 +1,22 @@ -#include "cunit.h" +#include "test_runners.h" + #include -#include -#include -static int test_case_0(void) { +#define REPORT(label, r) printf(" %-14s Passed %d/%d\n\n", label ":", (r).passed, (r).total) - return 0; -} +int main(void) +{ + test_result_t r; + int total_passed = 0; + int total_tests = 0; -int main(void) { - RUN_TEST(test_case_0); + r = test_cuc_run_all(); + REPORT("cuc", r); + total_passed += r.passed; + total_tests += r.total; - if (cunit_overall_failures == 0) { - printf("ALL TESTS PASSED\n"); - return 0; - } else { - printf("%d TEST(S) FAILED\n", cunit_overall_failures); - return 1; - } -} \ No newline at end of file + printf(" ------------------------------\n"); + printf(" %-14s Passed %d/%d\n", "All UT:", total_passed, total_tests); + + return (total_passed == total_tests) ? 0 : 1; +} diff --git a/tools/coverage-html.sh b/tools/coverage-html.sh index 49e354f..f38fabd 100644 --- a/tools/coverage-html.sh +++ b/tools/coverage-html.sh @@ -8,7 +8,9 @@ if [[ "${OUT_FILE}" != /* ]]; then OUT_FILE="${ROOT_DIR}/${OUT_FILE}" fi -COVERAGE_CFLAGS='-O0 -g --coverage -std=c11 -Iinclude -Itests -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wcast-align -Wcast-qual -Wpointer-arith -Wformat=2 -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wundef' +# Instrumentation is the ONLY thing that differs from the normal build; the C standard, +# include paths and warning set all come from the Makefile so they cannot drift apart. +COVERAGE_OPT='-O0 -g --coverage' cd "${ROOT_DIR}" @@ -19,14 +21,21 @@ if ! command -v gcovr >/dev/null 2>&1; then fi make clean >/dev/null -make build/tests/ctest CFLAGS="${COVERAGE_CFLAGS}" >/dev/null -./build/tests/ctest +make build/tests/ctest OPT="${COVERAGE_OPT}" >/dev/null +./build/tests/ctest >/dev/null mkdir -p "$(dirname "${OUT_FILE}")" + +# Emit the HTML report and a text summary (line + branch) in a single gcovr pass, so +# the console output is not duplicated. gcovr's chatty "(INFO)" progress lines are +# filtered from stderr; warnings and errors still pass through and preserve the exit code. +echo "Coverage:" gcovr -r "${ROOT_DIR}" \ --filter "${ROOT_DIR}/src" \ - --html \ --html-details \ - --output "${OUT_FILE}" + --output "${OUT_FILE}" \ + --txt - \ + --txt-summary \ + 2> >(grep -v '^(INFO)' >&2) -echo "Coverage HTML report written to: ${OUT_FILE}" +echo "Coverage HTML report written to: ${OUT_FILE}" \ No newline at end of file