diff --git a/.clang-format b/.clang-format index a6de219..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 @@ -58,4 +58,4 @@ MaxEmptyLinesToKeep: 1 KeepEmptyLinesAtTheStartOfBlocks: false # Do not indent extern "C" blocks -IndentExternBlock: NoIndent \ No newline at end of file +IndentExternBlock: NoIndent diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c26db2d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + unit-tests: + name: Unit tests and example (${{ matrix.cc }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + steps: + - uses: actions/checkout@v4 + - name: Build libraries, run both test binaries and the example (-Werror) + run: make 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, report, and enforce 100% line and branch + run: make coverage-html + - 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 (${{ matrix.cc }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + steps: + - uses: actions/checkout@v4 + - name: Build and run tests and example under ASan and UBSan + run: make sanitize CC=${{ matrix.cc }} + + format: + name: clang-format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install clang-format + run: sudo apt-get update && sudo apt-get install -y clang-format + - name: Check formatting against .clang-format + run: git ls-files '*.c' '*.h' | xargs clang-format --dry-run --Werror diff --git a/.gitignore b/.gitignore index e7b9b6c..356dec0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ build/ .vscode/ .idea/ +# Local review notes +TODO.md + diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ca9e6be..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "external/EmbeddedSpacePacket"] - path = external/EmbeddedSpacePacket - url = https://github.com/OpenSpaceCode/EmbeddedSpacePacket 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 40dc2b7..a3b00fe 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,15 @@ CC ?= cc -# Stronger warnings for code quality -CFLAGS ?= -O2 -Iinclude -Wall -Wextra -Wpedantic -Wconversion -Wshadow \ +# Optimisation / instrumentation flags; overridden by the sanitize target +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 -CFLAGS += -I./external -fPIC +CFLAGS += -fPIC BUILD_DIR ?= build OBJ_DIR := $(BUILD_DIR)/obj @@ -15,59 +19,80 @@ BIN_DIR := $(BUILD_DIR)/bin # Source files CORE_SRCS := src/spacewire_spw_packet.c \ src/spacewire_router.c \ - src/spacewire_packet.c - -ESP_SRCS := external/EmbeddedSpacePacket/src/space_packet.c + src/spacewire_ptp.c +# Weak default payload hooks, kept apart because an application may replace them. +WEAK_SRCS := src/weak/spacewire_payload.c EXAMPLE_SRCS := examples/main.c TEST_SRCS := tests/unit_tests.c \ tests/test_spw_packet.c \ tests/test_router.c \ - tests/test_packet.c + tests/test_ptp.c \ + tests/test_payload.c +# Linked as an object file, so its strong sw_payload_*() definitions replace the +# library's weak defaults (an override inside a static archive would be ignored). +OVERRIDE_TEST_SRCS := tests/test_payload_override.c # Object files CORE_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(CORE_SRCS)) -ESP_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(ESP_SRCS)) +WEAK_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(WEAK_SRCS)) EXAMPLE_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(EXAMPLE_SRCS)) TEST_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(TEST_SRCS)) +OVERRIDE_TEST_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(OVERRIDE_TEST_SRCS)) # Output files LIB_STATIC := $(LIB_DIR)/libspacewire.a LIB_SHARED := $(LIB_DIR)/libspacewire.so EXAMPLE_BIN := $(BIN_DIR)/spacewire_example TEST_BIN := $(BIN_DIR)/spacewire_tests +OVERRIDE_TEST_BIN := $(BIN_DIR)/spacewire_override_tests + +# The instrumented build lives apart from the normal one and is removed after the run. +SANITIZE_DIR := $(BUILD_DIR)/sanitize +SANITIZE_BINS := spacewire_tests spacewire_override_tests spacewire_example # Build targets -.PHONY: all clean test example lib coverage-html help distclean +.PHONY: all clean test example lib coverage-html sanitize help distclean -all: lib test +all: lib test example lib: $(LIB_STATIC) $(LIB_SHARED) -$(LIB_STATIC): $(CORE_OBJS) $(ESP_OBJS) +$(LIB_STATIC): $(CORE_OBJS) $(WEAK_OBJS) @mkdir -p $(dir $@) $(AR) rcs $@ $^ -$(LIB_SHARED): $(CORE_OBJS) $(ESP_OBJS) +$(LIB_SHARED): $(CORE_OBJS) $(WEAK_OBJS) @mkdir -p $(dir $@) $(CC) -shared -o $@ $^ example: $(EXAMPLE_BIN) + $(EXAMPLE_BIN) $(EXAMPLE_BIN): $(EXAMPLE_OBJS) $(LIB_STATIC) @mkdir -p $(dir $@) $(CC) $(CFLAGS) -o $@ $^ -test: $(TEST_BIN) - ./$(TEST_BIN) +test: $(TEST_BIN) $(OVERRIDE_TEST_BIN) + $(TEST_BIN) + $(OVERRIDE_TEST_BIN) $(TEST_BIN): $(TEST_OBJS) $(LIB_STATIC) @mkdir -p $(dir $@) $(CC) $(CFLAGS) -o $@ $^ +$(OVERRIDE_TEST_BIN): $(OVERRIDE_TEST_OBJS) $(LIB_STATIC) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -o $@ $^ + +# -MMD -MP write a .d file next to each object listing the headers it includes, so a +# header edit rebuilds exactly the objects that depend on it. $(OBJ_DIR)/%.o: %.c @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< + $(CC) $(CFLAGS) -MMD -MP -c -o $@ $< + +-include $(CORE_OBJS:.o=.d) $(WEAK_OBJS:.o=.d) $(EXAMPLE_OBJS:.o=.d) $(TEST_OBJS:.o=.d) \ + $(OVERRIDE_TEST_OBJS:.o=.d) clean: rm -rf $(BUILD_DIR) @@ -76,6 +101,29 @@ clean: coverage-html: bash tools/coverage_html.sh +# Rebuild with ASan + UBSan and run both test binaries 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: + @rm -rf $(SANITIZE_DIR) + @$(MAKE) --no-print-directory BUILD_DIR=$(SANITIZE_DIR) OPT="$(SANITIZE_OPT)" \ + $(addprefix $(SANITIZE_DIR)/bin/,$(SANITIZE_BINS)) >/dev/null \ + || { rm -rf $(SANITIZE_DIR); exit 1; } + @echo "Sanitizers (ASan + UBSan):" + @for bin in $(SANITIZE_BINS); do \ + if ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \ + $(SANITIZE_DIR)/bin/$$bin >$(SANITIZE_DIR)/$$bin.log; then \ + printf ' %-26s no errors detected\n' "$$bin"; \ + else \ + cat $(SANITIZE_DIR)/$$bin.log; \ + printf ' %-26s FAILED\n' "$$bin"; \ + rm -rf $(SANITIZE_DIR); \ + exit 1; \ + fi; \ + done + @rm -rf $(SANITIZE_DIR) + @echo "Result: PASS" + distclean: clean find . -name "*.o" -delete @@ -83,11 +131,12 @@ help: @echo "EmbeddedSpaceWire Build System" @echo "==============================" @echo "Targets:" - @echo " all - Build library and examples (default)" + @echo " all - Build the libraries, run the unit tests and the example (default)" @echo " lib - Build static and shared libraries" - @echo " example - Build example application" - @echo " test - Build and run unit tests" + @echo " example - Build and run the example application" + @echo " test - Build and run unit tests (default and overridden payload hooks)" @echo " coverage-html - Generate HTML coverage report" + @echo " sanitize - Build with ASan + UBSan and run the tests and the example" @echo " output - Build artifacts are written to ./build" @echo " clean - Remove build artifacts" @echo " distclean - Clean including object files" diff --git a/README.md b/README.md index 4937c23..ca4e308 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,19 @@ Minimal, embedded-optimized implementation of the **SpaceWire** packet and netwo - **ECSS-E-ST-50-12C** Rev.1: SpaceWire — links, nodes, routers and networks - **ECSS-E-ST-50-53C**: SpaceWire — CCSDS packet transfer protocol - **ECSS-E-ST-50-51C**: SpaceWire — protocol identification (Protocol ID `0x02`) -- **CCSDS 133.0-B-2**: Space Packet Protocol +- **CCSDS 133.0-B-2**: Space Packet Protocol — the packet being carried; supplied by + the application through the payload adapter, not implemented here ## Features ### Core Protocol Implementation - **Network layer**: SpaceWire packets and routing — path (0–31) and logical - (32–254) addressing with header deletion (ECSS-E-ST-50-12C §5.6) -- **CCSDS Packet Transfer Protocol**: encapsulation/extraction of CCSDS Space - Packets with EOP/EEP receive status (ECSS-E-ST-50-53C) -- **CCSDS Integration**: built on the EmbeddedSpacePacket library + (32–254) addressing with header deletion (ECSS-E-ST-50-12C §5.6.8) +- **CCSDS Packet Transfer Protocol**: encapsulation/extraction of the carried + packet with EOP/EEP receive status (ECSS-E-ST-50-53C) +- **Payload adapter**: the carried packet is reached through three overridable + hooks, so the application plugs in its own Space Packet implementation ### Scope (hardware boundary) @@ -31,7 +33,7 @@ library as out-of-band metadata, not as bytes within packet buffers. - **Minimal footprint**: small static library, no dynamic allocation - **Zero allocation**: stack- and caller-owned buffers only -- **Embedded-optimized**: no external dependencies except EmbeddedSpacePacket +- **Standalone**: no external dependencies - **Portable**: pure C99, big-endian network byte order - **Standards-driven**: no non-standard framing or checksums on the wire @@ -40,39 +42,57 @@ library as out-of-band metadata, not as bytes within packet buffers. ``` EmbeddedSpaceWire/ ├── include/ -│ ├── spacewire.h # SpaceWire packet + network (routing) layer -│ └── spacewire_packet.h # CCSDS packet transfer protocol (ECSS-E-ST-50-53C) +│ ├── spacewire.h # SpaceWire packet + network (routing) layer +│ ├── spacewire_ptp.h # CCSDS packet transfer protocol (ECSS-E-ST-50-53C) +│ └── spacewire_payload.h # Payload adapter hooks ├── src/ -│ ├── spacewire_frame.c # SpaceWire packet builder -│ ├── spacewire_router.c # Routing switch + link state -│ └── spacewire_packet.c # CCSDS packet transfer protocol -├── external/ -│ └── EmbeddedSpacePacket/ # CCSDS Space Packet library (submodule) +│ ├── spacewire_spw_packet.c # SpaceWire packet builder +│ ├── spacewire_router.c # Routing switch +│ ├── spacewire_ptp.c # CCSDS packet transfer protocol +│ └── weak/ +│ └── spacewire_payload.c # Default (raw-octet) payload hooks, replaceable +├── docs/ +│ └── space_packet_integration.md # Carrying CCSDS Space Packets ├── examples/ -│ └── main.c # End-to-end usage +│ └── main.c # End-to-end usage ├── tests/ -│ ├── cunit.h # Tiny C test helpers -│ ├── test_frame.c # Packet-builder tests -│ ├── test_router.c # Routing + link tests -│ ├── test_packet.c # CCSDS PTP tests (+ golden wire vector) -│ └── unit_tests.c # Test runner +│ ├── cunit.h # Tiny C test helpers +│ ├── test_runners.h # Suite runner declarations +│ ├── test_spw_packet.c # Packet-builder tests +│ ├── test_router.c # Routing tests +│ ├── test_ptp.c # CCSDS PTP tests (+ golden wire vector) +│ ├── test_payload.c # Default payload hook tests +│ ├── test_payload_override.c # Overridden payload hooks (own binary) +│ └── unit_tests.c # Test runner ├── tools/ -│ └── coverage_html.sh # HTML coverage generator +│ └── coverage_html.sh # HTML coverage report + 100% gate +├── .github/workflows/ +│ └── ci.yml # CI: tests, coverage, sanitizers, format +├── .clang-format # Code style ├── Makefile └── README.md ``` ## Building -### Prerequisites +### Requirements -This repository vendors the CCSDS Space Packet library as a git submodule. -After cloning, initialise it: +- A C99 compiler. The build must be warning-free under both **gcc** and + **clang**; CI checks both. +- `make` and `ar` +- Optional: `gcovr` for coverage, `clang-format` for the format check + +Pick the compiler with `CC`: ```bash -git submodule update --init --recursive +make CC=gcc +make CC=clang ``` +The Makefile builds with `-Werror` and a strict warning set (`-Wall -Wextra +-Wpedantic -Wconversion -Wshadow` and others), so any warning fails the build. +`OPT` sets the optimisation flags (default `-O2`), e.g. `make OPT="-O0 -g"`. + ### Build Everything ```bash @@ -99,7 +119,22 @@ make example make test ``` -Test binary path: `build/bin/spacewire_tests`. +Two binaries are built and run: `build/bin/spacewire_tests` uses the default +payload hooks, and `build/bin/spacewire_override_tests` replaces them with its +own. + +### Sanitizers + +```bash +make sanitize +make sanitize CC=clang +``` + +Rebuilds the library, both test binaries and the example with AddressSanitizer +and UndefinedBehaviorSanitizer (leak detection on, every report fatal), runs +them, and prints one result line per binary. Program output is shown only on +failure. The instrumented build goes to `build/sanitize/` and is removed +afterwards. ### Coverage (HTML) @@ -107,7 +142,36 @@ Test binary path: `build/bin/spacewire_tests`. make coverage-html ``` -Coverage report is generated at `build/coverage/index.html`. +Requires `gcovr`. Runs `make clean` first, so any existing `build/` output is +removed. It then builds instrumented tests in `build/coverage-build/`, runs +both test binaries, and writes the report to `build/coverage/index.html`. +Coverage of `src/` must be **100% line and 100% branch**; below that the +command still writes the report, then exits non-zero. + +### Formatting + +Code style is defined in [.clang-format](.clang-format). Check every C file +with: + +```bash +git ls-files '*.c' '*.h' | xargs clang-format --dry-run --Werror +``` + +Drop `--dry-run --Werror` and add `-i` to reformat in place. + +### Continuous Integration + +[.github/workflows/ci.yml](.github/workflows/ci.yml) runs on every push to +`main` and on every pull request: + +| Job | What it checks | +|---|---| +| Unit tests | `make` with gcc and with clang: libraries, both test binaries, example | +| Coverage | `make coverage-html`; fails below 100% line or branch; report uploaded as an artifact | +| Sanitizers | `make sanitize` with gcc and with clang | +| clang-format | Every tracked `.c` and `.h` file matches `.clang-format` | + +Run the same checks locally before pushing. ### Clean @@ -120,21 +184,46 @@ make distclean # Deep clean including object files See [examples/main.c](examples/main.c). +## Payload Adapter + +The packet transfer layer never looks inside the packet it carries. It sizes, +serialises and parses it through three hooks declared in +[include/spacewire_payload.h](include/spacewire_payload.h): + +| Hook | Purpose | +|---|---| +| `sw_payload_size()` | Serialised size of a payload object | +| `sw_payload_serialize()` | Write a payload object into the output buffer | +| `sw_payload_parse()` | Fill a payload object from a received buffer | + +The library ships weak default hooks in `src/weak/` that treat the payload as +raw octets (`sw_payload_raw_t`), so it works on its own. To carry a structured +protocol, define the same three functions in the application; they replace the +defaults at link time. An application that compiles the library sources itself +and overrides all three hooks can leave `src/weak/` out of its build. + +The overrides must be linked as an object file. Overrides that exist only +inside a static library are silently ignored by the linker. + +[docs/space_packet_integration.md](docs/space_packet_integration.md) shows the +overrides for CCSDS Space Packets using EmbeddedSpacePacket. ## Memory Usage -- **Library code**: ~3 KB (`.text`); no dynamic allocation, all buffers caller-owned -- **`sw_packet_frame_t`**: 40 bytes (CCSDS PTP packet state) -- **`sw_router_t`**: ~1.4 KB — a 256-entry routing table (3 B/entry) plus per-port - link state; set `-DSW_NUM_PORTS=n` to shrink the per-router footprint +Figures for x86-64: + +- **Library code**: ~2.2 KB (`.text`); no dynamic allocation, all buffers caller-owned +- **`sw_ptp_frame_t`**: 16 bytes (CCSDS PTP addressing fields) +- **`sw_router_t`**: 784 bytes — a 256-entry routing table (3 B/entry) plus + counters ## Thread Safety -Packet, routing and link operations work entirely on caller-owned state passed +Packet and routing operations work entirely on caller-owned state passed by pointer and take no internal locks, so independent objects can be used from independent tasks. The one shared item is the library's statistics block: -`sw_packet_encode` and `sw_packet_decode` update a process-global counter (read -via `sw_get_statistics`, cleared via `sw_reset_statistics`), which is **not** +`sw_ptp_encode` and `sw_ptp_decode` update a process-global counter (read +via `sw_ptp_get_statistics`, cleared via `sw_ptp_reset_statistics`), which is **not** thread-safe. In a multi-task system, serialise the encode/decode/statistics calls or confine them to a single task. @@ -144,7 +233,9 @@ The library implements the packet and network layers; the following are out of scope or not yet implemented: - Character/signal and data-link levels — provided by the SpaceWire hardware CODEC -- Time-codes, broadcast codes and distributed interrupts (ECSS-E-ST-50-12C §5.6) +- Building and validating the carried CCSDS Space Packet — provided by the + application through the payload adapter +- Time-codes, broadcast codes and distributed interrupts (ECSS-E-ST-50-12C §5.6.3–5.6.5) - Group adaptive routing (one output port per logical address) - Guaranteed delivery — per ECSS-E-ST-50-53C the service is unconfirmed and incomplete (no acknowledgement, retransmission or QoS) diff --git a/docs/ECSS-E-ST-50-12C-Rev.1(15May2019).pdf b/docs/ECSS-E-ST-50-12C-Rev.1(15May2019).pdf deleted file mode 100644 index b220d6e..0000000 Binary files a/docs/ECSS-E-ST-50-12C-Rev.1(15May2019).pdf and /dev/null differ diff --git a/docs/ECSS-E-ST-50-53C5February2010.pdf b/docs/ECSS-E-ST-50-53C5February2010.pdf deleted file mode 100644 index d93c793..0000000 Binary files a/docs/ECSS-E-ST-50-53C5February2010.pdf and /dev/null differ diff --git a/docs/space_packet_integration.md b/docs/space_packet_integration.md new file mode 100644 index 0000000..23192d2 --- /dev/null +++ b/docs/space_packet_integration.md @@ -0,0 +1,141 @@ +# Carrying CCSDS Space Packets + +EmbeddedSpaceWire contains no Space Packet code. Its packet transfer layer +(ECSS-E-ST-50-53C) reaches the packet it carries through three hooks declared in +[`include/spacewire_payload.h`](../include/spacewire_payload.h). The library's +default hooks treat that packet as raw octets. This guide replaces them so that +`sw_ptp_encode()` and `sw_ptp_decode()` work directly on `sp_packet_t` +objects from [EmbeddedSpacePacket](https://github.com/OpenSpaceCode/EmbeddedSpacePacket). + +The code below was built and run against EmbeddedSpacePacket v2.0.0 with gcc +and clang. + +## 1. Add the overrides to your application + +Create one source file in the application, for example +`sw_payload_space_packet.c`: + +```c +#include "space_packet.h" +#include "spacewire_payload.h" + +uint32_t sw_payload_size(const void *payload) +{ + return sp_packet_serialize_size(payload); +} + +sw_status_t sw_payload_serialize(const void *payload, uint8_t *buf, uint32_t buf_len) +{ + /* sp_packet_serialize() returns the octets written, which is the serialised size. */ + return (sp_packet_serialize(payload, buf, buf_len) != 0u) ? SW_OK : SW_PAYLOAD_ERROR; +} + +sw_status_t sw_payload_parse(void *payload, const uint8_t *buf, uint32_t buf_len) +{ + /* sp_packet_parse() returns 1 on success and 0 on failure. */ + return (sp_packet_parse(payload, buf, buf_len) != 0) ? SW_OK : SW_PAYLOAD_ERROR; +} +``` + +Define all three. Together they decide what type sits behind the `void *` +payload object; a mix of overridden and default hooks would disagree about it. + +## 2. Link the overrides as an object file + +```sh +cc -Iinclude -Ipath/to/EmbeddedSpacePacket/include \ + app.c sw_payload_space_packet.c path/to/EmbeddedSpacePacket/src/space_packet.c \ + build/lib/libspacewire.a -o app +``` + +The overrides must reach the linker as an object file (or a source file on the +compiler command line). If they exist only inside a static library, the linker +keeps the weak defaults without any warning, and the defaults then misread +your `sp_packet_t` as raw octets. + +To check which definitions were linked: + +```sh +nm app | grep sw_payload_ +``` + +`T` marks your overrides; `W` means the weak defaults are still in use. + +If you compile the library sources into the application instead of linking +`libspacewire.a`, you can leave `src/weak/` out of the build altogether: it +holds nothing but the default hooks. + +## 3. Pass `sp_packet_t` as the payload object + +```c +#include "space_packet.h" +#include "spacewire_ptp.h" + +static const uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + +sp_packet_t pkt; +sp_packet_init(&pkt); +sp_set_primary_header(&pkt, SP_PACKET_TYPE_TC, 0, 0x042, SP_SEQ_FLAG_UNSEGMENTED, 1); +sp_set_data(&pkt, data, sizeof(data)); + +const sw_ptp_frame_t frame = {.path = NULL, + .path_len = 0, + .logical_addr = SW_PTP_LOGICAL_ADDR_DEFAULT, + .user_app = 0x00}; +uint8_t wire[64]; +uint32_t n = 0; +if (sw_ptp_encode(&frame, &pkt, wire, sizeof(wire), &n) == SW_OK) +{ + /* wire[0..n-1]: FE 02 00 00 | 10 42 C0 01 00 03 | DE AD BE EF */ +} + +sw_ptp_frame_t rx_frame; +sp_packet_t rx_pkt; +sw_ptp_status_t status; +if (sw_ptp_decode(&rx_frame, &rx_pkt, wire, n, SW_END_EOP, &status) == SW_OK) +{ + /* rx_pkt.ph.apid == 0x042; rx_pkt.data points into wire (zero-copy). */ +} +``` + +`rx_pkt.data` points into `wire`, so keep the receive buffer alive for as long +as the decoded packet is in use. + +## What the receive path rejects + +The packet transfer layer checks only the encapsulation header: end marker, +Protocol Identifier and Reserved field (clause 5.5.4). It does not check the +carried packet's length or format (clauses 4.3 and 5.5.4.2 a); every octet after +the header goes to `sw_payload_parse()`. + +With these overrides, `sw_ptp_decode()` therefore leaves `rx_frame`, `rx_pkt` +and `status` unchanged and returns: + +| Input | Returns | Detected by | +|---|---|---| +| Fewer than 4 octets (no complete header) | `SW_WRONG_LENGTH` | the packet transfer layer | +| Protocol Identifier other than 0x02 | `SW_WRONG_PROTOCOL` | the packet transfer layer (clause 5.5.4.1) | +| Fewer than 6 octets after the header | `SW_PAYLOAD_ERROR` | `sp_packet_parse()` | +| Packet Version Number other than 0 | `SW_PAYLOAD_ERROR` | `sp_packet_parse()` | +| Space Packet cut short | `SW_PAYLOAD_ERROR` | `sp_packet_parse()` | + +The two discards the standard defines, EEP and a non-zero Reserved field, return +`SW_DISCARDED` and set `status` to `SW_PTP_STATUS_EEP` or +`SW_PTP_STATUS_RESERVED_NONZERO`. + +Octets left over after the Space Packet are not rejected: `sp_packet_parse()` +reads the length the packet declares and ignores the rest. + +## Contract for any override + +The same rules apply if you carry a protocol other than Space Packets: + +- Return `SW_OK` on success and a negative `sw_status_t` on failure (any code; + the library reports every hook failure as `SW_PAYLOAD_ERROR`). +- Leave outputs unchanged on failure. `sw_ptp_encode()` and + `sw_ptp_decode()` rely on this to keep their own buffers and objects + untouched when they fail. +- On `SW_OK`, `sw_payload_serialize()` must have written exactly + `sw_payload_size()` octets for the same object. +- `sw_payload_parse()` receives every octet after the header, unchecked. It must + not read past `buf_len`, and returns an error if it cannot extract a packet. diff --git a/examples/main.c b/examples/main.c index 91cfcd9..079de27 100644 --- a/examples/main.c +++ b/examples/main.c @@ -1,179 +1,220 @@ /** - * @file main.c - * @brief EmbeddedSpaceWire example — SpaceWire packets, routing and CCSDS packet transfer. + * @file main.c + * @brief EmbeddedSpaceWire example: SpaceWire packets, packet transfer and routing * - * The character/signal and link levels are handled by the SpaceWire hardware - * CODEC; this example exercises the packet and network layers in software. + * Exercises the packet and network layers in software, using the default + * payload hooks, so the carried packet is raw octets. The character/signal and + * link levels are handled by the SpaceWire hardware CODEC. + * See also: docs/space_packet_integration.md + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "spacewire.h" -#include "spacewire_packet.h" +#include "spacewire_payload.h" +#include "spacewire_ptp.h" +#include #include -#include -int main(void) +/** + * @brief Print a labelled buffer as hex octets on one line. + * + * @param[in] label Text printed before the octets. + * @param[in] buf Octets to print. + * @param[in] len Number of octets. + */ +static void example_print_hex(const char *label, const uint8_t *buf, size_t len) { - printf("===============================================\n"); - printf("SpaceWire packet + network layer - Example\n"); - printf("===============================================\n\n"); - - /* ========== SPACEWIRE PACKET (LOGICAL ADDRESSING) ========== */ - printf("[1] SpaceWire Packet Test\n"); - printf(" Building a logical-addressed packet (no CRC; EOP added by link)\n"); - - const uint8_t cargo_data[] = {0x01, 0x02, 0x03, 0x04, 0x05}; - const uint8_t logical_dest[] = {0x40}; /* logical address 64 */ - - uint8_t spw_buf[256]; - size_t spw_len = sw_spw_packet_build(logical_dest, - sizeof(logical_dest), - cargo_data, - sizeof(cargo_data), - spw_buf, - sizeof(spw_buf)); - printf(" Packet size: %zu bytes\n", spw_len); - printf(" Packet data (hex): "); - for (size_t i = 0; i < spw_len; i++) + printf(" %-18s", label); + for (size_t i = 0; i < len; i++) { - printf("%02X ", spw_buf[i]); + printf(" %02X", buf[i]); } - printf("\n ✓ Packet build working\n\n"); - - /* ========== SPACEWIRE PACKET (PATH ADDRESSING) ========== */ - printf("[2] SpaceWire Path Addressing Test\n"); - printf(" Building a multi-hop path-addressed packet\n"); - - const uint8_t path_dest[] = {2, 1, 3}; /* take port 2, then 1, then 3 */ - uint8_t path_buf[256]; - size_t path_len = sw_spw_packet_build(path_dest, - sizeof(path_dest), - cargo_data, - sizeof(cargo_data), - path_buf, - sizeof(path_buf)); - printf(" Path address: %u hops, packet %zu bytes\n", (unsigned)sizeof(path_dest), path_len); - printf(" Packet data (hex): "); - for (size_t i = 0; i < path_len; i++) + printf("\n"); +} + +/** + * @brief Build a logical-addressed and a path-addressed SpaceWire packet + * (ECSS-E-ST-50-12C clause 5.6.2). + * + * @return 0 on success, 1 on failure. + */ +static int example_spw_packets(void) +{ + static const uint8_t cargo[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + static const uint8_t logical_dest[] = {0x40}; + static const uint8_t path_dest[] = {2, 1, 3}; /* leave by port 2, then 1, then 3 */ + uint8_t buf[16]; + + printf("[1] SpaceWire packets (no CRC; the link appends EOP)\n"); + + size_t len = 0; + if (sw_spw_packet_build(logical_dest, + sizeof(logical_dest), + cargo, + sizeof(cargo), + buf, + sizeof(buf), + &len) != SW_OK) { - printf("%02X ", path_buf[i]); + return 1; } - printf("\n ✓ Path-addressed packet built\n\n"); - - /* ========== CCSDS PACKET + SPACEWIRE INTEGRATION ========== */ - printf("[3] CCSDS Packet Transfer Protocol\n"); - printf(" Encapsulating a CCSDS packet (ECSS-E-ST-50-53C)\n"); - - sw_packet_config_t pkt_config = {.path = NULL, - .path_len = 0, - .logical_addr = SW_PTP_LOGICAL_ADDR_DEFAULT, - .user_app = 0x00}; - - sw_packet_frame_t pf; - sw_packet_init(&pf, &pkt_config); - - /* Set CCSDS packet fields */ - pf.packet.ph.apid = 0x0042; /* APID = 0x0042 */ - pf.packet.ph.seq_count = 1; - const char *msg = "Hello Space Wire"; - pf.packet.data = (const uint8_t *)msg; - pf.packet.data_len = (uint16_t)strlen(msg); - - uint8_t pkt_buf[512]; - size_t pkt_size = sw_packet_encode(&pf, pkt_buf, sizeof(pkt_buf)); - if (pkt_size > 0) + example_print_hex("logical-addressed:", buf, len); + + if (sw_spw_packet_build(path_dest, + sizeof(path_dest), + cargo, + sizeof(cargo), + buf, + sizeof(buf), + &len) != SW_OK) { - printf(" Packet frame size: %zu bytes\n", pkt_size); - printf(" APID: 0x%04X\n", pf.packet.ph.apid); - printf(" Payload: \"%s\"\n", msg); - printf(" ✓ Packet frame created\n\n"); + return 1; } + example_print_hex("path-addressed:", buf, len); + + return 0; +} - /* ========== PACKET DECODING EXAMPLE ========== */ - printf("[4] Packet Decoding Test\n"); - printf(" Extracting the CCSDS packet at the target\n"); +/** + * @brief Encapsulate a carried packet and extract it again (ECSS-E-ST-50-53C). + * + * @return 0 on success, 1 on failure. + */ +static int example_packet_transfer(void) +{ + /* With the default payload hooks the carried packet is raw octets: here a + * ready-made CCSDS Space Packet (TM, APID 0x042, 4 data octets). */ + static const uint8_t space_packet[] = + {0x00, 0x42, 0xC0, 0x01, 0x00, 0x03, 0xDE, 0xAD, 0xBE, 0xEF}; + const sw_payload_raw_t payload = {.data = space_packet, .len = sizeof(space_packet)}; + const sw_ptp_frame_t frame = {.path = NULL, + .path_len = 0, + .logical_addr = SW_PTP_LOGICAL_ADDR_DEFAULT, + .user_app = 0x00}; + uint8_t buf[32]; + + printf("[2] CCSDS Packet Transfer Protocol\n"); + + uint32_t len = 0; + if (sw_ptp_encode(&frame, &payload, buf, sizeof(buf), &len) != SW_OK) + { + return 1; + } + example_print_hex("encoded:", buf, len); - sw_packet_frame_t decoded_pf; + sw_ptp_frame_t received; + sw_payload_raw_t carried; sw_ptp_status_t status; - if (sw_packet_decode(&decoded_pf, pkt_buf, pkt_size, SW_END_EOP, &status) == SW_OK) + if (sw_ptp_decode(&received, &carried, buf, len, SW_END_EOP, &status) != SW_OK) + { + return 1; + } + printf(" decoded: logical address 0x%02X, user application 0x%02X, status %u\n", + received.logical_addr, + received.user_app, + (unsigned)status); + example_print_hex("carried packet:", carried.data, carried.len); + + return 0; +} + +/** + * @brief Route one packet and print the decision. + * + * @param[in,out] router Router to ask. + * @param[in] packet Packet octets; the destination address leads. + * @param[in] len Packet length in octets. + * @param[in] expected Status the example expects. + * @return 0 if the router decided as expected, 1 otherwise. + */ +static int example_route_one(sw_router_t *router, + const uint8_t *packet, + size_t len, + sw_status_t expected) +{ + uint8_t port = 0; + uint8_t delete_leading = 0; + const sw_status_t result = sw_router_route(router, packet, len, &port, &delete_leading); + + if (result == SW_OK) { - printf(" Decoded APID: 0x%04X\n", decoded_pf.packet.ph.apid); - printf(" Decoded user application: 0x%02X\n", decoded_pf.user_app); - printf(" Decoded payload length: %u bytes\n", decoded_pf.packet.data_len); - printf(" Decoded payload: \"%.*s\"\n", - decoded_pf.packet.data_len, - (const char *)decoded_pf.packet.data); - printf(" Status: %u (0 = OK)\n", (unsigned)status); - printf(" ✓ Packet decoding successful\n\n"); + printf(" leading 0x%02X -> port %u (delete leading: %u)\n", + packet[0], + port, + delete_leading); } else { - printf(" ✗ Packet decoding failed\n\n"); + printf(" leading 0x%02X -> discarded (status %d)\n", packet[0], (int)result); } - /* ========== ROUTER EXAMPLE ========== */ - printf("[5] Router Configuration Test\n"); - printf(" Setting up a routing table (port 0 = configuration port)\n"); + return (result == expected) ? 0 : 1; +} +/** + * @brief Configure a routing switch and route three packets + * (ECSS-E-ST-50-12C clause 5.6.8). + * + * @return 0 on success, 1 on failure. + */ +static int example_routing(void) +{ + static const uint8_t logical_pkt[] = {0x40, 0xAA, 0xBB}; + static const uint8_t path_pkt[] = {2, 0xAA, 0xBB}; + static const uint8_t unknown_pkt[] = {0x77, 0xAA}; sw_router_t router; - sw_router_init(&router, 4); /* ports 0..3 (port 0 = configuration port) */ + int failures = 0; - sw_router_add_route(&router, 0x40, 1, 0); /* logical 0x40 -> port 1, retain */ - sw_router_add_route(&router, 0x41, 2, 1); /* logical 0x41 -> port 2, delete */ + printf("[3] Routing (port 0 is the configuration port)\n"); - printf(" Number of ports: %u\n", router.num_ports); - printf(" Routing table configured:\n"); - printf(" logical 0x40 -> port 1 (address retained)\n"); - printf(" logical 0x41 -> port 2 (address deleted)\n"); - printf(" ✓ Router initialized\n\n"); + sw_router_init(&router, 4); + if ((sw_router_add_route(&router, 0x40, 1, 0) != SW_OK) || + (sw_router_add_route(&router, 0x41, 2, 1) != SW_OK)) + { + return 1; + } - /* ========== ROUTING EXAMPLE ========== */ - printf("[6] Packet Routing Test\n"); - printf(" Routing by leading destination-address character\n"); + failures += example_route_one(&router, logical_pkt, sizeof(logical_pkt), SW_OK); + failures += example_route_one(&router, path_pkt, sizeof(path_pkt), SW_OK); + failures += example_route_one(&router, unknown_pkt, sizeof(unknown_pkt), SW_WRONG_ADDRESS); + printf(" invalid-address errors: %" PRIu32 "\n", router.invalid_address_errors); - uint8_t out_port = 0; - uint8_t delete_leading = 0; + return (failures == 0) ? 0 : 1; +} - /* Logical addressing: leading char 0x40 is looked up in the routing table. */ - const uint8_t logical_pkt[] = {0x40, 0xAA, 0xBB}; - if (sw_router_route(&router, logical_pkt, sizeof(logical_pkt), &out_port, &delete_leading) == - SW_ROUTE_OK) - { - printf(" logical 0x40 -> port %u (delete leading: %u)\n", out_port, delete_leading); - } +/** + * @brief Print the packet transfer counters gathered by the steps above. + */ +static void example_statistics(void) +{ + sw_ptp_statistics_t stats; + + sw_ptp_get_statistics(&stats); + printf("[4] Statistics\n"); + printf(" packets sent/received/discarded: %" PRIu32 "/%" PRIu32 "/%" PRIu32 "\n", + stats.packets_sent, + stats.packets_received, + stats.packets_discarded); + printf(" octets sent/received: %" PRIu32 "/%" PRIu32 "\n", + stats.bytes_sent, + stats.bytes_received); +} - /* Path addressing: leading char 2 selects port 2 directly and is deleted. */ - const uint8_t path_pkt[] = {2, 0xAA, 0xBB}; - if (sw_router_route(&router, path_pkt, sizeof(path_pkt), &out_port, &delete_leading) == - SW_ROUTE_OK) - { - printf(" path 2 -> port %u (delete leading: %u)\n", out_port, delete_leading); - } +int main(void) +{ + int failures = 0; - /* An unconfigured logical address is discarded with an invalid-address error. */ - const uint8_t bad_pkt[] = {0x77, 0xAA}; - if (sw_router_route(&router, bad_pkt, sizeof(bad_pkt), &out_port, &delete_leading) == - SW_ROUTE_DISCARD) - { - printf(" logical 0x77 -> DISCARD (invalid-address errors: %u)\n", - router.invalid_address_errors); - } - printf(" ✓ Routing successful\n\n"); - - /* ========== STATISTICS ========== */ - printf("[7] Statistics\n"); - sw_statistics_t stats; - sw_get_statistics(&stats); - printf(" Packets sent: %u\n", stats.packets_sent); - printf(" Packets received: %u\n", stats.packets_received); - printf(" Bytes sent: %u\n", stats.bytes_sent); - printf(" Bytes received: %u\n", stats.bytes_received); - printf(" Packets discarded: %u\n", stats.packets_discarded); - printf(" ✓ Statistics available\n\n"); - - printf("===============================================\n"); - printf("All tests completed successfully!\n"); - printf("===============================================\n"); + failures += example_spw_packets(); + failures += example_packet_transfer(); + failures += example_routing(); + example_statistics(); - return 0; + printf("%s\n", (failures == 0) ? "Example completed." : "Example FAILED."); + + return (failures == 0) ? 0 : 1; } diff --git a/external/EmbeddedSpacePacket b/external/EmbeddedSpacePacket deleted file mode 160000 index 9f92cce..0000000 --- a/external/EmbeddedSpacePacket +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9f92cce63f7979cb3521a2f34b9745b4f9346483 diff --git a/include/spacewire.h b/include/spacewire.h index c1d1139..64667d7 100644 --- a/include/spacewire.h +++ b/include/spacewire.h @@ -1,20 +1,24 @@ /** - * @file spacewire.h - * @brief SpaceWire packet and network (routing) layers. + * @file spacewire.h + * @brief SpaceWire packet builder and routing switch * - * Minimal, embedded-optimized implementation following: - * - ECSS-E-ST-50-12C Rev.1 (SpaceWire — links, nodes, routers and networks): - * the SpaceWire packet structure and routing (this file). - * - ECSS-E-ST-50-53C (SpaceWire — CCSDS packet transfer protocol): see - * spacewire_packet.h. + * Minimal, embedded-optimized implementation of the SpaceWire packet + * structure and routing as per ECSS-E-ST-50-12C Rev.1 (SpaceWire — links, + * nodes, routers and networks), clauses 5.6.2 and 5.6.8. The CCSDS packet + * transfer protocol (ECSS-E-ST-50-53C) is declared in spacewire_ptp.h. * * Scope: this library implements the packet and network layers only. The * character/signal and data-link levels — character encoding and parity, * data-strobe (DS) signalling, link initialisation and flow control, and the * generation/detection of the EOP/EEP end-of-packet markers — are provided by * the SpaceWire hardware CODEC. EOP/EEP are therefore exchanged with this - * library as out-of-band metadata (see ::sw_end_marker_t in spacewire_packet.h), - * not as bytes within packet buffers. + * library as out-of-band metadata (::sw_end_marker_t), not as bytes within + * packet buffers. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef SPACEWIRE_H @@ -28,72 +32,15 @@ extern "C" { #endif -/* ============================================================================ - * RETURN CODES - * ============================================================================ */ +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ -/** - * @brief Generic success/failure result for functions that report status. - * - * Values are chosen so that ::SW_OK is truthy and ::SW_ERR is falsy. - */ -typedef enum -{ - SW_OK = 0, /**< Operation succeeded, or a packet was delivered. */ - SW_ERR = -0x01, /**< Operation failed, or no packet was delivered. */ - SW_INVALID_PARAM = -0x02, /**< Invalid parameter */ - SW_WRONG_ADDRESS = -0x03, /**< Invalid address */ - SW_WRONG_PORT = -0x04, /**< Invalid port */ -} sw_result_t; - -/* ============================================================================ - * SPACEWIRE PACKET (ECSS-E-ST-50-12C clause 5.6.2) - * ============================================================================ */ - -/** - * @brief Build a SpaceWire packet by prefixing a destination address to a cargo. - * - * A SpaceWire packet is a destination address followed by cargo and terminated - * by an EOP/EEP marker (clause 5.6.2). The marker is a link-layer control - * character emitted by the hardware CODEC, so it is not stored in the buffer, - * and no checksum is added. Copies `[dest][cargo]` into @p buf; either part may - * be empty (a NULL pointer with length 0). - * - * @param[in] dest Destination-address octets, or NULL if @p dest_len is 0. - * @param[in] dest_len Number of destination-address octets. - * @param[in] cargo Cargo octets, or NULL if @p cargo_len is 0. - * @param[in] cargo_len Number of cargo octets. - * @param[out] buf Output buffer. - * @param[in] buf_len Buffer capacity in octets. - * @return Total octets written, or 0 on error (a NULL pointer paired with a - * non-zero length, an empty result, or a buffer that is too small). - */ -size_t sw_spw_packet_build(const uint8_t *dest, - size_t dest_len, - const uint8_t *cargo, - size_t cargo_len, - uint8_t *buf, - size_t buf_len); - -/* ============================================================================ - * SPACEWIRE ROUTING (ECSS-E-ST-50-12C clause 5.6.8) - * ============================================================================ */ - -/** @brief Configuration port number; reachable by path addressing only (clause 5.6.8.2). */ +/** @brief Configuration port; reached by path addressing only (clauses 5.6.8.2 a, 5.6.8.3 f). */ #define SW_PORT_CONFIG 0u -/** @brief Highest external port number (clause 5.6.8.2). */ -#define SW_PORT_EXTERNAL_MAX 31u - -/** - * @brief Number of ports a router can have (0..31). - * - * Override with `-DSW_NUM_PORTS=n` to shrink the per-router footprint for small - * nodes. - */ -#ifndef SW_NUM_PORTS -# define SW_NUM_PORTS 32u -#endif +/** @brief Most ports a router can have, configuration port 0 included (clause 5.6.8.2). */ +#define SW_NUM_PORTS 32u /** @brief Largest path-address character; 0..31 select an output port (clause 5.6.8.3). */ #define SW_PATH_ADDR_MAX 31u @@ -104,52 +51,61 @@ size_t sw_spw_packet_build(const uint8_t *dest, /** @brief Reserved logical address that must not be used (clause 5.6.8.4 d). */ #define SW_LOGICAL_ADDR_RESERVED 255u -/** @brief Default Target Logical Address. */ -#define SW_LOGICAL_ADDR_DEFAULT 254u - /** @brief Routing-table size; indexed directly by the leading data character. */ #define SW_ROUTE_TABLE_SIZE 256u +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + /** - * @brief SpaceWire link state (clause 5.5.7 link initialisation). + * @brief Status returned by every library function that can fail, and by the + * payload adapter hooks. + * + * ::SW_OK is zero and every error is negative, so `status < 0` tests for failure. + * Lengths are returned through an `out_len` parameter, never through the status. + * On any error, every output of the call is left unchanged. */ typedef enum { - SW_LINK_UNINITIALIZED = 0, /**< Link not yet started. */ - SW_LINK_READY = 1, /**< Ready to start. */ - SW_LINK_STARTED = 2, /**< Start requested. */ - SW_LINK_CONNECTED = 3, /**< Connected and running. */ - SW_LINK_ERROR = 4 /**< Link error. */ -} sw_link_state_t; - -/** - * @brief Per-port link state and counters. + SW_OK = 0, /**< Operation succeeded, or a packet was delivered. */ + SW_INVALID_PARAM = -0x01, /**< NULL pointer, pointer/length mismatch or bad argument. */ + SW_BUFFER_TOO_SMALL = -0x02, /**< Output buffer cannot hold the result. */ + SW_WRONG_ADDRESS = -0x03, /**< Address outside its range, or unroutable. */ + SW_WRONG_PORT = -0x04, /**< Port missing, or not usable for this route. */ + SW_WRONG_LENGTH = -0x05, /**< Packet or payload length out of range. */ + SW_WRONG_PROTOCOL = -0x06, /**< Protocol Identifier of another protocol. */ + SW_DISCARDED = -0x07, /**< Packet discarded by protocol rule; see its status. */ + SW_PAYLOAD_ERROR = -0x08, /**< A payload adapter hook failed. */ +} sw_status_t; + +/** + * @brief End-of-packet marker reported by the SpaceWire link layer (clause 5.4.3.2 d, e). + * + * EOP and EEP are control characters, not data octets, so they are exchanged + * with this library as out-of-band metadata rather than stored in packet buffers. */ -typedef struct +typedef enum { - uint8_t port_id; /**< Port number. */ - sw_link_state_t state; /**< Current link state. */ - uint32_t tx_packets; /**< Packets transmitted on this port. */ - uint32_t rx_packets; /**< Packets received on this port. */ - uint32_t errors; /**< Errors observed on this port. */ -} sw_link_t; + SW_END_EOP = 0, /**< Normal End Of Packet. */ + SW_END_EEP = 1 /**< Error End of Packet. */ +} sw_end_marker_t; /** * @brief One routing-table entry mapping a logical address to an output port. */ typedef struct { - uint8_t output_port; /**< Port the packet is forwarded through. */ + uint8_t output_port; /**< Port the packet is forwarded through; never ::SW_PORT_CONFIG. */ uint8_t configured; /**< 1 if this logical address has a valid route. */ uint8_t delete_addr; /**< 1 to delete the logical address before forwarding (clause 5.6.8.6). */ } sw_route_entry_t; /** - * @brief A SpaceWire routing switch: ports, a routing table and counters. + * @brief A SpaceWire routing switch: a port count, a routing table and counters. */ typedef struct { - sw_link_t links[SW_NUM_PORTS]; /**< Per-port link state. */ sw_route_entry_t routes[SW_ROUTE_TABLE_SIZE]; /**< Logical-address routing table. */ uint8_t num_ports; /**< Ports present (port 0 = config). */ uint32_t invalid_address_errors; /**< Invalid-address discards (clause 5.6.8.5). */ @@ -157,14 +113,38 @@ typedef struct uint32_t packets_discarded; /**< Packets discarded. */ } sw_router_t; +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + /** - * @brief Result of a routing decision. + * @brief Build a SpaceWire packet by prefixing a destination address to a cargo. + * + * A SpaceWire packet is a destination address followed by cargo and terminated + * by an EOP/EEP marker (clause 5.6.2). The marker is a link-layer control + * character emitted by the hardware CODEC, so it is not stored in the buffer, + * and no checksum is added. Copies `[dest][cargo]` into @p buf; either part may + * be empty (a NULL pointer with length 0). On failure, @p buf is left unchanged. + * + * @param[in] dest Destination-address octets, or NULL if @p dest_len is 0. + * @param[in] dest_len Number of destination-address octets. + * @param[in] cargo Cargo octets, or NULL if @p cargo_len is 0. + * @param[in] cargo_len Number of cargo octets. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] out_len Total octets written; set only on ::SW_OK. + * @return ::SW_OK on success; ::SW_INVALID_PARAM if @p buf or @p out_len is NULL + * or a NULL part has a non-zero length; ::SW_WRONG_LENGTH if both parts + * are empty (a packet has at least one data character, clause 5.6.2.1); + * ::SW_BUFFER_TOO_SMALL if the packet does not fit @p buf_len. */ -typedef enum -{ - SW_ROUTE_OK = 0, /**< Forward via the returned output port. */ - SW_ROUTE_DISCARD = 1 /**< Discard: empty packet, non-existent port, or unconfigured address. */ -} sw_route_result_t; +sw_status_t sw_spw_packet_build(const uint8_t *dest, + size_t dest_len, + const uint8_t *cargo, + size_t cargo_len, + uint8_t *buf, + size_t buf_len, + size_t *out_len); /** * @brief Initialize a router. @@ -178,14 +158,21 @@ void sw_router_init(sw_router_t *router, uint8_t num_ports); /** * @brief Configure a logical-address route (clause 5.6.8.4). * + * On failure, the routing table is left unchanged. + * * @param[in,out] router Target router. * @param[in] logical_addr Logical address to route; must be 32..254. - * @param[in] output_port Existing output port to forward through. + * @param[in] output_port Existing external output port to forward through + * (1..num_ports - 1). * @param[in] delete_addr Non-zero to delete the logical address before * forwarding (logical-address deletion, clause 5.6.8.6). - * @return ::SW_OK on success, error code otherwise. + * @return ::SW_OK on success, ::SW_INVALID_PARAM if @p router is NULL, + * ::SW_WRONG_ADDRESS if @p logical_addr is outside 32..254, or + * ::SW_WRONG_PORT if @p output_port is the configuration port, which is + * reached by path addressing only (clause 5.6.8.3 f), or a port the + * router does not have. */ -sw_result_t sw_router_add_route(sw_router_t *router, +sw_status_t sw_router_add_route(sw_router_t *router, uint8_t logical_addr, uint8_t output_port, int delete_addr); @@ -197,73 +184,27 @@ sw_result_t sw_router_add_route(sw_router_t *router, * logical addresses (32..254) are looked up in the routing table and retained * unless the entry requests deletion (Table 5-11, clauses 5.6.8.3–5.6.8.6). * + * On any error the packet is discarded, and @p output_port and + * @p delete_leading are left unchanged. + * * @param[in,out] router Router (its counters are updated). * @param[in] packet Packet octets (the destination address leads). * @param[in] len Packet length in octets. - * @param[out] output_port Selected output port (valid on ::SW_ROUTE_OK). + * @param[out] output_port Selected output port. * @param[out] delete_leading 1 if the leading character must be removed before - * forwarding, else 0 (valid on ::SW_ROUTE_OK). - * @return ::SW_ROUTE_OK to forward, or ::SW_ROUTE_DISCARD to drop the packet. A - * non-existent port or unconfigured address also bumps the router's - * invalid-address counter (clause 5.6.8.5). - */ -sw_route_result_t sw_router_route(sw_router_t *router, - const uint8_t *packet, - size_t len, - uint8_t *output_port, - uint8_t *delete_leading); - -/* ============================================================================ - * SPACEWIRE LINK LAYER - * ============================================================================ */ - -/** - * @brief Host-visible SpaceWire link configuration. - * - * The link layer proper (encoding, flow control, link initialisation) lives in - * the SpaceWire hardware CODEC; these fields configure and observe it. - */ -typedef struct -{ - uint32_t bit_rate; /**< Signalling rate in bits per second. */ - uint32_t disconnect_timeout; /**< Disconnect timeout in microseconds. */ - uint8_t rx_credit_max; /**< Maximum receive buffer credits. */ -} sw_link_config_t; - -/** - * @brief Link layer state. - */ -typedef struct -{ - sw_link_config_t config; /**< Link configuration. */ - sw_link_state_t state; /**< Current link state. */ - uint8_t rx_credits; /**< Available receive credits. */ - uint8_t tx_credits; /**< Available transmit credits. */ -} sw_link_layer_t; - -/** - * @brief Initialise the link layer from configuration. - * - * @param[out] link Link layer to initialise. No-op if NULL. - * @param[in] config Link configuration. No-op if NULL. - */ -void sw_link_init(sw_link_layer_t *link, const sw_link_config_t *config); - -/** - * @brief Get the current link state. - * - * @param[in] link Link layer. - * @return Current state, or ::SW_LINK_ERROR if @p link is NULL. - */ -sw_link_state_t sw_link_get_state(const sw_link_layer_t *link); - -/** - * @brief Set the link state. - * - * @param[out] link Link layer. No-op if NULL. - * @param[in] state New state. - */ -void sw_link_set_state(sw_link_layer_t *link, sw_link_state_t state); + * forwarding, else 0. + * @return ::SW_OK to forward through @p output_port; ::SW_INVALID_PARAM for a NULL + * argument (no counter is touched); ::SW_WRONG_LENGTH for an empty packet + * (clause 5.6.2.1); ::SW_WRONG_ADDRESS for a leading address that names a + * non-existent port, is unconfigured, or is a logical address routed to the + * configuration port, which also bumps the router's invalid-address counter + * (clauses 5.6.8.3 f, 5.6.8.5). + */ +sw_status_t sw_router_route(sw_router_t *router, + const uint8_t *packet, + size_t len, + uint8_t *output_port, + uint8_t *delete_leading); #ifdef __cplusplus } diff --git a/include/spacewire_packet.h b/include/spacewire_packet.h deleted file mode 100644 index 1e0b56f..0000000 --- a/include/spacewire_packet.h +++ /dev/null @@ -1,263 +0,0 @@ -/** - * @file spacewire_packet.h - * @brief CCSDS Packet Transfer Protocol over SpaceWire (ECSS-E-ST-50-53C). - * - * Encapsulates a CCSDS Space Packet (CCSDS 133.0-B-2) into a SpaceWire packet on - * the send path and extracts it on the receive path. The protocol adds no - * checksum of its own — ECSS-E-ST-50-53C defines none; error detection is - * provided by the underlying SpaceWire link (character parity) and by the - * EOP/EEP end-of-packet markers. - */ - -#ifndef SPACEWIRE_PACKET_H -#define SPACEWIRE_PACKET_H - -#include "EmbeddedSpacePacket/include/space_packet.h" -#include "spacewire.h" - -#ifdef __cplusplus -extern "C" -{ -#endif - -/** @brief Protocol Identifier for the CCSDS Packet Transfer Protocol (clause 5.3.3). */ -#define SW_PTP_PROTOCOL_ID 0x02u - -/** @brief Reserved field value (clause 5.3.4). */ -#define SW_PTP_RESERVED 0x00u - -/** - * @brief Default Target Logical Address when the target has no specific one - * (clause 5.3.2, NOTE 2). - * - * @note Logical address 255 (0xFF) is reserved and must not be used. - */ -#define SW_PTP_LOGICAL_ADDR_DEFAULT 0xFEu - -/** - * @brief Encapsulation header length in octets. - * - * Target Logical Address + Protocol Identifier + Reserved + User Application. - */ -#define SW_PTP_HEADER_LEN 4u - -/** @brief Minimum CCSDS packet length: 6-octet primary header + 1 data octet (clause 5.1.2). */ -#define SW_PTP_CCSDS_MIN_LEN 7u - -/** @brief Maximum CCSDS packet length: 6-octet primary header + 65536 data octets (clause 5.1.2). - */ -#define SW_PTP_CCSDS_MAX_LEN 65542u - -/** - * @brief Largest valid path-address octet. - * - * A SpaceWire path address character selects an output port and is in the range - * 0..31 (ECSS-E-ST-50-12C clause 5.6.8.3). - */ -#define SW_PTP_PATH_OCTET_MAX 31u - -/** - * @brief End-of-packet marker supplied by the SpaceWire link layer - * (ECSS-E-ST-50-12C clause 5.4.3.2). - * - * In a SpaceWire network the terminator is a control character, not a data - * octet, so it is carried here as out-of-band metadata rather than appended to - * the encoded buffer. - */ -typedef enum -{ - SW_END_EOP = 0, /**< Normal End Of Packet. */ - SW_END_EEP = 1 /**< Error End of Packet. */ -} sw_end_marker_t; - -/** - * @brief Receive status reported to the target user application (clauses 5.1.3, 5.2.3.2). - * - * The first three values are the status codes defined by the standard. - * ::SW_PTP_STATUS_INVALID is an implementation sentinel for input that is not a - * recognisable CCSDS PTP packet. - */ -typedef enum -{ - SW_PTP_STATUS_OK = 0x00, /**< Packet arrived with no known error. */ - SW_PTP_STATUS_EEP = 0x01, /**< Packet terminated by EEP. */ - SW_PTP_STATUS_RESERVED_NONZERO = 0x02, /**< Reserved field was non-zero. */ - SW_PTP_STATUS_INVALID = 0xFF /**< Not a CCSDS PTP packet / malformed. */ -} sw_ptp_status_t; - -/** - * @brief A CCSDS Packet Transfer Protocol packet. - * - * The wire layout produced by sw_packet_encode() follows ECSS-E-ST-50-53C - * Figure 5-1: - * - * [ Target SpW Address: path, 0..N octets, each 0-31 ] (optional) - * [ Target Logical Address | 0x02 | 0x00 | User Application ] - * [ CCSDS Space Packet: 6 + data_len octets ] - * - * followed by an EOP that the link layer appends (not stored in the buffer). - * - * @note The Target SpaceWire (path) Address is stripped by the network before - * the packet reaches the target, so it is absent on the receive path. - */ -typedef struct -{ - const uint8_t *path; /**< Target SpaceWire (path) Address; NULL if unused (clause 5.3.1). */ - uint8_t path_len; /**< Number of path octets (each must be 0..31). */ - uint8_t logical_addr; /**< Target Logical Address (clause 5.3.2). */ - uint8_t user_app; /**< User Application field; may carry a virtual channel (clause 5.3.5). */ - sp_packet_t packet; /**< Encapsulated CCSDS Space Packet. */ -} sw_packet_frame_t; - -/** - * @brief Configuration for a CCSDS PTP packet endpoint. - */ -typedef struct -{ - const uint8_t *path; /**< Optional path address; NULL if unused. */ - uint8_t path_len; /**< Number of path octets. */ - uint8_t logical_addr; /**< Target Logical Address. */ - uint8_t user_app; /**< User Application value. */ -} sw_packet_config_t; - -/** - * @brief Virtual channel number. - * - * ECSS-E-ST-50-53C clause 5.3.5 (NOTE 2) allows the User Application field to - * carry a virtual channel number, so a virtual channel is represented as that - * 8-bit value rather than as separate router state. - */ -typedef uint8_t sw_virtual_channel_t; - -/** - * @brief Select a virtual channel by setting the User Application field. - * - * @param[out] pf Packet frame. No-op if NULL. - * @param[in] vc Virtual channel number. - */ -static inline void sw_packet_set_virtual_channel(sw_packet_frame_t *pf, sw_virtual_channel_t vc) -{ - if (pf) - { - pf->user_app = vc; - } -} - -/** - * @brief Read the virtual channel carried in the User Application field. - * - * @param[in] pf Packet frame. - * @return Virtual channel number, or 0 if @p pf is NULL. - */ -static inline sw_virtual_channel_t sw_packet_virtual_channel(const sw_packet_frame_t *pf) -{ - return pf ? pf->user_app : (sw_virtual_channel_t)0; -} - -/** - * @brief Initialise a packet frame from configuration. - * - * The encapsulated CCSDS packet is zero-initialised; set its header fields and - * data before encoding. - * - * @param[out] pf Packet frame to initialise. No-op if NULL. - * @param[in] config Endpoint configuration. No-op if NULL. - */ -void sw_packet_init(sw_packet_frame_t *pf, const sw_packet_config_t *config); - -/** - * @brief Encode a complete CCSDS PTP packet into a buffer. - * - * Writes the optional path address, the 4-octet encapsulation header and the - * serialised CCSDS packet. No checksum and no in-band EOP are added. - * - * @param[in] pf Packet frame to encode. - * @param[out] buf Output buffer. - * @param[in] buf_len Buffer capacity in octets. - * @return Octets written, or 0 on error (NULL args, invalid path octet, CCSDS - * length out of bounds, or buffer too small). - */ -size_t sw_packet_encode(const sw_packet_frame_t *pf, uint8_t *buf, size_t buf_len); - -/** - * @brief Decode a received CCSDS PTP packet. - * - * The input is the packet as delivered to the target, i.e. with the path address - * already stripped by the network, beginning at the Target Logical Address: - * - * [ Target Logical Address | 0x02 | 0x00 | User Application | CCSDS packet ] - * - * On a successful return the decoded CCSDS packet, Target Logical Address and - * User Application value are available in @p pf. On discard the delivered CCSDS - * packet and User Application value are cleared (clause 5.2.3.2 b). - * - * @param[out] pf Decoded packet frame. - * @param[in] buf Received buffer to parse. - * @param[in] buf_len Buffer length in octets. - * @param[in] end End-of-packet marker reported by the link layer. - * @param[out] status Receive status (clause 5.1.3); may be NULL. Set to - * ::SW_PTP_STATUS_OK on delivery, ::SW_PTP_STATUS_EEP or - * ::SW_PTP_STATUS_RESERVED_NONZERO on a recognised discard, - * or ::SW_PTP_STATUS_INVALID for a bad Protocol Id / malformed input. - * @return ::SW_OK if a valid packet is delivered, ::SW_ERR if it is discarded. - */ -sw_result_t sw_packet_decode(sw_packet_frame_t *pf, - const uint8_t *buf, - size_t buf_len, - sw_end_marker_t end, - sw_ptp_status_t *status); - -/** - * @brief Build a complete CCSDS PTP packet carrying a single APID payload. - * - * Convenience wrapper that uses logical addressing only (no path address). - * - * @param[in] logical_addr Target Logical Address. - * @param[in] user_app User Application value. - * @param[in] apid APID — excess bits beyond 11 are masked. - * @param[in] payload CCSDS Packet Data Field (not copied). - * @param[in] payload_len Length of @p payload in octets. - * @param[out] buf Output buffer. - * @param[in] buf_len Buffer capacity in octets. - * @return Octets written, or 0 on error. - */ -size_t sw_packet_create(uint8_t logical_addr, - uint8_t user_app, - uint16_t apid, - const uint8_t *payload, - uint16_t payload_len, - uint8_t *buf, - size_t buf_len); - -/** - * @brief Cumulative CCSDS PTP traffic counters. - * - * @note The statistics are a single process-global instance updated by - * sw_packet_encode() and sw_packet_decode(); access is not thread-safe. - */ -typedef struct -{ - uint32_t packets_sent; /**< Successfully encoded packets. */ - uint32_t packets_received; /**< Successfully decoded packets. */ - uint32_t packets_discarded; /**< Discarded (EEP / Reserved!=0 / bad ID / malformed). */ - uint32_t bytes_sent; /**< Total octets emitted by sw_packet_encode(). */ - uint32_t bytes_received; /**< Total octets consumed by successful decode. */ -} sw_statistics_t; - -/** - * @brief Copy the current statistics. - * - * @param[out] stats Destination. No-op if NULL. - */ -void sw_get_statistics(sw_statistics_t *stats); - -/** - * @brief Reset all statistics counters to zero. - */ -void sw_reset_statistics(void); - -#ifdef __cplusplus -} -#endif - -#endif /* SPACEWIRE_PACKET_H */ diff --git a/include/spacewire_payload.h b/include/spacewire_payload.h new file mode 100644 index 0000000..bd508ea --- /dev/null +++ b/include/spacewire_payload.h @@ -0,0 +1,139 @@ +/** + * @file spacewire_payload.h + * @brief Payload adapter between SpaceWire and the protocol it carries + * + * The SpaceWire packet transfer layer (ECSS-E-ST-50-53C, see spacewire_ptp.h) + * never interprets the packet it carries. It reaches it only through the three + * sw_payload_*() hooks declared here, which keeps this library independent of + * any particular upper-layer protocol implementation. + * + * The library ships weak default hooks (src/weak/spacewire_payload.c) that treat the + * payload as raw octets described by ::sw_payload_raw_t. An application that + * carries a structured protocol, for example CCSDS Space Packets + * (CCSDS 133.0-B-2), defines the same three functions itself; its definitions + * replace the defaults at link time. + * + * Rules for an override: + * - Define all three hooks, so they agree on the type behind the payload object. + * - Do not mark the definitions weak; this header deliberately declares the + * hooks without ::SW_WEAK. + * - Link the overrides as an object file. A linker does not pull an archive + * member to replace a symbol that is already defined, so overrides that exist + * only inside a static library are silently ignored. + * - Honour the contract documented on each hook: return ::SW_OK on success or a + * negative ::sw_status_t on failure, and leave outputs unchanged on failure. + * The library reports any hook failure to its caller as ::SW_PAYLOAD_ERROR. + * + * See also: docs/space_packet_integration.md + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef SPACEWIRE_PAYLOAD_H +#define SPACEWIRE_PAYLOAD_H + +#include "spacewire.h" + +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +#ifndef SW_WEAK +# if (defined(__GNUC__)) || (defined(__clang__)) +/** + * @brief Marks a function definition as weak, so another definition can replace it at link time. + * + * Weak symbols are not part of ISO C99. Toolchains that are not GNU-compatible + * must supply their own spelling at build time, e.g. `-DSW_WEAK=__weak` for IAR + * and Keil. + */ +# define SW_WEAK __attribute__((weak)) +# else +# error "SW_WEAK: unknown compiler - define SW_WEAK to its weak-symbol attribute" +# endif +#endif + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief Raw-octet payload object understood by the default hooks. + * + * @note @p data points into caller-owned memory; the library neither copies nor + * frees it. After the default sw_payload_parse() it points into the + * parsed buffer (zero-copy), which must outlive this object. + */ +typedef struct +{ + const uint8_t *data; /**< Payload octets. */ + uint32_t len; /**< Number of payload octets. */ +} sw_payload_raw_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Return the serialised size of a payload object. + * + * Default: @p payload is a ::sw_payload_raw_t and the result is its length. + * + * @param[in] payload Payload object; its type is defined by the hook implementation. + * @return Size in octets, or 0 if @p payload cannot be serialised (default: + * NULL object, NULL data or zero length). + */ +uint32_t sw_payload_size(const void *payload); + +/** + * @brief Serialise a payload object into a caller-supplied buffer. + * + * On ::SW_OK, exactly sw_payload_size() octets for the same object have been + * written to @p buf. On failure, @p buf is left unchanged. + * + * Default: copies the octets of a ::sw_payload_raw_t. + * + * @param[in] payload Payload object; its type is defined by the hook implementation. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @return ::SW_OK on success, or a negative ::sw_status_t (default: + * ::SW_INVALID_PARAM for NULL args, ::SW_WRONG_LENGTH for zero length, + * ::SW_BUFFER_TOO_SMALL if the octets do not fit). + */ +sw_status_t sw_payload_serialize(const void *payload, uint8_t *buf, uint32_t buf_len); + +/** + * @brief Parse a serialised payload into a payload object. + * + * @p buf_len is every octet that followed the encapsulation header, unchecked: + * the packet transfer layer does not check the carried packet's length or + * format (ECSS-E-ST-50-53C clause 4.3). An implementation must not read past + * @p buf_len, and decides itself what to do with octets its packet does not use. + * Returning an error discards the packet. + * + * Default: points a ::sw_payload_raw_t at @p buf (zero-copy), zero length + * included. On failure, @p payload is left unchanged. + * + * @param[out] payload Payload object; its type is defined by the hook implementation. + * @param[in] buf Serialised payload. + * @param[in] buf_len Octets available at @p buf; may be 0. + * @return ::SW_OK on success, or a negative ::sw_status_t (default: + * ::SW_INVALID_PARAM for NULL args). + */ +sw_status_t sw_payload_parse(void *payload, const uint8_t *buf, uint32_t buf_len); + +#ifdef __cplusplus +} +#endif + +#endif /* SPACEWIRE_PAYLOAD_H */ diff --git a/include/spacewire_ptp.h b/include/spacewire_ptp.h new file mode 100644 index 0000000..005553c --- /dev/null +++ b/include/spacewire_ptp.h @@ -0,0 +1,275 @@ +/** + * @file spacewire_ptp.h + * @brief CCSDS Packet Transfer Protocol over SpaceWire (ECSS-E-ST-50-53C) + * + * Encapsulates a packet into a SpaceWire packet on the send path and extracts + * it on the receive path. The carried packet is opaque to this layer: it is + * sized, serialised and parsed through the payload adapter hooks declared in + * spacewire_payload.h, which an application overrides to carry CCSDS Space + * Packets (CCSDS 133.0-B-2). The protocol adds no checksum of its own — + * ECSS-E-ST-50-53C defines none; error detection is provided by the underlying + * SpaceWire link (character parity) and by the EOP/EEP end-of-packet markers. + * See also: docs/space_packet_integration.md + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#ifndef SPACEWIRE_PTP_H +#define SPACEWIRE_PTP_H + +#include "spacewire.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +/* ------------------------------------------------------------------------- + * Constants + * ---------------------------------------------------------------------- */ + +/** @brief Protocol Identifier for the CCSDS Packet Transfer Protocol (clause 5.3.3). */ +#define SW_PTP_PROTOCOL_ID 0x02u + +/** @brief Reserved field value (clause 5.3.4). */ +#define SW_PTP_RESERVED 0x00u + +/** + * @brief Default Target Logical Address when the target has no specific one + * (clause 5.3.2, NOTE 2). + * + * @note Logical address 255 (0xFF) is reserved and must not be used. + */ +#define SW_PTP_LOGICAL_ADDR_DEFAULT 0xFEu + +/** + * @brief Encapsulation header length in octets. + * + * Target Logical Address + Protocol Identifier + Reserved + User Application. + */ +#define SW_PTP_HEADER_LEN 4u + +/** @brief Minimum CCSDS packet length: 6-octet primary header + 1 data octet (clause 5.1.2). */ +#define SW_PTP_CCSDS_MIN_LEN 7u + +/** @brief Maximum CCSDS packet length: 6-octet primary header + 65536 data octets (clause 5.1.2). + */ +#define SW_PTP_CCSDS_MAX_LEN 65542u + +/* ------------------------------------------------------------------------- + * Types + * ---------------------------------------------------------------------- */ + +/** + * @brief Receive status reported to the target user application (clauses 5.1.3, 5.2.3.2). + * + * Exactly the three status codes the standard defines (clauses 5.1.3 b, 5.2.3.2 c). + * Input that is not delivered for any other reason is reported through the + * ::sw_status_t returned by sw_ptp_decode(), not through this value. + */ +typedef enum +{ + SW_PTP_STATUS_OK = 0x00, /**< Packet arrived with no known error. */ + SW_PTP_STATUS_EEP = 0x01, /**< Packet terminated by EEP. */ + SW_PTP_STATUS_RESERVED_NONZERO = 0x02 /**< Reserved field was non-zero. */ +} sw_ptp_status_t; + +/** + * @brief Addressing and encapsulation fields of a CCSDS Packet Transfer Protocol packet. + * + * The wire layout produced by sw_ptp_encode() follows ECSS-E-ST-50-53C + * Figure 5-1: + * + * [ Target SpW Address: 0..N octets, each 0-254 ] (optional) + * [ Target Logical Address | 0x02 | 0x00 | User Application ] + * [ Carried packet: 7 to 65542 octets ] + * + * followed by an EOP that the link layer appends (not stored in the buffer). + * The carried packet is not part of this structure; it is passed to + * sw_ptp_encode() and sw_ptp_decode() as a separate payload object. + * + * @note @p path points into caller-owned memory; the library neither copies nor + * frees it. + * @note The Target SpaceWire Address is stripped by the network before the + * packet reaches the target, so it is absent on the receive path. + */ +typedef struct +{ + const uint8_t *path; /**< Target SpaceWire Address; NULL if unused (clause 5.3.1). Path (0..31) + * and regional logical (32..254) octets may be mixed; 255 is reserved. */ + uint8_t path_len; /**< Number of Target SpaceWire Address octets. */ + uint8_t logical_addr; /**< Target Logical Address (clause 5.3.2); 32..254 per ECSS-E-ST-50-12C + clause 5.6.8.4. */ + uint8_t user_app; /**< User Application field; may carry a virtual channel (clause 5.3.5). */ +} sw_ptp_frame_t; + +/** + * @brief Virtual channel number. + * + * ECSS-E-ST-50-53C clause 5.3.5 (NOTE 2) allows the User Application field to + * carry a virtual channel number, so a virtual channel is represented as that + * 8-bit value rather than as separate router state. + */ +typedef uint8_t sw_ptp_virtual_channel_t; + +/** + * @brief Cumulative CCSDS PTP traffic counters. + * + * @note The statistics are a single process-global instance updated by + * sw_ptp_encode() and sw_ptp_decode(); access is not thread-safe. + */ +typedef struct +{ + uint32_t packets_sent; /**< Successfully encoded packets. */ + uint32_t packets_received; /**< Successfully decoded packets. */ + uint32_t packets_discarded; /**< Received but not delivered (any decode error but NULL). */ + uint32_t bytes_sent; /**< Total octets emitted by sw_ptp_encode(). */ + uint32_t bytes_received; /**< Total octets consumed by successful decode. */ +} sw_ptp_statistics_t; + +/* ------------------------------------------------------------------------- + * Function Declarations + * ---------------------------------------------------------------------- */ + +/** + * @brief Select a virtual channel by setting the User Application field. + * + * @param[in,out] pf Packet frame. No-op if NULL. + * @param[in] vc Virtual channel number. + */ +static inline void sw_ptp_set_virtual_channel(sw_ptp_frame_t *pf, sw_ptp_virtual_channel_t vc) +{ + if (pf) + { + pf->user_app = vc; + } +} + +/** + * @brief Read the virtual channel carried in the User Application field. + * + * @param[in] pf Packet frame. + * @return Virtual channel number, or 0 if @p pf is NULL. + */ +static inline sw_ptp_virtual_channel_t sw_ptp_get_virtual_channel(const sw_ptp_frame_t *pf) +{ + return pf ? pf->user_app : (sw_ptp_virtual_channel_t)0; +} + +/** + * @brief Encode a complete CCSDS PTP packet into a buffer. + * + * Writes the optional Target SpaceWire Address, the 4-octet encapsulation header + * and the carried packet, which is sized by sw_payload_size() and serialised by + * sw_payload_serialize(). No checksum and no in-band EOP are added. + * + * On failure, @p buf and @p out_len are left unchanged, provided + * sw_payload_serialize() honours its own contract. + * + * @param[in] pf Packet frame to encode. + * @param[in] payload Payload object handed to the payload adapter hooks; with + * the default hooks, a ::sw_payload_raw_t. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] out_len Octets written; set only on ::SW_OK. + * @return ::SW_OK on success; ::SW_INVALID_PARAM for a NULL argument or a NULL + * path with a non-zero length; ::SW_WRONG_ADDRESS for a Target Logical + * Address outside 32..254 or a Target SpaceWire Address octet of 255; + * ::SW_WRONG_LENGTH for a carried packet outside ::SW_PTP_CCSDS_MIN_LEN to + * ::SW_PTP_CCSDS_MAX_LEN octets (clauses 5.1.2 a, 5.5.2 c); + * ::SW_BUFFER_TOO_SMALL; or ::SW_PAYLOAD_ERROR if sw_payload_serialize() fails. + */ +sw_status_t sw_ptp_encode(const sw_ptp_frame_t *pf, + const void *payload, + uint8_t *buf, + uint32_t buf_len, + uint32_t *out_len); + +/** + * @brief Decode a received CCSDS PTP packet. + * + * The input is the packet as delivered to the target, i.e. with the Target + * SpaceWire Address already stripped by the network, beginning at the Target + * Logical Address: + * + * [ Target Logical Address | 0x02 | 0x00 | User Application | carried packet ] + * + * Only the header is checked: Protocol Identifier, Reserved field and end + * marker (clause 5.5.4). Every octet after the header is handed to + * sw_payload_parse() as the carried packet, whatever its length or format + * (clauses 4.3, 5.5.4.2 a); the send-side bounds of clause 5.1.2 are not + * applied here. + * + * On failure, @p pf and @p payload are left unchanged. A discarded packet is + * thereby "null" in the sense of clause 5.2.3.2 b: nothing is delivered. + * + * @param[out] pf Decoded Target Logical Address and User Application value. + * @param[out] payload Payload object filled by sw_payload_parse(); with the + * default hooks, a ::sw_payload_raw_t that points into @p buf. + * @param[in] buf Received buffer to parse. + * @param[in] buf_len Buffer length in octets. + * @param[in] end End-of-packet marker reported by the link layer. Only + * ::SW_END_EOP can deliver a packet; any value other than + * ::SW_END_EOP or ::SW_END_EEP is treated as invalid. + * @param[out] status Receive status for the target user application (clause + * 5.1.3); may be NULL. Written only when the return value is + * ::SW_OK (::SW_PTP_STATUS_OK) or ::SW_DISCARDED + * (::SW_PTP_STATUS_EEP or ::SW_PTP_STATUS_RESERVED_NONZERO); + * left unchanged otherwise. + * @return ::SW_OK if the packet is delivered; ::SW_DISCARDED for a discard the + * standard defines (EEP, clause 5.5.4.4; non-zero Reserved field, clause + * 5.5.4.3); ::SW_INVALID_PARAM if @p pf, @p payload or @p buf is NULL or + * @p end is neither EOP nor EEP; ::SW_WRONG_LENGTH for fewer than + * ::SW_PTP_HEADER_LEN octets; ::SW_WRONG_PROTOCOL for a Protocol + * Identifier other than 0x02 (clause 5.5.4.1); or ::SW_PAYLOAD_ERROR if + * sw_payload_parse() fails. Every error except a NULL argument counts as a + * discarded packet in the statistics. + */ +sw_status_t sw_ptp_decode(sw_ptp_frame_t *pf, + void *payload, + const uint8_t *buf, + uint32_t buf_len, + sw_end_marker_t end, + sw_ptp_status_t *status); + +/** + * @brief Build a complete CCSDS PTP packet using logical addressing only. + * + * Convenience wrapper around sw_ptp_encode() for a frame without a Target + * SpaceWire Address. On failure, @p buf and @p out_len are left unchanged. + * + * @param[in] logical_addr Target Logical Address. + * @param[in] user_app User Application value. + * @param[in] payload Payload object handed to the payload adapter hooks. + * @param[out] buf Output buffer. + * @param[in] buf_len Buffer capacity in octets. + * @param[out] out_len Octets written; set only on ::SW_OK. + * @return As sw_ptp_encode(). + */ +sw_status_t sw_ptp_create(uint8_t logical_addr, + uint8_t user_app, + const void *payload, + uint8_t *buf, + uint32_t buf_len, + uint32_t *out_len); + +/** + * @brief Copy the current statistics. + * + * @param[out] stats Destination. No-op if NULL. + */ +void sw_ptp_get_statistics(sw_ptp_statistics_t *stats); + +/** + * @brief Reset all statistics counters to zero. + */ +void sw_ptp_reset_statistics(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SPACEWIRE_PTP_H */ diff --git a/src/spacewire_packet.c b/src/spacewire_packet.c deleted file mode 100644 index d5e3bcf..0000000 --- a/src/spacewire_packet.c +++ /dev/null @@ -1,240 +0,0 @@ -/** - * @file spacewire_packet.c - * @brief CCSDS Packet Transfer Protocol over SpaceWire (ECSS-E-ST-50-53C). - * - * Encapsulates a CCSDS Space Packet into a SpaceWire packet on the send path - * and extracts it on the receive path. No checksum is added: ECSS-E-ST-50-53C - * relies on SpaceWire character parity and the EOP/EEP markers for error - * detection. - */ - -#include "spacewire_packet.h" - -#include - -/* Global statistics */ -static sw_statistics_t g_sw_stats = {0}; - -/* ============================================================================ - * PACKET INITIALISATION - * ============================================================================ */ - -void sw_packet_init(sw_packet_frame_t *pf, const sw_packet_config_t *config) -{ - if (!pf || !config) - return; - - memset(pf, 0, sizeof(*pf)); - - pf->path = config->path; - pf->path_len = config->path_len; - pf->logical_addr = config->logical_addr; - pf->user_app = config->user_app; - - sp_packet_init(&pf->packet); - - pf->packet.ph.version = 0; - pf->packet.ph.type = SP_PACKET_TYPE_TC; /* default Packet Type (CCSDS: TM=0, TC=1) */ - pf->packet.ph.sec_hdr_flag = 0; - pf->packet.ph.apid = 0; -} - -/* ============================================================================ - * PACKET ENCODING (clause 5.4) - * ============================================================================ */ - -size_t sw_packet_encode(const sw_packet_frame_t *pf, uint8_t *buf, size_t buf_len) -{ - if (!pf || !buf) - return 0; - - if (pf->packet.data_len > 0 && pf->packet.data == NULL) - return 0; - - /* Path octets must be valid SpaceWire path addresses (0..31). */ - if (pf->path_len > 0 && pf->path == NULL) - return 0; - - for (uint8_t i = 0; i < pf->path_len; i++) - { - if (pf->path[i] > SW_PTP_PATH_OCTET_MAX) - return 0; - } - - /* CCSDS packet length must be within bounds (clause 5.1.2). The upper bound - * cannot currently be exceeded: sp_packet_t.data_len is a uint16_t, so the - * serialised size is at most 6 + 65535 = 65541 < SW_PTP_CCSDS_MAX_LEN. The - * check is kept as a guard should that type ever widen, so its always-false - * branch is excluded from branch coverage. */ - const size_t ccsds_len = sp_packet_serialize_size(&pf->packet); - if (ccsds_len < SW_PTP_CCSDS_MIN_LEN || ccsds_len > SW_PTP_CCSDS_MAX_LEN) /* GCOVR_EXCL_BR_LINE */ - return 0; - - const size_t total = (size_t)pf->path_len + SW_PTP_HEADER_LEN + ccsds_len; - if (buf_len < total) - return 0; - - size_t offset = 0; - - /* Target SpaceWire (path) Address (clause 5.3.1). */ - if (pf->path_len > 0) - { - memcpy(&buf[offset], pf->path, pf->path_len); - offset += pf->path_len; - } - - /* Encapsulation header (clause 5.4.1). */ - buf[offset++] = pf->logical_addr; /* Target Logical Address (5.3.2) */ - buf[offset++] = (uint8_t)SW_PTP_PROTOCOL_ID; /* Protocol Identifier = 0x02 */ - buf[offset++] = (uint8_t)SW_PTP_RESERVED; /* Reserved = 0x00 */ - buf[offset++] = pf->user_app; /* User Application (5.3.5) */ - - /* CCSDS Space Packet (clause 5.3.6). The length bounds (sp_packet_serialize_size) - * and the buffer size were validated above, so serialisation cannot fail here. */ - const size_t written = sp_packet_serialize(&pf->packet, &buf[offset], buf_len - offset); - offset += written; - - g_sw_stats.packets_sent++; - g_sw_stats.bytes_sent += (uint32_t)offset; - - return offset; -} - -/* ============================================================================ - * PACKET DECODING (clause 5.5.4) - * ============================================================================ */ - -/** - * @brief Clear the delivered packet/user-application fields (clause 5.2.3.2 b). - * @param[out] pf Packet frame to clear. - */ -static void sw_packet_clear_payload(sw_packet_frame_t *pf) -{ - pf->path = NULL; - pf->path_len = 0; - pf->logical_addr = 0; - pf->user_app = 0; - - sp_packet_init(&pf->packet); -} - -/** - * @brief Discard a received packet: clear the delivered fields, count it, and - * report the status code (clause 5.5.4). - * @param[out] pf Packet frame; must be non-NULL. - * @param[out] status Receive-status output; may be NULL. - * @param[in] code Status code to report. - * @return Always ::SW_ERR. - */ -static sw_result_t sw_packet_discard(sw_packet_frame_t *pf, - sw_ptp_status_t *status, - sw_ptp_status_t code) -{ - sw_packet_clear_payload(pf); - g_sw_stats.packets_discarded++; - - if (status) - *status = code; - - return SW_ERR; -} - -sw_result_t sw_packet_decode(sw_packet_frame_t *pf, - const uint8_t *buf, - size_t buf_len, - sw_end_marker_t end, - sw_ptp_status_t *status) -{ - if (!pf || !buf) - { - if (status) - *status = SW_PTP_STATUS_INVALID; - - return SW_INVALID_PARAM; - } - - /* clause 5.5.4.4: a packet terminated by EEP shall be discarded. */ - if (end == SW_END_EEP) - return sw_packet_discard(pf, status, SW_PTP_STATUS_EEP); - - /* Need the encapsulation header plus a minimal CCSDS packet. */ - if (buf_len < (size_t)SW_PTP_HEADER_LEN + SW_PTP_CCSDS_MIN_LEN) - return sw_packet_discard(pf, status, SW_PTP_STATUS_INVALID); - - const uint8_t logical = buf[0]; - const uint8_t proto = buf[1]; - const uint8_t reserved = buf[2]; - const uint8_t user_app = buf[3]; - - /* clause 5.5.4.1: only Protocol Identifier 0x02 is a CCSDS PTP packet. */ - if (proto != SW_PTP_PROTOCOL_ID) - return sw_packet_discard(pf, status, SW_PTP_STATUS_INVALID); - - /* clause 5.5.4.3: Reserved field non-zero -> discard, packet/user app null. */ - if (reserved != SW_PTP_RESERVED) - return sw_packet_discard(pf, status, SW_PTP_STATUS_RESERVED_NONZERO); - - /* clause 5.5.4.2: extract the CCSDS packet and User Application value. */ - if (!sp_packet_parse(&pf->packet, &buf[SW_PTP_HEADER_LEN], buf_len - SW_PTP_HEADER_LEN)) - return sw_packet_discard(pf, status, SW_PTP_STATUS_INVALID); - - pf->path = NULL; - pf->path_len = 0; - pf->logical_addr = logical; - pf->user_app = user_app; - - g_sw_stats.packets_received++; - g_sw_stats.bytes_received += (uint32_t)buf_len; - - if (status) - *status = SW_PTP_STATUS_OK; - - return SW_OK; -} - -/* ============================================================================ - * CONVENIENCE FUNCTION - * ============================================================================ */ - -size_t sw_packet_create(uint8_t logical_addr, - uint8_t user_app, - uint16_t apid, - const uint8_t *payload, - uint16_t payload_len, - uint8_t *buf, - size_t buf_len) -{ - if (!buf) - return 0; - - const sw_packet_config_t config = {.path = NULL, - .path_len = 0, - .logical_addr = logical_addr, - .user_app = user_app}; - - sw_packet_frame_t pf; - sw_packet_init(&pf, &config); - - pf.packet.ph.apid = (unsigned)(apid & 0x7FFu); - pf.packet.data = payload; - pf.packet.data_len = payload_len; - - return sw_packet_encode(&pf, buf, buf_len); -} - -/* ============================================================================ - * STATISTICS - * ============================================================================ */ - -void sw_get_statistics(sw_statistics_t *stats) -{ - if (!stats) - return; - - *stats = g_sw_stats; -} - -void sw_reset_statistics(void) -{ - memset(&g_sw_stats, 0, sizeof(g_sw_stats)); -} diff --git a/src/spacewire_ptp.c b/src/spacewire_ptp.c new file mode 100644 index 0000000..72acec9 --- /dev/null +++ b/src/spacewire_ptp.c @@ -0,0 +1,274 @@ +/** + * @file spacewire_ptp.c + * @brief CCSDS Packet Transfer Protocol over SpaceWire + * + * Implements the encapsulation and extraction of ECSS-E-ST-50-53C clause 5. + * The carried packet is reached only through the payload adapter hooks + * (spacewire_payload.h), so this file knows nothing about its layout. No + * checksum is added: the standard relies on SpaceWire character parity and the + * EOP/EEP markers for error detection. + * See also: docs/space_packet_integration.md + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "spacewire_ptp.h" + +#include "spacewire_payload.h" + +#include + +/** @brief Offset of the Target Logical Address in the encapsulation header (clause 5.3.2). */ +#define SW_PTP_OFFSET_LOGICAL_ADDR 0u + +/** @brief Offset of the Protocol Identifier in the encapsulation header (clause 5.3.3). */ +#define SW_PTP_OFFSET_PROTOCOL_ID 1u + +/** @brief Offset of the Reserved field in the encapsulation header (clause 5.3.4). */ +#define SW_PTP_OFFSET_RESERVED 2u + +/** @brief Offset of the User Application field in the encapsulation header (clause 5.3.5). */ +#define SW_PTP_OFFSET_USER_APP 3u + +static sw_ptp_statistics_t g_sw_stats = {0}; + +/** + * @brief Check a frame's addressing fields and the carried packet's length. + * + * The Target Logical Address is a logical address, 32..254: 255 shall not be used + * (ECSS-E-ST-50-12C clause 5.6.8.4 a, d), and a value in 0..31 is a path address + * that the first router would take as a port number and delete (clause 5.6.8.3 a, + * c, e). The Target SpaceWire Address may mix path (0..31) and regional logical + * (32..254) octets (ECSS-E-ST-50-53C clause 5.3.1 b); only 255 is refused. The + * carried packet is 7..65542 octets (clause 5.1.2 a). + * + * @param[in] pf Packet frame; must be non-NULL. + * @param[in] payload_len Serialised size of the carried packet. + * @return ::SW_OK, ::SW_WRONG_ADDRESS, ::SW_INVALID_PARAM (address octets + * announced but @p pf->path is NULL) or ::SW_WRONG_LENGTH. + */ +static sw_status_t sw_ptp_validate(const sw_ptp_frame_t *pf, uint32_t payload_len) +{ + if ((pf->logical_addr < SW_LOGICAL_ADDR_MIN) || (pf->logical_addr == SW_LOGICAL_ADDR_RESERVED)) + { + return SW_WRONG_ADDRESS; + } + + if ((pf->path_len > 0u) && (!pf->path)) + { + return SW_INVALID_PARAM; + } + + for (uint8_t i = 0; i < pf->path_len; i++) + { + if (pf->path[i] == SW_LOGICAL_ADDR_RESERVED) + { + return SW_WRONG_ADDRESS; + } + } + + if ((payload_len < SW_PTP_CCSDS_MIN_LEN) || (payload_len > SW_PTP_CCSDS_MAX_LEN)) + { + return SW_WRONG_LENGTH; + } + + return SW_OK; +} + +/** + * @brief Write the optional Target SpaceWire Address and the encapsulation header + * (clauses 5.3.1 to 5.3.5). + * + * @param[in] pf Packet frame with a valid Target SpaceWire Address. + * @param[out] buf Output buffer with room for the address plus + * ::SW_PTP_HEADER_LEN octets. + */ +static void sw_ptp_write_header(const sw_ptp_frame_t *pf, uint8_t *buf) +{ + uint8_t *header = &buf[pf->path_len]; + + if (pf->path_len > 0u) + { + memcpy(buf, pf->path, pf->path_len); + } + + header[SW_PTP_OFFSET_LOGICAL_ADDR] = pf->logical_addr; + header[SW_PTP_OFFSET_PROTOCOL_ID] = (uint8_t)SW_PTP_PROTOCOL_ID; + header[SW_PTP_OFFSET_RESERVED] = (uint8_t)SW_PTP_RESERVED; + header[SW_PTP_OFFSET_USER_APP] = pf->user_app; +} + +sw_status_t sw_ptp_encode(const sw_ptp_frame_t *pf, + const void *payload, + uint8_t *buf, + uint32_t buf_len, + uint32_t *out_len) +{ + if ((!pf) || (!payload) || (!buf) || (!out_len)) + { + return SW_INVALID_PARAM; + } + + const uint32_t payload_len = sw_payload_size(payload); + const sw_status_t valid = sw_ptp_validate(pf, payload_len); + if (valid != SW_OK) + { + return valid; + } + + const uint32_t header_len = (uint32_t)pf->path_len + SW_PTP_HEADER_LEN; + const uint32_t total = header_len + payload_len; + if (buf_len < total) + { + return SW_BUFFER_TOO_SMALL; + } + + /* The carried packet is serialised before the header is written, so a + * failing hook leaves the caller's buffer untouched. */ + if (sw_payload_serialize(payload, &buf[header_len], buf_len - header_len) != SW_OK) + { + return SW_PAYLOAD_ERROR; + } + + sw_ptp_write_header(pf, buf); + + g_sw_stats.packets_sent++; + g_sw_stats.bytes_sent += total; + *out_len = total; + + return SW_OK; +} + +/** + * @brief Run the receive checks of clause 5.5.4. + * + * Only the encapsulation header is checked. The carried packet is passed on + * whatever its length or format (clauses 4.3 and 5.5.4.2 a); the payload adapter + * alone decides whether it can be extracted. + * + * @param[in] buf Received buffer; must be non-NULL. + * @param[in] buf_len Buffer length in octets. + * @param[in] end End-of-packet marker reported by the link layer. + * @param[out] code Receive status of a ::SW_DISCARDED packet; written only then. + * @return ::SW_OK if the packet may be extracted; ::SW_DISCARDED for a discard + * defined by the standard (EEP, non-zero Reserved field); otherwise + * ::SW_INVALID_PARAM, ::SW_WRONG_LENGTH or ::SW_WRONG_PROTOCOL. + */ +static sw_status_t sw_ptp_check(const uint8_t *buf, + uint32_t buf_len, + sw_end_marker_t end, + sw_ptp_status_t *code) +{ + /* clause 5.5.4.4: a packet terminated by EEP shall be discarded. */ + if (end == SW_END_EEP) + { + *code = SW_PTP_STATUS_EEP; + return SW_DISCARDED; + } + + /* Fail closed: a marker the link layer cannot have meant as EOP delivers nothing. */ + if (end != SW_END_EOP) + { + return SW_INVALID_PARAM; + } + + /* Without a complete header the packet cannot be identified at all. */ + if (buf_len < SW_PTP_HEADER_LEN) + { + return SW_WRONG_LENGTH; + } + + /* clause 5.5.4.1: only Protocol Identifier 0x02 is a CCSDS PTP packet. */ + if (buf[SW_PTP_OFFSET_PROTOCOL_ID] != SW_PTP_PROTOCOL_ID) + { + return SW_WRONG_PROTOCOL; + } + + /* clause 5.5.4.3: a non-zero Reserved field discards the packet. */ + if (buf[SW_PTP_OFFSET_RESERVED] != SW_PTP_RESERVED) + { + *code = SW_PTP_STATUS_RESERVED_NONZERO; + return SW_DISCARDED; + } + + return SW_OK; +} + +sw_status_t sw_ptp_decode(sw_ptp_frame_t *pf, + void *payload, + const uint8_t *buf, + uint32_t buf_len, + sw_end_marker_t end, + sw_ptp_status_t *status) +{ + if ((!pf) || (!payload) || (!buf)) + { + return SW_INVALID_PARAM; + } + + sw_ptp_status_t code = SW_PTP_STATUS_OK; + sw_status_t result = sw_ptp_check(buf, buf_len, end, &code); + + /* clause 5.5.4.2: extract the carried packet. A hook that cannot represent it + * fails and leaves the payload object unchanged; nothing is delivered then. */ + if ((result == SW_OK) && + (sw_payload_parse(payload, &buf[SW_PTP_HEADER_LEN], buf_len - SW_PTP_HEADER_LEN) != SW_OK)) + { + result = SW_PAYLOAD_ERROR; + } + + if (result != SW_OK) + { + g_sw_stats.packets_discarded++; + } + else + { + pf->path = NULL; + pf->path_len = 0; + pf->logical_addr = buf[SW_PTP_OFFSET_LOGICAL_ADDR]; + pf->user_app = buf[SW_PTP_OFFSET_USER_APP]; + g_sw_stats.packets_received++; + g_sw_stats.bytes_received += buf_len; + } + + /* The status carries the standard's codes only (clauses 5.1.3, 5.2.3.2 c). */ + if ((status) && ((result == SW_OK) || (result == SW_DISCARDED))) + { + *status = code; + } + + return result; +} + +sw_status_t sw_ptp_create(uint8_t logical_addr, + uint8_t user_app, + const void *payload, + uint8_t *buf, + uint32_t buf_len, + uint32_t *out_len) +{ + const sw_ptp_frame_t pf = {.path = NULL, + .path_len = 0, + .logical_addr = logical_addr, + .user_app = user_app}; + + return sw_ptp_encode(&pf, payload, buf, buf_len, out_len); +} + +void sw_ptp_get_statistics(sw_ptp_statistics_t *stats) +{ + if (!stats) + { + return; + } + + *stats = g_sw_stats; +} + +void sw_ptp_reset_statistics(void) +{ + memset(&g_sw_stats, 0, sizeof(g_sw_stats)); +} diff --git a/src/spacewire_router.c b/src/spacewire_router.c index f76dd2d..aac5478 100644 --- a/src/spacewire_router.c +++ b/src/spacewire_router.c @@ -1,56 +1,63 @@ /** - * @file spacewire_router.c - * @brief SpaceWire routing switch (ECSS-E-ST-50-12C clause 5.6.8) and link-state helpers. + * @file spacewire_router.c + * @brief SpaceWire routing switch + * + * Implements path and logical addressing, header deletion and invalid-address + * handling as per ECSS-E-ST-50-12C Rev.1, clause 5.6.8. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "spacewire.h" #include -/* ============================================================================ - * ROUTER INITIALISATION - * ============================================================================ */ - void sw_router_init(sw_router_t *router, uint8_t num_ports) { if (!router) + { return; + } memset(router, 0, sizeof(*router)); if (num_ports == 0) + { num_ports = 1; + } if (num_ports > SW_NUM_PORTS) + { num_ports = SW_NUM_PORTS; + } router->num_ports = num_ports; - - for (uint8_t i = 0; i < router->num_ports; i++) - { - router->links[i].port_id = i; - router->links[i].state = SW_LINK_UNINITIALIZED; - } } -/* ============================================================================ - * ROUTING CONFIGURATION - * ============================================================================ */ - -sw_result_t sw_router_add_route(sw_router_t *router, +sw_status_t sw_router_add_route(sw_router_t *router, uint8_t logical_addr, uint8_t output_port, int delete_addr) { if (!router) + { return SW_INVALID_PARAM; + } /* Only logical addresses 32..254 are configurable (clause 5.6.8.4). */ - if (logical_addr < SW_LOGICAL_ADDR_MIN || logical_addr == SW_LOGICAL_ADDR_RESERVED) + if ((logical_addr < SW_LOGICAL_ADDR_MIN) || (logical_addr == SW_LOGICAL_ADDR_RESERVED)) + { return SW_WRONG_ADDRESS; + } - if (output_port >= router->num_ports) + /* The configuration port is reached by path addressing only (clause 5.6.8.3 f). */ + if ((output_port == SW_PORT_CONFIG) || (output_port >= router->num_ports)) + { return SW_WRONG_PORT; + } router->routes[logical_addr].output_port = output_port; router->routes[logical_addr].configured = 1; @@ -59,100 +66,92 @@ sw_result_t sw_router_add_route(sw_router_t *router, return SW_OK; } -/* ============================================================================ - * ROUTING DECISION - * ============================================================================ */ - /** * @brief Discard a packet whose leading address references a non-existent port * or an unconfigured routing-table entry (clause 5.6.8.5). * @param[in,out] router Router whose counters are updated. - * @return Always ::SW_ROUTE_DISCARD. + * @return Always ::SW_WRONG_ADDRESS. */ -static sw_route_result_t sw_router_invalid_address(sw_router_t *router) +static sw_status_t sw_router_invalid_address(sw_router_t *router) { router->invalid_address_errors++; router->packets_discarded++; - return SW_ROUTE_DISCARD; + return SW_WRONG_ADDRESS; } -sw_route_result_t sw_router_route(sw_router_t *router, - const uint8_t *packet, - size_t len, - uint8_t *output_port, - uint8_t *delete_leading) +/** + * @brief Resolve a leading address character to a route. + * + * A path address (0..31) names the output port directly and is always deleted + * (clause 5.6.8.3, Table 5-11). A logical address is looked up in the routing + * table (clause 5.6.8.4). An address that is unconfigured, which includes the + * reserved address 255, or that leads to a port the router does not have, is + * invalid (clause 5.6.8.5). So is a logical address that leads to the + * configuration port, which is reached by path addressing only (clause 5.6.8.3 f). + * + * @param[in] router Router to consult. + * @param[in] lead Leading address character of the packet. + * @param[out] route Resolved route. On failure, left unchanged. + * @return 1 if @p lead leads to an existing output port, 0 if it is invalid. + */ +static int sw_router_resolve(const sw_router_t *router, uint8_t lead, sw_route_entry_t *route) { - if (!router || !packet || !output_port || !delete_leading) - return SW_ROUTE_DISCARD; - - *delete_leading = 0; + sw_route_entry_t candidate; + const int is_logical = (lead > SW_PATH_ADDR_MAX); - /* An empty packet is discarded by the first routing switch (clause 5.6.2.1). */ - if (len == 0) + if (!is_logical) { - router->packets_discarded++; - return SW_ROUTE_DISCARD; + candidate.output_port = lead; + candidate.configured = 1; + candidate.delete_addr = 1; } - - const uint8_t lead = packet[0]; - - if (lead <= SW_PATH_ADDR_MAX) + else { - /* Path addressing (clause 5.6.8.3): the character names the output port. */ - if (lead >= router->num_ports) - return sw_router_invalid_address(router); - - *output_port = lead; - *delete_leading = 1; /* a path address is always deleted (Table 5-11) */ - router->packets_routed++; - - return SW_ROUTE_OK; + candidate = router->routes[lead]; } - /* Logical addressing (clause 5.6.8.4): look up the routing table. A logical - * address that is unconfigured (including the reserved address 255) or that - * maps to a non-existent port is discarded with an invalid-address error - * (clause 5.6.8.5). */ - const sw_route_entry_t *entry = &router->routes[lead]; - - if (!entry->configured || entry->output_port >= router->num_ports) - return sw_router_invalid_address(router); + /* sw_router_add_route() refuses a logical route to the configuration port, but the + * table is caller-visible, so an entry written into it directly is refused here. */ + if ((!candidate.configured) || (candidate.output_port >= router->num_ports) || + ((is_logical) && (candidate.output_port == SW_PORT_CONFIG))) + { + return 0; + } - *output_port = entry->output_port; - *delete_leading = entry->delete_addr ? 1u : 0u; /* clause 5.6.8.6 */ - router->packets_routed++; + *route = candidate; - return SW_ROUTE_OK; + return 1; } -/* ============================================================================ - * LINK STATE MANAGEMENT (STUB) - * ============================================================================ */ - -void sw_link_init(sw_link_layer_t *link, const sw_link_config_t *config) +sw_status_t sw_router_route(sw_router_t *router, + const uint8_t *packet, + size_t len, + uint8_t *output_port, + uint8_t *delete_leading) { - if (!link || !config) - return; - - link->config = *config; - link->state = SW_LINK_UNINITIALIZED; - link->rx_credits = config->rx_credit_max; - link->tx_credits = 0; -} + if ((!router) || (!packet) || (!output_port) || (!delete_leading)) + { + return SW_INVALID_PARAM; + } -sw_link_state_t sw_link_get_state(const sw_link_layer_t *link) -{ - if (!link) - return SW_LINK_ERROR; + /* An empty packet is discarded by the first routing switch (clause 5.6.2.1). */ + if (len == 0) + { + router->packets_discarded++; + return SW_WRONG_LENGTH; + } - return link->state; -} + sw_route_entry_t route; + if (!sw_router_resolve(router, packet[0], &route)) + { + return sw_router_invalid_address(router); + } -void sw_link_set_state(sw_link_layer_t *link, sw_link_state_t state) -{ - if (!link) - return; + /* The routing table is caller-visible state, so the flag is normalised to 0 or 1. */ + *output_port = route.output_port; + *delete_leading = route.delete_addr ? 1u : 0u; + router->packets_routed++; - link->state = state; + return SW_OK; } diff --git a/src/spacewire_spw_packet.c b/src/spacewire_spw_packet.c index 64350c9..ddaa4de 100644 --- a/src/spacewire_spw_packet.c +++ b/src/spacewire_spw_packet.c @@ -1,41 +1,61 @@ /** - * @file spacewire_spw_packet.c - * @brief SpaceWire packet builder (ECSS-E-ST-50-12C clause 5.6.2). + * @file spacewire_spw_packet.c + * @brief SpaceWire packet builder * - * A SpaceWire packet is [destination address][cargo] terminated by an EOP/EEP - * marker. The marker is a link-layer control character and is not part of the - * packet buffer, and no checksum is added. + * Implements the SpaceWire packet as per ECSS-E-ST-50-12C Rev.1, clause 5.6.2: + * [destination address][cargo] terminated by an EOP/EEP marker. The marker is a + * link-layer control character and is not part of the packet buffer, and no + * checksum is added. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "spacewire.h" #include -size_t sw_spw_packet_build(const uint8_t *dest, - size_t dest_len, - const uint8_t *cargo, - size_t cargo_len, - uint8_t *buf, - size_t buf_len) +sw_status_t sw_spw_packet_build(const uint8_t *dest, + size_t dest_len, + const uint8_t *cargo, + size_t cargo_len, + uint8_t *buf, + size_t buf_len, + size_t *out_len) { - if (!buf) - return 0; - - if (dest_len > 0 && !dest) - return 0; - - if (cargo_len > 0 && !cargo) - return 0; + /* Either part may be empty, but a part with a length must have octets. */ + if ((!buf) || (!out_len) || ((dest_len > 0) && (!dest)) || ((cargo_len > 0) && (!cargo))) + { + return SW_INVALID_PARAM; + } + + /* A packet carries at least one data character (clause 5.6.2.1). */ + if ((dest_len == 0) && (cargo_len == 0)) + { + return SW_WRONG_LENGTH; + } + + /* Compared part by part: dest_len + cargo_len can wrap and pass a size check. */ + if ((dest_len > buf_len) || (cargo_len > (buf_len - dest_len))) + { + return SW_BUFFER_TOO_SMALL; + } const size_t total = dest_len + cargo_len; - if (total == 0 || total > buf_len) - return 0; if (dest_len > 0) + { memcpy(buf, dest, dest_len); + } if (cargo_len > 0) + { memcpy(&buf[dest_len], cargo, cargo_len); + } + + *out_len = total; - return total; + return SW_OK; } diff --git a/src/weak/spacewire_payload.c b/src/weak/spacewire_payload.c new file mode 100644 index 0000000..ce67202 --- /dev/null +++ b/src/weak/spacewire_payload.c @@ -0,0 +1,74 @@ +/** + * @file spacewire_payload.c + * @brief Default payload adapter hooks: the payload is raw octets + * + * Weak implementations of the hooks declared in spacewire_payload.h. They make + * the library usable on its own by treating whatever a SpaceWire packet + * carries as an opaque octet string (::sw_payload_raw_t). An application + * replaces them by defining the same functions; see the header for the rules. + * + * This is the only file in src/weak/. Everything here is replaceable, so an + * application that overrides all three hooks can leave the directory out of its + * build; linked from the static library, the file is simply not pulled in. + * See also: docs/space_packet_integration.md + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ + +#include "spacewire_payload.h" + +#include + +SW_WEAK uint32_t sw_payload_size(const void *payload) +{ + const sw_payload_raw_t *raw = payload; + + if ((!raw) || (!raw->data)) + { + return 0; + } + + return raw->len; +} + +SW_WEAK sw_status_t sw_payload_serialize(const void *payload, uint8_t *buf, uint32_t buf_len) +{ + const sw_payload_raw_t *raw = payload; + + if ((!raw) || (!raw->data) || (!buf)) + { + return SW_INVALID_PARAM; + } + + if (raw->len == 0u) + { + return SW_WRONG_LENGTH; + } + + if (buf_len < raw->len) + { + return SW_BUFFER_TOO_SMALL; + } + + memcpy(buf, raw->data, raw->len); + + return SW_OK; +} + +SW_WEAK sw_status_t sw_payload_parse(void *payload, const uint8_t *buf, uint32_t buf_len) +{ + sw_payload_raw_t *raw = payload; + + if ((!raw) || (!buf)) + { + return SW_INVALID_PARAM; + } + + raw->data = buf; + raw->len = buf_len; + + return SW_OK; +} diff --git a/tests/cunit.h b/tests/cunit.h index 6be8be9..01721ad 100644 --- a/tests/cunit.h +++ b/tests/cunit.h @@ -1,9 +1,15 @@ /** - * @file cunit.h - * @brief Tiny C unit-test helpers (assertions and a test-runner macro). + * @file cunit.h + * @brief Tiny C unit-test helpers: assertions and a test-runner macro * - * Include in a single C test file and implement test functions returning 0 on - * success, non-zero on failure. Use RUN_TEST(fn). + * Include in a C test file and implement test functions returning 0 on + * success, non-zero on failure. Use RUN_TEST(fn). The counters are static, + * so each translation unit keeps its own totals. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef CUNIT_H #define CUNIT_H @@ -11,9 +17,13 @@ #include #include +/** @brief Number of failed tests in this translation unit. */ static int cunit_overall_failures = 0; + +/** @brief Number of tests run in this translation unit. */ static int cunit_total_tests = 0; +/** @brief Fail the test unless two values are equal when converted to int. */ #define ASSERT_EQ_INT(expected, actual) \ do \ { \ @@ -28,6 +38,22 @@ static int cunit_total_tests = 0; } \ } while (0) +/** @brief Fail the test unless two unsigned values are equal, compared without truncation. */ +#define ASSERT_EQ_UINT(expected, actual) \ + do \ + { \ + if ((unsigned long long)(expected) != (unsigned long long)(actual)) \ + { \ + printf("ASSERT_EQ_UINT failed: %s:%d: expected %llu got %llu\n", \ + __FILE__, \ + __LINE__, \ + (unsigned long long)(expected), \ + (unsigned long long)(actual)); \ + return 1; \ + } \ + } while (0) + +/** @brief Fail the test unless the first @p len octets of @p a and @p b are equal. */ #define ASSERT_EQ_MEM(a, b, len) \ do \ { \ @@ -38,6 +64,7 @@ static int cunit_total_tests = 0; } \ } while (0) +/** @brief Fail the test unless @p cond is true. */ #define ASSERT_TRUE(cond) \ do \ { \ @@ -48,6 +75,7 @@ static int cunit_total_tests = 0; } \ } while (0) +/** @brief Run test function @p fn, print RUN/PASS/FAIL and update the counters. */ #define RUN_TEST(fn) \ do \ { \ @@ -55,7 +83,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); \ @@ -63,4 +93,4 @@ static int cunit_total_tests = 0; } \ } while (0) -#endif /* CUNIT_H */ \ No newline at end of file +#endif /* CUNIT_H */ diff --git a/tests/test_packet.c b/tests/test_packet.c deleted file mode 100644 index bc51190..0000000 --- a/tests/test_packet.c +++ /dev/null @@ -1,379 +0,0 @@ -/** - * @file test_packet.c - * @brief Unit tests for the CCSDS Packet Transfer Protocol layer. - */ -#include "cunit.h" -#include "spacewire.h" -#include "spacewire_packet.h" -#include "test_runners.h" - -#include - -/* Build a representative packet: logical addressing, no path. */ -static void make_sample(sw_packet_frame_t *pf, const uint8_t *data, uint16_t data_len) -{ - const sw_packet_config_t config = {.path = NULL, - .path_len = 0, - .logical_addr = 0x20, - .user_app = 0x05}; - sw_packet_init(pf, &config); - pf->packet.ph.apid = 0x0042; - pf->packet.data = data; - pf->packet.data_len = data_len; -} - -/* The encoded packet must match the ECSS-E-ST-50-53C Figure 5-1 layout exactly: - * [logical | 0x02 | 0x00 | user_app | CCSDS primary header | data]. */ -static int test_packet_wire_format(void) -{ - static const uint8_t data[3] = {0xAA, 0xBB, 0xCC}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - - /* 4-octet header + 6-octet CCSDS primary header + 3 data octets. */ - ASSERT_EQ_INT(4 + 6 + 3, (int)n); - - const uint8_t expected[13] = { - 0x20, - 0x02, - 0x00, - 0x05, /* logical, proto=0x02, reserved=0, user_app */ - 0x10, - 0x42, - 0x00, - 0x00, - 0x00, - 0x02, /* CCSDS hdr: TC, APID 0x042, seq 0, len=2 */ - 0xAA, - 0xBB, - 0xCC /* data field */ - }; - ASSERT_EQ_MEM(buf, expected, sizeof(expected)); - return 0; -} - -static int test_packet_encode_decode_roundtrip(void) -{ - static const uint8_t data[12] = "Test payload"; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - ASSERT_TRUE(n > 0); - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_INVALID; - sw_result_t ok = sw_packet_decode(&out, buf, n, SW_END_EOP, &status); - - ASSERT_EQ_INT(SW_OK, ok); - ASSERT_EQ_INT(SW_PTP_STATUS_OK, status); - ASSERT_EQ_INT(0x20, out.logical_addr); - ASSERT_EQ_INT(0x05, out.user_app); - ASSERT_EQ_INT(0x0042, out.packet.ph.apid); - ASSERT_EQ_INT((int)sizeof(data), out.packet.data_len); - ASSERT_EQ_MEM(out.packet.data, data, sizeof(data)); - return 0; -} - -/* A path address is emitted ahead of the header and stripped before decode. */ -static int test_packet_with_path(void) -{ - static const uint8_t path[3] = {1, 2, 3}; - static const uint8_t data[4] = {0xDE, 0xAD, 0xBE, 0xEF}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - pf.path = path; - pf.path_len = sizeof(path); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - ASSERT_EQ_INT(3 + 4 + 6 + 4, (int)n); /* path + header + CCSDS hdr + data */ - ASSERT_EQ_MEM(buf, path, sizeof(path)); - ASSERT_EQ_INT(0x20, buf[3]); /* logical address follows the path */ - ASSERT_EQ_INT(0x02, buf[4]); /* protocol identifier */ - - /* The network strips the path; the target decodes from the logical address. */ - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_INVALID; - sw_result_t ok = - sw_packet_decode(&out, buf + sizeof(path), n - sizeof(path), SW_END_EOP, &status); - ASSERT_EQ_INT(SW_OK, ok); - ASSERT_EQ_INT(SW_PTP_STATUS_OK, status); - ASSERT_EQ_MEM(out.packet.data, data, sizeof(data)); - return 0; -} - -static int test_packet_decode_eep_discarded(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_OK; - sw_result_t ok = sw_packet_decode(&out, buf, n, SW_END_EEP, &status); - - ASSERT_EQ_INT(SW_ERR, ok); /* clause 5.5.4.4 */ - ASSERT_EQ_INT(SW_PTP_STATUS_EEP, status); - ASSERT_TRUE(out.packet.data == NULL); /* clause 5.2.3.2 b */ - return 0; -} - -static int test_packet_decode_reserved_nonzero(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - buf[2] = 0x01; /* corrupt the Reserved field */ - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_OK; - sw_result_t ok = sw_packet_decode(&out, buf, n, SW_END_EOP, &status); - - ASSERT_EQ_INT(SW_ERR, ok); /* clause 5.5.4.3 */ - ASSERT_EQ_INT(SW_PTP_STATUS_RESERVED_NONZERO, status); - ASSERT_TRUE(out.packet.data == NULL); - return 0; -} - -static int test_packet_decode_bad_protocol_id(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - buf[1] = 0x01; /* not the CCSDS PTP protocol identifier */ - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_OK; - sw_result_t ok = sw_packet_decode(&out, buf, n, SW_END_EOP, &status); - - ASSERT_EQ_INT(SW_ERR, ok); /* clause 5.5.4.1 */ - ASSERT_EQ_INT(SW_PTP_STATUS_INVALID, status); - return 0; -} - -/* Valid PTP header, but the CCSDS length field declares more data than is - * present, so the encapsulated CCSDS parse fails (clause 5.5.4.2). */ -static int test_packet_decode_malformed_ccsds(void) -{ - const uint8_t buf[11] = { - 0x20, - 0x02, - 0x00, - 0x05, /* logical, proto=0x02, reserved=0, user_app */ - 0x10, - 0x42, - 0x00, - 0x00, - 0x00, - 0x05, /* CCSDS hdr: length field 5 -> data_len 6 */ - 0xAA /* only 1 of the 6 declared data octets */ - }; - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_OK; - sw_result_t ok = sw_packet_decode(&out, buf, sizeof(buf), SW_END_EOP, &status); - - ASSERT_EQ_INT(SW_ERR, ok); - ASSERT_EQ_INT(SW_PTP_STATUS_INVALID, status); - ASSERT_TRUE(out.packet.data == NULL); /* cleared on discard */ - return 0; -} - -static int test_packet_init_defaults(void) -{ - const sw_packet_config_t config = {.path = NULL, - .path_len = 0, - .logical_addr = 0x55, - .user_app = 0x99}; - - sw_packet_frame_t pf; - memset(&pf, 0xA5, sizeof(pf)); - - sw_packet_init(NULL, &config); - sw_packet_init(&pf, NULL); - - sw_packet_init(&pf, &config); - ASSERT_EQ_INT(0x55, pf.logical_addr); - ASSERT_EQ_INT(0x99, pf.user_app); - ASSERT_EQ_INT(0, pf.path_len); - ASSERT_EQ_INT(0, pf.packet.ph.version); - ASSERT_EQ_INT(SP_PACKET_TYPE_TC, pf.packet.ph.type); - ASSERT_EQ_INT(0, pf.packet.ph.sec_hdr_flag); - ASSERT_EQ_INT(0, pf.packet.ph.apid); - return 0; -} - -static int test_packet_encode_error_paths(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - - ASSERT_EQ_INT(0, sw_packet_encode(NULL, buf, sizeof(buf))); - ASSERT_EQ_INT(0, sw_packet_encode(&pf, NULL, sizeof(buf))); - - /* CCSDS data pointer NULL with non-zero length. */ - make_sample(&pf, NULL, 4); - ASSERT_EQ_INT(0, sw_packet_encode(&pf, buf, sizeof(buf))); - - /* Empty CCSDS data field is below the minimum packet length. */ - make_sample(&pf, data, 0); - ASSERT_EQ_INT(0, sw_packet_encode(&pf, buf, sizeof(buf))); - - /* path_len set but path pointer NULL. */ - make_sample(&pf, data, sizeof(data)); - pf.path = NULL; - pf.path_len = 2; - ASSERT_EQ_INT(0, sw_packet_encode(&pf, buf, sizeof(buf))); - - /* path octet outside the valid 0..31 range. */ - static const uint8_t bad_path[1] = {32}; - make_sample(&pf, data, sizeof(data)); - pf.path = bad_path; - pf.path_len = 1; - ASSERT_EQ_INT(0, sw_packet_encode(&pf, buf, sizeof(buf))); - - /* Output buffer too small. */ - make_sample(&pf, data, sizeof(data)); - ASSERT_EQ_INT(0, sw_packet_encode(&pf, buf, 5)); - return 0; -} - -static int test_packet_decode_error_paths(void) -{ - const uint8_t buf[16] = {0}; - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_OK; - - ASSERT_EQ_INT(SW_INVALID_PARAM, sw_packet_decode(NULL, buf, sizeof(buf), SW_END_EOP, &status)); - ASSERT_EQ_INT(SW_PTP_STATUS_INVALID, status); - - status = SW_PTP_STATUS_OK; - ASSERT_EQ_INT(SW_INVALID_PARAM, sw_packet_decode(&out, NULL, sizeof(buf), SW_END_EOP, &status)); - ASSERT_EQ_INT(SW_PTP_STATUS_INVALID, status); - - /* Shorter than header + minimal CCSDS packet (11 octets). */ - status = SW_PTP_STATUS_OK; - ASSERT_EQ_INT(SW_ERR, sw_packet_decode(&out, buf, 10, SW_END_EOP, &status)); - ASSERT_EQ_INT(SW_PTP_STATUS_INVALID, status); - - /* A NULL status pointer is tolerated. */ - ASSERT_EQ_INT(SW_ERR, sw_packet_decode(&out, buf, 10, SW_END_EOP, NULL)); - - /* Invalid arguments with a NULL status pointer must also be tolerated. */ - ASSERT_EQ_INT(SW_INVALID_PARAM, sw_packet_decode(NULL, buf, sizeof(buf), SW_END_EOP, NULL)); - return 0; -} - -static int test_packet_create_convenience(void) -{ - const uint8_t payload[3] = {0x11, 0x22, 0x33}; - uint8_t buf[64]; - - size_t n = sw_packet_create(0xFE, 0x07, 0x0042, payload, sizeof(payload), buf, sizeof(buf)); - ASSERT_EQ_INT(4 + 6 + 3, (int)n); - ASSERT_EQ_INT(0xFE, buf[0]); - ASSERT_EQ_INT(0x02, buf[1]); - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_INVALID; - ASSERT_EQ_INT(SW_OK, sw_packet_decode(&out, buf, n, SW_END_EOP, &status)); - ASSERT_EQ_INT(SW_PTP_STATUS_OK, status); - ASSERT_EQ_INT(0x07, out.user_app); - ASSERT_EQ_INT(0x0042, out.packet.ph.apid); - - ASSERT_EQ_INT(0, - sw_packet_create(0xFE, 0, 0x0042, payload, sizeof(payload), NULL, sizeof(buf))); - return 0; -} - -static int test_packet_statistics(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - uint8_t buf[64]; - - sw_reset_statistics(); - sw_statistics_t stats; - sw_get_statistics(NULL); /* tolerate NULL */ - sw_get_statistics(&stats); - ASSERT_EQ_INT(0, stats.packets_sent); - - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - sw_packet_frame_t out; - ASSERT_EQ_INT(SW_OK, sw_packet_decode(&out, buf, n, SW_END_EOP, NULL)); - ASSERT_EQ_INT(SW_ERR, sw_packet_decode(&out, buf, n, SW_END_EEP, NULL)); /* discarded */ - - sw_get_statistics(&stats); - ASSERT_EQ_INT(1, stats.packets_sent); - ASSERT_EQ_INT(1, stats.packets_received); - ASSERT_EQ_INT(1, stats.packets_discarded); - ASSERT_TRUE(stats.bytes_sent > 0); - ASSERT_TRUE(stats.bytes_received > 0); - - sw_reset_statistics(); - sw_get_statistics(&stats); - ASSERT_EQ_INT(0, stats.packets_received); - return 0; -} - -/* The virtual channel rides in the User Application field (clause 5.3.5 NOTE 2). */ -static int test_packet_virtual_channel(void) -{ - static const uint8_t data[4] = {1, 2, 3, 4}; - sw_packet_frame_t pf; - make_sample(&pf, data, sizeof(data)); - - sw_packet_set_virtual_channel(&pf, 0x07); - ASSERT_EQ_INT(0x07, sw_packet_virtual_channel(&pf)); - ASSERT_EQ_INT(0x07, pf.user_app); - - uint8_t buf[64]; - size_t n = sw_packet_encode(&pf, buf, sizeof(buf)); - ASSERT_TRUE(n > 0); - - sw_packet_frame_t out; - sw_ptp_status_t status = SW_PTP_STATUS_INVALID; - ASSERT_EQ_INT(SW_OK, sw_packet_decode(&out, buf, n, SW_END_EOP, &status)); - ASSERT_EQ_INT(0x07, sw_packet_virtual_channel(&out)); - - sw_packet_set_virtual_channel(NULL, 0x09); /* no-op, must not crash */ - ASSERT_EQ_INT(0, sw_packet_virtual_channel(NULL)); - return 0; -} - -test_result_t test_spacewire_packet_run_all(void) -{ - RUN_TEST(test_packet_wire_format); - RUN_TEST(test_packet_encode_decode_roundtrip); - RUN_TEST(test_packet_with_path); - RUN_TEST(test_packet_decode_eep_discarded); - RUN_TEST(test_packet_decode_reserved_nonzero); - RUN_TEST(test_packet_decode_bad_protocol_id); - RUN_TEST(test_packet_decode_malformed_ccsds); - RUN_TEST(test_packet_init_defaults); - RUN_TEST(test_packet_encode_error_paths); - RUN_TEST(test_packet_decode_error_paths); - RUN_TEST(test_packet_create_convenience); - RUN_TEST(test_packet_virtual_channel); - RUN_TEST(test_packet_statistics); - return (test_result_t){cunit_total_tests - cunit_overall_failures, cunit_total_tests}; -} diff --git a/tests/test_payload.c b/tests/test_payload.c new file mode 100644 index 0000000..519efdd --- /dev/null +++ b/tests/test_payload.c @@ -0,0 +1,132 @@ +/** + * @file test_payload.c + * @brief Unit tests for the default (raw-octet) payload adapter hooks + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ +#include "cunit.h" +#include "spacewire_payload.h" +#include "test_runners.h" + +#include +#include + +/** @brief Fill value used to detect writes into a buffer that must stay untouched. */ +#define FILL 0x5Au + +static const uint8_t sample[5] = {0x11, 0x22, 0x33, 0x44, 0x55}; + +static int test_payload_size(void) +{ + const sw_payload_raw_t raw = {.data = sample, .len = sizeof(sample)}; + const sw_payload_raw_t one_octet = {.data = sample, .len = 1}; + const sw_payload_raw_t no_data = {.data = NULL, .len = sizeof(sample)}; + const sw_payload_raw_t empty = {.data = sample, .len = 0}; + + ASSERT_EQ_INT(5, sw_payload_size(&raw)); + ASSERT_EQ_INT(1, sw_payload_size(&one_octet)); + ASSERT_EQ_INT(0, sw_payload_size(NULL)); + ASSERT_EQ_INT(0, sw_payload_size(&no_data)); + ASSERT_EQ_INT(0, sw_payload_size(&empty)); + return 0; +} + +/* The buffer is exactly as large as the payload, so ASan would flag any overrun. */ +static int test_payload_serialize_exact_buffer(void) +{ + const sw_payload_raw_t raw = {.data = sample, .len = sizeof(sample)}; + uint8_t *buf = (uint8_t *)malloc(sizeof(sample)); + if (!buf) + { + return 1; + } + memset(buf, FILL, sizeof(sample)); + + const sw_status_t status = sw_payload_serialize(&raw, buf, sizeof(sample)); + const int copied = (memcmp(buf, sample, sizeof(sample)) == 0); + free(buf); + + ASSERT_EQ_INT(SW_OK, status); + ASSERT_TRUE(copied); + return 0; +} + +static int test_payload_serialize_short_buffer_unchanged(void) +{ + const sw_payload_raw_t raw = {.data = sample, .len = sizeof(sample)}; + const uint8_t untouched[4] = {FILL, FILL, FILL, FILL}; + uint8_t *one_short = (uint8_t *)malloc(sizeof(untouched)); + if (!one_short) + { + return 1; + } + memset(one_short, FILL, sizeof(untouched)); + + const sw_status_t status = sw_payload_serialize(&raw, one_short, sizeof(untouched)); + const int unchanged = (memcmp(one_short, untouched, sizeof(untouched)) == 0); + free(one_short); + + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, status); + ASSERT_TRUE(unchanged); + return 0; +} + +static int test_payload_serialize_invalid_args_unchanged(void) +{ + const sw_payload_raw_t raw = {.data = sample, .len = sizeof(sample)}; + const sw_payload_raw_t no_data = {.data = NULL, .len = sizeof(sample)}; + const sw_payload_raw_t empty = {.data = sample, .len = 0}; + const uint8_t untouched[5] = {FILL, FILL, FILL, FILL, FILL}; + uint8_t buf[5] = {FILL, FILL, FILL, FILL, FILL}; + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_payload_serialize(NULL, buf, sizeof(buf))); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_payload_serialize(&no_data, buf, sizeof(buf))); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_payload_serialize(&raw, NULL, sizeof(buf))); + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_payload_serialize(&empty, buf, sizeof(buf))); + ASSERT_EQ_MEM(buf, untouched, sizeof(untouched)); + return 0; +} + +static int test_payload_parse_zero_copy(void) +{ + sw_payload_raw_t raw = {.data = NULL, .len = 0}; + + ASSERT_EQ_INT(SW_OK, sw_payload_parse(&raw, sample, sizeof(sample))); + ASSERT_TRUE(raw.data == sample); + ASSERT_EQ_INT(5, raw.len); + + ASSERT_EQ_INT(SW_OK, sw_payload_parse(&raw, sample, 1)); + ASSERT_EQ_INT(1, raw.len); + + /* An empty carried packet is still delivered (ECSS-E-ST-50-53C clause 5.5.4.2 a). */ + ASSERT_EQ_INT(SW_OK, sw_payload_parse(&raw, sample, 0)); + ASSERT_TRUE(raw.data == sample); + ASSERT_EQ_INT(0, raw.len); + return 0; +} + +static int test_payload_parse_failure_unchanged(void) +{ + const uint8_t other[1] = {0}; + sw_payload_raw_t raw = {.data = other, .len = sizeof(other)}; + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_payload_parse(NULL, sample, sizeof(sample))); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_payload_parse(&raw, NULL, sizeof(sample))); + ASSERT_TRUE(raw.data == other); + ASSERT_EQ_INT(1, raw.len); + return 0; +} + +test_result_t test_spacewire_payload_run_all(void) +{ + RUN_TEST(test_payload_size); + RUN_TEST(test_payload_serialize_exact_buffer); + RUN_TEST(test_payload_serialize_short_buffer_unchanged); + RUN_TEST(test_payload_serialize_invalid_args_unchanged); + RUN_TEST(test_payload_parse_zero_copy); + RUN_TEST(test_payload_parse_failure_unchanged); + return (test_result_t){cunit_total_tests - cunit_overall_failures, cunit_total_tests}; +} diff --git a/tests/test_payload_override.c b/tests/test_payload_override.c new file mode 100644 index 0000000..a2a4afd --- /dev/null +++ b/tests/test_payload_override.c @@ -0,0 +1,272 @@ +/** + * @file test_payload_override.c + * @brief Unit tests for the payload adapter with application-supplied hooks + * + * Built into its own binary. The strong sw_payload_*() definitions in this + * file replace the library's weak defaults at link time, which proves the + * override mechanism and reaches the hook-failure paths of the packet transfer + * layer that the default raw-octet hooks cannot trigger. + * + * The hooks implement a toy length-prefixed protocol: [total length][body]. Its + * parse hook checks the length prefix itself, since the packet transfer layer + * hands it every octet after the header unchecked. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ +#include "cunit.h" +#include "spacewire_payload.h" +#include "spacewire_ptp.h" + +#include +#include + +/** @brief Length of the toy protocol's length prefix in octets. */ +#define TOY_PREFIX_LEN 1u + +/** @brief Body length used by the tests; with the prefix it exceeds the 7-octet minimum. */ +#define TOY_BODY_LEN 8u + +/** @brief Serialised length of the test packet: prefix plus body. */ +#define TOY_PACKET_LEN (TOY_PREFIX_LEN + TOY_BODY_LEN) + +/** @brief Length of the encoded test packet: encapsulation header plus toy packet. */ +#define TOY_WIRE_LEN (SW_PTP_HEADER_LEN + TOY_PACKET_LEN) + +/** @brief Fill value used to detect writes into memory that must stay untouched. */ +#define FILL 0x5Au + +/** + * @brief Payload object of the toy protocol. + * + * @note @p body points into caller-owned memory, or into the parsed buffer. + */ +typedef struct +{ + const uint8_t *body; /**< Body octets. */ + uint8_t body_len; /**< Number of body octets. */ +} toy_packet_t; + +static int g_fail_serialize = 0; +static int g_fail_parse = 0; + +static const uint8_t toy_body[TOY_BODY_LEN] = {1, 2, 3, 4, 5, 6, 7, 8}; +static const toy_packet_t toy_packet = {.body = toy_body, .body_len = TOY_BODY_LEN}; +static const sw_ptp_frame_t toy_frame = {.path = NULL, + .path_len = 0, + .logical_addr = 0x20, + .user_app = 0x05}; + +uint32_t sw_payload_size(const void *payload) +{ + const toy_packet_t *pkt = payload; + + return (uint32_t)pkt->body_len + TOY_PREFIX_LEN; +} + +sw_status_t sw_payload_serialize(const void *payload, uint8_t *buf, uint32_t buf_len) +{ + const toy_packet_t *pkt = payload; + const uint32_t need = (uint32_t)pkt->body_len + TOY_PREFIX_LEN; + + /* A hook-specific code: the library must still report SW_PAYLOAD_ERROR. */ + if ((g_fail_serialize) || (buf_len < need)) + { + return SW_BUFFER_TOO_SMALL; + } + + buf[0] = (uint8_t)need; + memcpy(&buf[TOY_PREFIX_LEN], pkt->body, pkt->body_len); + + return SW_OK; +} + +sw_status_t sw_payload_parse(void *payload, const uint8_t *buf, uint32_t buf_len) +{ + toy_packet_t *pkt = payload; + + /* Trailing octets beyond the declared length are ignored; a packet cut short fails. */ + if ((g_fail_parse) || (buf_len < TOY_PREFIX_LEN) || (buf[0] < TOY_PREFIX_LEN) || + (buf[0] > buf_len)) + { + return SW_WRONG_LENGTH; + } + + pkt->body = &buf[TOY_PREFIX_LEN]; + pkt->body_len = (uint8_t)(buf[0] - TOY_PREFIX_LEN); + + return SW_OK; +} + +/** + * @brief Decode a buffer the parse hook must refuse, and check nothing was delivered. + * + * @param[in] buf Received buffer. + * @param[in] buf_len Buffer length in octets. + * @return 0 if the decode returned ::SW_PAYLOAD_ERROR and left the frame, the + * payload object and the receive status unchanged, 1 otherwise. + */ +static int expect_invalid(const uint8_t *buf, uint32_t buf_len) +{ + sw_ptp_frame_t pf; + sw_ptp_frame_t pf_before; + toy_packet_t pkt; + toy_packet_t pkt_before; + memset(&pf, FILL, sizeof(pf)); + memset(&pkt, FILL, sizeof(pkt)); + memcpy(&pf_before, &pf, sizeof(pf)); + memcpy(&pkt_before, &pkt, sizeof(pkt)); + + sw_ptp_status_t status = (sw_ptp_status_t)FILL; + ASSERT_EQ_INT(SW_PAYLOAD_ERROR, sw_ptp_decode(&pf, &pkt, buf, buf_len, SW_END_EOP, &status)); + ASSERT_EQ_INT(FILL, status); + ASSERT_EQ_MEM(&pf, &pf_before, sizeof(pf)); + ASSERT_EQ_MEM(&pkt, &pkt_before, sizeof(pkt)); + return 0; +} + +/** + * @brief Encode the test packet into a heap buffer sized for a given received length. + * + * @param[in] wire_len Buffer length to allocate; octets beyond the encoded + * packet are zero, a shorter buffer truncates it. + * @return Buffer of @p wire_len octets owned by the caller, or NULL if allocation fails. + */ +static uint8_t *alloc_toy_wire(uint32_t wire_len) +{ + uint8_t encoded[TOY_WIRE_LEN]; + uint8_t *wire = (uint8_t *)calloc(wire_len, 1); + uint32_t n = 0; + + if ((wire) && (sw_ptp_encode(&toy_frame, &toy_packet, encoded, sizeof(encoded), &n) == SW_OK)) + { + memcpy(wire, encoded, (wire_len < TOY_WIRE_LEN) ? wire_len : TOY_WIRE_LEN); + } + + return wire; +} + +/* The length prefix on the wire can only come from the hooks in this file. */ +static int test_override_replaces_defaults(void) +{ + uint8_t *wire = alloc_toy_wire(TOY_WIRE_LEN); + if (!wire) + { + return 1; + } + + sw_ptp_frame_t pf; + toy_packet_t pkt = {.body = NULL, .body_len = 0}; + const sw_status_t result = sw_ptp_decode(&pf, &pkt, wire, TOY_WIRE_LEN, SW_END_EOP, NULL); + const uint8_t prefix = wire[SW_PTP_HEADER_LEN]; + const int zero_copy = (pkt.body == &wire[SW_PTP_HEADER_LEN + TOY_PREFIX_LEN]); + const int same_body = + (pkt.body_len == TOY_BODY_LEN) && (memcmp(pkt.body, toy_body, TOY_BODY_LEN) == 0); + free(wire); + + ASSERT_EQ_INT(TOY_PACKET_LEN, prefix); + ASSERT_EQ_INT(SW_OK, result); + ASSERT_EQ_INT(0x20, pf.logical_addr); + ASSERT_TRUE(zero_copy); + ASSERT_TRUE(same_body); + return 0; +} + +/* A trailing octet is not checked by the packet transfer layer (ECSS-E-ST-50-53C + * clause 4.3); every octet reaches the parse hook, which here ignores the extra one. */ +static int test_override_trailing_octet_reaches_hook(void) +{ + uint8_t *wire = alloc_toy_wire(TOY_WIRE_LEN + 1u); + if (!wire) + { + return 1; + } + + sw_ptp_frame_t pf; + toy_packet_t pkt = {.body = NULL, .body_len = 0}; + const sw_status_t result = sw_ptp_decode(&pf, &pkt, wire, TOY_WIRE_LEN + 1u, SW_END_EOP, NULL); + free(wire); + + ASSERT_EQ_INT(SW_OK, result); + ASSERT_EQ_INT(TOY_BODY_LEN, pkt.body_len); + return 0; +} + +/* The carried packet declares one octet more than arrived: the parse hook rejects it. */ +static int test_override_truncated_packet_rejected(void) +{ + uint8_t *wire = alloc_toy_wire(TOY_WIRE_LEN - 1u); + if (!wire) + { + return 1; + } + + const int failed = expect_invalid(wire, TOY_WIRE_LEN - 1u); + free(wire); + + ASSERT_EQ_INT(0, failed); + return 0; +} + +static int test_override_parse_failure_discards(void) +{ + uint8_t *wire = alloc_toy_wire(TOY_WIRE_LEN); + if (!wire) + { + return 1; + } + + sw_ptp_statistics_t stats; + sw_ptp_reset_statistics(); + g_fail_parse = 1; + const int failed = expect_invalid(wire, TOY_WIRE_LEN); + g_fail_parse = 0; + sw_ptp_get_statistics(&stats); + free(wire); + + ASSERT_EQ_INT(0, failed); + ASSERT_EQ_INT(1, stats.packets_discarded); + ASSERT_EQ_INT(0, stats.packets_received); + return 0; +} + +/* The header is written only after the hook succeeded, so the buffer stays untouched. */ +static int test_override_serialize_failure_leaves_buffer_unchanged(void) +{ + uint8_t untouched[TOY_WIRE_LEN]; + uint8_t wire[TOY_WIRE_LEN]; + sw_ptp_statistics_t stats; + memset(untouched, FILL, sizeof(untouched)); + memset(wire, FILL, sizeof(wire)); + + sw_ptp_reset_statistics(); + g_fail_serialize = 1; + uint32_t n = UINT32_MAX; + const sw_status_t status = sw_ptp_encode(&toy_frame, &toy_packet, wire, sizeof(wire), &n); + g_fail_serialize = 0; + sw_ptp_get_statistics(&stats); + + ASSERT_EQ_INT(SW_PAYLOAD_ERROR, status); + ASSERT_EQ_UINT(UINT32_MAX, n); + ASSERT_EQ_MEM(wire, untouched, sizeof(wire)); + ASSERT_EQ_INT(0, stats.packets_sent); + return 0; +} + +int main(void) +{ + RUN_TEST(test_override_replaces_defaults); + RUN_TEST(test_override_trailing_octet_reaches_hook); + RUN_TEST(test_override_truncated_packet_rejected); + RUN_TEST(test_override_parse_failure_discards); + RUN_TEST(test_override_serialize_failure_leaves_buffer_unchanged); + + printf(" %-14s Passed %d/%d\n", + "override:", + cunit_total_tests - cunit_overall_failures, + cunit_total_tests); + + return (cunit_overall_failures == 0) ? 0 : 1; +} diff --git a/tests/test_ptp.c b/tests/test_ptp.c new file mode 100644 index 0000000..063907d --- /dev/null +++ b/tests/test_ptp.c @@ -0,0 +1,687 @@ +/** + * @file test_ptp.c + * @brief Unit tests for the CCSDS Packet Transfer Protocol layer + * + * Runs against the default payload hooks, so the carried packet is a + * ::sw_payload_raw_t. The hook-failure paths that the defaults cannot trigger + * are covered by test_payload_override.c. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode + */ +#include "cunit.h" +#include "spacewire_payload.h" +#include "spacewire_ptp.h" +#include "test_runners.h" + +#include +#include + +/** @brief Length of the carried sample packet in octets. */ +#define SAMPLE_PACKET_LEN 9u + +/** @brief Length of the encoded sample: encapsulation header plus carried packet. */ +#define SAMPLE_WIRE_LEN (SW_PTP_HEADER_LEN + SAMPLE_PACKET_LEN) + +/** @brief Fill value used to detect writes into memory that must stay untouched. */ +#define FILL 0x5Au + +/* A CCSDS Space Packet (TC, APID 0x042, unsegmented, 3 data octets), carried as raw octets. */ +static const uint8_t sample_packet[SAMPLE_PACKET_LEN] = + {0x10, 0x42, 0xC0, 0x00, 0x00, 0x02, 0xAA, 0xBB, 0xCC}; + +static const sw_payload_raw_t sample_payload = {.data = sample_packet, .len = SAMPLE_PACKET_LEN}; + +static const sw_ptp_frame_t sample_frame = {.path = NULL, + .path_len = 0, + .logical_addr = 0x20, + .user_app = 0x05}; + +/** + * @brief Check that every octet of a buffer still holds ::FILL. + * + * @param[in] buf Buffer to inspect. + * @param[in] len Buffer length in octets. + * @return 1 if the buffer is untouched, 0 otherwise. + */ +static int is_untouched(const uint8_t *buf, uint32_t len) +{ + for (uint32_t i = 0; i < len; i++) + { + if (buf[i] != FILL) + { + return 0; + } + } + + return 1; +} + +/** + * @brief Encode the sample packet into an exact-size heap buffer. + * + * Exact-size buffers let ASan flag any one-octet overrun or over-read. + * + * @return Buffer of ::SAMPLE_WIRE_LEN octets owned by the caller, or NULL if + * allocation or encoding fails. + */ +static uint8_t *alloc_sample_wire(void) +{ + uint8_t *wire = (uint8_t *)malloc(SAMPLE_WIRE_LEN); + uint32_t n = 0; + + if ((wire) && + ((sw_ptp_encode(&sample_frame, &sample_payload, wire, SAMPLE_WIRE_LEN, &n) != SW_OK) || + (n != SAMPLE_WIRE_LEN))) + { + free(wire); + wire = NULL; + } + + return wire; +} + +/** + * @brief Decode a buffer that must not be delivered and check nothing was. + * + * @param[in] buf Received buffer. + * @param[in] buf_len Buffer length in octets. + * @param[in] end End-of-packet marker. + * @param[in] expected Status sw_ptp_decode() must return. + * @param[in] expected_status Receive status it must report, or ::FILL if the + * receive status must be left unchanged. + * @return 0 if sw_ptp_decode() returned @p expected, reported @p expected_status + * and left the frame and the payload object unchanged, 1 otherwise. + */ +static int expect_not_delivered(const uint8_t *buf, + uint32_t buf_len, + sw_end_marker_t end, + sw_status_t expected, + int expected_status) +{ + sw_ptp_frame_t pf; + sw_ptp_frame_t pf_before; + sw_payload_raw_t payload; + sw_payload_raw_t payload_before; + memset(&pf, FILL, sizeof(pf)); + memset(&payload, FILL, sizeof(payload)); + memcpy(&pf_before, &pf, sizeof(pf)); + memcpy(&payload_before, &payload, sizeof(payload)); + + sw_ptp_status_t status = (sw_ptp_status_t)FILL; + ASSERT_EQ_INT(expected, sw_ptp_decode(&pf, &payload, buf, buf_len, end, &status)); + ASSERT_EQ_INT(expected_status, status); + ASSERT_EQ_MEM(&pf, &pf_before, sizeof(pf)); + ASSERT_EQ_MEM(&payload, &payload_before, sizeof(payload)); + return 0; +} + +/** + * @brief Overwrite one octet of the encoded sample and expect it not to be delivered. + * + * @param[in] offset Offset of the octet to overwrite. + * @param[in] value Value to write there. + * @param[in] end End-of-packet marker. + * @param[in] expected Status sw_ptp_decode() must return. + * @param[in] expected_status Receive status it must report, or ::FILL if unchanged. + * @return 0 on the expected outcome, 1 otherwise. + */ +static int discard_corrupted(uint32_t offset, + uint8_t value, + sw_end_marker_t end, + sw_status_t expected, + int expected_status) +{ + uint8_t *wire = alloc_sample_wire(); + if (!wire) + { + return 1; + } + wire[offset] = value; + + const int failed = expect_not_delivered(wire, SAMPLE_WIRE_LEN, end, expected, expected_status); + free(wire); + + return failed; +} + +/** + * @brief Encode a zero-filled carried packet of a given length into an exact-size buffer. + * + * @param[in] payload_len Length of the carried packet in octets. + * @param[out] encoded_len Octets written; left unchanged unless the encode succeeds. + * @return Result of sw_ptp_encode(), or ::SW_INVALID_PARAM if allocation fails. + */ +static sw_status_t encode_payload_of(uint32_t payload_len, uint32_t *encoded_len) +{ + const uint32_t wire_len = SW_PTP_HEADER_LEN + payload_len; + uint8_t *data = (uint8_t *)calloc(payload_len, 1); + uint8_t *wire = (uint8_t *)malloc(wire_len); + sw_status_t result = SW_INVALID_PARAM; + + if ((data) && (wire)) + { + const sw_payload_raw_t payload = {.data = data, .len = payload_len}; + result = sw_ptp_encode(&sample_frame, &payload, wire, wire_len, encoded_len); + } + + free(wire); + free(data); + + return result; +} + +/** + * @brief Decode a zero-filled carried packet of a given length from an exact-size buffer. + * + * @param[in] payload_len Length of the carried packet in octets. + * @param[out] decoded_len Length of the delivered payload, or 0 if none was delivered. + * @return Result of sw_ptp_decode(), or ::SW_INVALID_PARAM if allocation fails. + */ +static sw_status_t decode_payload_of(uint32_t payload_len, uint32_t *decoded_len) +{ + const uint32_t wire_len = SW_PTP_HEADER_LEN + payload_len; + uint8_t *wire = (uint8_t *)calloc(wire_len, 1); + if (!wire) + { + return SW_INVALID_PARAM; + } + wire[0] = sample_frame.logical_addr; + wire[1] = SW_PTP_PROTOCOL_ID; + + sw_ptp_frame_t pf; + sw_payload_raw_t payload = {.data = NULL, .len = 0}; + const sw_status_t result = sw_ptp_decode(&pf, &payload, wire, wire_len, SW_END_EOP, NULL); + *decoded_len = payload.len; + free(wire); + + return result; +} + +/* The encoded packet must match the ECSS-E-ST-50-53C Figure 5-1 layout exactly: + * [logical | 0x02 | 0x00 | user_app | carried packet]. */ +static int test_ptp_wire_format(void) +{ + const uint8_t expected[SAMPLE_WIRE_LEN] = { + 0x20, + 0x02, + 0x00, + 0x05, /* logical, proto=0x02, reserved=0, user_app */ + 0x10, + 0x42, + 0xC0, + 0x00, + 0x00, + 0x02, /* carried packet: CCSDS primary header */ + 0xAA, + 0xBB, + 0xCC /* carried packet: data field */ + }; + + uint8_t *wire = alloc_sample_wire(); + if (!wire) + { + return 1; + } + const int matches = (memcmp(wire, expected, sizeof(expected)) == 0); + free(wire); + + ASSERT_TRUE(matches); + return 0; +} + +/* The lowest and highest valid header field values are emitted byte-for-byte. */ +static int test_ptp_wire_format_field_extremes(void) +{ + const sw_ptp_frame_t lowest = {.path = NULL, + .path_len = 0, + .logical_addr = SW_LOGICAL_ADDR_MIN, + .user_app = 0x00}; + const sw_ptp_frame_t highest = {.path = NULL, + .path_len = 0, + .logical_addr = SW_LOGICAL_ADDR_RESERVED - 1u, + .user_app = 0xFF}; + uint8_t wire[SAMPLE_WIRE_LEN]; + uint32_t n = 0; + + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&lowest, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(SAMPLE_WIRE_LEN, n); + ASSERT_EQ_INT(0x20, wire[0]); + ASSERT_EQ_INT(0x02, wire[1]); + ASSERT_EQ_INT(0x00, wire[2]); + ASSERT_EQ_INT(0x00, wire[3]); + + n = 0; + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&highest, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(SAMPLE_WIRE_LEN, n); + ASSERT_EQ_INT(0xFE, wire[0]); + ASSERT_EQ_INT(0x02, wire[1]); + ASSERT_EQ_INT(0x00, wire[2]); + ASSERT_EQ_INT(0xFF, wire[3]); + return 0; +} + +/* Only 32..254 is a Target Logical Address: 31 would route as a path, 255 is reserved. */ +static int test_ptp_encode_logical_addr_bounds(void) +{ + uint8_t wire[SAMPLE_WIRE_LEN]; + sw_ptp_frame_t pf = sample_frame; + uint32_t n = UINT32_MAX; + memset(wire, FILL, sizeof(wire)); + + pf.logical_addr = SW_LOGICAL_ADDR_MIN - 1u; + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + pf.logical_addr = 0; + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + pf.logical_addr = SW_LOGICAL_ADDR_RESERVED; + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_INT( + SW_WRONG_ADDRESS, + sw_ptp_create(SW_LOGICAL_ADDR_RESERVED, 0, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_TRUE(is_untouched(wire, sizeof(wire))); + ASSERT_EQ_UINT(UINT32_MAX, n); + + pf.logical_addr = SW_LOGICAL_ADDR_MIN; + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(SAMPLE_WIRE_LEN, n); + pf.logical_addr = SW_LOGICAL_ADDR_RESERVED - 1u; + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + return 0; +} + +static int test_ptp_encode_decode_roundtrip(void) +{ + uint8_t *wire = alloc_sample_wire(); + if (!wire) + { + return 1; + } + + sw_ptp_frame_t pf; + sw_payload_raw_t payload = {.data = NULL, .len = 0}; + sw_ptp_status_t status = (sw_ptp_status_t)FILL; + memset(&pf, FILL, sizeof(pf)); + const sw_status_t result = + sw_ptp_decode(&pf, &payload, wire, SAMPLE_WIRE_LEN, SW_END_EOP, &status); + const int zero_copy = (payload.data == &wire[SW_PTP_HEADER_LEN]); + const int same_data = (payload.len == SAMPLE_PACKET_LEN) && + (memcmp(payload.data, sample_packet, payload.len) == 0); + free(wire); + + ASSERT_EQ_INT(SW_OK, result); + ASSERT_EQ_INT(SW_PTP_STATUS_OK, status); + ASSERT_EQ_INT(0x20, pf.logical_addr); + ASSERT_EQ_INT(0x05, pf.user_app); + ASSERT_TRUE(!pf.path); + ASSERT_EQ_INT(0, pf.path_len); + ASSERT_TRUE(zero_copy); + ASSERT_TRUE(same_data); + return 0; +} + +/* A path address is emitted ahead of the header; 31 is the largest valid path octet. */ +static int test_ptp_with_path(void) +{ + static const uint8_t path[3] = {0, 2, 31}; + sw_ptp_frame_t pf = sample_frame; + pf.path = path; + pf.path_len = sizeof(path); + + uint8_t wire[sizeof(path) + SAMPLE_WIRE_LEN]; + uint32_t n = 0; + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(sizeof(wire), n); + ASSERT_EQ_MEM(wire, path, sizeof(path)); + ASSERT_EQ_INT(0x20, wire[3]); /* logical address follows the path */ + ASSERT_EQ_INT(0x02, wire[4]); /* protocol identifier */ + ASSERT_EQ_MEM(&wire[sizeof(path) + SW_PTP_HEADER_LEN], sample_packet, SAMPLE_PACKET_LEN); + + /* The network strips the path; the target decodes from the logical address. */ + sw_ptp_frame_t out; + sw_payload_raw_t payload = {.data = NULL, .len = 0}; + ASSERT_EQ_INT( + SW_OK, + sw_ptp_decode(&out, &payload, &wire[sizeof(path)], SAMPLE_WIRE_LEN, SW_END_EOP, NULL)); + ASSERT_EQ_INT(SAMPLE_PACKET_LEN, payload.len); + return 0; +} + +/* Regional addressing (clause 5.3.1 b): logical-address octets 32..254 may sit in the + * Target SpaceWire Address, alone or mixed with path octets. */ +static int test_ptp_with_regional_address(void) +{ + static const uint8_t address[4] = {3, SW_LOGICAL_ADDR_MIN, 7, SW_LOGICAL_ADDR_RESERVED - 1u}; + sw_ptp_frame_t pf = sample_frame; + pf.path = address; + pf.path_len = sizeof(address); + + uint8_t wire[sizeof(address) + SAMPLE_WIRE_LEN]; + uint32_t n = 0; + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(sizeof(wire), n); + ASSERT_EQ_MEM(wire, address, sizeof(address)); + ASSERT_EQ_INT(0x20, wire[sizeof(address)]); + ASSERT_EQ_INT(0x02, wire[sizeof(address) + 1u]); + return 0; +} + +/* The longest path the frame can describe (255 octets) fits an exact-size buffer. */ +static int test_ptp_encode_max_path(void) +{ + static const uint8_t path[UINT8_MAX] = {0}; + const uint32_t wire_len = UINT8_MAX + SAMPLE_WIRE_LEN; + sw_ptp_frame_t pf = sample_frame; + pf.path = path; + pf.path_len = UINT8_MAX; + + uint8_t *wire = (uint8_t *)calloc(wire_len, 1); + if (!wire) + { + return 1; + } + uint32_t n = 0; + const sw_status_t result = sw_ptp_encode(&pf, &sample_payload, wire, wire_len, &n); + const int header_follows_path = (wire[UINT8_MAX] == 0x20) && (wire[UINT8_MAX + 1] == 0x02); + free(wire); + + ASSERT_EQ_INT(SW_OK, result); + ASSERT_EQ_UINT(wire_len, n); + ASSERT_TRUE(header_follows_path); + return 0; +} + +static int test_ptp_encode_invalid_path_unchanged(void) +{ + /* 254 is the largest usable octet; 255 is the reserved logical address. */ + static const uint8_t bad_path[2] = {SW_LOGICAL_ADDR_RESERVED - 1u, SW_LOGICAL_ADDR_RESERVED}; + uint8_t wire[2 + SAMPLE_WIRE_LEN]; + uint32_t n = UINT32_MAX; + memset(wire, FILL, sizeof(wire)); + + sw_ptp_frame_t pf = sample_frame; + pf.path = bad_path; + pf.path_len = sizeof(bad_path); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + + pf.path = NULL; /* path_len set but no path */ + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_TRUE(is_untouched(wire, sizeof(wire))); + ASSERT_EQ_UINT(UINT32_MAX, n); + return 0; +} + +static int test_ptp_encode_null_args_unchanged(void) +{ + const sw_payload_raw_t no_data = {.data = NULL, .len = SAMPLE_PACKET_LEN}; + uint8_t wire[SAMPLE_WIRE_LEN]; + uint32_t n = UINT32_MAX; + memset(wire, FILL, sizeof(wire)); + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_ptp_encode(NULL, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_ptp_encode(&sample_frame, NULL, wire, sizeof(wire), &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_encode(&sample_frame, &sample_payload, NULL, sizeof(wire), &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_encode(&sample_frame, &sample_payload, wire, sizeof(wire), NULL)); + /* The default size hook reports 0 octets for a payload without data. */ + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_ptp_encode(&sample_frame, &no_data, wire, sizeof(wire), &n)); + ASSERT_TRUE(is_untouched(wire, sizeof(wire))); + ASSERT_EQ_UINT(UINT32_MAX, n); + return 0; +} + +/* clause 5.1.2: the carried packet is 7 to 65542 octets long. */ +static int test_ptp_encode_payload_length_bounds(void) +{ + uint32_t n = UINT32_MAX; + + ASSERT_EQ_INT(SW_WRONG_LENGTH, encode_payload_of(SW_PTP_CCSDS_MIN_LEN - 1u, &n)); + ASSERT_EQ_INT(SW_WRONG_LENGTH, encode_payload_of(SW_PTP_CCSDS_MAX_LEN + 1u, &n)); + ASSERT_EQ_UINT(UINT32_MAX, n); + ASSERT_EQ_INT(SW_OK, encode_payload_of(SW_PTP_CCSDS_MIN_LEN, &n)); + ASSERT_EQ_UINT(SW_PTP_HEADER_LEN + SW_PTP_CCSDS_MIN_LEN, n); + ASSERT_EQ_INT(SW_OK, encode_payload_of(SW_PTP_CCSDS_MAX_LEN, &n)); + ASSERT_EQ_UINT(SW_PTP_HEADER_LEN + SW_PTP_CCSDS_MAX_LEN, n); + return 0; +} + +/* An exact-size buffer is accepted; one octet less is rejected and left untouched. */ +static int test_ptp_encode_buffer_boundary(void) +{ + uint8_t *exact = alloc_sample_wire(); + uint8_t *one_short = (uint8_t *)malloc(SAMPLE_WIRE_LEN - 1u); + if ((!exact) || (!one_short)) + { + free(exact); + free(one_short); + return 1; + } + memset(one_short, FILL, SAMPLE_WIRE_LEN - 1u); + + uint32_t n = UINT32_MAX; + const sw_status_t result = + sw_ptp_encode(&sample_frame, &sample_payload, one_short, SAMPLE_WIRE_LEN - 1u, &n); + const int unchanged = is_untouched(one_short, SAMPLE_WIRE_LEN - 1u); + free(exact); + free(one_short); + + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, result); + ASSERT_EQ_UINT(UINT32_MAX, n); + ASSERT_TRUE(unchanged); + return 0; +} + +/* clause 5.5.4.4: an otherwise valid packet terminated by EEP is discarded. */ +static int test_ptp_decode_eep_discarded(void) +{ + ASSERT_EQ_INT(0, discard_corrupted(0, 0x20, SW_END_EEP, SW_DISCARDED, SW_PTP_STATUS_EEP)); + return 0; +} + +/* An end marker that is neither EOP nor EEP is never taken for EOP; the receive + * status, which has no code for it, is left unchanged. */ +static int test_ptp_decode_unknown_end_marker_discarded(void) +{ + ASSERT_EQ_INT(0, discard_corrupted(0, 0x20, (sw_end_marker_t)2, SW_INVALID_PARAM, FILL)); + ASSERT_EQ_INT(0, discard_corrupted(0, 0x20, (sw_end_marker_t)7, SW_INVALID_PARAM, FILL)); + return 0; +} + +/* clause 5.5.4.3: any non-zero Reserved field discards the packet. */ +static int test_ptp_decode_reserved_nonzero(void) +{ + ASSERT_EQ_INT( + 0, + discard_corrupted(2, 0x01, SW_END_EOP, SW_DISCARDED, SW_PTP_STATUS_RESERVED_NONZERO)); + ASSERT_EQ_INT( + 0, + discard_corrupted(2, 0xFF, SW_END_EOP, SW_DISCARDED, SW_PTP_STATUS_RESERVED_NONZERO)); + return 0; +} + +/* clause 5.5.4.1: the neighbours of Protocol Identifier 0x02 are not CCSDS PTP. */ +static int test_ptp_decode_bad_protocol_id(void) +{ + ASSERT_EQ_INT(0, discard_corrupted(1, 0x01, SW_END_EOP, SW_WRONG_PROTOCOL, FILL)); + ASSERT_EQ_INT(0, discard_corrupted(1, 0x03, SW_END_EOP, SW_WRONG_PROTOCOL, FILL)); + return 0; +} + +/* Every input shorter than the encapsulation header is rejected. */ +static int test_ptp_decode_short_lengths_rejected(void) +{ + for (uint32_t len = 0; len < SW_PTP_HEADER_LEN; len++) + { + uint8_t *wire = (uint8_t *)calloc(len ? len : 1u, 1); + if (!wire) + { + return 1; + } + if (len > 1u) + { + wire[1] = SW_PTP_PROTOCOL_ID; + } + + const int failed = expect_not_delivered(wire, len, SW_END_EOP, SW_WRONG_LENGTH, FILL); + free(wire); + ASSERT_EQ_INT(0, failed); + } + return 0; +} + +/* clause 5.5.4.2 a: with a valid header the carried packet is delivered whatever its + * length. The send-side bounds of clause 5.1.2 (7..65542) are not applied on receive. */ +static int test_ptp_decode_any_payload_length_delivered(void) +{ + static const uint32_t lengths[] = {0u, + 1u, + SW_PTP_CCSDS_MIN_LEN - 1u, + SW_PTP_CCSDS_MIN_LEN, + SW_PTP_CCSDS_MAX_LEN, + SW_PTP_CCSDS_MAX_LEN + 1u}; + + for (size_t i = 0; i < (sizeof(lengths) / sizeof(lengths[0])); i++) + { + uint32_t len = UINT32_MAX; + ASSERT_EQ_INT(SW_OK, decode_payload_of(lengths[i], &len)); + ASSERT_EQ_UINT(lengths[i], len); + } + return 0; +} + +static int test_ptp_decode_null_args_unchanged(void) +{ + uint8_t wire[SAMPLE_WIRE_LEN] = {0}; + sw_ptp_frame_t pf = sample_frame; + sw_payload_raw_t payload = sample_payload; + sw_ptp_status_t status = (sw_ptp_status_t)FILL; + + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_decode(NULL, &payload, wire, sizeof(wire), SW_END_EOP, &status)); + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_decode(&pf, NULL, wire, sizeof(wire), SW_END_EOP, &status)); + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_decode(&pf, &payload, NULL, sizeof(wire), SW_END_EOP, &status)); + ASSERT_EQ_INT(FILL, status); + + /* A NULL status pointer is tolerated on every path. */ + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_decode(&pf, &payload, NULL, sizeof(wire), SW_END_EOP, NULL)); + ASSERT_EQ_INT(SW_WRONG_PROTOCOL, + sw_ptp_decode(&pf, &payload, wire, sizeof(wire), SW_END_EOP, NULL)); + ASSERT_EQ_INT(SW_DISCARDED, sw_ptp_decode(&pf, &payload, wire, sizeof(wire), SW_END_EEP, NULL)); + + ASSERT_EQ_INT(sample_frame.logical_addr, pf.logical_addr); + ASSERT_TRUE(payload.data == sample_packet); + return 0; +} + +static int test_ptp_create(void) +{ + uint8_t wire[SAMPLE_WIRE_LEN]; + uint32_t n = UINT32_MAX; + memset(wire, FILL, sizeof(wire)); + + /* One octet short: rejected, buffer and length untouched. */ + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, + sw_ptp_create(0xFE, 0x07, &sample_payload, wire, sizeof(wire) - 1u, &n)); + ASSERT_TRUE(is_untouched(wire, sizeof(wire))); + ASSERT_EQ_UINT(UINT32_MAX, n); + + ASSERT_EQ_INT(SW_OK, sw_ptp_create(0xFE, 0x07, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_UINT(SAMPLE_WIRE_LEN, n); + ASSERT_EQ_INT(0xFE, wire[0]); + ASSERT_EQ_INT(0x02, wire[1]); + ASSERT_EQ_INT(0x00, wire[2]); + ASSERT_EQ_INT(0x07, wire[3]); + ASSERT_EQ_MEM(&wire[SW_PTP_HEADER_LEN], sample_packet, SAMPLE_PACKET_LEN); + return 0; +} + +static int test_ptp_statistics(void) +{ + uint8_t wire[SAMPLE_WIRE_LEN]; + sw_ptp_frame_t pf; + sw_payload_raw_t payload; + sw_ptp_statistics_t stats; + uint32_t n = 0; + + sw_ptp_reset_statistics(); + sw_ptp_get_statistics(NULL); /* tolerate NULL */ + + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&sample_frame, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, + sw_ptp_encode(&sample_frame, &sample_payload, wire, 1, &n)); /* not sent */ + ASSERT_EQ_INT(SW_OK, sw_ptp_decode(&pf, &payload, wire, sizeof(wire), SW_END_EOP, NULL)); + ASSERT_EQ_INT(SW_DISCARDED, sw_ptp_decode(&pf, &payload, wire, sizeof(wire), SW_END_EEP, NULL)); + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_ptp_decode(&pf, &payload, wire, 1, SW_END_EOP, NULL)); + /* Invalid arguments are not a received packet, so they are not counted. */ + ASSERT_EQ_INT(SW_INVALID_PARAM, + sw_ptp_decode(NULL, &payload, wire, sizeof(wire), SW_END_EOP, NULL)); + + sw_ptp_get_statistics(&stats); + ASSERT_EQ_INT(1, stats.packets_sent); + ASSERT_EQ_INT(SAMPLE_WIRE_LEN, stats.bytes_sent); + ASSERT_EQ_INT(1, stats.packets_received); + ASSERT_EQ_INT(SAMPLE_WIRE_LEN, stats.bytes_received); + ASSERT_EQ_INT(2, stats.packets_discarded); + + sw_ptp_reset_statistics(); + sw_ptp_get_statistics(&stats); + ASSERT_EQ_INT(0, stats.packets_sent + stats.packets_received + stats.packets_discarded); + ASSERT_EQ_INT(0, stats.bytes_sent + stats.bytes_received); + return 0; +} + +/* The virtual channel rides in the User Application field (clause 5.3.5 NOTE 2). */ +static int test_ptp_virtual_channel(void) +{ + sw_ptp_frame_t pf = sample_frame; + uint8_t wire[SAMPLE_WIRE_LEN]; + + uint32_t n = 0; + + sw_ptp_set_virtual_channel(&pf, 0x07); + ASSERT_EQ_INT(0x07, sw_ptp_get_virtual_channel(&pf)); + ASSERT_EQ_INT(0x07, pf.user_app); + + ASSERT_EQ_INT(SW_OK, sw_ptp_encode(&pf, &sample_payload, wire, sizeof(wire), &n)); + ASSERT_EQ_INT(0x07, wire[3]); + + sw_ptp_frame_t out; + sw_payload_raw_t payload; + ASSERT_EQ_INT(SW_OK, sw_ptp_decode(&out, &payload, wire, sizeof(wire), SW_END_EOP, NULL)); + ASSERT_EQ_INT(0x07, sw_ptp_get_virtual_channel(&out)); + + sw_ptp_set_virtual_channel(NULL, 0x09); /* no-op, must not crash */ + ASSERT_EQ_INT(0, sw_ptp_get_virtual_channel(NULL)); + return 0; +} + +test_result_t test_spacewire_ptp_run_all(void) +{ + RUN_TEST(test_ptp_wire_format); + RUN_TEST(test_ptp_wire_format_field_extremes); + RUN_TEST(test_ptp_encode_logical_addr_bounds); + RUN_TEST(test_ptp_encode_decode_roundtrip); + RUN_TEST(test_ptp_with_path); + RUN_TEST(test_ptp_with_regional_address); + RUN_TEST(test_ptp_encode_max_path); + RUN_TEST(test_ptp_encode_invalid_path_unchanged); + RUN_TEST(test_ptp_encode_null_args_unchanged); + RUN_TEST(test_ptp_encode_payload_length_bounds); + RUN_TEST(test_ptp_encode_buffer_boundary); + RUN_TEST(test_ptp_decode_eep_discarded); + RUN_TEST(test_ptp_decode_unknown_end_marker_discarded); + RUN_TEST(test_ptp_decode_reserved_nonzero); + RUN_TEST(test_ptp_decode_bad_protocol_id); + RUN_TEST(test_ptp_decode_short_lengths_rejected); + RUN_TEST(test_ptp_decode_any_payload_length_delivered); + RUN_TEST(test_ptp_decode_null_args_unchanged); + RUN_TEST(test_ptp_create); + RUN_TEST(test_ptp_statistics); + RUN_TEST(test_ptp_virtual_channel); + return (test_result_t){cunit_total_tests - cunit_overall_failures, cunit_total_tests}; +} diff --git a/tests/test_router.c b/tests/test_router.c index 191b222..22dfc78 100644 --- a/tests/test_router.c +++ b/tests/test_router.c @@ -1,29 +1,45 @@ /** - * @file test_router.c - * @brief Unit tests for the SpaceWire router and link-state helpers. + * @file test_router.c + * @brief Unit tests for the SpaceWire routing switch + * + * Exercises path and logical addressing, header deletion and invalid-address + * handling of ECSS-E-ST-50-12C Rev.1, clause 5.6.8. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "cunit.h" #include "spacewire.h" #include "test_runners.h" -#include - static int test_router_init(void) { sw_router_t router; sw_router_init(&router, 4); ASSERT_EQ_INT(4, router.num_ports); - ASSERT_EQ_INT(0, router.links[0].port_id); - ASSERT_EQ_INT(SW_LINK_UNINITIALIZED, router.links[0].state); ASSERT_EQ_INT(0, (int)router.invalid_address_errors); + ASSERT_EQ_INT(0, (int)router.packets_routed); + ASSERT_EQ_INT(0, (int)router.packets_discarded); + + sw_router_init(NULL, 4); /* must not crash */ + return 0; +} + +/* num_ports is clamped to [1, SW_NUM_PORTS]: each limit and its neighbour. */ +static int test_router_init_port_count_bounds(void) +{ + sw_router_t router; - /* num_ports is clamped to [1, SW_NUM_PORTS]. */ sw_router_init(&router, 0); ASSERT_EQ_INT(1, router.num_ports); - sw_router_init(&router, 200); + sw_router_init(&router, 1); + ASSERT_EQ_INT(1, router.num_ports); + sw_router_init(&router, SW_NUM_PORTS); + ASSERT_EQ_INT(SW_NUM_PORTS, router.num_ports); + sw_router_init(&router, SW_NUM_PORTS + 1u); ASSERT_EQ_INT(SW_NUM_PORTS, router.num_ports); - - sw_router_init(NULL, 4); /* must not crash */ return 0; } @@ -37,23 +53,76 @@ static int test_router_path_addressing(void) /* Leading char 3 -> output port 3, always deleted (clause 5.6.8.3). */ const uint8_t pkt[3] = {3, 0xAA, 0xBB}; - ASSERT_EQ_INT(SW_ROUTE_OK, sw_router_route(&router, pkt, sizeof(pkt), &port, &del)); + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, pkt, sizeof(pkt), &port, &del)); ASSERT_EQ_INT(3, port); ASSERT_EQ_INT(1, del); /* Leading char 0 -> configuration port. */ const uint8_t cfg[2] = {0, 0x11}; - ASSERT_EQ_INT(SW_ROUTE_OK, sw_router_route(&router, cfg, sizeof(cfg), &port, &del)); + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, cfg, sizeof(cfg), &port, &del)); ASSERT_EQ_INT(SW_PORT_CONFIG, port); ASSERT_EQ_INT(1, del); /* A path char referencing a non-existent port is discarded (clause 5.6.8.5). */ const uint8_t bad[2] = {20, 0x11}; - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, bad, sizeof(bad), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_route(&router, bad, sizeof(bad), &port, &del)); + ASSERT_EQ_INT(1, (int)router.invalid_address_errors); + ASSERT_EQ_INT(2, (int)router.packets_routed); + ASSERT_EQ_INT(1, (int)router.packets_discarded); + return 0; +} + +/* Path addressing: the last existing port is reachable, the next one is not. */ +static int test_router_path_port_bounds(void) +{ + sw_router_t router; + sw_router_init(&router, 8); /* ports 0..7 */ + + uint8_t port = 0xEE; + uint8_t del = 0xEE; + const uint8_t last_port[1] = {7}; + const uint8_t first_missing[1] = {8}; + + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, last_port, sizeof(last_port), &port, &del)); + ASSERT_EQ_INT(7, port); + ASSERT_EQ_INT(1, del); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, + sw_router_route(&router, first_missing, sizeof(first_missing), &port, &del)); + ASSERT_EQ_INT(1, (int)router.packets_routed); ASSERT_EQ_INT(1, (int)router.invalid_address_errors); return 0; } +/* 31 is the highest path address; 32 is the lowest logical address (clauses 5.6.8.3-4). */ +static int test_router_path_logical_boundary(void) +{ + sw_router_t router; + sw_router_init(&router, SW_NUM_PORTS); /* ports 0..31 */ + + uint8_t port = 0xEE; + uint8_t del = 0xEE; + const uint8_t highest_path[1] = {31}; + const uint8_t lowest_logical[1] = {32}; + + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, highest_path, sizeof(highest_path), &port, &del)); + ASSERT_EQ_INT(31, port); + ASSERT_EQ_INT(1, del); + + /* Unconfigured, 32 is an invalid logical address, not a path to port 32. */ + ASSERT_EQ_INT(SW_WRONG_ADDRESS, + sw_router_route(&router, lowest_logical, sizeof(lowest_logical), &port, &del)); + ASSERT_EQ_INT(1, (int)router.invalid_address_errors); + + /* Configured, it follows the routing table and keeps its address. */ + ASSERT_EQ_INT(SW_OK, sw_router_add_route(&router, 32, 5, 0)); + ASSERT_EQ_INT(SW_OK, + sw_router_route(&router, lowest_logical, sizeof(lowest_logical), &port, &del)); + ASSERT_EQ_INT(5, port); + ASSERT_EQ_INT(0, del); + ASSERT_EQ_INT(2, (int)router.packets_routed); + return 0; +} + static int test_router_logical_addressing(void) { sw_router_t router; @@ -67,24 +136,26 @@ static int test_router_logical_addressing(void) /* Logical address retained by default (clause 5.6.8.4 f). */ const uint8_t p1[2] = {0x40, 0x99}; - ASSERT_EQ_INT(SW_ROUTE_OK, sw_router_route(&router, p1, sizeof(p1), &port, &del)); + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, p1, sizeof(p1), &port, &del)); ASSERT_EQ_INT(5, port); ASSERT_EQ_INT(0, del); /* Logical address deleted when configured to (clause 5.6.8.6). */ const uint8_t p2[2] = {0x41, 0x99}; - ASSERT_EQ_INT(SW_ROUTE_OK, sw_router_route(&router, p2, sizeof(p2), &port, &del)); + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, p2, sizeof(p2), &port, &del)); ASSERT_EQ_INT(6, port); ASSERT_EQ_INT(1, del); /* Unconfigured logical address -> discard + error. */ const uint8_t p3[2] = {0x77, 0x99}; - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, p3, sizeof(p3), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_route(&router, p3, sizeof(p3), &port, &del)); /* Reserved logical address 255 -> discard + error (clause 5.6.8.5 NOTE). */ const uint8_t p4[2] = {0xFF, 0x99}; - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, p4, sizeof(p4), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_route(&router, p4, sizeof(p4), &port, &del)); ASSERT_EQ_INT(2, (int)router.invalid_address_errors); + ASSERT_EQ_INT(2, (int)router.packets_routed); + ASSERT_EQ_INT(2, (int)router.packets_discarded); return 0; } @@ -103,26 +174,56 @@ static int test_router_logical_route_stale_port(void) uint8_t port = 0xFF; uint8_t del = 0xFF; const uint8_t pkt[2] = {0x40, 0x99}; - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, pkt, sizeof(pkt), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_route(&router, pkt, sizeof(pkt), &port, &del)); ASSERT_EQ_INT(1, (int)router.invalid_address_errors); return 0; } +/* A logical address never reaches the configuration port (clause 5.6.8.3 f), even + * through an entry written into the table directly; path address 0 still does. */ +static int test_router_logical_route_to_config_port(void) +{ + sw_router_t router; + sw_router_init(&router, 4); /* ports 0..3 */ + + router.routes[0x40].configured = 1; + router.routes[0x40].output_port = SW_PORT_CONFIG; + router.routes[0x40].delete_addr = 0; + + uint8_t port = 0xEE; + uint8_t del = 0xEE; + const uint8_t logical[2] = {0x40, 0x99}; + ASSERT_EQ_INT(SW_WRONG_ADDRESS, + sw_router_route(&router, logical, sizeof(logical), &port, &del)); + ASSERT_EQ_INT(1, (int)router.invalid_address_errors); + ASSERT_EQ_INT(0xEE, port); + ASSERT_EQ_INT(0xEE, del); + + const uint8_t path[2] = {SW_PORT_CONFIG, 0x99}; + ASSERT_EQ_INT(SW_OK, sw_router_route(&router, path, sizeof(path), &port, &del)); + ASSERT_EQ_INT(SW_PORT_CONFIG, port); + ASSERT_EQ_INT(1, del); + return 0; +} + static int test_router_add_route_validation(void) { sw_router_t router; sw_router_init(&router, 4); /* ports 0..3 */ ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_add_route(NULL, 0x40, 1, 0)); - /* A path-range address may not be used as a logical route. */ + /* A path-range address may not be used as a logical route; 31 is the highest. */ ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_add_route(&router, 0x10, 1, 0)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_add_route(&router, 31, 1, 0)); /* The reserved address 255 may not be configured. */ ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_add_route(&router, 0xFF, 1, 0)); /* Output port must exist. */ ASSERT_EQ_INT(SW_WRONG_PORT, sw_router_add_route(&router, 0x40, 4, 0)); - /* Boundaries: lowest logical address and highest valid port. */ + /* The configuration port is reached by path addressing only (clause 5.6.8.3 f). */ + ASSERT_EQ_INT(SW_WRONG_PORT, sw_router_add_route(&router, 0x40, SW_PORT_CONFIG, 0)); + /* Boundaries: lowest and highest logical address, lowest and highest external port. */ ASSERT_EQ_INT(SW_OK, sw_router_add_route(&router, 0x20, 3, 0)); - ASSERT_EQ_INT(SW_OK, sw_router_add_route(&router, 0xFE, 0, 0)); + ASSERT_EQ_INT(SW_OK, sw_router_add_route(&router, 0xFE, 1, 0)); return 0; } @@ -135,58 +236,83 @@ static int test_router_route_invalid_args_and_empty(void) uint8_t port = 0; uint8_t del = 0; - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(NULL, pkt, sizeof(pkt), &port, &del)); - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, NULL, sizeof(pkt), &port, &del)); - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, pkt, sizeof(pkt), NULL, &del)); - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, pkt, sizeof(pkt), &port, NULL)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(NULL, pkt, sizeof(pkt), &port, &del)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(&router, NULL, sizeof(pkt), &port, &del)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(&router, pkt, sizeof(pkt), NULL, &del)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(&router, pkt, sizeof(pkt), &port, NULL)); /* Invalid arguments are rejected before any counter is touched. */ ASSERT_EQ_INT(0, (int)router.packets_discarded); /* An empty packet is discarded by the first routing switch (clause 5.6.2.1). */ - ASSERT_EQ_INT(SW_ROUTE_DISCARD, sw_router_route(&router, pkt, 0, &port, &del)); + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_router_route(&router, pkt, 0, &port, &del)); ASSERT_EQ_INT(1, (int)router.packets_discarded); return 0; } -static int test_link_layer_state_helpers(void) +/* Every kind of discard leaves both outputs exactly as the caller passed them. */ +static int test_router_route_discard_leaves_outputs_unchanged(void) { - const sw_link_config_t config = { - .bit_rate = 1000000, - .disconnect_timeout = 2500, - .rx_credit_max = 12, - }; - - sw_link_layer_t link; - memset(&link, 0xA5, sizeof(link)); - - sw_link_init(NULL, &config); - sw_link_init(&link, NULL); - - sw_link_init(&link, &config); - ASSERT_EQ_INT(config.bit_rate, link.config.bit_rate); - ASSERT_EQ_INT(config.disconnect_timeout, link.config.disconnect_timeout); - ASSERT_EQ_INT(config.rx_credit_max, link.rx_credits); - ASSERT_EQ_INT(0, link.tx_credits); - ASSERT_EQ_INT(SW_LINK_UNINITIALIZED, link.state); - - ASSERT_EQ_INT(SW_LINK_ERROR, sw_link_get_state(NULL)); - ASSERT_EQ_INT(SW_LINK_UNINITIALIZED, sw_link_get_state(&link)); + sw_router_t router; + sw_router_init(&router, 4); /* ports 0..3 */ + router.routes[0x41].configured = 1; + router.routes[0x41].output_port = 9; /* stale entry: the port does not exist */ + + const uint8_t missing_port[2] = {4, 0x99}; /* path address beyond the last port */ + const uint8_t unconfigured[2] = {0x40, 0x99}; /* logical address without a route */ + const uint8_t stale[2] = {0x41, 0x99}; + uint8_t port = 0xEE; + uint8_t del = 0xEE; + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(NULL, stale, sizeof(stale), &port, &del)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_router_route(&router, NULL, sizeof(stale), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_router_route(&router, stale, 0, &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, + sw_router_route(&router, missing_port, sizeof(missing_port), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, + sw_router_route(&router, unconfigured, sizeof(unconfigured), &port, &del)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_route(&router, stale, sizeof(stale), &port, &del)); + + ASSERT_EQ_INT(0xEE, port); + ASSERT_EQ_INT(0xEE, del); + return 0; +} - sw_link_set_state(NULL, SW_LINK_CONNECTED); - sw_link_set_state(&link, SW_LINK_CONNECTED); - ASSERT_EQ_INT(SW_LINK_CONNECTED, sw_link_get_state(&link)); +/* A rejected call does not disturb the routing table. */ +static int test_router_add_route_failure_leaves_table_unchanged(void) +{ + sw_router_t router; + sw_router_init(&router, 4); /* ports 0..3 */ + ASSERT_EQ_INT(SW_OK, sw_router_add_route(&router, 0x40, 2, 1)); + /* Overwriting an existing route with a port that does not exist, or with the + * configuration port. */ + ASSERT_EQ_INT(SW_WRONG_PORT, sw_router_add_route(&router, 0x40, 4, 0)); + ASSERT_EQ_INT(SW_WRONG_PORT, sw_router_add_route(&router, 0x40, SW_PORT_CONFIG, 0)); + ASSERT_EQ_INT(2, router.routes[0x40].output_port); + ASSERT_EQ_INT(1, router.routes[0x40].configured); + ASSERT_EQ_INT(1, router.routes[0x40].delete_addr); + + /* Addresses outside 32..254 never reach the table. */ + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_add_route(&router, 31, 1, 0)); + ASSERT_EQ_INT(SW_WRONG_ADDRESS, sw_router_add_route(&router, 255, 1, 0)); + ASSERT_EQ_INT(0, router.routes[31].configured); + ASSERT_EQ_INT(0, router.routes[255].configured); return 0; } test_result_t test_spacewire_router_run_all(void) { RUN_TEST(test_router_init); + RUN_TEST(test_router_init_port_count_bounds); RUN_TEST(test_router_path_addressing); + RUN_TEST(test_router_path_port_bounds); + RUN_TEST(test_router_path_logical_boundary); RUN_TEST(test_router_logical_addressing); RUN_TEST(test_router_logical_route_stale_port); + RUN_TEST(test_router_logical_route_to_config_port); RUN_TEST(test_router_add_route_validation); RUN_TEST(test_router_route_invalid_args_and_empty); - RUN_TEST(test_link_layer_state_helpers); + RUN_TEST(test_router_route_discard_leaves_outputs_unchanged); + RUN_TEST(test_router_add_route_failure_leaves_table_unchanged); return (test_result_t){cunit_total_tests - cunit_overall_failures, cunit_total_tests}; } diff --git a/tests/test_runners.h b/tests/test_runners.h index 13617dc..fe51c40 100644 --- a/tests/test_runners.h +++ b/tests/test_runners.h @@ -1,18 +1,50 @@ /** - * @file test_runners.h - * @brief Declarations of the per-suite test runners. + * @file test_runners.h + * @brief Declarations of the per-suite test runners + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #ifndef TEST_RUNNERS_H #define TEST_RUNNERS_H +/** + * @brief Outcome of one test suite. + */ typedef struct { - int passed; - int total; + int passed; /**< Tests that passed. */ + int total; /**< Tests that ran. */ } test_result_t; +/** + * @brief Run every SpaceWire packet builder test. + * + * @return Passed and total test counts of the suite. + */ test_result_t test_spacewire_spw_packet_run_all(void); + +/** + * @brief Run every routing switch test. + * + * @return Passed and total test counts of the suite. + */ test_result_t test_spacewire_router_run_all(void); -test_result_t test_spacewire_packet_run_all(void); + +/** + * @brief Run every CCSDS packet transfer protocol test. + * + * @return Passed and total test counts of the suite. + */ +test_result_t test_spacewire_ptp_run_all(void); + +/** + * @brief Run every test of the default payload hooks. + * + * @return Passed and total test counts of the suite. + */ +test_result_t test_spacewire_payload_run_all(void); #endif /* TEST_RUNNERS_H */ diff --git a/tests/test_spw_packet.c b/tests/test_spw_packet.c index a49979e..38787b7 100644 --- a/tests/test_spw_packet.c +++ b/tests/test_spw_packet.c @@ -1,92 +1,239 @@ /** - * @file test_spw_packet.c - * @brief Unit tests for the SpaceWire packet builder (sw_spw_packet_build()). + * @file test_spw_packet.c + * @brief Unit tests for the SpaceWire packet builder + * + * Checks sw_spw_packet_build() against the packet layout of + * ECSS-E-ST-50-12C Rev.1, clause 5.6.2. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "cunit.h" #include "spacewire.h" #include "test_runners.h" +#include #include +/** @brief Fill value used to detect writes into a buffer that must stay untouched. */ +#define FILL 0x5Au + +/** + * @brief Build a packet into an exact-size heap buffer and compare it with the expected octets. + * + * The buffer is exactly @p expected_len octets, so ASan flags any overrun. + * + * @param[in] dest Destination-address octets, or NULL. + * @param[in] dest_len Number of destination-address octets. + * @param[in] cargo Cargo octets, or NULL. + * @param[in] cargo_len Number of cargo octets. + * @param[in] expected Expected packet octets. + * @param[in] expected_len Expected packet length. + * @return 0 if the packet was built exactly as expected, 1 otherwise. + */ +static int build_exact(const uint8_t *dest, + size_t dest_len, + const uint8_t *cargo, + size_t cargo_len, + const uint8_t *expected, + size_t expected_len) +{ + uint8_t *buf = (uint8_t *)malloc(expected_len); + if (!buf) + { + return 1; + } + + size_t n = 0; + const sw_status_t status = + sw_spw_packet_build(dest, dest_len, cargo, cargo_len, buf, expected_len, &n); + const int matches = (n == expected_len) && (memcmp(buf, expected, expected_len) == 0); + free(buf); + + ASSERT_EQ_INT(SW_OK, status); + ASSERT_EQ_UINT(expected_len, n); + ASSERT_TRUE(matches); + return 0; +} + /* A logical-addressed packet: one address octet followed by the cargo. */ -static int test_packet_build_logical(void) +static int test_spw_packet_build_logical(void) { const uint8_t dest[1] = {0x40}; const uint8_t cargo[3] = {0xAA, 0xBB, 0xCC}; - uint8_t buf[16]; - - size_t n = sw_spw_packet_build(dest, sizeof(dest), cargo, sizeof(cargo), buf, sizeof(buf)); - ASSERT_EQ_INT(4, (int)n); - const uint8_t expected[4] = {0x40, 0xAA, 0xBB, 0xCC}; - ASSERT_EQ_MEM(buf, expected, sizeof(expected)); + + ASSERT_EQ_INT( + 0, + build_exact(dest, sizeof(dest), cargo, sizeof(cargo), expected, sizeof(expected))); return 0; } /* A path-addressed packet: a multi-hop path address followed by the cargo. */ -static int test_packet_build_path(void) +static int test_spw_packet_build_path(void) { const uint8_t dest[3] = {2, 1, 3}; const uint8_t cargo[2] = {0xDE, 0xAD}; - uint8_t buf[16]; - - size_t n = sw_spw_packet_build(dest, sizeof(dest), cargo, sizeof(cargo), buf, sizeof(buf)); - ASSERT_EQ_INT(5, (int)n); - const uint8_t expected[5] = {2, 1, 3, 0xDE, 0xAD}; - ASSERT_EQ_MEM(buf, expected, sizeof(expected)); + + ASSERT_EQ_INT( + 0, + build_exact(dest, sizeof(dest), cargo, sizeof(cargo), expected, sizeof(expected))); return 0; } /* A point-to-point link needs no destination address (clause 4.2.3.3). */ -static int test_packet_build_cargo_only(void) +static int test_spw_packet_build_cargo_only(void) { const uint8_t cargo[2] = {0x11, 0x22}; - uint8_t buf[8]; - size_t n = sw_spw_packet_build(NULL, 0, cargo, sizeof(cargo), buf, sizeof(buf)); - ASSERT_EQ_INT(2, (int)n); - ASSERT_EQ_MEM(buf, cargo, sizeof(cargo)); + ASSERT_EQ_INT(0, build_exact(NULL, 0, cargo, sizeof(cargo), cargo, sizeof(cargo))); return 0; } /* A destination address with no cargo (e.g. an addressed empty-payload frame). */ -static int test_packet_build_dest_only(void) +static int test_spw_packet_build_dest_only(void) { const uint8_t dest[2] = {0x40, 0x05}; + + ASSERT_EQ_INT(0, build_exact(dest, sizeof(dest), NULL, 0, dest, sizeof(dest))); + return 0; +} + +/* The smallest packet (one octet) fits an exact one-octet buffer. */ +static int test_spw_packet_build_single_octet(void) +{ + const uint8_t dest[1] = {0x07}; + + ASSERT_EQ_INT(0, build_exact(dest, sizeof(dest), NULL, 0, dest, sizeof(dest))); + return 0; +} + +/* One octet short of the packet: rejected, and the heap buffer is left untouched. */ +static int test_spw_packet_build_one_short(void) +{ + const uint8_t dest[1] = {0x40}; + const uint8_t cargo[3] = {0xAA, 0xBB, 0xCC}; + const size_t one_short = sizeof(dest) + sizeof(cargo) - 1u; + uint8_t *buf = (uint8_t *)malloc(one_short); + if (!buf) + { + return 1; + } + memset(buf, FILL, one_short); + + size_t n = SIZE_MAX; + const sw_status_t status = + sw_spw_packet_build(dest, sizeof(dest), cargo, sizeof(cargo), buf, one_short, &n); + int untouched = 1; + for (size_t i = 0; i < one_short; i++) + { + untouched = (untouched) && (buf[i] == FILL); + } + free(buf); + + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, status); + ASSERT_EQ_UINT(SIZE_MAX, n); + ASSERT_TRUE(untouched); + return 0; +} + +/** + * @brief Expect a rejected build and an untouched buffer. + * + * Only the lengths matter: the pointers are valid but small, so the call must + * be rejected before anything is copied. + */ +static int expect_length_rejected(size_t dest_len, size_t cargo_len, size_t buf_len) +{ + const uint8_t dest[8] = {0x40}; + const uint8_t cargo[8] = {0x11}; + const uint8_t untouched[8] = {FILL, FILL, FILL, FILL, FILL, FILL, FILL, FILL}; uint8_t buf[8]; + size_t n = SIZE_MAX; + memset(buf, FILL, sizeof(buf)); - size_t n = sw_spw_packet_build(dest, sizeof(dest), NULL, 0, buf, sizeof(buf)); - ASSERT_EQ_INT(2, (int)n); - ASSERT_EQ_MEM(buf, dest, sizeof(dest)); + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, + sw_spw_packet_build(dest, dest_len, cargo, cargo_len, buf, buf_len, &n)); + ASSERT_EQ_MEM(buf, untouched, sizeof(untouched)); + ASSERT_EQ_UINT(SIZE_MAX, n); return 0; } -static int test_packet_build_errors(void) +/* Lengths whose sum wraps around SIZE_MAX are rejected before any copy. */ +static int test_spw_packet_build_length_overflow_rejected(void) +{ + ASSERT_EQ_INT(0, expect_length_rejected(SIZE_MAX, 5, 8)); /* sum wraps to 4 */ + ASSERT_EQ_INT(0, expect_length_rejected(5, SIZE_MAX, 8)); /* sum wraps to 4 */ + ASSERT_EQ_INT(0, expect_length_rejected(SIZE_MAX, SIZE_MAX, 8)); + ASSERT_EQ_INT(0, expect_length_rejected(SIZE_MAX, 1, 8)); /* sum wraps to 0 */ + return 0; +} + +/* A destination address alone longer than the buffer: 4 octets fit, 5 do not. */ +static int test_spw_packet_build_dest_longer_than_buffer(void) +{ + const uint8_t dest[5] = {1, 2, 3, 4, 5}; + + ASSERT_EQ_INT(0, build_exact(dest, 4, NULL, 0, dest, 4)); + ASSERT_EQ_INT(0, expect_length_rejected(5, 0, 4)); + return 0; +} + +/* Each kind of bad call is reported with its own status. The buffer is large enough + * for success, so no check is masked by the size check. */ +static int test_spw_packet_build_errors(void) { const uint8_t dest[1] = {0x40}; const uint8_t cargo[2] = {0x11, 0x22}; - uint8_t buf[2]; - - /* NULL output buffer. */ - ASSERT_EQ_INT(0, sw_spw_packet_build(dest, 1, cargo, 2, NULL, 8)); - /* dest_len > 0 but dest NULL. */ - ASSERT_EQ_INT(0, sw_spw_packet_build(NULL, 1, cargo, 2, buf, sizeof(buf))); - /* cargo_len > 0 but cargo NULL. */ - ASSERT_EQ_INT(0, sw_spw_packet_build(dest, 1, NULL, 2, buf, sizeof(buf))); + uint8_t buf[8]; + size_t n = 0; + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(dest, 1, cargo, 2, NULL, 8, &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(dest, 1, cargo, 2, buf, sizeof(buf), NULL)); + /* A part with a non-zero length but no octets. */ + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(NULL, 1, cargo, 2, buf, sizeof(buf), &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(dest, 1, NULL, 2, buf, sizeof(buf), &n)); + /* A packet carries at least one data character (clause 5.6.2.1). */ + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_spw_packet_build(NULL, 0, NULL, 0, buf, sizeof(buf), &n)); /* Total length exceeds the buffer (1 + 2 > 2). */ - ASSERT_EQ_INT(0, sw_spw_packet_build(dest, 1, cargo, 2, buf, sizeof(buf))); - /* Empty packet (no data characters). */ - ASSERT_EQ_INT(0, sw_spw_packet_build(NULL, 0, NULL, 0, buf, sizeof(buf))); + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, sw_spw_packet_build(dest, 1, cargo, 2, buf, 2, &n)); + return 0; +} + +/* Every rejected call leaves the output buffer and the length exactly as the caller + * passed them. */ +static int test_spw_packet_build_failure_leaves_outputs_unchanged(void) +{ + const uint8_t dest[1] = {0x40}; + const uint8_t cargo[2] = {0x11, 0x22}; + const uint8_t untouched[2] = {FILL, FILL}; + uint8_t buf[2] = {FILL, FILL}; + size_t n = SIZE_MAX; + + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(NULL, 1, cargo, 2, buf, sizeof(buf), &n)); + ASSERT_EQ_INT(SW_INVALID_PARAM, sw_spw_packet_build(dest, 1, NULL, 2, buf, sizeof(buf), &n)); + ASSERT_EQ_INT(SW_BUFFER_TOO_SMALL, + sw_spw_packet_build(dest, 1, cargo, 2, buf, sizeof(buf), &n)); /* one short */ + ASSERT_EQ_INT(SW_WRONG_LENGTH, sw_spw_packet_build(NULL, 0, NULL, 0, buf, sizeof(buf), &n)); + ASSERT_EQ_MEM(buf, untouched, sizeof(untouched)); + ASSERT_EQ_UINT(SIZE_MAX, n); return 0; } test_result_t test_spacewire_spw_packet_run_all(void) { - RUN_TEST(test_packet_build_logical); - RUN_TEST(test_packet_build_path); - RUN_TEST(test_packet_build_cargo_only); - RUN_TEST(test_packet_build_dest_only); - RUN_TEST(test_packet_build_errors); + RUN_TEST(test_spw_packet_build_logical); + RUN_TEST(test_spw_packet_build_path); + RUN_TEST(test_spw_packet_build_cargo_only); + RUN_TEST(test_spw_packet_build_dest_only); + RUN_TEST(test_spw_packet_build_single_octet); + RUN_TEST(test_spw_packet_build_one_short); + RUN_TEST(test_spw_packet_build_length_overflow_rejected); + RUN_TEST(test_spw_packet_build_dest_longer_than_buffer); + RUN_TEST(test_spw_packet_build_errors); + RUN_TEST(test_spw_packet_build_failure_leaves_outputs_unchanged); return (test_result_t){cunit_total_tests - cunit_overall_failures, cunit_total_tests}; } diff --git a/tests/unit_tests.c b/tests/unit_tests.c index bae7c04..4891447 100644 --- a/tests/unit_tests.c +++ b/tests/unit_tests.c @@ -1,11 +1,20 @@ /** - * @file unit_tests.c - * @brief Unit-test entry point: runs each suite and reports totals. + * @file unit_tests.c + * @brief Unit-test entry point: runs each suite and reports totals + * + * Covers the default payload hooks; the overridden hooks are tested by the + * separate binary built from test_payload_override.c. + * + * Copyright 2026 OpenSpaceCode contributors + * SPDX-License-Identifier: Apache-2.0 + * + * OpenSpaceCode — https://github.com/OpenSpaceCode */ #include "test_runners.h" #include +/** @brief Print one suite's pass count under a label. */ #define REPORT(label, r) printf(" %-14s Passed %d/%d\n\n", label ":", (r).passed, (r).total) int main(void) @@ -24,8 +33,13 @@ int main(void) total_passed += r.passed; total_tests += r.total; - r = test_spacewire_packet_run_all(); - REPORT("packet", r); + r = test_spacewire_ptp_run_all(); + REPORT("ptp", r); + total_passed += r.passed; + total_tests += r.total; + + r = test_spacewire_payload_run_all(); + REPORT("payload", r); total_passed += r.passed; total_tests += r.total; diff --git a/tools/coverage_html.sh b/tools/coverage_html.sh index 61a8fe9..6f44fd3 100644 --- a/tools/coverage_html.sh +++ b/tools/coverage_html.sh @@ -9,7 +9,9 @@ if [[ "${OUT_FILE}" != /* ]]; then OUT_FILE="${ROOT_DIR}/${OUT_FILE}" fi -COVERAGE_CFLAGS='-O0 -g --coverage -Iinclude -I./external -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 (including -Werror) come from the Makefile. +COVERAGE_OPT='-O0 -g --coverage' cd "${ROOT_DIR}" @@ -20,19 +22,28 @@ if ! command -v gcovr >/dev/null 2>&1; then fi make clean >/dev/null -make BUILD_DIR="${COVERAGE_BUILD_DIR}" test CFLAGS="${COVERAGE_CFLAGS}" >/dev/null +make BUILD_DIR="${COVERAGE_BUILD_DIR}" test OPT="${COVERAGE_OPT}" >/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. +# the console output is not duplicated, and fail below 100% line or branch coverage. +# gcovr writes the report before it checks the thresholds, so the report exists either +# way. Its chatty "(INFO)" progress lines are filtered from stderr; warnings and errors +# still pass through. echo "Coverage:" +status=0 gcovr -r "${ROOT_DIR}" \ --object-directory "${ROOT_DIR}/${COVERAGE_BUILD_DIR}/obj" \ --filter "${ROOT_DIR}/src" \ --html-details --output "${OUT_FILE}" \ --txt - --txt-summary \ - 2> >(grep -v '^(INFO)' >&2) + --fail-under-line 100 \ + --fail-under-branch 100 \ + 2> >(grep -v '^(INFO)' >&2) || status=$? -echo "Coverage HTML report written to: ${OUT_FILE}" \ No newline at end of file +echo "Coverage HTML report written to: ${OUT_FILE}" +if [[ ${status} -ne 0 ]]; then + echo "Error: coverage is below 100% line or branch (gcovr exit ${status})." >&2 +fi +exit "${status}"