Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
69663da
Phase 2: Pi 5 hub + aarch64 CI + Android NDK bridge + integration tut…
claude Jun 27, 2026
8bb2497
Phase 1 cleanup: rename include guards to INCLUDED_OSCTAP_*; close Sm…
claude Jun 27, 2026
c24f522
Phase 2: non-throwing TryInit / TryValidatePacket for untrusted input
claude Jun 27, 2026
25fc5c2
Phase 2: win32 socket backend compile/link smoke
claude Jun 27, 2026
c261239
TCP v1 (issue #14), part 1: length-prefix stream framing codec
claude Jun 27, 2026
14b82cc
TCP v1 (issue #14), part 2: posix TCP sockets (client + multi-connect…
claude Jun 27, 2026
cda89d6
TCP v1 (issue #14), part 3: win32 TCP backend + MinGW compile-smoke
claude Jun 27, 2026
930ccf4
TCP v1 (issue #14), part 4: docs + ROADMAP/STATUS
claude Jun 27, 2026
ccd07cb
TCP v1 (issue #14): runnable demo pair (tcp_server + tcp_send)
claude Jun 27, 2026
66f109a
Testing cleanup: UDP loopback test (+ LocalPort bug fix), revive exam…
claude Jun 27, 2026
ad66c23
Item 3: win32 sockets runtime-tested under Wine (+ self-contained TCP…
claude Jun 27, 2026
80d6ed3
Item 4: code-coverage measurement in CI (gcovr)
claude Jun 27, 2026
6999a85
Docs: add Getting Started + API reference; archive legacy oscpack files
claude Jun 28, 2026
7851c26
Review fixes: win32 break-socket + FD_SETSIZE guards, hygiene, CI cle…
claude Jun 28, 2026
5c1c040
Fix CI: MSVC C4505 on win32 timer helper; Wine prefix; gate examples …
claude Jun 28, 2026
c7ae9a1
Fix CI: make win32 backend /W4-clean; drop ineffective /WX-
claude Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@
# ($LIB_FUZZING_ENGINE) rather than going through the project's CMake (which pins
# its own sanitizer flags for local use). WORKDIR is the project root.

# fuzz_parse: the OSC packet parser (untrusted bytes -> ReceivedPacket/...).
$CXX $CXXFLAGS -std=c++17 -I osctap \
fuzz/fuzz_parse.cpp \
$LIB_FUZZING_ENGINE \
-o "$OUT/fuzz_parse"

# Ship the seed corpus next to the target. OSS-Fuzz / ClusterFuzzLite
# fuzz_deframe: the OSC-over-TCP stream deframer + parser (length-prefix
# reassembly across arbitrary chunk boundaries, bounded-buffer / DoS guard).
$CXX $CXXFLAGS -std=c++17 -I osctap \
fuzz/fuzz_deframe.cpp \
$LIB_FUZZING_ENGINE \
-o "$OUT/fuzz_deframe"

# Ship the seed corpora next to the targets. OSS-Fuzz / ClusterFuzzLite
# automatically load <target>_seed_corpus.zip before fuzzing.
zip -j "$OUT/fuzz_parse_seed_corpus.zip" fuzz/corpus/*
zip -j "$OUT/fuzz_deframe_seed_corpus.zip" fuzz/corpus_deframe/*
124 changes: 120 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,18 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Configure standalone fuzzer
- name: Configure standalone fuzzers
run: cmake -S . -B build-fuzz -DOSCPACK_BUILD_EXAMPLES=OFF -DOSCTAP_FUZZER_STANDALONE=ON

- name: Build fuzzer
run: cmake --build build-fuzz --target fuzz_parse
- name: Build fuzzers
run: cmake --build build-fuzz --target fuzz_parse fuzz_deframe

- name: Replay corpus and run mutation loop
- name: Replay corpus and run mutation loop (parser)
run: ./build-fuzz/fuzz_parse fuzz/corpus/*

- name: Replay corpus and run mutation loop (TCP deframer)
run: ./build-fuzz/fuzz_deframe fuzz/corpus_deframe/*

rtsan:
name: RTSan + function-effects (realtime hot path)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -136,6 +139,119 @@ jobs:
- name: Run freestanding smoke test
run: ctest --test-dir build-freestanding -R OscFreestandingTest --output-on-failure

aarch64-qemu:
name: aarch64 cross-build + qemu (Pi-5-class / big-endian-agnostic)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Cross-compile for 64-bit Arm (the Raspberry Pi 5 ISA) and run the suite
# under qemu-user, so the aarch64 build is proven green on every push. This
# also exercises the endian-agnostic (de)serialization on a non-x86 target.
- name: Install aarch64 toolchain + qemu-user
run: |
sudo apt-get update
sudo apt-get install -y g++-aarch64-linux-gnu qemu-user-static

- name: Configure (aarch64 cross; qemu as the test emulator)
run: >
cmake -S . -B build-aarch64 -DOSCPACK_BUILD_EXAMPLES=ON
-DOSCTAP_WARNINGS_AS_ERRORS=ON
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc
-DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++
"-DCMAKE_CROSSCOMPILING_EMULATOR=qemu-aarch64-static;-L;/usr/aarch64-linux-gnu"

- name: Build
run: cmake --build build-aarch64

# OscConcurrencyTest is excluded: it uses real threads + loopback sockets,
# which are flaky under qemu-user emulation. Native TSan/POSIX legs already
# cover it; here we vet the parse/serialize suite on the aarch64 ISA.
- name: Run suite under qemu
run: ctest --test-dir build-aarch64 -E OscConcurrencyTest --output-on-failure

win32-wine:
name: win32 sockets runtime (MinGW + Wine)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# The win32 socket backends are otherwise only compile-checked (the MSVC
# legs + the MinGW smoke). Here we cross-compile the real socket loopback
# tests for win32 and run them under Wine, so the win32 UDP + TCP backends
# get actual runtime coverage on every push.
- name: Install MinGW-w64 + Wine
# The tests are cross-compiled to 64-bit .exe, so only 64-bit Wine is
# needed (no i386 multiarch, which is an unnecessary failure surface).
run: |
sudo apt-get update
sudo apt-get install -y g++-mingw-w64-x86-64 wine64 wine

- name: Cross-compile the socket loopback tests for win32
run: |
for t in OscUdpTest OscTcpTest; do
x86_64-w64-mingw32-g++ -std=c++17 -O1 -static -I . -I osctap \
tests/$t.cpp -o $t.exe -lws2_32 -lwinmm
done

- name: Run under Wine
env:
# A runner-owned prefix dir: Wine refuses to use /tmp when it isn't
# owned by the runner user ("'/tmp' is not owned by you").
WINEPREFIX: ${{ runner.temp }}/wineprefix
WINEDEBUG: "-all"
WINEDLLOVERRIDES: "mscoree,mshtml=" # skip the mono/gecko install prompts
run: |
wineboot -i || true
wineserver -w || true
for t in OscUdpTest OscTcpTest; do
echo "=== $t (win32, under Wine) ==="
wine ./$t.exe
done

coverage:
name: Code coverage (gcovr)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install gcovr
run: |
sudo apt-get update
sudo apt-get install -y gcovr

# Instrument with gcov (-O0 --coverage) and run the full suite. Coverage is
# measured over the library headers only (osctap/), since the library is
# header-only -- the test/demo TUs are the carriers. gcovr handles the
# inline-functions-across-many-binaries case cleanly (lcov over-/under-counts
# it). Fails if line coverage drops below 80% (currently ~85%), so coverage
# regressions are caught; a Cobertura XML is uploaded for trend tooling.
- name: Configure with coverage instrumentation
run: >
cmake -S . -B build-cov -DOSCPACK_BUILD_EXAMPLES=ON
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_CXX_FLAGS="-O0 --coverage"
-DCMAKE_EXE_LINKER_FLAGS="--coverage"

- name: Build
run: cmake --build build-cov

- name: Run tests
run: ctest --test-dir build-cov --output-on-failure

- name: Coverage report (library headers; fail under 80% lines)
run: >
gcovr --root . --filter 'osctap/' --print-summary
--fail-under-line 80 --cobertura coverage.xml

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-cobertura
path: coverage.xml

tsan:
name: ThreadSanitizer (receive loop concurrency)
runs-on: ubuntu-latest
Expand Down
106 changes: 81 additions & 25 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ if(OSCPACK_BUILD_EXAMPLES)
target_link_libraries(CompatIncludeShim oscpack)
add_test(NAME CompatIncludeShim COMMAND CompatIncludeShim)

# Differential test: the non-throwing TryValidatePacket() gate must agree with
# the throwing parse path (single source of truth). See OscValidateTest.cpp.
add_executable(OscValidateTest tests/OscValidateTest.cpp)
target_link_libraries(OscValidateTest oscpack)
add_test(NAME OscValidateTest COMMAND OscValidateTest)

# Length-prefix stream framing (OSC over TCP). The reassembly/DoS-guard logic is
# transport-independent and tested without sockets. See OscStreamFramingTest.cpp.
add_executable(OscStreamFramingTest tests/OscStreamFramingTest.cpp)
target_link_libraries(OscStreamFramingTest oscpack)
add_test(NAME OscStreamFramingTest COMMAND OscStreamFramingTest)

# Compile/link smoke for the win32 socket backend, which otherwise has no
# compiled coverage (the POSIX demos/tests are gated off Windows). Built and run
# by the existing windows-latest CI legs. The win32 backend is /W4-clean, so it
# builds under the INTERFACE /W4 /WX like everything else (a per-target /WX-
# would be ineffective anyway -- CMake appends INTERFACE options last).
if(WIN32)
add_executable(Win32SocketSmoke tests/Win32SocketSmoke.cpp)
target_link_libraries(Win32SocketSmoke oscpack)
add_test(NAME Win32SocketSmoke COMMAND Win32SocketSmoke)
endif()

# Realtime-safety test. Runs as a plain functional test of the read/dispatch
# hot path everywhere (OSCTAP_REALTIME is a no-op off Clang>=20). With
# -DOSCTAP_RTSAN=ON (Clang>=20) it additionally enforces the realtime contract:
Expand Down Expand Up @@ -75,22 +98,30 @@ if(OSCPACK_BUILD_EXAMPLES)
target_compile_options(OscConcurrencyTest PRIVATE -fsanitize=thread -g)
target_link_options(OscConcurrencyTest PRIVATE -fsanitize=thread)
endif()
endif()

#add_executable(OscSendTests tests/OscSendTests.cpp)
#target_link_libraries(OscSendTests oscpack)

#add_executable(OscReceiveTest tests/OscReceiveTest.cpp)
#target_link_libraries(OscReceiveTest oscpack)

#add_executable(OscDump examples/OscDump.cpp)
#target_link_libraries(OscDump oscpack)

#add_executable(SimpleReceive examples/SimpleReceive.cpp)
#target_link_libraries(SimpleReceive oscpack)
# OSC-over-UDP and OSC-over-TCP loopback tests: real socket client + server,
# asserting that messages arrive and decode correctly (the TCP one also spans
# TCP segments to exercise deframer reassembly). Both SKIP gracefully if the
# environment forbids loopback networking. POSIX-only (std::thread + sockets);
# also built under the TSan job to vet Run() vs AsynchronousBreak().
foreach(socktest OscUdpTest OscTcpTest)
add_executable(${socktest} tests/${socktest}.cpp)
target_link_libraries(${socktest} oscpack Threads::Threads)
add_test(NAME ${socktest} COMMAND ${socktest})
if(OSCTAP_TSAN)
target_compile_options(${socktest} PRIVATE -fsanitize=thread -g)
target_link_options(${socktest} PRIVATE -fsanitize=thread)
endif()
endforeach()
endif()

#add_executable(SimpleSend examples/SimpleSend.cpp)
#target_link_libraries(SimpleSend oscpack)
# Canonical oscpack examples, compile-checked so they can't bit-rot (they use
# the deprecated oscpack:: alias on purpose). Not run -- they bind sockets /
# block on input; see demos/ for the modern, tested equivalents.
foreach(example SimpleSend SimpleReceive OscDump)
add_executable(${example} examples/${example}.cpp)
target_link_libraries(${example} oscpack)
endforeach()
endif()

# Freestanding / embedded profile (Phase 2 "Reach"). Builds a single smoke test
Expand All @@ -111,21 +142,46 @@ if(OSCTAP_FREESTANDING)
add_test(NAME OscFreestandingTest COMMAND OscFreestandingTest)
endif()

# Integration demos (Pi 5 <-> Pico 2W <-> Android tutorial). Real, runnable OSC
# programs over the POSIX UDP backend: a hub/router and a CLI sender. POSIX-only
# (they use the ip/posix sockets + a SIGINT handler), mirroring the Pi 5 target.
# See docs/INTEGRATION_PI5_PICO_ANDROID.md.
set(OSCTAP_BUILD_DEMOS OFF CACHE BOOL "Build the Pi 5 integration demos (POSIX sockets)")
if(OSCTAP_BUILD_DEMOS AND NOT WIN32)
add_executable(pi5_hub demos/pi5_hub.cpp)
target_link_libraries(pi5_hub oscpack)

add_executable(osc_send demos/osc_send.cpp)
target_link_libraries(osc_send oscpack)

# OSC-over-TCP demo pair (see docs/OSC_OVER_TCP.md): a server/monitor and a
# CLI client, counterparts to pi5_hub / osc_send.
add_executable(tcp_server demos/tcp_server.cpp)
target_link_libraries(tcp_server oscpack)

add_executable(tcp_send demos/tcp_send.cpp)
target_link_libraries(tcp_send oscpack)
endif()

set(OSCTAP_BUILD_FUZZERS OFF CACHE BOOL "Build the libFuzzer fuzz target (requires Clang)")
set(OSCTAP_FUZZER_STANDALONE OFF CACHE BOOL "Build the fuzz target with the standalone driver instead of libFuzzer (works with g++/ASan, no fuzzer runtime needed)")

if(OSCTAP_BUILD_FUZZERS OR OSCTAP_FUZZER_STANDALONE)
if(OSCTAP_FUZZER_STANDALONE)
add_executable(fuzz_parse fuzz/fuzz_parse.cpp fuzz/standalone_main.cpp)
target_compile_options(fuzz_parse PRIVATE -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all)
target_link_libraries(fuzz_parse oscpack -fsanitize=address,undefined)
else()
# real coverage-guided libFuzzer build (Clang only)
add_executable(fuzz_parse fuzz/fuzz_parse.cpp)
target_compile_options(fuzz_parse PRIVATE -g -O1 -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all)
target_link_libraries(fuzz_parse oscpack -fsanitize=fuzzer,address,undefined)
endif()
set_target_properties(fuzz_parse PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
# fuzz_parse: the OSC packet parser. fuzz_deframe: the OSC-over-TCP stream
# deframer + parser (length-prefix reassembly, the bounded-buffer/DoS guard).
foreach(fuzzer fuzz_parse fuzz_deframe)
if(OSCTAP_FUZZER_STANDALONE)
add_executable(${fuzzer} fuzz/${fuzzer}.cpp fuzz/standalone_main.cpp)
target_compile_options(${fuzzer} PRIVATE -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all)
target_link_libraries(${fuzzer} oscpack -fsanitize=address,undefined)
else()
# real coverage-guided libFuzzer build (Clang only)
add_executable(${fuzzer} fuzz/${fuzzer}.cpp)
target_compile_options(${fuzzer} PRIVATE -g -O1 -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all)
target_link_libraries(${fuzzer} oscpack -fsanitize=fuzzer,address,undefined)
endif()
set_target_properties(${fuzzer} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON)
endforeach()
endif()

# Warnings-as-errors is opt-in (default OFF) so that consumers building against
Expand Down
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
**OscTap** is the actively-maintained, security-hardened continuation of
[oscpack](http://www.rossbencina.com/code/oscpack) — Ross Bencina's C++ library for
packing and unpacking [Open Sound Control](https://opensoundcontrol.stanford.edu/)
(OSC) packets, with a minimal set of UDP networking classes for Windows and POSIX.
(OSC) packets, with UDP **and TCP** networking classes for Windows and POSIX.

It is a drop-in successor: the `oscpack` namespace and include paths are retained as a
deprecated compatibility alias, so existing code keeps building while new code uses the
`osctap` name.

> **Status:** modernization in progress. The parsing path has been audited and
> hardened (see the security fixes in the history), fuzzed, and is covered by CI across
> Linux/macOS/Windows and GCC/Clang/MSVC at C++17 and C++20. See
> [`ROADMAP.md`](ROADMAP.md) for what's done and what's next.
> **Status:** actively modernized. The parsing path is audited, hardened, and fuzzed;
> a non-throwing validation gate, a freestanding/embedded profile, OSC-over-TCP, and an
> aarch64 (Raspberry Pi 5) target have landed. CI spans Linux/macOS/Windows ×
> GCC/Clang/MSVC at C++17 and C++20, plus ASan/UBSan, RTSan, TSan, fuzzing, aarch64
> (QEMU), Windows-runtime (Wine), and ~85% coverage. See [`ROADMAP.md`](ROADMAP.md).

## What it is

Expand Down Expand Up @@ -45,19 +46,27 @@ The library itself is header-only; the build targets are the tests and examples.

| Path | Contents |
|------|----------|
| `osctap/osc/` | OSC packet classes (parsing, printing, outbound packing, listeners) |
| `osctap/ip/` | UDP networking; `posix/` and `win32/` backends |
| `osctap/osc/` | OSC packet classes (parsing, printing, outbound packing, listeners, stream framing) |
| `osctap/ip/` | UDP + TCP networking; `posix/` and `win32/` backends |
| `oscpack/` | redirect shim — old `<oscpack/...>` include paths forwarding to `<osctap/...>` (deprecated compatibility) |
| `tests/` | unit tests (incl. malformed-input regression tests), the compat-shim guard, and send/receive examples |
| `examples/` | `OscDump`, `SimpleSend`, `SimpleReceive` |
| `fuzz/` | libFuzzer harness, corpus, and standalone driver — see [`fuzz/README.md`](fuzz/README.md) |
| `tests/` | unit tests (malformed-input regression, validation, framing, UDP/TCP loopback), the compat-shim guard |
| `demos/` | runnable OSC programs (UDP hub + sender, TCP server + sender) |
| `examples/` | the canonical oscpack examples — `OscDump`, `SimpleSend`, `SimpleReceive` |
| `android/` | Android NDK JNI bridge + Kotlin facade |
| `fuzz/` | libFuzzer harnesses (parser + TCP deframer), corpora, standalone driver — see [`fuzz/README.md`](fuzz/README.md) |
| `docs/` | guides (below); `docs/legacy/` keeps the original oscpack README/CHANGES/TODO |

## Documentation

- [`docs/GETTING_STARTED.md`](docs/GETTING_STARTED.md) — **start here**: build, send, receive, parse (OSC over UDP).
- [`docs/API.md`](docs/API.md) — the public API, grouped by header.
- [`docs/OSC_OVER_TCP.md`](docs/OSC_OVER_TCP.md) — reliable/stream transport.
- [`docs/EMBEDDED_PICO2W.md`](docs/EMBEDDED_PICO2W.md) — no-heap / no-exceptions builds (Raspberry Pi Pico 2W).
- [`docs/INTEGRATION_PI5_PICO_ANDROID.md`](docs/INTEGRATION_PI5_PICO_ANDROID.md) — a worked Pi 5 ⇄ Pico 2W ⇄ Android system.
- [`ROADMAP.md`](ROADMAP.md) — plan of record, design decisions, sanitizer strategy.
- [`docs/HERITAGE.md`](docs/HERITAGE.md) — lineage and credits.
- [`fuzz/README.md`](fuzz/README.md) — how to fuzz the parser.
- [`README`](README) — original oscpack build notes (legacy reference).
- [`fuzz/README.md`](fuzz/README.md) — how to fuzz the parser and deframer.
- [`docs/legacy/`](docs/legacy/) — original oscpack README / CHANGES / TODO (historical).
- [`LICENSE`](LICENSE) — MIT-style license.

## Heritage & license
Expand Down
Loading
Loading