diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh index 8ca10d5..8eea43d 100755 --- a/.clusterfuzzlite/build.sh +++ b/.clusterfuzzlite/build.sh @@ -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 _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/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 113f75a..6e1d95a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index a9f9e49..dbef9c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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: @@ -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 @@ -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 diff --git a/README.md b/README.md index f2a9562..c1a789f 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 `` include paths forwarding to `` (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 diff --git a/ROADMAP.md b/ROADMAP.md index 5bdc2ef..b590443 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,9 +5,10 @@ OscTap is the actively-maintained, security-hardened, modern-C++ continuation of document is the source of truth for the rebrand and the work plan. See [`docs/HERITAGE.md`](docs/HERITAGE.md) for lineage and credits. -> Status: Phase 0 and Phase 1 complete. Phase 2 ("Reach") prep in progress — the -> freestanding/embedded profile groundwork has landed (see Phase 2 below); the -> remaining Reach items stay demand-gated. +> Status: Phase 0 and Phase 1 complete. Phase 2 ("Reach") underway — freestanding +> profile, aarch64/Pi 5 CI, and the Pi 5 ⇄ Pico 2W ⇄ Android integration (demos + +> tutorial + Android JNI bridge) have landed (see Phase 2 below). Remaining: +> multicast, armv7, and a full Android sample app. ## Why OscTap exists @@ -153,16 +154,48 @@ See [Sanitizer strategy](#sanitizer-strategy) for scope and rationale. `-fno-exceptions -fno-rtti`; a `freestanding` CI job (GCC + Clang) keeps it green. Hosted builds are byte-for-byte unchanged. **Demand signal: Raspberry Pi Pico 2W (RP2350)** — see [`docs/EMBEDDED_PICO2W.md`](docs/EMBEDDED_PICO2W.md). - Deferred: a **non-throwing `TryInit`/validate** entry point so a no-exceptions - build can *reject* untrusted packets by returning an error instead of aborting - (today, malformed input on an exceptions-off build is fatal — safe only on a - trusted link; open networks should keep exceptions on and `catch`). -- [ ] QEMU aarch64 / armv7 CI. *(Demand-gated — adds standing CI surface; stand up - only when a real big-endian/cross target needs it. The freestanding profile - already exercises the no-OS code paths without QEMU's cost.)* -- [ ] Android NDK build. *(Demand-gated — pursue when an Android consumer appears.)* + **Non-throwing `TryInit`/validate landed**: `ReceivedMessage::TryInit` / + `ReceivedBundle::TryInit` and the recursive `TryValidatePacket()` gate return a + status (static error string, `nullptr` == valid) instead of throwing, so a + no-exceptions build can *reject* untrusted packets rather than abort. Built from + a single source of truth — the throwing constructors delegate to the same + validators — and guarded by `OscValidateTest` (differential vs. the throwing + path) plus a freestanding-build check that malformed input returns instead of + aborting. +- [x] **aarch64 (Raspberry Pi 5) CI under QEMU** — *landed.* The `aarch64-qemu` CI + job cross-compiles the suite with `aarch64-linux-gnu-g++` and runs it under + `qemu-user` (via `CMAKE_CROSSCOMPILING_EMULATOR`), proving the Pi-5-class build + green on every push and exercising the endian-agnostic (de)serialization on a + non-x86 ISA. `OscConcurrencyTest` is excluded from the emulated run (real + threads + loopback sockets are flaky under `qemu-user`; the native TSan/POSIX + legs cover it). Deferred: **armv7** (32-bit) and **real-hardware** runners. +- [x] **Android NDK build** — *groundwork landed.* A JNI bridge (`android/osctap_jni.cpp`) + exposes the header-only core to Kotlin (`buildMessage`/`describe`), with an NDK + `android/CMakeLists.txt` and a `android/OscTap.kt` facade (JVM UDP transport). + The bridge is compile-verified against `jni.h` + the core. Deferred: a full + Gradle sample app and optional NDK CI (needs the NDK image; weigh against the + standing-surface caution). +- [x] **Pi 5 ⇄ Pico 2W ⇄ Android integration** — runnable Pi 5 hub/router + CLI + sender demos (`demos/`, `OSCTAP_BUILD_DEMOS`, POSIX sockets — the first + compiled coverage of the `ip/` layer) and an end-to-end tutorial + ([`docs/INTEGRATION_PI5_PICO_ANDROID.md`](docs/INTEGRATION_PI5_PICO_ANDROID.md)) + tying all three nodes together over OSC/UDP. +- [x] **OSC over TCP (issue #14)** — *v1 landed.* Length-prefix framing + (`osc/OscStreamFraming.h`: encoder + bounded streaming deframer with a + frame-size cap, fuzzed via `fuzz/fuzz_deframe.cpp`), and public TCP socket + types (`ip/TcpSocket.h`): `TcpTransmitSocket` (client, `TCP_NODELAY`, + partial-write loop) and a single-threaded, multi-connection + `TcpListeningReceiveSocket` (per-connection deframer → `PacketListener`). + POSIX backend is runtime-tested (`OscTcpTest`, a real loopback incl. a + segment-spanning message; ASan/UBSan/TSan-clean); the win32 backend mirrors it + and is **runtime-tested under Wine** (the `win32-wine` CI job cross-compiles + `OscUdpTest`/`OscTcpTest` with MinGW and runs them under Wine). See + [`docs/OSC_OVER_TCP.md`](docs/OSC_OVER_TCP.md). Deferred: SLIP framing, TLS, + WebSocket, and `epoll`. - [ ] Multicast receive (cherry-pick from `stephram/oscpack`). *(Self-contained; - the next demand-driven feature pickup after the freestanding groundwork.)* + the next demand-driven feature pickup. Note: the `ip/*/UdpSocket.h` backends + now enter the compiled surface via the demos, so the deferred `strcpy`/ + `gethostbyname` cleanup from Phase 1 #4 can ride along here.)* ## Milestones → GitHub diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 0000000..24fbb02 --- /dev/null +++ b/android/CMakeLists.txt @@ -0,0 +1,32 @@ +# Android NDK build for the OscTap JNI bridge (libosctap_jni.so). +# +# Built by Gradle's externalNativeBuild, or directly with the NDK toolchain: +# +# cmake -B build-android android \ +# -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ +# -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 \ +# -DOSCTAP_ROOT=/abs/path/to/OscTap +# cmake --build build-android +# +# (Repeat per ABI: arm64-v8a, armeabi-v7a, x86_64 — or let Gradle fan out.) +cmake_minimum_required(VERSION 3.22) +project(osctap_jni CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# OscTap repo root (header-only core). Defaults to the parent of android/. +set(OSCTAP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." CACHE PATH "Path to the OscTap repo root") + +add_library(osctap_jni SHARED osctap_jni.cpp) +target_include_directories(osctap_jni PRIVATE ${OSCTAP_ROOT} ${OSCTAP_ROOT}/osctap) + +# Parsing untrusted OSC throws on malformed input; keep exceptions enabled so the +# bridge can catch and surface a Java exception instead of aborting. (RTTI is not +# required.) The NDK enables exceptions by default for CMake projects; we set it +# explicitly so the contract is unambiguous. +target_compile_options(osctap_jni PRIVATE -fexceptions -fno-rtti) + +# Uncomment if you add __android_log_print logging in the bridge: +# find_library(log-lib log) +# target_link_libraries(osctap_jni ${log-lib}) diff --git a/android/OscTap.kt b/android/OscTap.kt new file mode 100644 index 0000000..2a04391 --- /dev/null +++ b/android/OscTap.kt @@ -0,0 +1,73 @@ +/* + OscTap demo: Kotlin facade for the Android app. + + Part of the Pi 5 <-> Pico 2W <-> Android integration tutorial + (docs/INTEGRATION_PI5_PICO_ANDROID.md). The OScTap C++ core (via the JNI + bridge in osctap_jni.cpp -> libosctap_jni.so) builds and parses OSC; the UDP + transport is pure JVM (java.net.DatagramSocket), which is the idiomatic way + to do networking in an Android app. + + Requires in the + manifest. Do socket I/O off the main thread (a coroutine / thread). +*/ +package org.osctap.demo + +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress + +/** Native OSC (de)serialization, backed by the OscTap C++ core through JNI. */ +object OscTap { + init { System.loadLibrary("osctap_jni") } + + /** Build an OSC message. Each arg may be Int, Float, Boolean, or String. */ + external fun buildMessage(address: String, args: Array): ByteArray + + /** Parse one OSC *message* into a human-readable summary. + * Throws IllegalArgumentException on a malformed packet. */ + external fun describe(packet: ByteArray): String +} + +/** + * Minimal UDP OSC endpoint for the app. Construct once; call send() to talk to + * the Pi 5 hub, and receive() in a loop on a background thread for telemetry. + */ +class OscUdp(private val socket: DatagramSocket = DatagramSocket()) { + + /** Fire one OSC message at host:port (e.g. the Pi 5 hub on 9000). */ + fun send(host: String, port: Int, address: String, vararg args: Any) { + val bytes = OscTap.buildMessage(address, arrayOf(*args)) + socket.send(DatagramPacket(bytes, bytes.size, InetAddress.getByName(host), port)) + } + + /** Block for one inbound packet and return its parsed summary (or null if + * malformed). Call from a background thread bound to your telemetry port. */ + fun receive(): String? { + val buf = ByteArray(1500) + val pkt = DatagramPacket(buf, buf.size) + socket.receive(pkt) + return try { + OscTap.describe(pkt.data.copyOf(pkt.length)) + } catch (e: IllegalArgumentException) { + null // drop malformed datagram + } + } + + fun bind(port: Int): OscUdp = OscUdp(DatagramSocket(port)) + fun close() = socket.close() +} + +/* + Usage sketch (in a ViewModel / coroutine): + + val hub = "192.168.1.10" + val tx = OscUdp() + tx.send(hub, 9000, "/hub/led", 1) // turn an LED on via the hub + tx.send(hub, 9000, "/hub/pwm", 0.75f) // set a PWM duty + + // telemetry: the hub relays /sensor/* to us re-addressed as /ui/* + val rx = OscUdp(DatagramSocket(9001)) + thread { + while (true) rx.receive()?.let { println("telemetry: $it") } + } +*/ diff --git a/android/osctap_jni.cpp b/android/osctap_jni.cpp new file mode 100644 index 0000000..46e6600 --- /dev/null +++ b/android/osctap_jni.cpp @@ -0,0 +1,133 @@ +/* + OscTap demo: Android NDK / JNI bridge. + + Part of the Pi 5 <-> Pico 2W <-> Android integration tutorial + (docs/INTEGRATION_PI5_PICO_ANDROID.md). Exposes the OscTap C++ core to + Kotlin/Java so an Android app can build and parse OSC packets natively; the + UDP transport itself stays on the JVM side (java.net.DatagramSocket). + + Two entry points, matching OscTap.kt (package org.osctap.demo): + * buildMessage(String address, Object[] args) -> byte[] + args may be Integer / Float / Boolean / String (mapped to OSC i f T/F s). + * describe(byte[] packet) -> String + parse one OSC *message* and return a human-readable summary. + + Build with the Android NDK via android/CMakeLists.txt (see the tutorial). The + core is header-only, so there is nothing to link but this bridge. + + NOTE on untrusted input: parsing throws on malformed packets. Exceptions must + be ENABLED for the NDK target (the CMakeLists sets -fexceptions); describe() + catches and surfaces a Java exception instead of aborting. +*/ + +#include +#include + +#include "osc/OscReceivedElements.h" +#include "osc/OscOutboundPacketStream.h" + +namespace { + +// Append one boxed Java argument (Integer/Float/Boolean/String) as the matching +// OSC argument. Returns false if the type is unsupported. +bool AppendBoxedArg( JNIEnv* env, osctap::OutboundPacketStream& p, jobject arg ) +{ + if( arg == nullptr ) return false; + + jclass integerCls = env->FindClass( "java/lang/Integer" ); + jclass floatCls = env->FindClass( "java/lang/Float" ); + jclass boolCls = env->FindClass( "java/lang/Boolean" ); + jclass stringCls = env->FindClass( "java/lang/String" ); + + if( env->IsInstanceOf( arg, integerCls ) ){ + jint v = env->CallIntMethod( arg, env->GetMethodID( integerCls, "intValue", "()I" ) ); + p << (int32_t)v; + } else if( env->IsInstanceOf( arg, floatCls ) ){ + jfloat v = env->CallFloatMethod( arg, env->GetMethodID( floatCls, "floatValue", "()F" ) ); + p << (float)v; + } else if( env->IsInstanceOf( arg, boolCls ) ){ + jboolean v = env->CallBooleanMethod( arg, env->GetMethodID( boolCls, "booleanValue", "()Z" ) ); + p << (bool)(v == JNI_TRUE); + } else if( env->IsInstanceOf( arg, stringCls ) ){ + const char* s = env->GetStringUTFChars( (jstring)arg, nullptr ); + p << s; // const char* overload -> OSC string + env->ReleaseStringUTFChars( (jstring)arg, s ); + } else { + return false; + } + return true; +} + +void ThrowJava( JNIEnv* env, const char* cls, const char* msg ) +{ + jclass c = env->FindClass( cls ); + if( c ) env->ThrowNew( c, msg ); +} + +} // namespace + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_org_osctap_demo_OscTap_buildMessage( JNIEnv* env, jclass, jstring jaddress, jobjectArray args ) +{ + const char* address = env->GetStringUTFChars( jaddress, nullptr ); + char buffer[1024]; + jbyteArray result = nullptr; + + try { + osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); + p << osctap::BeginMessage( address ); + + const jsize n = args ? env->GetArrayLength( args ) : 0; + for( jsize i = 0; i < n; ++i ){ + jobject a = env->GetObjectArrayElement( args, i ); + if( !AppendBoxedArg( env, p, a ) ){ + env->ReleaseStringUTFChars( jaddress, address ); + ThrowJava( env, "java/lang/IllegalArgumentException", + "unsupported OSC argument type (use Integer/Float/Boolean/String)" ); + return nullptr; + } + } + p << osctap::EndMessage(); + + result = env->NewByteArray( (jsize)p.Size() ); + env->SetByteArrayRegion( result, 0, (jsize)p.Size(), + reinterpret_cast( p.Data() ) ); + } catch( const osctap::Exception& e ) { + ThrowJava( env, "java/lang/IllegalArgumentException", e.what() ); + } + + env->ReleaseStringUTFChars( jaddress, address ); + return result; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_org_osctap_demo_OscTap_describe( JNIEnv* env, jclass, jbyteArray packet ) +{ + const jsize n = env->GetArrayLength( packet ); + jbyte* bytes = env->GetByteArrayElements( packet, nullptr ); + + std::string out; + try { + // The parser uses byte-assembly (de)serialization, so the buffer needs + // no special alignment. Untrusted input -> wrap in try/catch. + osctap::ReceivedMessage m( osctap::ReceivedPacket( + reinterpret_cast( bytes ), (std::size_t)n ) ); + + out = m.AddressPattern(); + for( auto a = m.ArgumentsBegin(); a != m.ArgumentsEnd(); ++a ){ + out += ' '; + if( a->IsInt32() ) out += std::to_string( a->AsInt32Unchecked() ); + else if( a->IsFloat() ) out += std::to_string( a->AsFloatUnchecked() ); + else if( a->IsString() ) out += std::string("\"") + a->AsStringUnchecked() + "\""; + else if( a->IsBool() ) out += a->AsBoolUnchecked() ? "true" : "false"; + else out += '?'; + } + } catch( const osctap::Exception& e ) { + env->ReleaseByteArrayElements( packet, bytes, JNI_ABORT ); + ThrowJava( env, "java/lang/IllegalArgumentException", e.what() ); + return nullptr; + } + + env->ReleaseByteArrayElements( packet, bytes, JNI_ABORT ); + return env->NewStringUTF( out.c_str() ); +} diff --git a/demos/osc_send.cpp b/demos/osc_send.cpp new file mode 100644 index 0000000..76dcd57 --- /dev/null +++ b/demos/osc_send.cpp @@ -0,0 +1,106 @@ +/* + OscTap demo: command-line OSC sender. + + Part of the Pi 5 <-> Pico 2W <-> Android integration tutorial + (docs/INTEGRATION_PI5_PICO_ANDROID.md). Sends a single OSC message, so you + can drive the Pi 5 hub (or a Pico) from a shell to test the wiring before the + real Android app / firmware exists. + + Build via the OSCTAP_BUILD_DEMOS CMake option, or directly: + g++ -std=c++17 -I. -Iosctap demos/osc_send.cpp -o osc_send + + Usage: + osc_send
[args...] + + Each arg is typed by a one-letter prefix (default is auto: int if it parses + as an integer, else float if it parses as a float, else string): + i:42 int32 f:3.14 float + s:hello string T / F bool true / false + + Examples: + osc_send 192.168.1.10 9000 /hub/led i:1 + osc_send 192.168.1.10 9000 /hub/pwm f:0.75 + osc_send 192.168.1.10 9000 /sensor/temp f:21.4 +*/ + +#include "ip/UdpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include +#include + +namespace { + +bool ParseInt( const char *s, int32_t& out ) +{ + char *end = nullptr; + long v = std::strtol( s, &end, 10 ); + if( end == s || *end != '\0' ) return false; + out = static_cast( v ); + return true; +} + +bool ParseFloat( const char *s, float& out ) +{ + char *end = nullptr; + float v = std::strtof( s, &end ); + if( end == s || *end != '\0' ) return false; + out = v; + return true; +} + +void AppendArg( osctap::OutboundPacketStream& p, const char *tok ) +{ + if( std::strcmp( tok, "T" ) == 0 ) { p << true; return; } + if( std::strcmp( tok, "F" ) == 0 ) { p << false; return; } + + if( std::strncmp( tok, "i:", 2 ) == 0 ) { p << (int32_t)std::atoi( tok + 2 ); return; } + if( std::strncmp( tok, "f:", 2 ) == 0 ) { p << (float)std::atof( tok + 2 ); return; } + if( std::strncmp( tok, "s:", 2 ) == 0 ) { const char *s = tok + 2; p << s; return; } + + // Auto: int, else float, else string. + int32_t i; float f; + if( ParseInt( tok, i ) ) p << i; + else if( ParseFloat( tok, f ) ) p << f; + else p << tok; +} + +} // namespace + +int main( int argc, char *argv[] ) +{ + if( argc < 4 ){ + std::cerr << "usage: osc_send
[args...]\n"; + return 2; + } + const char *host = argv[1]; + int port = std::atoi( argv[2] ); + const char *address = argv[3]; + + char buffer[1024]; + osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); + try { + p << osctap::BeginMessage( address ); + for( int i = 4; i < argc; ++i ) + AppendArg( p, argv[i] ); + p << osctap::EndMessage(); + } catch( const osctap::Exception& e ) { + std::cerr << "failed to build message: " << e.what() << '\n'; + return 1; + } + + try { + osctap::UdpTransmitSocket( osctap::IpEndpointName( host, port ) ) + .Send( p.Data(), p.Size() ); + } catch( const std::exception& e ) { + std::cerr << "send failed: " << e.what() << '\n'; + return 1; + } + + std::cout << "sent " << p.Size() << " bytes to " << host << ':' << port + << " " << address << '\n'; + return 0; +} diff --git a/demos/pi5_hub.cpp b/demos/pi5_hub.cpp new file mode 100644 index 0000000..3616d84 --- /dev/null +++ b/demos/pi5_hub.cpp @@ -0,0 +1,163 @@ +/* + OscTap demo: Raspberry Pi 5 OSC hub / router. + + Part of the Pi 5 <-> Pico 2W <-> Android integration tutorial + (docs/INTEGRATION_PI5_PICO_ANDROID.md). Runs on the Pi 5 (or any POSIX host; + the source path is identical on aarch64 and x86-64) as the central node: + + * binds a UDP socket and listens for OSC, + * prints every message it receives (address + typed arguments + sender), + * translates/relays between the controller (Android) and the device (Pico): + - from Android: /hub/led -> Pico as /led + /hub/pwm -> Pico as /pwm + - from Pico: /sensor/ ... -> Android as /ui/ ... (telemetry) + + Build via the OSCTAP_BUILD_DEMOS CMake option, or directly: + g++ -std=c++17 -I. -Iosctap demos/pi5_hub.cpp -o pi5_hub + + Usage: + pi5_hub [listenPort] [picoHost:picoPort] [androidHost:androidPort] + Defaults: + listenPort 9000 + pico 192.168.1.50:9000 + android 192.168.1.20:9001 +*/ + +#include "ip/UdpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscPacketListener.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +// Parse "host:port" (port optional -> fallback). IPv4 dotted or hostname. +osctap::IpEndpointName ParseEndpoint( const char *s, int fallbackPort ) +{ + const char *colon = std::strrchr( s, ':' ); + if( !colon ) + return osctap::IpEndpointName( s, fallbackPort ); + std::string host( s, colon - s ); + int port = std::atoi( colon + 1 ); + return osctap::IpEndpointName( host.c_str(), port ? port : fallbackPort ); +} + +void PrintMessage( const osctap::ReceivedMessage& m, const osctap::IpEndpointName& from ) +{ + char who[ osctap::IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ]; + from.AddressAndPortAsString( who ); + std::cout << "[recv " << who << "] " << m.AddressPattern() + << " (" << m.ArgumentCount() << " args)"; + for( auto a = m.ArgumentsBegin(); a != m.ArgumentsEnd(); ++a ){ + std::cout << ' '; + if( a->IsInt32() ) std::cout << a->AsInt32Unchecked(); + else if( a->IsFloat() ) std::cout << a->AsFloatUnchecked(); + else if( a->IsString() ) std::cout << '"' << a->AsStringUnchecked() << '"'; + else if( a->IsBool() ) std::cout << (a->AsBoolUnchecked() ? "true" : "false"); + else std::cout << '?'; + } + std::cout << '\n'; +} + +class HubListener : public osctap::OscPacketListener { +public: + HubListener( const osctap::IpEndpointName& pico, const osctap::IpEndpointName& android ) + : pico_( pico ), android_( android ) {} + + // Guard the dispatch: parsing untrusted UDP can throw on a malformed packet. + // Catch it so one bad datagram drops instead of taking the hub down. + void ProcessPacket( const char *data, int size, const osctap::IpEndpointName& from ) override + { + try { + osctap::OscPacketListener::ProcessPacket( data, size, from ); + } catch( const osctap::Exception& e ) { + std::cerr << "[drop] malformed packet (" << e.what() << ")\n"; + } + } + +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, const osctap::IpEndpointName& from ) override + { + PrintMessage( m, from ); + + const char *addr = m.AddressPattern(); + char out[256]; + + // Controller -> device: re-address /hub/ to / and forward to Pico. + if( std::strncmp( addr, "/hub/", 5 ) == 0 ){ + osctap::OutboundPacketStream p( out, sizeof(out) ); + p << osctap::BeginMessage( addr + 4 ); // "/hub/led" -> "/led" + for( auto a = m.ArgumentsBegin(); a != m.ArgumentsEnd(); ++a ) + CopyArg( p, a ); + p << osctap::EndMessage(); + Forward( pico_, p, "Pico" ); + } + // Device -> controller: re-address /sensor/ to /ui/ (telemetry). + else if( std::strncmp( addr, "/sensor/", 8 ) == 0 ){ + std::string ui = std::string("/ui/") + (addr + 8); + osctap::OutboundPacketStream p( out, sizeof(out) ); + p << osctap::BeginMessage( ui.c_str() ); + for( auto a = m.ArgumentsBegin(); a != m.ArgumentsEnd(); ++a ) + CopyArg( p, a ); + p << osctap::EndMessage(); + Forward( android_, p, "Android" ); + } + } + +private: + static void CopyArg( osctap::OutboundPacketStream& p, + osctap::ReceivedMessage::const_iterator a ) + { + if( a->IsInt32() ) p << a->AsInt32Unchecked(); + else if( a->IsFloat() ) p << a->AsFloatUnchecked(); + else if( a->IsString() ) p << a->AsStringUnchecked(); + else if( a->IsBool() ) p << a->AsBoolUnchecked(); + } + + void Forward( const osctap::IpEndpointName& to, const osctap::OutboundPacketStream& p, + const char *label ) + { + try { + osctap::UdpTransmitSocket( to ).Send( p.Data(), p.Size() ); + char dst[ osctap::IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ]; + to.AddressAndPortAsString( dst ); + std::cout << " -> " << label << " (" << dst << ")\n"; + } catch( const std::exception& e ) { + std::cerr << " -> " << label << " send failed: " << e.what() << '\n'; + } + } + + osctap::IpEndpointName pico_; + osctap::IpEndpointName android_; +}; + +osctap::UdpListeningReceiveSocket *gSocket = nullptr; +void HandleSigInt( int ) { if( gSocket ) gSocket->AsynchronousBreak(); } + +} // namespace + +int main( int argc, char *argv[] ) +{ + int listenPort = (argc > 1) ? std::atoi( argv[1] ) : 9000; + osctap::IpEndpointName pico = (argc > 2) ? ParseEndpoint( argv[2], 9000 ) + : osctap::IpEndpointName( "192.168.1.50", 9000 ); + osctap::IpEndpointName android = (argc > 3) ? ParseEndpoint( argv[3], 9001 ) + : osctap::IpEndpointName( "192.168.1.20", 9001 ); + + HubListener listener( pico, android ); + osctap::UdpListeningReceiveSocket socket( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, listenPort ), &listener ); + gSocket = &socket; + std::signal( SIGINT, HandleSigInt ); + + std::cout << "OscTap Pi 5 hub listening on UDP " << listenPort << " (Ctrl-C to stop)\n"; + socket.Run(); + std::cout << "\nhub stopped.\n"; + return 0; +} diff --git a/demos/tcp_send.cpp b/demos/tcp_send.cpp new file mode 100644 index 0000000..b60f614 --- /dev/null +++ b/demos/tcp_send.cpp @@ -0,0 +1,102 @@ +/* + OscTap demo: command-line OSC-over-TCP sender. + + Connects to an OSC-over-TCP server (e.g. tcp_server) and sends one OSC message. + TCP counterpart to the UDP osc_send demo. See docs/OSC_OVER_TCP.md. + + Build via the OSCTAP_BUILD_DEMOS CMake option, or directly: + g++ -std=c++17 -I. -Iosctap demos/tcp_send.cpp -o tcp_send + + Usage: + tcp_send
[args...] + + Each arg is typed by a one-letter prefix (default is auto: int, else float, + else string): + i:42 int32 f:3.14 float + s:hello string T / F bool true / false + + Examples: + tcp_send 127.0.0.1 9000 /fader/1 f:0.75 + tcp_send 127.0.0.1 9000 /chat s:hello T +*/ + +#include "ip/TcpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include +#include + +namespace { + +bool ParseInt( const char *s, int32_t& out ) +{ + char *end = nullptr; + long v = std::strtol( s, &end, 10 ); + if( end == s || *end != '\0' ) return false; + out = static_cast( v ); + return true; +} + +bool ParseFloat( const char *s, float& out ) +{ + char *end = nullptr; + float v = std::strtof( s, &end ); + if( end == s || *end != '\0' ) return false; + out = v; + return true; +} + +void AppendArg( osctap::OutboundPacketStream& p, const char *tok ) +{ + if( std::strcmp( tok, "T" ) == 0 ) { p << true; return; } + if( std::strcmp( tok, "F" ) == 0 ) { p << false; return; } + + if( std::strncmp( tok, "i:", 2 ) == 0 ) { p << (int32_t)std::atoi( tok + 2 ); return; } + if( std::strncmp( tok, "f:", 2 ) == 0 ) { p << (float)std::atof( tok + 2 ); return; } + if( std::strncmp( tok, "s:", 2 ) == 0 ) { const char *s = tok + 2; p << s; return; } + + int32_t i; float f; + if( ParseInt( tok, i ) ) p << i; + else if( ParseFloat( tok, f ) ) p << f; + else p << tok; +} + +} // namespace + +int main( int argc, char *argv[] ) +{ + if( argc < 4 ){ + std::cerr << "usage: tcp_send
[args...]\n"; + return 2; + } + const char *host = argv[1]; + const int port = std::atoi( argv[2] ); + const char *address = argv[3]; + + char buffer[1024]; + osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); + try { + p << osctap::BeginMessage( address ); + for( int i = 4; i < argc; ++i ) + AppendArg( p, argv[i] ); + p << osctap::EndMessage(); + } catch( const osctap::Exception& e ) { + std::cerr << "failed to build message: " << e.what() << '\n'; + return 1; + } + + try { + osctap::TcpTransmitSocket client( osctap::IpEndpointName( host, port ) ); + client.Send( p.Data(), p.Size() ); + } catch( const std::exception& e ) { + std::cerr << "send failed: " << e.what() << '\n'; + return 1; + } + + std::cout << "sent " << p.Size() << " bytes to " << host << ':' << port + << " " << address << " (TCP)\n"; + return 0; +} diff --git a/demos/tcp_server.cpp b/demos/tcp_server.cpp new file mode 100644 index 0000000..5187a05 --- /dev/null +++ b/demos/tcp_server.cpp @@ -0,0 +1,76 @@ +/* + OscTap demo: OSC-over-TCP server / monitor. + + Listens for OSC-over-TCP clients and prints every message it receives (address + + typed arguments + peer). A TCP counterpart to the UDP pi5_hub demo, and the + server side for testing tcp_send. See docs/OSC_OVER_TCP.md. + + Build via the OSCTAP_BUILD_DEMOS CMake option, or directly: + g++ -std=c++17 -I. -Iosctap demos/tcp_server.cpp -o tcp_server + + Usage: + tcp_server [port] (default port 9000) +*/ + +#include "ip/TcpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscPacketListener.h" + +#include +#include +#include +#include + +namespace { + +class PrintingListener : public osctap::OscPacketListener { + // Untrusted input: parsing can throw on a malformed frame. Catch it so one bad + // client can't take the server down. + void ProcessPacket( const char *data, int size, const osctap::IpEndpointName& from ) override + { + try { + osctap::OscPacketListener::ProcessPacket( data, size, from ); + } catch( const osctap::Exception& e ) { + std::cerr << "[drop] malformed packet (" << e.what() << ")\n"; + } + } + +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, const osctap::IpEndpointName& from ) override + { + char who[ osctap::IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ]; + from.AddressAndPortAsString( who ); + std::cout << "[recv " << who << "] " << m.AddressPattern() + << " (" << m.ArgumentCount() << " args)"; + for( auto a = m.ArgumentsBegin(); a != m.ArgumentsEnd(); ++a ){ + std::cout << ' '; + if( a->IsInt32() ) std::cout << a->AsInt32Unchecked(); + else if( a->IsFloat() ) std::cout << a->AsFloatUnchecked(); + else if( a->IsString() ) std::cout << '"' << a->AsStringUnchecked() << '"'; + else if( a->IsBool() ) std::cout << (a->AsBoolUnchecked() ? "true" : "false"); + else std::cout << '?'; + } + std::cout << '\n'; + } +}; + +osctap::TcpListeningReceiveSocket *gSocket = nullptr; +void HandleSigInt( int ) { if( gSocket ) gSocket->AsynchronousBreak(); } + +} // namespace + +int main( int argc, char *argv[] ) +{ + const int port = (argc > 1) ? std::atoi( argv[1] ) : 9000; + + PrintingListener listener; + osctap::TcpListeningReceiveSocket socket( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, port ), &listener ); + gSocket = &socket; + std::signal( SIGINT, HandleSigInt ); + + std::cout << "OscTap TCP server listening on TCP " << port << " (Ctrl-C to stop)\n"; + socket.Run(); + std::cout << "\nserver stopped.\n"; + return 0; +} diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..10b7074 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,161 @@ +# OscTap API reference + +A curated reference of the public surface, grouped by header. Everything lives in +namespace `osctap` (the `oscpack` alias is retained, deprecated). This is a +hand-written overview — the headers themselves are the authoritative source. + +New here? Start with [Getting Started](GETTING_STARTED.md). + +Conventions: +- **(RT)** = realtime-safe (allocation-/exception-free; annotated `OSCTAP_REALTIME`). +- **(throws)** = validates and may throw on bad input / state. + +--- + +## Building OSC — `osc/OscOutboundPacketStream.h` + +### `OutboundPacketStream` +Serializes OSC into a caller-owned buffer (no heap). + +| Member | Notes | +|--------|-------| +| `OutboundPacketStream(char* buf, size_t capacity)` | wrap a buffer | +| `const char* Data() const` · `size_t Size() const` | the framed bytes so far | +| `size_t Capacity() const` | buffer size | +| `void Clear()` | reset to empty | +| `bool IsReady() const` | all messages/bundles closed | +| `bool IsMessageInProgress() const` · `bool IsBundleInProgress() const` | | +| `operator<<( T )` | append a value or manipulator (below) | + +Streamed value types: `bool` (→`T`/`F`), `int32_t`, `int64_t`, `float`, `double`, +`char`, `const char*` / `std::string` / `string_view` (→ string), `Blob`, +`Symbol`, `TimeTag`, `MidiMessage`, `RgbaColor`. Overflowing the buffer throws +`OutOfBufferMemoryException`. + +### Manipulators & types — `osc/OscTypes.h` +`BeginMessage(addr)` · `EndMessage()` · `BeginBundle(timeTag=1)` · +`BeginBundleImmediate()` · `EndBundle()` · `BeginArray()` · `EndArray()` · +`OscNil()` · `Infinitum()`. Value wrappers: `Blob(ptr, size)`, `Symbol(s)`, +`TimeTag(v)`, `MidiMessage(v)`, `RgbaColor(v)`. + +### Exceptions +`OutOfBufferMemoryException`, `MessageInProgressException`, +`MessageNotInProgressException`, `BundleNotInProgressException` — all derive from +`osctap::Exception`. + +--- + +## Parsing OSC — `osc/OscReceivedElements.h` + +### `ReceivedPacket` +| Member | Notes | +|--------|-------| +| `ReceivedPacket(const char* data, size_t size)` | (throws) validates size | +| `bool IsBundle() const` · `bool IsMessage() const` | | +| `const char* Contents() const` · `osc_bundle_element_size_t Size() const` | | +| `static const char* ValidateSizeNoThrow(size)` | non-throwing size check (`nullptr` = ok) | + +### `ReceivedMessage` +| Member | Notes | +|--------|-------| +| `ReceivedMessage(const ReceivedPacket&)` / `(const ReceivedBundleElement&)` | (throws) | +| `ReceivedMessage()` + `const char* TryInit(data, size)` | **non-throwing** parse (`nullptr` = ok) | +| `static const char* Validate(data, size)` | non-throwing structural check | +| `const char* AddressPattern() const` | (RT) | +| `uint32_t ArgumentCount() const` · `const char* TypeTags() const` | (RT) | +| `const_iterator ArgumentsBegin() / ArgumentsEnd() const` | (RT) | +| `ReceivedMessageArgumentStream ArgumentStream() const` | for `>>` reads | + +### `ReceivedMessageArgument` +Type tests (all `bool`, RT): `IsBool IsInt32 IsInt64 IsFloat IsDouble IsChar +IsString IsSymbol IsBlob IsRgbaColor IsMidiMessage IsTimeTag IsNil IsInfinitum +IsArrayBegin IsArrayEnd`. + +Accessors — checked (throw `WrongArgumentTypeException`) and `*Unchecked` (RT): +`AsBool AsInt32 AsInt64 AsFloat AsDouble AsChar AsString AsSymbol AsRgbaColor +AsMidiMessage AsTimeTag` (+ `…Unchecked`). Blobs: +`void AsBlob(const void*& data, osc_bundle_element_size_t& size)` (+ `Unchecked`). + +`char TypeTag() const` (RT) returns the raw tag. Iterate with +`ReceivedMessageArgumentIterator`, or read positionally with +`ReceivedMessageArgumentStream`: `args >> b >> i >> f >> endTag;` (where `endTag` +is a `MessageTerminator` lvalue; an over-read throws `ExcessArgumentException`). + +### `ReceivedBundle` +`TimeTag()`, `ElementCount()`, `ElementsBegin()/ElementsEnd()`, plus the same +non-throwing `TryInit` / `static Validate` pair as `ReceivedMessage`. Iterate with +`ReceivedBundleElementIterator`; each element is a `ReceivedBundleElement` you +construct a `ReceivedMessage`/`ReceivedBundle` from. + +### Non-throwing whole-packet gate +```cpp +const char* TryValidatePacket(const char* data, osc_bundle_element_size_t size, + unsigned int maxBundleNestingDepth = 64); +``` +Returns `nullptr` iff the packet (message, or bundle whose elements are all +well-formed, recursively) is safe to construct **and read in full** without any +throw/abort. The gate to use for untrusted input on a no-exceptions build. + +### Exceptions +`MalformedPacketException`, `MalformedMessageException`, `MalformedBundleException`, +`WrongArgumentTypeException`, `MissingArgumentException`, `ExcessArgumentException`. + +--- + +## Dispatch — `osc/OscPacketListener.h` +`OscPacketListener : public PacketListener` — unpacks bundles (depth-bounded) and +calls your `ProcessMessage`. +- `virtual void ProcessMessage(const ReceivedMessage&, const IpEndpointName&)` — override this. +- `virtual void ProcessBundle(...)` — override to handle time tags. +- `void SetMaxBundleNestingDepth(unsigned)` (default 64). + +## Pretty-printing — `osc/OscPrintReceivedElements.h` +`std::ostream& operator<<(std::ostream&, const ReceivedPacket& | ReceivedMessage& +| ReceivedBundle& | ReceivedMessageArgument&)` — human-readable dump (see `OscDump`). + +--- + +## Stream framing (OSC over TCP) — `osc/OscStreamFraming.h` +- `void WriteOscStreamFrameHeader(char header[4], uint32_t packetSize)` — length prefix. +- `size_t FrameOscPacket(const char* pkt, uint32_t size, char* out, size_t cap)` — frame into one buffer (`0` if it doesn't fit). +- `class OscStreamDeframer` — `explicit OscStreamDeframer(uint32_t maxFrameSize = 65536)`; `bool Consume(const char* data, size_t size, Sink&& sink)` (calls `sink(packet, size)` per complete packet; returns `false` on an over-cap frame); `void Reset()`; `uint32_t MaxFrameSize() const`. See [OSC over TCP](OSC_OVER_TCP.md). + +--- + +## Networking — `ip/` + +### `IpEndpointName` (`ip/IpEndpointName.h`) +Ctors: `(const char* host, int port)`, `(uint32_t ip, int port)`, +`(a,b,c,d, port)`, `(int port)`. Constants `ANY_ADDRESS`, `ANY_PORT`. Helpers +`AddressAsString(char*)`, `AddressAndPortAsString(char*)`, `IsMulticastAddress()`. + +### `PacketListener` (`ip/PacketListener.h`) +Abstract base: `virtual void ProcessPacket(const char* data, int size, const IpEndpointName& from)`. + +### UDP (`ip/UdpSocket.h`) +- `UdpTransmitSocket(const IpEndpointName& remote)` — `Send(data, size)`, `SendTo(to, data, size)`. +- `UdpReceiveSocket`, and `UdpListeningReceiveSocket(local, PacketListener*)` — + `Run()` (blocks), `Break()`, `AsynchronousBreak()`, `int LocalPort()`. +- `SocketReceiveMultiplexer` — multiple sockets/timers in one `Run()` loop + (`AttachSocketListener`, `AttachPeriodicTimerListener`, …). + +### TCP (`ip/TcpSocket.h`) +- `TcpTransmitSocket(const IpEndpointName& remote)` — `Send(data, size)` (length-prefixed; `TCP_NODELAY` on). +- `TcpListeningReceiveSocket(local, PacketListener*, uint32_t maxFrameSize = 65536)` — + `Run()`, `Break()`, `AsynchronousBreak()`, `IpEndpointName LocalEndpointFor(requested)`. + Accepts multiple clients, each deframed independently. + +--- + +## Build-configuration seam — `osc/OscConfig.h` +- `OSCTAP_HAS_EXCEPTIONS` — auto-detected (0 under `-fno-exceptions`). +- `OSCTAP_FREESTANDING` — define to drop hosted-only facilities (``, + `std::vector` `OwnedMessage`, `std::string operator<<`). +- `OSCTAP_THROW(EXC)` — `throw` when exceptions are on; otherwise a non-returning + fatal handler. +- `OSCTAP_FATAL_HANDLER(whatCStr)` — pre-define to route the no-exceptions failure + path (default `std::abort()`). See [Embedded (Pico 2W)](EMBEDDED_PICO2W.md). + +## Base exception — `osc/OscException.h` +`class Exception : public std::exception` — base of every OscTap exception; +`const char* what() const noexcept`. diff --git a/docs/EMBEDDED_PICO2W.md b/docs/EMBEDDED_PICO2W.md index 8dfb600..63ea881 100644 --- a/docs/EMBEDDED_PICO2W.md +++ b/docs/EMBEDDED_PICO2W.md @@ -68,14 +68,24 @@ Pick the model that matches your threat surface: identical either way — `OSCTAP_THROW` is just `throw` here. You still get the rest of the embedded posture (no heap on the hot path, small code). +3. **Untrusted / open network, exceptions still off** — gate with the non-throwing + validator. `osctap::TryValidatePacket(data, size)` returns `nullptr` when the + packet is fully well-formed (and therefore safe to construct and read without any + `OSCTAP_THROW` firing), else a static error string. It recurses through bundles + with a nesting bound and never throws or allocates, so you can reject bad input + on a `-fno-exceptions` build instead of aborting: + + ```cpp + if (osctap::TryValidatePacket(buf, n) == nullptr) { + osctap::ReceivedMessage m(osctap::ReceivedPacket(buf, n)); // won't abort + // ... read m ... + } // else: drop the datagram + ``` + Either way, **validation runs in the lwIP receive callback, off any audio/render thread** — consistent with OscTap's realtime contract (validation may throw and is not part of the RT hot path; the throw-free `*Unchecked` accessors are). -> Future work (tracked in Phase 2): a non-throwing `TryInit`/validate entry point -> so a no-exceptions build can *reject* untrusted packets by returning an error -> instead of aborting. Until then, option 2 is the safe choice for open networks. - ## Build integration (CMake + Pico SDK) Add the OscTap include root and define the profile. OscTap's core is diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md new file mode 100644 index 0000000..09d543a --- /dev/null +++ b/docs/GETTING_STARTED.md @@ -0,0 +1,165 @@ +# Getting started with OscTap + +A 10-minute tour: build OscTap, send an OSC message over UDP, receive and parse +one. This covers the common case (OSC over UDP on a desktop/SBC). For other +transports and targets see [OSC over TCP](OSC_OVER_TCP.md), the +[Pico 2W embedded guide](EMBEDDED_PICO2W.md), and the +[Pi 5 ⇄ Pico ⇄ Android tutorial](INTEGRATION_PI5_PICO_ANDROID.md). For the full +public surface see the [API reference](API.md). + +## Install / build + +OscTap's **core is header-only** — add the repo root to your include path and +`#include` what you need; there's nothing to compile or link for the OSC packet +classes. The UDP/TCP socket classes are also header-only but link a platform +socket library (`ws2_32`/`winmm` on Windows; nothing extra on POSIX). + +With CMake, link the interface target: + +```cmake +add_subdirectory(OscTap) # or vendor the headers +target_link_libraries(myapp PRIVATE oscpack) # header-only INTERFACE target +``` + +Or just point at the headers: + +```sh +g++ -std=c++17 -I path/to/OscTap -I path/to/OscTap/osctap myapp.cpp -o myapp +``` + +Public headers live under ``; the old `` paths and the +`oscpack::` namespace still work as a deprecated compatibility alias, so code +written for oscpack keeps compiling. + +## Send a message + +Serialize into a buffer **you** own (no heap allocation), then put the bytes on +the wire. The buffer can be on the stack. + +```cpp +#include "osc/OscOutboundPacketStream.h" +#include "ip/UdpSocket.h" + +int main() +{ + osctap::UdpTransmitSocket tx( osctap::IpEndpointName( "127.0.0.1", 7000 ) ); + + char buffer[1024]; + osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); + p << osctap::BeginMessage( "/synth/freq" ) + << 440.0f << true << "sine" + << osctap::EndMessage(); + + tx.Send( p.Data(), p.Size() ); +} +``` + +`OutboundPacketStream` writes OSC types from their C++ counterparts: `int32_t`, +`float`, `double`, `bool` (→ `T`/`F`), `const char*`/`std::string` (→ string), +plus `Blob`, `TimeTag`, `MidiMessage`, `RgbaColor`, `OscNil()`, `Infinitum()`. If +the message would overflow your buffer it throws `OutOfBufferMemoryException`, so +size the buffer for your largest message. + +## Receive and parse a message + +Subclass `OscPacketListener` (it unpacks bundles for you and bounds nesting +depth), bind a socket, and run the receive loop. + +```cpp +#include "osc/OscPacketListener.h" +#include "ip/UdpSocket.h" +#include +#include + +class MyListener : public osctap::OscPacketListener { +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, + const osctap::IpEndpointName& from ) override + { + try { + if( std::strcmp( m.AddressPattern(), "/synth/freq" ) == 0 ) { + auto arg = m.ArgumentsBegin(); + float freq = (arg++)->AsFloat(); + bool on = (arg++)->AsBool(); + const char* wave = (arg++)->AsString(); + std::cout << "freq=" << freq << " on=" << on << " wave=" << wave << "\n"; + } + } catch( const osctap::Exception& e ) { + // wrong/missing argument types are reported by exception + std::cout << "bad message: " << e.what() << "\n"; + } + } +}; + +int main() +{ + MyListener listener; + osctap::UdpListeningReceiveSocket s( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, 7000 ), &listener ); + s.Run(); // blocks; call s.AsynchronousBreak() from another thread/handler to stop +} +``` + +### Reading arguments + +Three styles, pick what fits: + +- **Checked accessors** — `arg->AsInt32()`, `AsFloat()`, `AsString()`, … throw + `WrongArgumentTypeException` / `MissingArgumentException` on a mismatch. Safe + default. +- **Type-test then unchecked** — `if (arg->IsFloat()) arg->AsFloatUnchecked()`. + The `*Unchecked` accessors don't validate; they're the realtime-safe read path + once you've checked the tag (or validated the whole packet — see below). +- **Argument stream** — `m.ArgumentStream() >> a >> b >> endTag;` for fixed + layouts. + +### Bundles + +`OscPacketListener` traverses bundles automatically and calls `ProcessMessage` +for each contained message. To build one: + +```cpp +p << osctap::BeginBundleImmediate() + << osctap::BeginMessage( "/a" ) << 1 << osctap::EndMessage() + << osctap::BeginMessage( "/b" ) << 2 << osctap::EndMessage() + << osctap::EndBundle(); +``` + +## Handling untrusted input + +OSC arriving over a network is untrusted. By default the parser **throws** on a +malformed packet (`Malformed{Packet,Message,Bundle}Exception`), which you catch to +drop it. If you build with **exceptions disabled** (the freestanding/embedded +profile), validation instead aborts — so first gate the bytes: + +```cpp +if( osctap::TryValidatePacket( data, size ) == nullptr ) { + osctap::ReceivedPacket p( data, size ); // guaranteed safe to read + // ... +} // else: drop it +``` + +`TryValidatePacket` is non-throwing, non-allocating, and recurses through bundles +with a nesting bound. See the [Pico 2W guide](EMBEDDED_PICO2W.md) for the embedded +story. + +## Try the demos + +Runnable programs under [`../demos/`](../demos) (`-DOSCTAP_BUILD_DEMOS=ON`): + +```sh +cmake -S . -B build -DOSCTAP_BUILD_DEMOS=ON +cmake --build build --target osc_send pi5_hub +./build/pi5_hub 9000 & # a UDP hub/monitor + router +./build/osc_send 127.0.0.1 9000 /hub/led i:1 # a typed CLI sender +``` + +The canonical oscpack examples (`SimpleSend`, `SimpleReceive`, `OscDump`) are in +[`../examples/`](../examples). + +## Where next + +- [API reference](API.md) — every public type, grouped by header. +- [OSC over TCP](OSC_OVER_TCP.md) — reliable/stream transport. +- [Embedded (Pico 2W)](EMBEDDED_PICO2W.md) — no-heap / no-exceptions builds. +- [`../ROADMAP.md`](../ROADMAP.md) — design decisions and project direction. diff --git a/docs/INTEGRATION_PI5_PICO_ANDROID.md b/docs/INTEGRATION_PI5_PICO_ANDROID.md new file mode 100644 index 0000000..679d204 --- /dev/null +++ b/docs/INTEGRATION_PI5_PICO_ANDROID.md @@ -0,0 +1,250 @@ +# Integration tutorial: Raspberry Pi 5 ⇄ Pico 2W ⇄ Android over OSC + +A worked end-to-end example wiring three nodes together with OscTap and OSC/UDP: + +- **Raspberry Pi 5** — full Linux (aarch64). The **hub / router**: receives OSC, + prints it, and relays/translates between the controller and the device. +- **Raspberry Pi Pico 2W** — RP2350 microcontroller + Wi-Fi. The **I/O device**: + drives an LED/PWM from OSC and publishes sensor readings. +- **Android app** — the **controller / UI**: sends commands, shows telemetry. + +OscTap runs the OSC (de)serialization on **all three** (header-only C++ core); each +node uses whatever UDP transport is native to it. The OSC *wire format* is the +contract, so any node could equally be a different OSC implementation. + +``` + ┌────────────────────┐ /hub/led, /hub/pwm ┌────────────────────┐ + │ Android app │ ─────────────────────────▶ │ Raspberry Pi 5 │ + │ (controller/UI) │ │ hub / router │ + │ java.net UDP + │ ◀───────────────────────── │ pi5_hub (POSIX) │ + │ OscTap via NDK │ /ui/temp, /ui/name └─────────┬──────────┘ + └────────────────────┘ (relayed telemetry) │ + /led, /pwm │ ▲ /sensor/temp + ▼ │ /sensor/name + ┌────────────────────┐ + │ Pico 2W (RP2350) │ + │ lwIP UDP + │ + │ OscTap freestyle │ + └────────────────────┘ +``` + +> What's verified in this repo: the Pi 5 hub + sender demos build and run +> (loopback), the aarch64 build runs green under qemu (CI `aarch64-qemu` job), and +> the Android JNI bridge compiles against `jni.h` + the OscTap core. The Pico +> firmware and the full Android NDK/Gradle build are integration recipes — they +> need the Pico SDK / Android NDK on your machine. + +## 1. The OSC contract (address map) + +Agree this up front; it's the only thing the three nodes truly share. + +| Direction | Address | Args | Meaning | +|-----------|---------|------|---------| +| Android → Pi 5 | `/hub/led` | `int` (0/1) | request LED state | +| Android → Pi 5 | `/hub/pwm` | `float` (0..1) | request PWM duty | +| Pi 5 → Pico | `/led` | `int` | hub relays the LED command | +| Pi 5 → Pico | `/pwm` | `float` | hub relays the PWM command | +| Pico → Pi 5 | `/sensor/temp` | `float` (°C) | temperature reading | +| Pico → Pi 5 | `/sensor/name` | `string` | device label | +| Pi 5 → Android | `/ui/temp` | `float` | relayed telemetry | +| Pi 5 → Android | `/ui/name` | `string` | relayed telemetry | + +The hub's rule is mechanical: `/hub/` → `/` to the Pico; `/sensor/` → +`/ui/` to the Android. See `demos/pi5_hub.cpp`. + +## 2. Addresses & ports + +Put all three on the same LAN/subnet. Example: + +| Node | IP (example) | Listens on | Sends to | +|------|--------------|-----------|----------| +| Pi 5 hub | `192.168.1.10` | `9000` | Pico `:9000`, Android `:9001` | +| Pico 2W | `192.168.1.50` | `9000` | Pi 5 `:9000` | +| Android | `192.168.1.20` | `9001` | Pi 5 `:9000` | + +Substitute your real IPs (find them with `hostname -I` on the Pi, your router's +DHCP table for the Pico, and Wi-Fi settings on the phone). + +## 3. Node 1 — Raspberry Pi 5 (the hub) + +Build the demos (POSIX sockets; identical source on aarch64 and x86-64): + +```sh +cmake -S . -B build -DOSCTAP_BUILD_DEMOS=ON +cmake --build build --target pi5_hub osc_send +``` + +Run the hub, telling it where the Pico and Android are: + +```sh +./build/pi5_hub 9000 192.168.1.50:9000 192.168.1.20:9001 +# OscTap Pi 5 hub listening on UDP 9000 (Ctrl-C to stop) +``` + +It prints every message it receives and logs each relay. `osc_send` is a CLI for +poking the system before the real devices exist: + +```sh +./build/osc_send 192.168.1.10 9000 /hub/led i:1 +./build/osc_send 192.168.1.10 9000 /hub/pwm f:0.75 +./build/osc_send 192.168.1.10 9000 /sensor/temp f:21.4 +``` + +The hub guards itself against malformed UDP: parsing throws, and it catches the +exception to drop the bad datagram rather than crash (see `HubListener::ProcessPacket`). + +## 4. Node 2 — Raspberry Pi Pico 2W (the device) + +Full embedded setup is in [`EMBEDDED_PICO2W.md`](EMBEDDED_PICO2W.md) (freestanding +profile, Pico SDK + lwIP, the exceptions/untrusted-input trade-off). For *this* +contract, the Pico does two things. + +**Receive `/led` and `/pwm`** in the lwIP `udp_recv` callback: + +```cpp +#include "osc/OscReceivedElements.h" + +static void on_osc(void*, udp_pcb*, pbuf* p, const ip_addr_t*, u16_t) { + if (!p) return; + alignas(4) static char buf[256]; + const u16_t n = pbuf_copy_partial(p, buf, sizeof(buf), 0); + pbuf_free(p); + + osctap::ReceivedMessage m(osctap::ReceivedPacket(buf, n)); // (try/catch if exceptions on) + const char* a = m.AddressPattern(); + auto arg = m.ArgumentsBegin(); + if (std::strcmp(a, "/led") == 0) set_led(arg->AsInt32Unchecked() != 0); + else if (std::strcmp(a, "/pwm") == 0) set_pwm(arg->AsFloatUnchecked()); +} +``` + +**Publish `/sensor/temp`** periodically into a `pbuf` and `udp_sendto` the Pi 5 +(buffer on the stack, no heap): + +```cpp +#include "osc/OscOutboundPacketStream.h" + +void publish_temp(udp_pcb* pcb, const ip_addr_t* hub, uint16_t port, float c) { + char buffer[64]; + osctap::OutboundPacketStream p(buffer, sizeof(buffer)); + p << osctap::BeginMessage("/sensor/temp") << c << osctap::EndMessage(); + pbuf* pb = pbuf_alloc(PBUF_TRANSPORT, p.Size(), PBUF_RAM); + std::memcpy(pb->payload, p.Data(), p.Size()); + udp_sendto(pcb, pb, hub, port); + pbuf_free(pb); +} +``` + +> Untrusted-input reminder: on a Wi-Fi LAN, prefer **exceptions enabled** on the +> Pico so a malformed packet is caught and dropped, not fatal. On a trusted/closed +> link you can run the no-exceptions freestanding profile. Full rationale in the +> Pico guide. + +## 5. Node 3 — Android (the controller) + +Two ways to speak OSC from the app. Pick one. + +### 5a. OscTap via the NDK (native core) + +Reuse the exact OscTap C++ core on Android through a thin JNI bridge. Files in +[`../android/`](../android): `osctap_jni.cpp` (bridge), `CMakeLists.txt` (NDK +build), `OscTap.kt` (Kotlin facade + JVM UDP transport). + +Wire it into your app module's `build.gradle`: + +```gradle +android { + defaultConfig { + externalNativeBuild { cmake { cppFlags "-std=c++17 -fexceptions" } } + ndk { abiFilters "arm64-v8a", "armeabi-v7a", "x86_64" } + } + externalNativeBuild { + cmake { path "../android/CMakeLists.txt" ; version "3.22.1" } + } +} +``` + +The CMake passes `OSCTAP_ROOT` to the header-only core (override if the repo +lives elsewhere). Or build a single ABI by hand to sanity-check the toolchain: + +```sh +cmake -B build-android android \ + -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-24 \ + -DOSCTAP_ROOT=$PWD +cmake --build build-android # -> libosctap_jni.so +``` + +Then in Kotlin (`OscTap.kt`): + +```kotlin +val hub = "192.168.1.10" +val tx = OscUdp() +tx.send(hub, 9000, "/hub/led", 1) // Int -> OSC int +tx.send(hub, 9000, "/hub/pwm", 0.75f) // Float -> OSC float + +val rx = OscUdp(DatagramSocket(9001)) // telemetry relayed by the hub +thread { while (true) rx.receive()?.let { updateUi(it) } } // off the main thread +``` + +Add the permission to `AndroidManifest.xml`: + +```xml + +``` + +The bridge builds/parses OSC natively; `java.net.DatagramSocket` carries the +bytes (idiomatic Android networking). Parsing throws on malformed input and the +bridge re-throws it as a Kotlin `IllegalArgumentException`, which `receive()` +catches to drop the datagram. + +### 5b. Simpler: a JVM OSC library (no native code) + +If you don't want an NDK toolchain in your build, the app can use a pure-JVM OSC +library (e.g. **JavaOSC / illposed**) and talk to the same hub — the wire format +is identical, so OscTap on the Pi/Pico interoperates with it transparently: + +```kotlin +// implementation("com.illposed.osc:javaosc-core:0.8") +val sender = OSCPortOut(InetAddress.getByName("192.168.1.10"), 9000) +sender.send(OSCMessage("/hub/led", listOf(1))) +``` + +Trade-off: 5a dogfoods OscTap end-to-end and shares one code path across all +nodes; 5b is less setup for app developers. Both are wire-compatible. + +## 6. Bring-up order (test each leg in isolation) + +1. **Hub alone**: run `pi5_hub`, then `osc_send … /hub/led i:1` from the same Pi. + You should see the recv line and a `-> Pico` relay line. +2. **Add a fake Pico**: run another `osc_send … /sensor/temp f:20` and watch the + `-> Android` relay. Point the hub's Android target at a host running + `nc -ul 9001` (or another listener) to confirm bytes arrive. +3. **Real Pico**: flash the firmware; confirm `/sensor/*` shows up at the hub and + `/led`/`/pwm` actuate. +4. **Real Android**: send from the app; watch the hub log; confirm telemetry + reaches the phone. + +## 7. Troubleshooting + +- **Nothing arrives**: same subnet? Host firewall (`sudo ufw allow 9000/udp` on + the Pi)? Phone on Wi-Fi, not cellular? Bind the receiver to `ANY_ADDRESS` + (the hub does) so it accepts on all interfaces, not just loopback. +- **Garbled values**: OSC is big-endian; OscTap handles byte order on every node, + so garbling almost always means an **address/type mismatch** vs. the table in §1 + (e.g. sending an int where a float is expected). The hub prints the decoded type + per arg — compare against the contract. +- **String shows as `true`/garbage**: you're on an OscTap build predating the + `const char*` overload fix — pass `osctap::string_view`/`std::string`, or update. +- **App ANR / NetworkOnMainThreadException**: do all socket I/O off the main + thread (a coroutine or `thread { }`). +- **Pico resets on bad input**: that's the no-exceptions fatal handler — switch to + exceptions-enabled on the Pico for untrusted LANs (Pico guide, §"Handling + validation when exceptions are off"). + +## See also + +- [`EMBEDDED_PICO2W.md`](EMBEDDED_PICO2W.md) — the Pico 2W deep dive. +- [`../demos/pi5_hub.cpp`](../demos/pi5_hub.cpp), [`../demos/osc_send.cpp`](../demos/osc_send.cpp) — the runnable Pi 5 side. +- [`../android/`](../android) — the JNI bridge, NDK CMake, and Kotlin facade. +- [`../ROADMAP.md`](../ROADMAP.md) — where this fits in Phase 2 ("Reach"). diff --git a/docs/OSC_OVER_TCP.md b/docs/OSC_OVER_TCP.md new file mode 100644 index 0000000..3af589e --- /dev/null +++ b/docs/OSC_OVER_TCP.md @@ -0,0 +1,139 @@ +# OSC over TCP + +OscTap speaks OSC over TCP as well as UDP. This is useful when you need reliable, +ordered delivery (no dropped packets), to traverse a connection-oriented link, or +to interoperate with the many tools that default to OSC-over-TCP (SuperCollider, +Max, JUCE, liblo's `osc.tcp`). + +> Status: v1 (issue #14). Length-prefix framing, a single-threaded +> multi-connection server, `TCP_NODELAY`, and a frame-size cap. SLIP framing, TLS, +> WebSocket, and `epoll` are intentionally deferred until there's demand. + +## Framing + +UDP gives you message boundaries for free — one datagram is exactly one OSC +packet. TCP is a byte stream with **no** boundaries, so packets must be framed. +OscTap uses the de-facto convention: each packet is a **4-byte big-endian length** +followed by that many payload bytes (the same shape as a bundle element's size +slot). Both ends must agree on this out of band — it's what SuperCollider/Max/ +JUCE/liblo's `osc.tcp` use, so OscTap interoperates with them. + +The framing codec ([`osc/OscStreamFraming.h`](../osctap/osc/OscStreamFraming.h)) is +transport-agnostic and usable on its own (e.g. over a serial link or your own +socket loop): + +```cpp +#include "osc/OscStreamFraming.h" + +// Decode: feed received bytes in whatever chunks arrive; the deframer reassembles +// complete packets and calls your sink once per packet. +osctap::OscStreamDeframer deframer; // default 64 KiB frame-size cap +bool ok = deframer.Consume( data, n, []( const char* packet, uint32_t size ){ + osctap::ReceivedPacket p( packet, size ); // one whole OSC packet + // ... dispatch ... +}); +if( !ok ) { /* a peer announced an over-cap frame: drop the connection */ } +``` + +## Server (receive) + +`TcpListeningReceiveSocket` accepts any number of clients and dispatches each +complete packet to your `PacketListener` / `OscPacketListener`, exactly like the +UDP `UdpListeningReceiveSocket` — the listener contract is identical, so existing +listeners work unchanged. + +```cpp +#include "ip/TcpSocket.h" +#include "osc/OscPacketListener.h" + +class MyListener : public osctap::OscPacketListener { +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, + const osctap::IpEndpointName& from ) override { + // ... handle m ... + } +}; + +MyListener listener; +osctap::TcpListeningReceiveSocket server( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, 9000 ), &listener ); +server.Run(); // blocks; call server.AsynchronousBreak() to stop +``` + +`Run()` is single-threaded and `select()`-based; `Break()` / +`AsynchronousBreak()` stop it (the latter from another thread or a signal +handler). Connections are reaped on disconnect. + +## Client (send) + +`TcpTransmitSocket` connects and sends length-prefixed packets, looping over +partial writes; `TCP_NODELAY` is on (Nagle off — without it OSC-over-TCP latency +is a classic footgun). + +```cpp +#include "ip/TcpSocket.h" +#include "osc/OscOutboundPacketStream.h" + +osctap::TcpTransmitSocket client( osctap::IpEndpointName( "127.0.0.1", 9000 ) ); + +char buf[1024]; +osctap::OutboundPacketStream p( buf, sizeof(buf) ); +p << osctap::BeginMessage( "/fader/1" ) << 0.75f << osctap::EndMessage(); +client.Send( p.Data(), p.Size() ); // prepends the 4-byte length frame +``` + +## Security: the length prefix is attacker-controlled + +A hostile peer can announce a 2 GB frame. The deframer therefore **caps** the +frame size (default 64 KiB; configurable per socket/deframer) and refuses to +buffer beyond it — `Consume()` returns `false` and the server drops that +connection. This is the same bounded-size discipline as the blob-size fixes +(audit #1/#4), and the deframer is continuously fuzzed (`fuzz/fuzz_deframe.cpp`, +wired into ClusterFuzzLite). + +```cpp +osctap::TcpListeningReceiveSocket server( endpoint, &listener, /*maxFrameSize=*/ 4096 ); +``` + +As always, parsing the reassembled packet still validates the OSC structure +itself (and throws on malformed input, or — on a no-exceptions build — gate it +with `TryValidatePacket()`; see the embedded guide). + +## Realtime note + +Reassembly buffering happens on the network thread, which is **off** the realtime +contract by design — consistent with OscTap's split between validation (may +allocate/throw, off the audio thread) and the throw-free `*Unchecked` read path. +Parsing a reassembled packet is the same allocation-free RT read path as for UDP. + +## Status / caveats + +- **POSIX is the runtime-verified backend** (`tests/OscTcpTest.cpp` exercises a + real loopback client+server, including a message that spans TCP segments, and is + clean under ASan/UBSan and TSan). +- **Windows** (`ip/win32/TcpSocket.h`) mirrors it on Winsock and is **runtime-tested** + via the `win32-wine` CI job (MinGW cross-compile + Wine), in addition to the + compile/link checks on the windows-latest legs. +- Deferred to a future version: SLIP framing, TLS, WebSocket transport, and an + `epoll`/`poll` loop for very high connection counts (`select()`/`FD_SETSIZE` is + fine for a handful of connections). + +## Try it (runnable demos) + +A server/monitor and a CLI client (`OSCTAP_BUILD_DEMOS`, POSIX), counterparts to +the UDP `pi5_hub` / `osc_send`: + +```sh +cmake -S . -B build -DOSCTAP_BUILD_DEMOS=ON +cmake --build build --target tcp_server tcp_send + +./build/tcp_server 9000 & # prints each received message +./build/tcp_send 127.0.0.1 9000 /fader/1 f:0.75 +./build/tcp_send 127.0.0.1 9000 /chat s:hello T +``` + +## See also + +- [`../osctap/osc/OscStreamFraming.h`](../osctap/osc/OscStreamFraming.h) — the framing codec. +- [`../osctap/ip/TcpSocket.h`](../osctap/ip/TcpSocket.h) — the public TCP types. +- [`../ROADMAP.md`](../ROADMAP.md) — where this fits in Phase 2. diff --git a/docs/STATUS.md b/docs/STATUS.md index 37581da..950176f 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -10,11 +10,15 @@ the original conversation. For the full plan and rationale see See the scorecard in `ROADMAP.md`. - **Phase 1 is complete** (directory rename + shim, ClusterFuzzLite, `bit_cast`/ `constexpr` parsing, warnings-as-errors, RTSan, TSan). -- **Phase 2 ("Reach") prep has started**: the freestanding/embedded profile - groundwork is landed — the `OSCTAP_THROW` seam (`osc/OscConfig.h`), the - `OSCTAP_FREESTANDING` CMake option + `freestanding` CI job, and the Raspberry Pi - Pico 2W guide ([`EMBEDDED_PICO2W.md`](EMBEDDED_PICO2W.md)). The remaining Reach - items (QEMU, Android NDK, multicast) stay demand-gated. +- **Phase 2 ("Reach") is underway**: landed so far — + - the freestanding/embedded profile (the `OSCTAP_THROW` seam in `osc/OscConfig.h`, + `OSCTAP_FREESTANDING` + the `freestanding` CI job) and the Pico 2W guide + ([`EMBEDDED_PICO2W.md`](EMBEDDED_PICO2W.md)); + - **aarch64 / Raspberry Pi 5 CI** under `qemu-user` (the `aarch64-qemu` job); + - the **Pi 5 ⇄ Pico 2W ⇄ Android integration**: runnable Pi 5 demos + (`demos/`, `OSCTAP_BUILD_DEMOS`), an Android JNI bridge (`android/`), and the + tutorial ([`INTEGRATION_PI5_PICO_ANDROID.md`](INTEGRATION_PI5_PICO_ANDROID.md)). + Remaining Reach items (multicast, armv7, a full Android sample app) stay demand-gated. - All six audit findings are fixed with regression tests; see commit history and `tests/OscUnitTests.cpp` (`test4`/`test5`). - **CI is the source of truth for build health.** `.github/workflows/ci.yml` builds and @@ -100,12 +104,95 @@ cmake --build build-fs --target OscFreestandingTest && ./build-fs/OscFreestandin or it will break the `freestanding` job. The freestanding flags are **PRIVATE to the `OscFreestandingTest` target**, so the exception-based tests are unaffected. Embedded posture and the Pico 2W recipe live in `docs/EMBEDDED_PICO2W.md`. -- **Untrusted input on a no-exceptions build is fatal.** The parser validates by - throwing; with exceptions off there is nothing to catch, so a malformed packet - hits the fatal handler (a remote reset/DoS). Safe only on a trusted link; open - networks should keep exceptions on and `catch` the `Malformed*Exception` types. A - non-throwing `TryInit`/validate is the tracked Phase 2 follow-up. -- **Include guards are still named `INCLUDED_OSCPACK_*`** — cosmetic, left as-is. +- **Untrusted input on a no-exceptions build: gate it with `TryValidatePacket()`.** + The parser validates by throwing, and with exceptions off `OSCTAP_THROW` aborts — + so on a freestanding/no-exceptions build, call `osctap::TryValidatePacket(data, size)` + (returns `nullptr` when fully well-formed, else a static error string) before + constructing/reading; it recurses through bundles with a nesting bound and never + throws/allocates. `ReceivedMessage::TryInit`/`ReceivedBundle::TryInit` are the + per-element non-throwing parses. **These are the single source of truth** — the + throwing `Init()`/constructors delegate to them (`Init` = `TryInit` + `OSCTAP_THROW` + on the returned error), so **don't fork the validation logic**: edit `TryInit` and + both paths stay in lock-step. `OscValidateTest` is the differential guard (gate vs. + throwing path) and must stay green. Note `ReceivedMessage::size_` is no longer + `const` (TryInit assigns it) and both classes gained a default ctor for the + default-construct-then-`TryInit` pattern. +- **A runtime `const char*` now serializes as an OSC string**, via a dedicated + `OutboundPacketStream::operator<<(const char*)`. Before, a `const char*` bound to + `operator<<(bool)` (standard pointer→bool conversion outranks the user-defined + `string_view` conversion) and was silently sent as a boolean — only string + *literals* (the `const char(&)[N]` overload) worked. The fix is freestanding-safe + (forwards to `string_view`); `OscFreestandingTest` sends a runtime `const char*` + to guard it. **Don't remove it**, and don't assume `<< somePtr` ever meant bool. +- **The `ip/` networking layer now has compiled coverage on both platforms.** POSIX + via the `demos/` (`OSCTAP_BUILD_DEMOS`) and the `aarch64-qemu` CI job; **win32** via + `tests/Win32SocketSmoke.cpp` (a WIN32-gated target the existing windows-latest legs + build — it instantiates + links the win32 `UdpSocket`/multiplexer/`getaddrinfo` + paths but guards the socket calls off the runtime path, so CI needs no live + network). The win32 backend is now **`/W4`-clean and built under `/WX`** like + everything else — the deferred cleanup was forced once the examples/smoke pulled it + into the compiled surface (fixed C4505 by making `CompareScheduledTimerCalls` `inline`, + and a C4456 `currentTimeMs` shadow). NB: a per-target `/WX-` is **ineffective** here — + CMake appends a linked INTERFACE's options *after* the target's own, so the INTERFACE + `/WX` always wins; don't try to suppress warnings that way, fix them. Approximate MSVC + `/W4` locally with MinGW `-Wall -Wextra -Wshadow -Werror`. (Open: the `timeGetTime` + 40-day `FIXME`; the Phase 1 `strcpy`/`gethostbyname` deferral is fully resolved.) +- **`OSCTAP_BUILD_DEMOS` and the `aarch64-qemu` job**: demos are POSIX-only + (`ip/posix` + a SIGINT handler) and gated `NOT WIN32`. The aarch64 job runs the + suite under `qemu-user` but **excludes `OscConcurrencyTest`** (emulated threads + + loopback sockets are flaky); keep new socket/thread tests off the emulated run or + they'll flake CI. +- **Android lives in `android/`** (`osctap_jni.cpp` bridge, NDK `CMakeLists.txt`, + `OscTap.kt`). The bridge is compile-checked against `jni.h` in CI-adjacent dev but + there is **no NDK build in CI** yet — changes there aren't gated, so keep the + bridge's OscTap API usage in sync by hand (or add an NDK job when one's warranted). +- **OSC over TCP lives in `osc/OscStreamFraming.h` + `ip/TcpSocket.h`** (issue #14). + The framing codec is transport-agnostic and the **single place** length-prefix + framing is defined — the sockets only call it; don't reimplement the length + handling in the socket layer. The deframer **caps the frame size** (default 64 KiB) + so a hostile prefix can't exhaust memory; keep that guard and the `fuzz_deframe` + coverage. Unlike the UDP facade (template-over-`Implementation`), the TCP types are + **concrete per-platform classes** (`posix::`/`win32::`) selected by `using` in the + facade. **POSIX is runtime-tested** (`OscTcpTest`, POSIX-only like + `OscConcurrencyTest`, also under TSan). **win32 is now runtime-tested too** via the + `win32-wine` CI job: it cross-compiles `OscUdpTest`/`OscTcpTest` with MinGW and runs + them under Wine (both pass), so the win32 UDP **and** TCP backends have real runtime + coverage, not just compile/link. The win32 break uses a self-connected loopback UDP + socket (no `pipe()` on Windows). NB the TCP backend headers must be **self-contained** + (they `#include ` explicitly) — including `TcpSocket.h` + without `UdpSocket.h` first previously failed on win32; the Wine job guards that. +- **Socket loopback tests** (`OscUdpTest`, `OscTcpTest`, POSIX-only): real client+ + server asserting that messages arrive and decode. They **SKIP** (print a notice, + exit 0) if the environment denies loopback networking, so they don't false-fail on + restricted runners — but a real regression still fails them. Writing `OscUdpTest` + surfaced a latent bug: `UdpSocketImplementation::Bind()` never read the OS-assigned + port back, so `LocalPort()` returned 0 after a port-0 bind (a sender using it + targeted port 0 → nothing delivered; the old `OscConcurrencyTest` packet was + "best-effort, never asserted", which hid it). Fixed in both posix and win32 via + `getsockname()` after bind. **Don't drop that read-back.** +- **Docs map**: `docs/GETTING_STARTED.md` (OSC-101 over UDP) and `docs/API.md` + (public-surface reference) are the entry points, linked from `README.md`; feature + guides are `OSC_OVER_TCP.md`, `EMBEDDED_PICO2W.md`, `INTEGRATION_PI5_PICO_ANDROID.md`. + The original oscpack `README`/`CHANGES`/`TODO` were moved to `docs/legacy/` + (historical; don't treat as current). `API.md` is hand-maintained — update it when + the public surface changes. +- **Code coverage** is measured by the `coverage` CI job with **gcovr** (not lcov — + lcov 2.0 mis-merges header-only inline functions across the many test binaries, + producing >100%/0% artifacts). Baseline is **~85% lines / ~94% functions** over + `osctap/`; the job **fails under 80% lines**, so coverage regressions are caught. + Known-uncovered: `ip/posix/NetworkingUtils.h` `GetHostByName` (tests use numeric + IPs, not hostname resolution) and some error/EINTR branches in the socket loops. +- **`examples/` (SimpleSend/SimpleReceive/OscDump) are compile-checked, not run** + (they bind sockets / block on input). They use the `oscpack::` alias on purpose + (extra shim coverage). `demos/` are the modern, tested equivalents. The old + interactive `tests/OscSendTests`/`OscReceiveTest` (dead `osc::` namespace) were + deleted — superseded by `demos/` + the loopback tests. +- **Include guards are named `INCLUDED_OSCTAP_*`** (renamed from `INCLUDED_OSCPACK_*` + in Phase 2 cleanup). The `` *include paths* still work via the redirect + shim tree — only the internal guard macro names changed. +- **`osc/SmallString.h` is an intentionally-empty, guarded no-op** (audit #6 closed). + The outbound stream no longer includes it; it's kept only so the public path and its + `` shim keep resolving. Don't "fill it in" without a real need. - The test harness (`NewMessageBuffer`/`AllocateAligned4`) **intentionally leaks** its aligned scratch buffers, which is why the ASan job runs with `ASAN_OPTIONS=detect_leaks=0`. (Cleaning this up is a fine low-priority task.) diff --git a/docs/legacy/README.md b/docs/legacy/README.md new file mode 100644 index 0000000..b26f5ca --- /dev/null +++ b/docs/legacy/README.md @@ -0,0 +1,15 @@ +# Legacy oscpack files + +These are the original **oscpack** project files, preserved verbatim for heritage +and reference. They predate the OscTap rebrand and the current build/CI, so they +are **historical, not current** — for up-to-date information use the top-level +[`README.md`](../../README.md), [`ROADMAP.md`](../../ROADMAP.md), and the guides in +[`docs/`](../). + +| File | What it was | +|------|-------------| +| [`oscpack-README.txt`](oscpack-README.txt) | oscpack's original README / build notes (Makefile-era) | +| [`oscpack-CHANGES.txt`](oscpack-CHANGES.txt) | oscpack's changelog, last entry April 2013 | +| [`oscpack-TODO.txt`](oscpack-TODO.txt) | oscpack's TODO list | + +For OscTap's lineage and credits see [`docs/HERITAGE.md`](../HERITAGE.md). diff --git a/CHANGES b/docs/legacy/oscpack-CHANGES.txt similarity index 100% rename from CHANGES rename to docs/legacy/oscpack-CHANGES.txt diff --git a/README b/docs/legacy/oscpack-README.txt similarity index 100% rename from README rename to docs/legacy/oscpack-README.txt diff --git a/TODO b/docs/legacy/oscpack-TODO.txt similarity index 100% rename from TODO rename to docs/legacy/oscpack-TODO.txt diff --git a/examples/SimpleReceive.cpp b/examples/SimpleReceive.cpp index 07822fa..d6df5d3 100644 --- a/examples/SimpleReceive.cpp +++ b/examples/SimpleReceive.cpp @@ -1,86 +1,81 @@ -/* - Example of two different ways to process received OSC messages using oscpack. - Receives the messages from the SimpleSend.cpp example. +/* + Example of two ways to process received OSC messages using OscTap. + Receives the messages sent by SimpleSend.cpp. + + Canonical oscpack example, kept compiling against the current API (uses the + deprecated `oscpack` alias on purpose). For a routing/monitoring receiver see + demos/pi5_hub.cpp and demos/tcp_server.cpp. */ -#include +#include #include - -#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug -namespace std { -using ::__strcmp__; // avoid error: E2316 '__strcmp__' is not a member of 'std'. -} -#endif +#include #include "osc/OscReceivedElements.h" #include "osc/OscPacketListener.h" #include "ip/UdpSocket.h" +using namespace oscpack; // OscTap's deprecated oscpack:: alias, exercised here #define PORT 7000 -class ExamplePacketListener : public osc::OscPacketListener { +class ExamplePacketListener : public OscPacketListener { protected: - - virtual void ProcessMessage( const osc::ReceivedMessage& m, - const IpEndpointName& remoteEndpoint ) + void ProcessMessage( const ReceivedMessage& m, + const IpEndpointName& remoteEndpoint ) override { - (void) remoteEndpoint; // suppress unused parameter warning + (void) remoteEndpoint; try{ - // example of parsing single messages. osc::OsckPacketListener - // handles the bundle traversal. - + // OscPacketListener handles bundle traversal; we just read messages. if( std::strcmp( m.AddressPattern(), "/test1" ) == 0 ){ - // example #1 -- argument stream interface - osc::ReceivedMessageArgumentStream args = m.ArgumentStream(); - bool a1; - osc::int32_t a2; - float a3; - const char *a4; - args >> a1 >> a2 >> a3 >> a4 >> osc::EndMessage; - + // example #1 -- argument-stream interface + ReceivedMessageArgumentStream args = m.ArgumentStream(); + bool a1; int32_t a2; float a3; const char *a4; + MessageTerminator end; + args >> a1 >> a2 >> a3 >> a4 >> end; + std::cout << "received '/test1' message with arguments: " << a1 << " " << a2 << " " << a3 << " " << a4 << "\n"; - + }else if( std::strcmp( m.AddressPattern(), "/test2" ) == 0 ){ - // example #2 -- argument iterator interface, supports - // reflection for overloaded messages (eg you can call - // (*arg)->IsBool() to check if a bool was passed etc). - osc::ReceivedMessage::const_iterator arg = m.ArgumentsBegin(); + // example #2 -- argument-iterator interface (supports reflection, + // e.g. arg->IsBool() to check the type of an overloaded argument) + ReceivedMessage::const_iterator arg = m.ArgumentsBegin(); bool a1 = (arg++)->AsBool(); int a2 = (arg++)->AsInt32(); float a3 = (arg++)->AsFloat(); const char *a4 = (arg++)->AsString(); if( arg != m.ArgumentsEnd() ) - throw osc::ExcessArgumentException(); - + throw ExcessArgumentException(); + std::cout << "received '/test2' message with arguments: " << a1 << " " << a2 << " " << a3 << " " << a4 << "\n"; } - }catch( osc::Exception& e ){ - // any parsing errors such as unexpected argument types, or - // missing arguments get thrown as exceptions. + }catch( Exception& e ){ + // parsing errors (wrong/missing argument types) are thrown std::cout << "error while parsing message: " << m.AddressPattern() << ": " << e.what() << "\n"; } } }; +namespace { UdpListeningReceiveSocket* gSocket = nullptr; + void HandleSigInt( int ){ if( gSocket ) gSocket->AsynchronousBreak(); } } + int main(int argc, char* argv[]) { - (void) argc; // suppress unused parameter warnings - (void) argv; // suppress unused parameter warnings + (void) argc; (void) argv; ExamplePacketListener listener; UdpListeningReceiveSocket s( - IpEndpointName( IpEndpointName::ANY_ADDRESS, PORT ), - &listener ); + IpEndpointName( IpEndpointName::ANY_ADDRESS, PORT ), &listener ); - std::cout << "press ctrl-c to end\n"; + gSocket = &s; + std::signal( SIGINT, HandleSigInt ); - s.RunUntilSigInt(); + std::cout << "press ctrl-c to end\n"; + s.Run(); return 0; } - diff --git a/examples/SimpleSend.cpp b/examples/SimpleSend.cpp index 0049fcb..2215c35 100644 --- a/examples/SimpleSend.cpp +++ b/examples/SimpleSend.cpp @@ -1,10 +1,15 @@ -/* - Simple example of sending an OSC message using oscpack. +/* + Simple example of sending an OSC message bundle using OscTap. + + Canonical oscpack example, kept compiling against the current API (and using + the deprecated `oscpack` alias on purpose, to exercise the migration shim). + For a typed command-line sender see demos/osc_send.cpp. */ #include "osc/OscOutboundPacketStream.h" #include "ip/UdpSocket.h" +using namespace oscpack; // OscTap's deprecated oscpack:: alias, exercised here #define ADDRESS "127.0.0.1" #define PORT 7000 @@ -14,20 +19,21 @@ int main(int argc, char* argv[]) { (void) argc; // suppress unused parameter warnings - (void) argv; // suppress unused parameter warnings + (void) argv; UdpTransmitSocket transmitSocket( IpEndpointName( ADDRESS, PORT ) ); - + char buffer[OUTPUT_BUFFER_SIZE]; - osc::OutboundPacketStream p( buffer, OUTPUT_BUFFER_SIZE ); - - p << osc::BeginBundleImmediate - << osc::BeginMessage( "/test1" ) - << true << 23 << (float)3.1415 << "hello" << osc::EndMessage - << osc::BeginMessage( "/test2" ) - << true << 24 << (float)10.8 << "world" << osc::EndMessage - << osc::EndBundle; - + OutboundPacketStream p( buffer, OUTPUT_BUFFER_SIZE ); + + p << BeginBundleImmediate() + << BeginMessage( "/test1" ) + << true << (int32_t)23 << (float)3.1415f << "hello" << EndMessage() + << BeginMessage( "/test2" ) + << true << (int32_t)24 << (float)10.8f << "world" << EndMessage() + << EndBundle(); + transmitSocket.Send( p.Data(), p.Size() ); -} + return 0; +} diff --git a/fuzz/corpus_deframe/bundle.osc.framed b/fuzz/corpus_deframe/bundle.osc.framed new file mode 100644 index 0000000..02782ef Binary files /dev/null and b/fuzz/corpus_deframe/bundle.osc.framed differ diff --git a/fuzz/corpus_deframe/msg_blob_string.osc.framed b/fuzz/corpus_deframe/msg_blob_string.osc.framed new file mode 100644 index 0000000..4b17bb2 Binary files /dev/null and b/fuzz/corpus_deframe/msg_blob_string.osc.framed differ diff --git a/fuzz/corpus_deframe/msg_simple.osc.framed b/fuzz/corpus_deframe/msg_simple.osc.framed new file mode 100644 index 0000000..aa35bc6 Binary files /dev/null and b/fuzz/corpus_deframe/msg_simple.osc.framed differ diff --git a/fuzz/corpus_deframe/msg_types_array.osc.framed b/fuzz/corpus_deframe/msg_types_array.osc.framed new file mode 100644 index 0000000..19c8c92 Binary files /dev/null and b/fuzz/corpus_deframe/msg_types_array.osc.framed differ diff --git a/fuzz/corpus_deframe/multi.framed b/fuzz/corpus_deframe/multi.framed new file mode 100644 index 0000000..7e7bfc3 Binary files /dev/null and b/fuzz/corpus_deframe/multi.framed differ diff --git a/fuzz/fuzz_deframe.cpp b/fuzz/fuzz_deframe.cpp new file mode 100644 index 0000000..e7515d0 --- /dev/null +++ b/fuzz/fuzz_deframe.cpp @@ -0,0 +1,75 @@ +/* + oscpack / OscTap -- libFuzzer entry point for the OSC-over-TCP receive path. + + TCP delivers a byte stream with no message boundaries, so OscStreamDeframer + reassembles complete packets from arbitrarily chunked reads (and caps the frame + size so a hostile length prefix can't make it buffer unbounded data). This is + attacker-controlled input, so it is fuzzed: the harness feeds the input through + the deframer in pseudo-random chunks (exercising reassembly across every read + boundary) and runs each reassembled frame through the OSC parser, exactly as the + TcpListeningReceiveSocket loop does. Any out-of-bounds read surfaces under ASan. + + Build with real libFuzzer (preferred): + clang++ -std=c++17 -g -O1 -I oscpack \ + -fsanitize=fuzzer,address,undefined fuzz/fuzz_deframe.cpp -o fuzz_deframe + ./fuzz_deframe fuzz/corpus_deframe + + Or link the standalone driver in fuzz/standalone_main.cpp where the libFuzzer + runtime is unavailable (see fuzz/README.md). +*/ +#include +#include +#include +#include + +#include "osc/OscStreamFraming.h" +#include "osc/OscReceivedElements.h" + +using namespace oscpack; + +// Run one reassembled frame through the parser, exactly as a consumer would. +static void HandleFrame( const char* data, uint32_t size ) +{ + if( size == 0 ) + return; + // Exactly-sized heap copy: ASan redzones flag any read past the frame length. + std::vector buffer( data, data + size ); + try{ + ReceivedPacket p( buffer.data(), (osc_bundle_element_size_t)size ); + if( p.IsBundle() ){ + ReceivedBundle b( p ); + for( auto it = b.ElementsBegin(); it != b.ElementsEnd(); ++it ) + if( !it->IsBundle() ){ ReceivedMessage m( *it ); (void)m.ArgumentCount(); } + }else{ + ReceivedMessage m( p ); + for( auto it = m.ArgumentsBegin(); it != m.ArgumentsEnd(); ++it ) + (void)it->TypeTag(); + } + }catch( const oscpack::Exception& ){ + // Expected: malformed frame rejected by the parser. + }catch( const std::exception& ){ + // Tolerate std exceptions (e.g. bad_alloc) -- not a memory-safety finding. + } +} + +extern "C" int LLVMFuzzerTestOneInput( const uint8_t* data, size_t size ) +{ + // A small cap so the fuzzer reaches the oversized-frame rejection path quickly. + osctap::OscStreamDeframer deframer( 4096 ); + + // Feed the byte stream in pseudo-random 1..8-byte chunks (derived from the + // data itself) so reassembly across arbitrary read boundaries is exercised. + size_t i = 0; + while( i < size ){ + size_t chunk = ( (size_t)data[i] & 0x7 ) + 1; // 1..8 + if( i + chunk > size ) + chunk = size - i; + const bool ok = deframer.Consume( + reinterpret_cast( data ) + i, chunk, + []( const char* p, uint32_t n ){ HandleFrame( p, n ); } ); + if( !ok ) + break; // oversized frame: the real loop drops the connection here + i += chunk; + } + return 0; +} diff --git a/oscpack/ip/TcpSocket.h b/oscpack/ip/TcpSocket.h new file mode 100644 index 0000000..8ab1e47 --- /dev/null +++ b/oscpack/ip/TcpSocket.h @@ -0,0 +1,10 @@ +/* + OscTap compatibility shim. + + Redirects the old include path to the new + . (TCP support is new in OscTap; the shim exists only + so the deprecated prefix keeps working uniformly across the library.) + + Deprecated: prefer . See ROADMAP.md / docs/STATUS.md. +*/ +#include diff --git a/oscpack/osc/OscStreamFraming.h b/oscpack/osc/OscStreamFraming.h new file mode 100644 index 0000000..3615953 --- /dev/null +++ b/oscpack/osc/OscStreamFraming.h @@ -0,0 +1,11 @@ +/* + OscTap compatibility shim. + + The library directory and public include prefix moved from + to (the namespace likewise moved oscpack -> osctap, kept as a + deprecated alias). This header redirects the old include path to the new one + so existing consumers keep compiling unchanged. + + Deprecated: prefer . See ROADMAP.md / docs/STATUS.md. +*/ +#include diff --git a/osctap/ip/AbstractUdpSocket.h b/osctap/ip/AbstractUdpSocket.h index fbf5d84..0853f5d 100644 --- a/osctap/ip/AbstractUdpSocket.h +++ b/osctap/ip/AbstractUdpSocket.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_UDPSOCKET_H -#define INCLUDED_OSCPACK_UDPSOCKET_H +#ifndef INCLUDED_OSCTAP_UDPSOCKET_H +#define INCLUDED_OSCTAP_UDPSOCKET_H #include // size_t @@ -240,4 +240,4 @@ class UdpListeningReceiveSocket : public UdpSocket{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_UDPSOCKET_H */ +#endif /* INCLUDED_OSCTAP_UDPSOCKET_H */ diff --git a/osctap/ip/IpEndpointName.h b/osctap/ip/IpEndpointName.h index c30830f..7e1c22f 100644 --- a/osctap/ip/IpEndpointName.h +++ b/osctap/ip/IpEndpointName.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_IPENDPOINTNAME_H -#define INCLUDED_OSCPACK_IPENDPOINTNAME_H +#ifndef INCLUDED_OSCTAP_IPENDPOINTNAME_H +#define INCLUDED_OSCTAP_IPENDPOINTNAME_H #include @@ -132,4 +132,4 @@ inline bool operator!=( const IpEndpointName& lhs, const IpEndpointName& rhs ) // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_IPENDPOINTNAME_H */ +#endif /* INCLUDED_OSCTAP_IPENDPOINTNAME_H */ diff --git a/osctap/ip/PacketListener.h b/osctap/ip/PacketListener.h index b645b23..192cc7d 100644 --- a/osctap/ip/PacketListener.h +++ b/osctap/ip/PacketListener.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_PACKETLISTENER_H -#define INCLUDED_OSCPACK_PACKETLISTENER_H +#ifndef INCLUDED_OSCTAP_PACKETLISTENER_H +#define INCLUDED_OSCTAP_PACKETLISTENER_H namespace osctap { @@ -53,4 +53,4 @@ class PacketListener{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_PACKETLISTENER_H */ +#endif /* INCLUDED_OSCTAP_PACKETLISTENER_H */ diff --git a/osctap/ip/TcpSocket.h b/osctap/ip/TcpSocket.h new file mode 100644 index 0000000..9de5d66 --- /dev/null +++ b/osctap/ip/TcpSocket.h @@ -0,0 +1,36 @@ +#ifndef INCLUDED_OSCTAP_TCPSOCKET_H +#define INCLUDED_OSCTAP_TCPSOCKET_H + +// Public OSC-over-TCP socket types. Length-prefix framing (see +// osc/OscStreamFraming.h); the platform backends mirror the UDP ones. +// +// TcpTransmitSocket -- client: connect + Send(packet, size) +// TcpListeningReceiveSocket -- server: accept N clients, dispatch each +// complete packet to a PacketListener +// +// Unlike the UDP facade (which templates one class over a per-platform +// Implementation), the TCP types are concrete per-platform classes selected here +// by `using`. v1 scope: length-prefix framing, single-threaded multi-connection +// server, TCP_NODELAY, frame-size cap. SLIP/TLS/WebSocket/epoll are deferred. + +#if defined(_WIN32) +#include "win32/TcpSocket.h" +#else +#include "posix/TcpSocket.h" +#endif + +namespace osctap +{ +#if defined(_WIN32) +using TcpTransmitSocket = win32::TcpTransmitSocket; +using TcpListeningReceiveSocket = win32::TcpListeningReceiveSocket; +#else +using TcpTransmitSocket = posix::TcpTransmitSocket; +using TcpListeningReceiveSocket = posix::TcpListeningReceiveSocket; +#endif +} + +// Backwards-compatibility alias: this library was formerly named oscpack. +namespace oscpack = osctap; + +#endif /* INCLUDED_OSCTAP_TCPSOCKET_H */ diff --git a/osctap/ip/TimerListener.h b/osctap/ip/TimerListener.h index f875f08..e594b26 100644 --- a/osctap/ip/TimerListener.h +++ b/osctap/ip/TimerListener.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_TIMERLISTENER_H -#define INCLUDED_OSCPACK_TIMERLISTENER_H +#ifndef INCLUDED_OSCTAP_TIMERLISTENER_H +#define INCLUDED_OSCTAP_TIMERLISTENER_H namespace osctap { @@ -50,4 +50,4 @@ class TimerListener{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_TIMERLISTENER_H */ +#endif /* INCLUDED_OSCTAP_TIMERLISTENER_H */ diff --git a/osctap/ip/posix/TcpSocket.h b/osctap/ip/posix/TcpSocket.h new file mode 100644 index 0000000..cf6ecdf --- /dev/null +++ b/osctap/ip/posix/TcpSocket.h @@ -0,0 +1,299 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef INCLUDED_OSCTAP_POSIX_TCPSOCKET_H +#define INCLUDED_OSCTAP_POSIX_TCPSOCKET_H + +// Reuse the posix socket includes and the SockaddrFromIpEndpointName / +// IpEndpointNameFromSockaddr helpers defined in the UDP backend. +#include // complete type before the helpers below use it +#include +#include +#include + +#include // TCP_NODELAY +#include // errno (don't rely on transitive includes) +#include +#include + +namespace osctap +{ +namespace posix +{ + +// --------------------------------------------------------------------------- +// TcpTransmitSocket -- connect to a remote OSC-over-TCP server and send packets. +// +// Each Send() writes one length-prefixed frame (4-byte big-endian count + +// payload), looping over partial writes (a TCP send() may transfer fewer bytes +// than requested). TCP_NODELAY is enabled (Nagle off) -- OSC over TCP without it +// is a classic latency footgun. +// --------------------------------------------------------------------------- +class TcpTransmitSocket +{ +public: + explicit TcpTransmitSocket( const IpEndpointName& remoteEndpoint ) + { + socket_ = ::socket( AF_INET, SOCK_STREAM, 0 ); + if( socket_ == -1 ) + throw std::runtime_error( "unable to create tcp socket\n" ); + +#ifdef SO_NOSIGPIPE + int noSigpipe = 1; // macOS / BSD: suppress SIGPIPE on send to a closed peer + setsockopt( socket_, SOL_SOCKET, SO_NOSIGPIPE, &noSigpipe, sizeof(noSigpipe) ); +#endif + int one = 1; + setsockopt( socket_, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one) ); + + struct sockaddr_in addr; + SockaddrFromIpEndpointName( addr, remoteEndpoint ); + if( ::connect( socket_, (struct sockaddr*)&addr, sizeof(addr) ) < 0 ){ + ::close( socket_ ); + socket_ = -1; + throw std::runtime_error( "unable to connect tcp socket\n" ); + } + } + + ~TcpTransmitSocket() { if( socket_ != -1 ) ::close( socket_ ); } + + TcpTransmitSocket( const TcpTransmitSocket& ) = delete; + TcpTransmitSocket& operator=( const TcpTransmitSocket& ) = delete; + + // Send one complete OSC packet, length-prefixed. Blocks until fully written. + void Send( const char* data, std::size_t size ) + { + char header[OSC_STREAM_FRAME_HEADER_SIZE]; + WriteOscStreamFrameHeader( header, (uint32_t)size ); + SendAll( header, OSC_STREAM_FRAME_HEADER_SIZE ); + SendAll( data, size ); + } + + int Socket() const { return socket_; } + +private: + void SendAll( const char* p, std::size_t n ) + { +#ifdef MSG_NOSIGNAL + const int flags = MSG_NOSIGNAL; // Linux: don't raise SIGPIPE +#else + const int flags = 0; +#endif + std::size_t sent = 0; + while( sent < n ){ + ssize_t r = ::send( socket_, p + sent, n - sent, flags ); + if( r < 0 ){ + if( errno == EINTR ) continue; + throw std::runtime_error( "tcp send failed\n" ); + } + sent += (std::size_t)r; + } + } + + int socket_ = -1; +}; + + +// --------------------------------------------------------------------------- +// TcpListeningReceiveSocket -- listen for OSC-over-TCP clients and dispatch each +// complete packet to a PacketListener. +// +// Single-threaded, select()-based, and connection-aware: it accept()s any number +// of clients and keeps a per-connection OscStreamDeframer (each connection's byte +// stream reassembles independently). Run() blocks; Break()/AsynchronousBreak() +// stop it (the latter via a self-pipe, so it works from another thread or a +// signal handler -- mirroring the UDP multiplexer). +// --------------------------------------------------------------------------- +class TcpListeningReceiveSocket +{ + struct Connection + { + IpEndpointName peer; + OscStreamDeframer deframer; + Connection( const IpEndpointName& p, uint32_t maxFrame ) + : peer( p ), deframer( maxFrame ) {} + }; + +public: + TcpListeningReceiveSocket( const IpEndpointName& localEndpoint, PacketListener* listener, + uint32_t maxFrameSize = OSC_DEFAULT_MAX_FRAME_SIZE ) + : listener_( listener ), maxFrameSize_( maxFrameSize ) + { + if( pipe( breakPipe_ ) != 0 ) + throw std::runtime_error( "creation of asynchronous break pipes failed\n" ); + + listenSocket_ = ::socket( AF_INET, SOCK_STREAM, 0 ); + if( listenSocket_ == -1 ){ + close( breakPipe_[0] ); close( breakPipe_[1] ); + throw std::runtime_error( "unable to create tcp socket\n" ); + } + + int reuse = 1; + setsockopt( listenSocket_, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse) ); + + struct sockaddr_in addr; + SockaddrFromIpEndpointName( addr, localEndpoint ); + if( ::bind( listenSocket_, (struct sockaddr*)&addr, sizeof(addr) ) < 0 ){ + Cleanup(); + throw std::runtime_error( "unable to bind tcp socket\n" ); + } + if( ::listen( listenSocket_, SOMAXCONN ) < 0 ){ + Cleanup(); + throw std::runtime_error( "unable to listen on tcp socket\n" ); + } + } + + ~TcpListeningReceiveSocket() { Cleanup(); } + + TcpListeningReceiveSocket( const TcpListeningReceiveSocket& ) = delete; + TcpListeningReceiveSocket& operator=( const TcpListeningReceiveSocket& ) = delete; + + // The bound local endpoint (resolves the OS-assigned port when bound to 0). + IpEndpointName LocalEndpointFor( const IpEndpointName& requested ) const + { + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + if( getsockname( listenSocket_, (struct sockaddr*)&addr, &len ) == 0 ) + return IpEndpointName( requested.address, ntohs( addr.sin_port ) ); + return requested; + } + + void Run() + { + break_ = false; + char buf[4096]; + + while( !break_ ){ + fd_set readfds; + FD_ZERO( &readfds ); + FD_SET( listenSocket_, &readfds ); + FD_SET( breakPipe_[0], &readfds ); + int fdmax = listenSocket_ > breakPipe_[0] ? listenSocket_ : breakPipe_[0]; + for( const auto& kv : connections_ ){ + FD_SET( kv.first, &readfds ); + if( kv.first > fdmax ) fdmax = kv.first; + } + + if( select( fdmax + 1, &readfds, 0, 0, 0 ) < 0 ){ + if( break_ ) break; + if( errno == EINTR ) continue; + throw std::runtime_error( "select failed\n" ); + } + + if( FD_ISSET( breakPipe_[0], &readfds ) ){ + char c; ssize_t r = read( breakPipe_[0], &c, 1 ); (void)r; + } + if( break_ ) break; + + if( FD_ISSET( listenSocket_, &readfds ) ) + AcceptConnection(); + + // Collect ready connection fds first; processing may erase entries. + std::vector ready; + for( const auto& kv : connections_ ) + if( FD_ISSET( kv.first, &readfds ) ) ready.push_back( kv.first ); + + for( int fd : ready ){ + ServiceConnection( fd, buf, sizeof(buf) ); + if( break_ ) break; + } + } + } + + void Break() { break_ = true; } + + void AsynchronousBreak() + { + break_ = true; + ssize_t r = write( breakPipe_[1], "!", 1 ); (void)r; + } + + int Socket() const { return listenSocket_; } + +private: + void AcceptConnection() + { + struct sockaddr_in peerAddr; + socklen_t len = sizeof(peerAddr); + int conn = ::accept( listenSocket_, (struct sockaddr*)&peerAddr, &len ); + if( conn == -1 ) + return; + int one = 1; + setsockopt( conn, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one) ); + connections_.emplace( std::piecewise_construct, + std::forward_as_tuple( conn ), + std::forward_as_tuple( IpEndpointNameFromSockaddr( peerAddr ), maxFrameSize_ ) ); + } + + void ServiceConnection( int fd, char* buf, std::size_t bufSize ) + { + auto it = connections_.find( fd ); + if( it == connections_.end() ) + return; + + ssize_t n = ::recv( fd, buf, bufSize, 0 ); + if( n <= 0 ){ // 0 = peer closed; <0 = error -> drop the connection + CloseConnection( it ); + return; + } + + // Reassemble and dispatch every complete packet in this read. The sink + // runs synchronously, so `it` (and its peer) stays valid throughout. + const bool ok = it->second.deframer.Consume( buf, (std::size_t)n, + [&]( const char* packet, uint32_t size ){ + listener_->ProcessPacket( packet, (int)size, it->second.peer ); + } ); + + if( !ok ) // a frame exceeded maxFrameSize -> protocol violation + CloseConnection( it ); + } + + void CloseConnection( std::map::iterator it ) + { + ::close( it->first ); + connections_.erase( it ); + } + + void Cleanup() + { + for( auto& kv : connections_ ) + ::close( kv.first ); + connections_.clear(); + if( listenSocket_ != -1 ){ ::close( listenSocket_ ); listenSocket_ = -1; } + close( breakPipe_[0] ); + close( breakPipe_[1] ); + } + + int listenSocket_ = -1; + PacketListener* listener_; + uint32_t maxFrameSize_; + std::atomic_bool break_{ false }; + int breakPipe_[2]; + std::map connections_; +}; + +} // namespace posix +} // namespace osctap + +#endif /* INCLUDED_OSCTAP_POSIX_TCPSOCKET_H */ diff --git a/osctap/ip/posix/UdpSocket.h b/osctap/ip/posix/UdpSocket.h index 7f265d3..bd33a1d 100644 --- a/osctap/ip/posix/UdpSocket.h +++ b/osctap/ip/posix/UdpSocket.h @@ -234,6 +234,14 @@ class UdpSocketImplementation{ } isBound_ = true; + + // Read back the actual local port. When the caller binds to port 0 the OS + // assigns one; without this LocalPort() would still report 0 (so a sender + // using LocalPort() would target port 0 and nothing would be delivered). + struct sockaddr_in boundAddr; + socklen_t boundLen = sizeof(boundAddr); + if( getsockname( socket_, (struct sockaddr *)&boundAddr, &boundLen ) == 0 ) + localPort_ = ntohs( boundAddr.sin_port ); } bool IsBound() const { return isBound_; } diff --git a/osctap/ip/win32/TcpSocket.h b/osctap/ip/win32/TcpSocket.h new file mode 100644 index 0000000..1393333 --- /dev/null +++ b/osctap/ip/win32/TcpSocket.h @@ -0,0 +1,310 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef INCLUDED_OSCTAP_WIN32_TCPSOCKET_H +#define INCLUDED_OSCTAP_WIN32_TCPSOCKET_H + +// Reuse the win32 socket includes, NetworkInitializer (WSAStartup), and the +// SockaddrFromIpEndpointName / IpEndpointNameFromSockaddr helpers from the UDP +// backend. +#include // complete type before the helpers below use it +#include +#include +#include + +#include // TCP_NODELAY, IPPROTO_TCP +#include +#include + +namespace osctap +{ +namespace win32 +{ + +// Mirrors ip/posix/TcpSocket.h on Winsock. The connection-aware server uses +// select() (Winsock supports select() over SOCKETs) with a self-connected UDP +// "break" socket standing in for the posix self-pipe, so AsynchronousBreak() +// works from another thread / signal handler. +// +// NOTE: this backend is built by the windows-latest CI legs and cross-compiled + +// link-checked with MinGW, but -- unlike the posix backend -- it is not yet +// runtime-tested in CI (no Windows runner). The posix backend is the +// runtime-verified reference. + +class TcpTransmitSocket +{ +public: + explicit TcpTransmitSocket( const IpEndpointName& remoteEndpoint ) + { + NetworkInitializer::instance(); + + socket_ = ::socket( AF_INET, SOCK_STREAM, 0 ); + if( socket_ == INVALID_SOCKET ) + throw std::runtime_error( "unable to create tcp socket\n" ); + + int one = 1; + setsockopt( socket_, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one) ); + + struct sockaddr_in addr; + SockaddrFromIpEndpointName( addr, remoteEndpoint ); + if( ::connect( socket_, (struct sockaddr*)&addr, sizeof(addr) ) == SOCKET_ERROR ){ + closesocket( socket_ ); + socket_ = INVALID_SOCKET; + throw std::runtime_error( "unable to connect tcp socket\n" ); + } + } + + ~TcpTransmitSocket() { if( socket_ != INVALID_SOCKET ) closesocket( socket_ ); } + + TcpTransmitSocket( const TcpTransmitSocket& ) = delete; + TcpTransmitSocket& operator=( const TcpTransmitSocket& ) = delete; + + void Send( const char* data, std::size_t size ) + { + char header[OSC_STREAM_FRAME_HEADER_SIZE]; + WriteOscStreamFrameHeader( header, (uint32_t)size ); + SendAll( header, OSC_STREAM_FRAME_HEADER_SIZE ); + SendAll( data, size ); + } + + SOCKET Socket() const { return socket_; } + +private: + void SendAll( const char* p, std::size_t n ) + { + std::size_t sent = 0; + while( sent < n ){ + int r = ::send( socket_, p + sent, (int)( n - sent ), 0 ); + if( r == SOCKET_ERROR ) + throw std::runtime_error( "tcp send failed\n" ); + sent += (std::size_t)r; + } + } + + SOCKET socket_ = INVALID_SOCKET; +}; + + +class TcpListeningReceiveSocket +{ + struct Connection + { + IpEndpointName peer; + OscStreamDeframer deframer; + Connection( const IpEndpointName& p, uint32_t maxFrame ) + : peer( p ), deframer( maxFrame ) {} + }; + +public: + TcpListeningReceiveSocket( const IpEndpointName& localEndpoint, PacketListener* listener, + uint32_t maxFrameSize = OSC_DEFAULT_MAX_FRAME_SIZE ) + : listener_( listener ), maxFrameSize_( maxFrameSize ) + { + NetworkInitializer::instance(); + CreateBreakSocket(); + + listenSocket_ = ::socket( AF_INET, SOCK_STREAM, 0 ); + if( listenSocket_ == INVALID_SOCKET ){ + closesocket( breakSocket_ ); + throw std::runtime_error( "unable to create tcp socket\n" ); + } + + int reuse = 1; + setsockopt( listenSocket_, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse) ); + + struct sockaddr_in addr; + SockaddrFromIpEndpointName( addr, localEndpoint ); + if( ::bind( listenSocket_, (struct sockaddr*)&addr, sizeof(addr) ) == SOCKET_ERROR ){ + Cleanup(); + throw std::runtime_error( "unable to bind tcp socket\n" ); + } + if( ::listen( listenSocket_, SOMAXCONN ) == SOCKET_ERROR ){ + Cleanup(); + throw std::runtime_error( "unable to listen on tcp socket\n" ); + } + } + + ~TcpListeningReceiveSocket() { Cleanup(); } + + TcpListeningReceiveSocket( const TcpListeningReceiveSocket& ) = delete; + TcpListeningReceiveSocket& operator=( const TcpListeningReceiveSocket& ) = delete; + + IpEndpointName LocalEndpointFor( const IpEndpointName& requested ) const + { + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + if( getsockname( listenSocket_, (struct sockaddr*)&addr, &len ) == 0 ) + return IpEndpointName( requested.address, ntohs( addr.sin_port ) ); + return requested; + } + + void Run() + { + break_ = false; + char buf[4096]; + + while( !break_ ){ + fd_set readfds; + FD_ZERO( &readfds ); + FD_SET( listenSocket_, &readfds ); + FD_SET( breakSocket_, &readfds ); + for( const auto& kv : connections_ ) + FD_SET( kv.first, &readfds ); + + if( select( 0, &readfds, 0, 0, 0 ) == SOCKET_ERROR ){ + if( break_ ) break; + throw std::runtime_error( "select failed\n" ); + } + + if( FD_ISSET( breakSocket_, &readfds ) ){ + char c; recv( breakSocket_, &c, 1, 0 ); + } + if( break_ ) break; + + if( FD_ISSET( listenSocket_, &readfds ) ) + AcceptConnection(); + + std::vector ready; + for( const auto& kv : connections_ ) + if( FD_ISSET( kv.first, &readfds ) ) ready.push_back( kv.first ); + + for( SOCKET fd : ready ){ + ServiceConnection( fd, buf, sizeof(buf) ); + if( break_ ) break; + } + } + } + + void Break() { break_ = true; } + + void AsynchronousBreak() + { + break_ = true; + send( breakSocket_, "!", 1, 0 ); // wake select() + } + + SOCKET Socket() const { return listenSocket_; } + +private: + void CreateBreakSocket() + { + // A loopback UDP socket connected to itself: writing a byte to it wakes the + // select() loop (the Winsock analogue of the posix self-pipe). + breakSocket_ = ::socket( AF_INET, SOCK_DGRAM, 0 ); + if( breakSocket_ == INVALID_SOCKET ) + throw std::runtime_error( "creation of asynchronous break socket failed\n" ); + + struct sockaddr_in addr; + std::memset( &addr, 0, sizeof(addr) ); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl( INADDR_LOOPBACK ); + addr.sin_port = 0; + + // bind -> read back the assigned port -> connect to self. If any step + // fails, AsynchronousBreak() could never wake Run() (it would hang in + // select()), so treat it as fatal. + socklen_t len = sizeof(addr); + if( bind( breakSocket_, (struct sockaddr*)&addr, sizeof(addr) ) == SOCKET_ERROR + || getsockname( breakSocket_, (struct sockaddr*)&addr, &len ) == SOCKET_ERROR + || connect( breakSocket_, (struct sockaddr*)&addr, sizeof(addr) ) == SOCKET_ERROR ){ + closesocket( breakSocket_ ); + breakSocket_ = INVALID_SOCKET; + throw std::runtime_error( "setup of asynchronous break socket failed\n" ); + } + } + + void AcceptConnection() + { + struct sockaddr_in peerAddr; + socklen_t len = sizeof(peerAddr); + SOCKET conn = ::accept( listenSocket_, (struct sockaddr*)&peerAddr, &len ); + if( conn == INVALID_SOCKET ) + return; + + // Winsock select() works over an fd_set array bounded by FD_SETSIZE + // (default 64). Beyond it, FD_SET silently drops sockets and those + // connections would stall forever -- so refuse new connections at the + // limit instead. (v1 targets a handful of connections; high connection + // counts are a future poll/epoll concern -- see issue #14.) + if( connections_.size() + 2 >= FD_SETSIZE ){ + closesocket( conn ); + return; + } + + int one = 1; + setsockopt( conn, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one) ); + connections_.emplace( std::piecewise_construct, + std::forward_as_tuple( conn ), + std::forward_as_tuple( IpEndpointNameFromSockaddr( peerAddr ), maxFrameSize_ ) ); + } + + void ServiceConnection( SOCKET fd, char* buf, std::size_t bufSize ) + { + auto it = connections_.find( fd ); + if( it == connections_.end() ) + return; + + int n = ::recv( fd, buf, (int)bufSize, 0 ); + if( n <= 0 ){ // 0 = peer closed; SOCKET_ERROR -> drop + CloseConnection( it ); + return; + } + + const bool ok = it->second.deframer.Consume( buf, (std::size_t)n, + [&]( const char* packet, uint32_t size ){ + listener_->ProcessPacket( packet, (int)size, it->second.peer ); + } ); + + if( !ok ) + CloseConnection( it ); + } + + void CloseConnection( std::map::iterator it ) + { + closesocket( it->first ); + connections_.erase( it ); + } + + void Cleanup() + { + for( auto& kv : connections_ ) + closesocket( kv.first ); + connections_.clear(); + if( listenSocket_ != INVALID_SOCKET ){ closesocket( listenSocket_ ); listenSocket_ = INVALID_SOCKET; } + if( breakSocket_ != INVALID_SOCKET ){ closesocket( breakSocket_ ); breakSocket_ = INVALID_SOCKET; } + } + + SOCKET listenSocket_ = INVALID_SOCKET; + SOCKET breakSocket_ = INVALID_SOCKET; + PacketListener* listener_; + uint32_t maxFrameSize_; + std::atomic_bool break_{ false }; + std::map connections_; +}; + +} // namespace win32 +} // namespace osctap + +#endif /* INCLUDED_OSCTAP_WIN32_TCPSOCKET_H */ diff --git a/osctap/ip/win32/UdpSocket.h b/osctap/ip/win32/UdpSocket.h index d76c70b..13174a0 100644 --- a/osctap/ip/win32/UdpSocket.h +++ b/osctap/ip/win32/UdpSocket.h @@ -232,6 +232,13 @@ class UdpSocketImplementation{ } isBound_ = true; + + // Read back the actual local port (resolves an OS-assigned port when the + // caller binds to port 0; otherwise LocalPort() would still report 0). + struct sockaddr_in boundAddr; + socklen_t boundLen = sizeof(boundAddr); + if( getsockname( socket_, (struct sockaddr *)&boundAddr, &boundLen ) == 0 ) + localPort_ = ntohs( boundAddr.sin_port ); } bool IsBound() const { return isBound_; } @@ -268,7 +275,10 @@ struct AttachedTimerListener{ }; -static bool CompareScheduledTimerCalls( +// inline (not static) to match the posix backend: a static function is internal +// to each TU and trips MSVC C4505 in translation units that include this header +// but never instantiate the multiplexer's timer sort (e.g. a transmit-only TU). +inline bool CompareScheduledTimerCalls( const std::pair< double, AttachedTimerListener > & lhs, const std::pair< double, AttachedTimerListener > & rhs ) { return lhs.first < rhs.first; @@ -377,7 +387,7 @@ class SocketReceiveMultiplexerImplementation { while( !break_ ){ - double currentTimeMs = GetCurrentTimeMs(); + currentTimeMs = GetCurrentTimeMs(); // reuse outer (avoid MSVC C4456 shadow) DWORD waitTime = INFINITE; if( !timerQueue_.empty() ){ diff --git a/osctap/osc/MessageMappingOscPacketListener.h b/osctap/osc/MessageMappingOscPacketListener.h index 72c4100..1fb934f 100644 --- a/osctap/osc/MessageMappingOscPacketListener.h +++ b/osctap/osc/MessageMappingOscPacketListener.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H -#define INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H +#ifndef INCLUDED_OSCTAP_MESSAGEMAPPINGOSCPACKETLISTENER_H +#define INCLUDED_OSCTAP_MESSAGEMAPPINGOSCPACKETLISTENER_H #include #include @@ -82,4 +82,4 @@ class MessageMappingOscPacketListener : public OscPacketListener{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_MESSAGEMAPPINGOSCPACKETLISTENER_H */ \ No newline at end of file +#endif /* INCLUDED_OSCTAP_MESSAGEMAPPINGOSCPACKETLISTENER_H */ \ No newline at end of file diff --git a/osctap/osc/OscConfig.h b/osctap/osc/OscConfig.h index c3cf8d9..7514707 100644 --- a/osctap/osc/OscConfig.h +++ b/osctap/osc/OscConfig.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCCONFIG_H -#define INCLUDED_OSCPACK_OSCCONFIG_H +#ifndef INCLUDED_OSCTAP_OSCCONFIG_H +#define INCLUDED_OSCTAP_OSCCONFIG_H /* OscTap build-configuration seam. @@ -103,4 +103,4 @@ namespace detail { # endif #endif -#endif /* INCLUDED_OSCPACK_OSCCONFIG_H */ +#endif /* INCLUDED_OSCTAP_OSCCONFIG_H */ diff --git a/osctap/osc/OscException.h b/osctap/osc/OscException.h index ab462c7..eb0c0b3 100644 --- a/osctap/osc/OscException.h +++ b/osctap/osc/OscException.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCEXCEPTION_H -#define INCLUDED_OSCPACK_OSCEXCEPTION_H +#ifndef INCLUDED_OSCTAP_OSCEXCEPTION_H +#define INCLUDED_OSCTAP_OSCEXCEPTION_H #include @@ -64,4 +64,4 @@ class Exception : public std::exception { // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCEXCEPTION_H */ +#endif /* INCLUDED_OSCTAP_OSCEXCEPTION_H */ diff --git a/osctap/osc/OscHostEndianness.h b/osctap/osc/OscHostEndianness.h index 4edb370..e1fc441 100644 --- a/osctap/osc/OscHostEndianness.h +++ b/osctap/osc/OscHostEndianness.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H -#define INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H +#ifndef INCLUDED_OSCTAP_OSCHOSTENDIANNESS_H +#define INCLUDED_OSCTAP_OSCHOSTENDIANNESS_H /* Make sure either OSC_HOST_LITTLE_ENDIAN or OSC_HOST_BIG_ENDIAN is defined @@ -123,5 +123,5 @@ #endif -#endif /* INCLUDED_OSCPACK_OSCHOSTENDIANNESS_H */ +#endif /* INCLUDED_OSCTAP_OSCHOSTENDIANNESS_H */ diff --git a/osctap/osc/OscOutboundPacketStream.h b/osctap/osc/OscOutboundPacketStream.h index 8979acc..794884f 100644 --- a/osctap/osc/OscOutboundPacketStream.h +++ b/osctap/osc/OscOutboundPacketStream.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H -#define INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H +#ifndef INCLUDED_OSCTAP_OSCOUTBOUNDPACKETSTREAM_H +#define INCLUDED_OSCTAP_OSCOUTBOUNDPACKETSTREAM_H #include // size_t @@ -51,8 +51,6 @@ namespace osctap { using string_view = std::string_view; } -#include "SmallString.h" - #include "OscTypes.h" #include "OscException.h" #include "OscConfig.h" // OSCTAP_THROW, OSCTAP_FREESTANDING @@ -466,6 +464,19 @@ class OutboundPacketStream{ return *this; } + // A runtime const char* would otherwise bind to operator<<(bool): the + // standard pointer->bool conversion outranks the user-defined conversion to + // string_view, so a C-string pointer was silently serialized as a boolean. + // This overload makes a const char* serialize as an OSC string, as expected. + // (String *literals* still match the more specialised const char(&)[N] + // overload below; this catches decayed/runtime pointers.) Freestanding-safe: + // forwards to the string_view overload, no heap. + OutboundPacketStream& operator<<( const char *rhs ) + { + operator<<(osctap::string_view(rhs)); + return *this; + } + #ifndef OSCTAP_FREESTANDING // Hosted convenience: std::string pulls in (and heap). The // freestanding profile omits it; pass const char*, a char array, or @@ -665,4 +676,4 @@ class OutboundPacketStream{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCOUTBOUNDPACKETSTREAM_H */ +#endif /* INCLUDED_OSCTAP_OSCOUTBOUNDPACKETSTREAM_H */ diff --git a/osctap/osc/OscPacketListener.h b/osctap/osc/OscPacketListener.h index d956e50..125e957 100644 --- a/osctap/osc/OscPacketListener.h +++ b/osctap/osc/OscPacketListener.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCPACKETLISTENER_H -#define INCLUDED_OSCPACK_OSCPACKETLISTENER_H +#ifndef INCLUDED_OSCTAP_OSCPACKETLISTENER_H +#define INCLUDED_OSCTAP_OSCPACKETLISTENER_H #include "OscReceivedElements.h" #include "../ip/PacketListener.h" @@ -108,4 +108,4 @@ class OscPacketListener : public PacketListener{ // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCPACKETLISTENER_H */ +#endif /* INCLUDED_OSCTAP_OSCPACKETLISTENER_H */ diff --git a/osctap/osc/OscPrintReceivedElements.h b/osctap/osc/OscPrintReceivedElements.h index f4686bc..82c7a65 100644 --- a/osctap/osc/OscPrintReceivedElements.h +++ b/osctap/osc/OscPrintReceivedElements.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H -#define INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H +#ifndef INCLUDED_OSCTAP_OSCPRINTRECEIVEDELEMENTS_H +#define INCLUDED_OSCTAP_OSCPRINTRECEIVEDELEMENTS_H #include @@ -298,4 +298,4 @@ inline Ostream_T& operator<<( Ostream_T& os, const ReceivedPacket& p ) // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCPRINTRECEIVEDELEMENTS_H */ +#endif /* INCLUDED_OSCTAP_OSCPRINTRECEIVEDELEMENTS_H */ diff --git a/osctap/osc/OscReceivedElements.h b/osctap/osc/OscReceivedElements.h index 0f5b058..5ac23d8 100644 --- a/osctap/osc/OscReceivedElements.h +++ b/osctap/osc/OscReceivedElements.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H -#define INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H +#ifndef INCLUDED_OSCTAP_OSCRECEIVEDELEMENTS_H +#define INCLUDED_OSCTAP_OSCRECEIVEDELEMENTS_H #include #include @@ -117,22 +117,34 @@ class ReceivedPacket{ osc_bundle_element_size_t Size() const { return size_; } const char *Contents() const { return contents_; } - private: - const char *contents_; - osc_bundle_element_size_t size_; - - static osc_bundle_element_size_t ValidateSize( osc_bundle_element_size_t size ) + // Non-throwing size validation: returns nullptr if `size` is an acceptable + // packet/element size, else a static error string. The single source of the + // size rules, shared by the throwing ValidateSize() and the non-throwing + // TryValidatePacket(). + static const char* ValidateSizeNoThrow( osc_bundle_element_size_t size ) { // sanity check integer types declared in OscTypes.h // you'll need to fix OscTypes.h if any of these asserts fail if( !IsValidElementSizeValue(size) ) - OSCTAP_THROW( MalformedPacketException( "invalid packet size" ) ); + return "invalid packet size"; if( size == 0 ) - OSCTAP_THROW( MalformedPacketException( "zero length elements not permitted" ) ); + return "zero length elements not permitted"; if( !IsMultipleOf4(size) ) - OSCTAP_THROW( MalformedPacketException( "element size must be multiple of four" ) ); + return "element size must be multiple of four"; + + return nullptr; + } + + private: + const char *contents_; + osc_bundle_element_size_t size_; + + static osc_bundle_element_size_t ValidateSize( osc_bundle_element_size_t size ) + { + if( const char* err = ValidateSizeNoThrow( size ) ) + OSCTAP_THROW( MalformedPacketException( err ) ); return size; } @@ -717,23 +729,35 @@ class ReceivedMessageArgumentStream{ class ReceivedMessage{ - void Init( const char *message, osc_bundle_element_size_t size ) + public: + // Non-throwing parse + structural validation. Sets all boundary members and + // returns nullptr on success, or a static error string on malformed input. + // Use on no-exceptions / untrusted-input paths: + // ReceivedMessage m; + // if( m.TryInit(data, size) == nullptr ) { /* read m */ } + // This is the single source of truth: the throwing Init() below delegates + // here, as does the non-throwing Validate() / TryValidatePacket() gate for + // untrusted input on no-exceptions builds. + const char* TryInit( const char *message, osc_bundle_element_size_t size ) { + addressPattern_ = message; + size_ = size; + if( !IsValidElementSizeValue(size) ) - OSCTAP_THROW( MalformedMessageException( "invalid message size" ) ); + return "invalid message size"; if( size == 0 ) - OSCTAP_THROW( MalformedMessageException( "zero length messages not permitted" ) ); + return "zero length messages not permitted"; if( !IsMultipleOf4(size) ) - OSCTAP_THROW( MalformedMessageException( "message size must be multiple of four" ) ); + return "message size must be multiple of four"; const char *end = message + size; typeTagsBegin_ = FindStr4End( addressPattern_, end ); if( typeTagsBegin_ == 0 ){ // address pattern was not terminated before end - OSCTAP_THROW( MalformedMessageException( "unterminated address pattern" ) ); + return "unterminated address pattern"; } if( typeTagsBegin_ == end ){ @@ -744,7 +768,7 @@ class ReceivedMessage{ }else{ if( *typeTagsBegin_ != ',' ) - OSCTAP_THROW( MalformedMessageException( "type tags not present" ) ); + return "type tags not present"; if( *(typeTagsBegin_ + 1) == '\0' ){ // zero length type tags @@ -757,7 +781,7 @@ class ReceivedMessage{ arguments_ = FindStr4End( typeTagsBegin_, end ); if( arguments_ == 0 ){ - OSCTAP_THROW( MalformedMessageException( "type tags were not terminated before end of message" ) ); + return "type tags were not terminated before end of message"; } ++typeTagsBegin_; // advance past initial ',' @@ -785,7 +809,7 @@ class ReceivedMessage{ case ARRAY_END_TYPE_TAG: if( arrayLevel == 0 ) - OSCTAP_THROW( MalformedMessageException( "array close tag ']' without matching open tag '['" ) ); + return "array close tag ']' without matching open tag '['"; --arrayLevel; // (zero length argument data) break; @@ -797,10 +821,10 @@ class ReceivedMessage{ case MIDI_MESSAGE_TYPE_TAG: if( argument == end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; argument += 4; if( argument > end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; break; case INT64_TYPE_TAG: @@ -808,31 +832,31 @@ class ReceivedMessage{ case DOUBLE_TYPE_TAG: if( argument == end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; argument += 8; if( argument > end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; break; case STRING_TYPE_TAG: case SYMBOL_TYPE_TAG: if( argument == end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; argument = FindStr4End( argument, end ); if( argument == 0 ) - OSCTAP_THROW( MalformedMessageException( "unterminated string argument" ) ); + return "unterminated string argument"; break; case BLOB_TYPE_TAG: { if( argument + osctap::OSC_SIZEOF_INT32 > end ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; // treat blob size as an unsigned int for the purposes of this calculation uint32_t blobSize = ToUInt32( argument ); if( !IsValidElementSizeValue( (osc_bundle_element_size_t)blobSize ) ) - OSCTAP_THROW( MalformedMessageException( "invalid blob size" ) ); + return "invalid blob size"; // Compare sizes rather than advancing the pointer first: a huge // blobSize must not be allowed to overflow the pointer (or RoundUp4) @@ -840,21 +864,21 @@ class ReceivedMessage{ // guaranteed by the check above. const char *blobData = argument + osctap::OSC_SIZEOF_INT32; if( RoundUp4( blobSize ) > (uint32_t)(end - blobData) ) - OSCTAP_THROW( MalformedMessageException( "arguments exceed message size" ) ); + return "arguments exceed message size"; argument = blobData + RoundUp4( blobSize ); } break; default: - OSCTAP_THROW( MalformedMessageException( "unknown type tag" ) ); + return "unknown type tag"; } }while( *++typeTag != '\0' ); typeTagsEnd_ = typeTag; if( arrayLevel != 0 ) - OSCTAP_THROW( MalformedMessageException( "array was not terminated before end of message (expected ']' end of array tag)" ) ); + return "array was not terminated before end of message (expected ']' end of array tag)"; } // These invariants should be guaranteed by the above code. @@ -865,8 +889,23 @@ class ReceivedMessage{ assert( argumentCount <= OSC_INT32_MAX ); #endif } + + return nullptr; + } + + // Throwing wrapper used by the constructors (preserves the original API). + void Init( const char *message, osc_bundle_element_size_t size ) + { + if( const char* err = TryInit( message, size ) ) + OSCTAP_THROW( MalformedMessageException( err ) ); } public: + // Default-constructs an empty (invalid) message for use with the non-throwing + // TryInit() below. Reading it before a successful TryInit() is undefined. + ReceivedMessage() + : addressPattern_( nullptr ), typeTagsBegin_( nullptr ) + , typeTagsEnd_( nullptr ), arguments_( nullptr ), size_( 0 ) {} + explicit ReceivedMessage( const ReceivedPacket& packet ) : addressPattern_( packet.Contents() ), size_{packet.Size()} { @@ -877,6 +916,14 @@ class ReceivedMessage{ { Init( bundleElement.Contents(), bundleElement.Size() ); } + + // Non-throwing structural validation of a message body, without retaining the + // parsed object. nullptr == well-formed. + static const char* Validate( const char *message, osc_bundle_element_size_t size ) + { + ReceivedMessage m; + return m.TryInit( message, size ); + } const char *AddressPattern() const OSCTAP_REALTIME { return addressPattern_; } // Support for non-standard SuperCollider integer address patterns: @@ -936,7 +983,7 @@ class ReceivedMessage{ const char *typeTagsBegin_; const char *typeTagsEnd_; const char *arguments_; - const osc_bundle_element_size_t size_; + osc_bundle_element_size_t size_; // not const: TryInit() (re)assigns during parse }; #ifndef OSCTAP_FREESTANDING @@ -963,17 +1010,25 @@ class OwnedMessage #endif // OSCTAP_FREESTANDING class ReceivedBundle{ - void Init( const char *bundle, osc_bundle_element_size_t size ) + public: + // Non-throwing parse + structural validation of the bundle framing (size, + // "#bundle" tag, and element-size table). Returns nullptr on success (members + // set), or a static error string. Single source of truth; the throwing Init() + // delegates here. Note: this validates the bundle's own framing, not the + // contents of each element -- use TryValidatePacket() for a full recursive + // check before reading untrusted bundles on a no-exceptions build. + const char* TryInit( const char *bundle, osc_bundle_element_size_t size ) { + elementCount_ = 0; if( !IsValidElementSizeValue(size) ) - OSCTAP_THROW( MalformedBundleException( "invalid bundle size" ) ); + return "invalid bundle size"; if( size < 16 ) - OSCTAP_THROW( MalformedBundleException( "packet too short for bundle" ) ); + return "packet too short for bundle"; if( !IsMultipleOf4(size) ) - OSCTAP_THROW( MalformedBundleException( "bundle size must be multiple of four" ) ); + return "bundle size must be multiple of four"; if( bundle[0] != '#' || bundle[1] != 'b' @@ -983,7 +1038,7 @@ class ReceivedBundle{ || bundle[5] != 'l' || bundle[6] != 'e' || bundle[7] != '\0' ) - OSCTAP_THROW( MalformedBundleException( "bad bundle address pattern" ) ); + return "bad bundle address pattern"; end_ = bundle + size; @@ -993,18 +1048,18 @@ class ReceivedBundle{ while( p < end_ ){ if( p + osctap::OSC_SIZEOF_INT32 > end_ ) - OSCTAP_THROW( MalformedBundleException( "packet too short for elementSize" ) ); + return "packet too short for elementSize"; // treat element size as an unsigned int for the purposes of this calculation uint32_t elementSize = ToUInt32( p ); if( (elementSize & ((uint32_t)0x03)) != 0 ) - OSCTAP_THROW( MalformedBundleException( "bundle element size must be multiple of four" ) ); + return "bundle element size must be multiple of four"; // Compare sizes rather than advancing the pointer first, so that a huge // elementSize can't overflow the pointer and slip past the bounds check. const char *elementData = p + osctap::OSC_SIZEOF_INT32; if( elementSize > (uint32_t)(end_ - elementData) ) - OSCTAP_THROW( MalformedBundleException( "packet too short for bundle element" ) ); + return "packet too short for bundle element"; p = elementData + elementSize; @@ -1012,9 +1067,22 @@ class ReceivedBundle{ } if( p != end_ ) - OSCTAP_THROW( MalformedBundleException( "bundle contents " ) ); + return "bundle contents did not match bundle size"; + + return nullptr; } - public: + + // Throwing wrapper used by the constructors (preserves the original API). + void Init( const char *bundle, osc_bundle_element_size_t size ) + { + if( const char* err = TryInit( bundle, size ) ) + OSCTAP_THROW( MalformedBundleException( err ) ); + } + + // Default-constructs an empty (invalid) bundle for use with TryInit(). + ReceivedBundle() + : timeTag_( nullptr ), end_( nullptr ), elementCount_( 0 ) {} + explicit ReceivedBundle( const ReceivedPacket& packet ) : elementCount_( 0 ) { @@ -1026,6 +1094,14 @@ class ReceivedBundle{ Init( bundleElement.Contents(), bundleElement.Size() ); } + // Non-throwing structural validation of the bundle framing, without retaining + // the parsed object. nullptr == well-formed framing. + static const char* Validate( const char *bundle, osc_bundle_element_size_t size ) + { + ReceivedBundle b; + return b.TryInit( bundle, size ); + } + uint64_t TimeTag() const { return ToUInt64( timeTag_ ); @@ -1062,6 +1138,56 @@ inline auto end(const osctap::ReceivedMessage& mes) return mes.ArgumentsEnd(); } + +// Non-throwing, recursive validation of a complete OSC packet -- a message, or a +// bundle whose every element is itself well-formed, recursively. Returns nullptr +// if [data, data+size) is fully well-formed and therefore safe to construct *and +// read in full* without any OSCTAP_THROW firing; otherwise a static error string. +// +// This is the gate to use before handling untrusted input on a no-exceptions / +// freestanding build, where a malformed packet would otherwise hit the fatal +// handler (abort) during construction or iteration: +// +// if( osctap::TryValidatePacket(buf, n) == nullptr ) { +// osctap::ReceivedPacket p(buf, n); // won't abort +// ... read the message / iterate the bundle ... +// } else { +// ... drop the datagram ... +// } +// +// maxBundleNestingDepth bounds the recursion so a deeply-nested bundle from an +// attacker cannot exhaust the stack (mirrors OscPacketListener's dispatch bound). +inline const char* TryValidatePacket( const char *data, osc_bundle_element_size_t size, + unsigned int maxBundleNestingDepth = 64 ) +{ + if( const char* err = ReceivedPacket::ValidateSizeNoThrow( size ) ) + return err; + + if( size > 0 && data[0] == '#' ){ + // Bundle: validate the framing, then recurse into each element's contents. + if( const char* err = ReceivedBundle::Validate( data, size ) ) + return err; + if( maxBundleNestingDepth == 0 ) + return "bundle nested too deeply"; + + const char *end = data + size; + const char *p = data + 16; // skip "#bundle\0" (8) + time tag (8) + while( p < end ){ + // Framing was validated above: elementSize is multiple-of-4 and in bounds. + uint32_t elementSize = ToUInt32( p ); + const char *elementData = p + osctap::OSC_SIZEOF_INT32; + if( const char* err = TryValidatePacket( elementData, + (osc_bundle_element_size_t)elementSize, maxBundleNestingDepth - 1 ) ) + return err; + p = elementData + elementSize; + } + return nullptr; + } + + // Message. + return ReceivedMessage::Validate( data, size ); +} + } // namespace osctap @@ -1070,4 +1196,4 @@ inline auto end(const osctap::ReceivedMessage& mes) // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCRECEIVEDELEMENTS_H */ +#endif /* INCLUDED_OSCTAP_OSCRECEIVEDELEMENTS_H */ diff --git a/osctap/osc/OscStreamFraming.h b/osctap/osc/OscStreamFraming.h new file mode 100644 index 0000000..39dfcb5 --- /dev/null +++ b/osctap/osc/OscStreamFraming.h @@ -0,0 +1,188 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef INCLUDED_OSCTAP_OSCSTREAMFRAMING_H +#define INCLUDED_OSCTAP_OSCSTREAMFRAMING_H + +#include +#include +#include // memcpy +#include + +#include "OscUtilities.h" // FromUInt32 / ToUInt32 + +/* + Length-prefixed OSC stream framing -- the de-facto convention for OSC over a + reliable byte stream such as TCP (CNMAT, liblo's osc.tcp, SuperCollider, Max, + JUCE). Each packet on the wire is a 4-byte big-endian length followed by that + many payload bytes -- the same shape as a bundle element's size slot. + + A datagram transport (UDP) hands you message boundaries for free; a byte stream + does not, so the receiver must reassemble complete packets from arbitrarily + chunked reads. The encoder here is trivial (write the header, then the payload); + OscStreamDeframer is the real work, and it caps the frame size so a hostile + length prefix cannot make it buffer unbounded data (cf. the blob-size discipline + from audit findings #1/#4). + + This codec is transport-agnostic: it knows nothing about sockets. The TCP socket + types (ip/TcpSocket.h) use it; you can equally drive it from any byte source. + SLIP framing (the OSC 1.1 nominated alternative) is intentionally deferred. +*/ + +namespace osctap{ + +enum { OSC_STREAM_FRAME_HEADER_SIZE = 4 }; + +// Default cap on a single framed packet (and therefore on the per-connection +// reassembly buffer). 64 KiB comfortably exceeds any normal OSC packet while +// bounding the memory a peer can make you hold. Override per-deframer. +enum { OSC_DEFAULT_MAX_FRAME_SIZE = 64 * 1024 }; + +// Encoder: write the 4-byte big-endian length prefix for a `packetSize`-byte +// packet into `header`. The caller then writes the payload. Pure, non-allocating, +// freestanding-safe. (The socket transmit path writes the header then the payload +// directly, avoiding a copy; FrameOscPacket() below is the one-buffer convenience.) +inline void WriteOscStreamFrameHeader( char header[OSC_STREAM_FRAME_HEADER_SIZE], uint32_t packetSize ) +{ + FromUInt32( header, packetSize ); +} + +// Convenience: write [4-byte length][payload] contiguously into `out` (capacity +// `outCapacity`). Returns the framed size (4 + packetSize), or 0 if it does not +// fit. Non-allocating. +inline std::size_t FrameOscPacket( const char* packet, uint32_t packetSize, + char* out, std::size_t outCapacity ) +{ + if( (std::size_t)packetSize + OSC_STREAM_FRAME_HEADER_SIZE > outCapacity ) + return 0; + WriteOscStreamFrameHeader( out, packetSize ); + if( packetSize ) + std::memcpy( out + OSC_STREAM_FRAME_HEADER_SIZE, packet, packetSize ); + return (std::size_t)packetSize + OSC_STREAM_FRAME_HEADER_SIZE; +} + +// Streaming decoder: feed it received bytes in whatever chunks the transport +// delivers; it emits each complete OSC packet exactly once. One instance per +// connection (it holds that connection's reassembly state). +// +// Non-throwing. Allocates at most one bounded reassembly buffer (<= maxFrameSize), +// and only when a packet straddles a read boundary -- packets contained whole in a +// single chunk are dispatched in place with no copy. +class OscStreamDeframer{ +public: + explicit OscStreamDeframer( uint32_t maxFrameSize = OSC_DEFAULT_MAX_FRAME_SIZE ) + : maxFrameSize_( maxFrameSize ) + , frameSize_( 0 ) + , headerFill_( 0 ) + , haveHeader_( false ) {} + + uint32_t MaxFrameSize() const { return maxFrameSize_; } + + // Feed `size` bytes received from the stream. For each complete packet, calls + // sink(const char* packet, uint32_t packetSize). Returns true normally; returns + // false as soon as a frame header announces a size greater than maxFrameSize() + // -- a protocol violation / DoS attempt, on which the caller should drop the + // connection (the deframer's state is then undefined until Reset()). + template + bool Consume( const char* data, std::size_t size, Sink&& sink ) + { + const char* p = data; + const char* const end = data + size; + + while( p < end ){ + if( !haveHeader_ ){ + if( headerFill_ == 0 && (std::size_t)(end - p) >= OSC_STREAM_FRAME_HEADER_SIZE ){ + // whole header present contiguously, nothing carried over + frameSize_ = ToUInt32( p ); + p += OSC_STREAM_FRAME_HEADER_SIZE; + }else{ + // accumulate the length prefix across reads + while( headerFill_ < OSC_STREAM_FRAME_HEADER_SIZE && p < end ) + header_[headerFill_++] = *p++; + if( headerFill_ < OSC_STREAM_FRAME_HEADER_SIZE ) + return true; // need more bytes to complete the header + frameSize_ = ToUInt32( header_ ); + headerFill_ = 0; + } + + if( frameSize_ > maxFrameSize_ ) + return false; // oversized / hostile frame + + // A zero-length frame is structurally valid framing; it is + // forwarded as an empty packet, which the OSC layer + // (ReceivedPacket) then rejects -- framing doesn't judge OSC + // validity, it only delimits packets. + + haveHeader_ = true; + } + + // accumulate / dispatch the payload of frameSize_ bytes + if( buffer_.empty() && (std::size_t)(end - p) >= frameSize_ ){ + // whole payload present contiguously -> dispatch in place, no copy + sink( p, frameSize_ ); + p += frameSize_; + haveHeader_ = false; + }else{ + const std::size_t still = (std::size_t)frameSize_ - buffer_.size(); + const std::size_t avail = (std::size_t)(end - p); + const std::size_t take = avail < still ? avail : still; + buffer_.insert( buffer_.end(), p, p + take ); + p += take; + if( buffer_.size() == (std::size_t)frameSize_ ){ + sink( buffer_.data(), frameSize_ ); + buffer_.clear(); + haveHeader_ = false; + } + } + } + return true; + } + + // Discard any partial-frame state (e.g. after a connection reset). + void Reset() + { + buffer_.clear(); + frameSize_ = 0; + headerFill_ = 0; + haveHeader_ = false; + } + +private: + std::vector buffer_; // accumulates a payload that spans reads + uint32_t maxFrameSize_; + uint32_t frameSize_; // payload size of the frame in progress + char header_[OSC_STREAM_FRAME_HEADER_SIZE]; + uint32_t headerFill_; // bytes of the header accumulated so far + bool haveHeader_; // false: reading header; true: reading payload +}; + +} // namespace osctap + + +// Backwards-compatibility alias: this library was formerly named oscpack. +// Existing code that uses the oscpack:: namespace continues to compile. +namespace oscpack = osctap; + +#endif /* INCLUDED_OSCTAP_OSCSTREAMFRAMING_H */ diff --git a/osctap/osc/OscTypes.h b/osctap/osc/OscTypes.h index b17cf6b..c767ec3 100644 --- a/osctap/osc/OscTypes.h +++ b/osctap/osc/OscTypes.h @@ -34,8 +34,8 @@ requested that these non-binding requests be included whenever the above license is reproduced. */ -#ifndef INCLUDED_OSCPACK_OSCTYPES_H -#define INCLUDED_OSCPACK_OSCTYPES_H +#ifndef INCLUDED_OSCTAP_OSCTYPES_H +#define INCLUDED_OSCTAP_OSCTYPES_H #include // OSCTAP_REALTIME marks the allocation- and exception-free realtime hot path @@ -212,4 +212,4 @@ constexpr ArrayTerminator EndArray() { return {}; } // Existing code that uses the oscpack:: namespace continues to compile. namespace oscpack = osctap; -#endif /* INCLUDED_OSCPACK_OSCTYPES_H */ +#endif /* INCLUDED_OSCTAP_OSCTYPES_H */ diff --git a/osctap/osc/SmallString.h b/osctap/osc/SmallString.h index e69de29..1429ac8 100644 --- a/osctap/osc/SmallString.h +++ b/osctap/osc/SmallString.h @@ -0,0 +1,41 @@ +/* + oscpack -- Open Sound Control (OSC) packet manipulation library + http://www.rossbencina.com/code/oscpack + + Copyright (c) 2004-2013 Ross Bencina + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +#ifndef INCLUDED_OSCTAP_SMALLSTRING_H +#define INCLUDED_OSCTAP_SMALLSTRING_H + +/* + Intentionally empty. + + This header was a placeholder in a fork's small-string-optimisation experiment + and never carried a definition. OscTap's outbound stream now uses + std::string_view (plus a const char* overload), so nothing here is needed. The + file is retained only so the public include path -- and its + compatibility shim -- keep resolving for any downstream code that still + #includes it. Audit finding #6 ("empty SmallString.h") closed: it is now an + explicit, guarded no-op rather than a zero-byte mystery file. +*/ + +#endif /* INCLUDED_OSCTAP_SMALLSTRING_H */ diff --git a/tests/OscFreestandingTest.cpp b/tests/OscFreestandingTest.cpp index e8b9237..ce592b5 100644 --- a/tests/OscFreestandingTest.cpp +++ b/tests/OscFreestandingTest.cpp @@ -37,9 +37,11 @@ int main() { // --- serialize a message on the stack (no heap) ------------------------ char buffer[256]; + const char* runtimeStr = "pico"; // a runtime const char* (not a literal): + // must serialize as a string, not a bool. osctap::OutboundPacketStream p( buffer, sizeof(buffer) ); p << osctap::BeginMessage( "/freestanding" ) - << true << (int32_t)2350 << (float)3.14159f << "pico" + << true << (int32_t)2350 << (float)3.14159f << runtimeStr << osctap::EndMessage(); CHECK( p.IsReady() ); @@ -68,6 +70,25 @@ int main() CHECK( arg->AsFloatUnchecked() > 3.14f ); ++arg; CHECK( std::strcmp( arg->AsStringUnchecked(), "pico" ) == 0 ); + // --- non-throwing validation gate (the point of TryInit/TryValidatePacket) --- + // On this build OSCTAP_THROW would abort via the fatal handler, so the only + // safe way to handle untrusted input is to gate it first. Reaching these lines + // at all proves the gate returns instead of throwing/aborting. + typedef osctap::osc_bundle_element_size_t sz_t; + CHECK( osctap::TryValidatePacket( p.Data(), (sz_t)p.Size() ) == nullptr ); // valid -> accepted + + // truncate the valid message by one 4-byte word: arguments now exceed size. + CHECK( osctap::TryValidatePacket( p.Data(), (sz_t)(p.Size() - 4) ) != nullptr ); + + // structurally bogus little buffer (not a valid message): rejected, not fatal. + const char bad[6] = { '/', 'x', '\0', '\0', ',', 'i' }; + CHECK( osctap::TryValidatePacket( bad, (sz_t)sizeof(bad) ) != nullptr ); + + // The same gate also drives a no-abort ReceivedMessage parse: + osctap::ReceivedMessage probe; + CHECK( probe.TryInit( p.Data(), (sz_t)p.Size() ) == nullptr ); + CHECK( std::strcmp( probe.AddressPattern(), "/freestanding" ) == 0 ); + if( failures == 0 ) std::printf( "OscFreestandingTest: OK (exceptions disabled, freestanding)\n" ); return failures == 0 ? 0 : 1; diff --git a/tests/OscReceiveTest.cpp b/tests/OscReceiveTest.cpp deleted file mode 100644 index 72a7f07..0000000 --- a/tests/OscReceiveTest.cpp +++ /dev/null @@ -1,278 +0,0 @@ -/* - oscpack -- Open Sound Control (OSC) packet manipulation library - http://www.rossbencina.com/code/oscpack - - Copyright (c) 2004-2013 Ross Bencina - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* - The text above constitutes the entire oscpack license; however, - the oscpack developer(s) also make the following non-binding requests: - - Any person wishing to distribute modifications to the Software is - requested to send the modifications to the original developer so that - they can be incorporated into the canonical version. It is also - requested that these non-binding requests be included whenever the - above license is reproduced. -*/ -#include "OscReceiveTest.h" - -#include -#include -#include - -#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug -namespace std { -using ::__strcmp__; // avoid error: E2316 '__strcmp__' is not a member of 'std'. -} -#endif - -#include "osc/OscReceivedElements.h" - -#include "ip/UdpSocket.h" -#include "osc/OscPacketListener.h" - - -namespace osc{ - -class OscReceiveTestPacketListener : public OscPacketListener{ -protected: - - void ProcessMessage( const osc::ReceivedMessage& m, - const IpEndpointName& remoteEndpoint ) - { - (void) remoteEndpoint; // suppress unused parameter warning - - // a more complex scheme involving std::map or some other method of - // processing address patterns could be used here - // (see MessageMappingOscPacketListener.h for example). however, the main - // purpose of this example is to illustrate and test different argument - // parsing methods - - try { - // argument stream, and argument iterator, used in different - // examples below. - ReceivedMessageArgumentStream args = m.ArgumentStream(); - ReceivedMessage::const_iterator arg = m.ArgumentsBegin(); - - if( std::strcmp( m.AddressPattern(), "/test1" ) == 0 ){ - - // example #1: - // parse an expected format using the argument stream interface: - bool a1; - osc::int32_t a2; - float a3; - const char *a4; - args >> a1 >> a2 >> a3 >> a4 >> osc::EndMessage; - - std::cout << "received '/test1' message with arguments: " - << a1 << " " << a2 << " " << a3 << " " << a4 << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/test2" ) == 0 ){ - - // example #2: - // parse an expected format using the argument iterator interface - // this is a more complicated example of doing the same thing - // as above. - bool a1 = (arg++)->AsBool(); - int a2 = (arg++)->AsInt32(); - float a3 = (arg++)->AsFloat(); - const char *a4 = (arg++)->AsString(); - if( arg != m.ArgumentsEnd() ) - throw ExcessArgumentException(); - - std::cout << "received '/test2' message with arguments: " - << a1 << " " << a2 << " " << a3 << " " << a4 << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/test3" ) == 0 ){ - - // example #3: - // parse a variable argument format using the argument iterator - // interface. this is where it is necessary to use - // argument iterators instead of streams. - // When messages may contain arguments of varying type, you can - // use the argument iterator interface to query the types at - // runtime. this is more flexible that the argument stream - // interface, which requires each argument to have a fixed type - - if( arg->IsBool() ){ - bool a = (arg++)->AsBoolUnchecked(); - std::cout << "received '/test3' message with bool argument: " - << a << "\n"; - }else if( arg->IsInt32() ){ - int a = (arg++)->AsInt32Unchecked(); - std::cout << "received '/test3' message with int32_t argument: " - << a << "\n"; - }else if( arg->IsFloat() ){ - float a = (arg++)->AsFloatUnchecked(); - std::cout << "received '/test3' message with float argument: " - << a << "\n"; - }else if( arg->IsString() ){ - const char *a = (arg++)->AsStringUnchecked(); - std::cout << "received '/test3' message with string argument: '" - << a << "'\n"; - }else{ - std::cout << "received '/test3' message with unexpected argument type\n"; - } - - if( arg != m.ArgumentsEnd() ) - throw ExcessArgumentException(); - - - }else if( std::strcmp( m.AddressPattern(), "/no_arguments" ) == 0 ){ - - args >> osc::EndMessage; - std::cout << "received '/no_arguments' message\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_bool" ) == 0 ){ - - bool a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_bool' message: " << a << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/nil" ) == 0 ){ - - std::cout << "received '/nil' message\n"; - - }else if( std::strcmp( m.AddressPattern(), "/inf" ) == 0 ){ - - std::cout << "received '/inf' message\n"; - - }else if( std::strcmp( m.AddressPattern(), "/an_int" ) == 0 ){ - - osc::int32_t a; - args >> a >> osc::EndMessage; - std::cout << "received '/an_int' message: " << a << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_float" ) == 0 ){ - - float a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_float' message: " << a << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_char" ) == 0 ){ - - char a; - args >> a >> osc::EndMessage; - char s[2] = {0}; - s[0] = a; - std::cout << "received '/a_char' message: '" << s << "'\n"; - - }else if( std::strcmp( m.AddressPattern(), "/an_rgba_color" ) == 0 ){ - - osc::RgbaColor a; - args >> a >> osc::EndMessage; - std::cout << "received '/an_rgba_color' message: " << a.value << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_midi_message" ) == 0 ){ - - osc::MidiMessage a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_midi_message' message: " << a.value << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/an_int64_t" ) == 0 ){ - - osc::int64_t a; - args >> a >> osc::EndMessage; - std::cout << "received '/an_int64_t' message: " << a << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_time_tag" ) == 0 ){ - - osc::TimeTag a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_time_tag' message: " << a.value << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_double" ) == 0 ){ - - double a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_double' message: " << a << "\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_string" ) == 0 ){ - - const char *a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_string' message: '" << a << "'\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_symbol" ) == 0 ){ - - osc::Symbol a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_symbol' message: '" << a.value << "'\n"; - - }else if( std::strcmp( m.AddressPattern(), "/a_blob" ) == 0 ){ - - osc::Blob a; - args >> a >> osc::EndMessage; - std::cout << "received '/a_blob' message\n"; - - }else{ - std::cout << "unrecognised address pattern: " - << m.AddressPattern() << "\n"; - } - - }catch( Exception& e ){ - std::cout << "error while parsing message: " - << m.AddressPattern() << ": " << e.what() << "\n"; - } - } -}; - - -void RunReceiveTest( int port ) -{ - osc::OscReceiveTestPacketListener listener; - UdpListeningReceiveSocket s( - IpEndpointName( IpEndpointName::ANY_ADDRESS, port ), - &listener ); - - std::cout << "listening for input on port " << port << "...\n"; - std::cout << "press ctrl-c to end\n"; - - s.RunUntilSigInt(); - - std::cout << "finishing.\n"; -} - -} // namespace osc - -#ifndef NO_OSC_TEST_MAIN - -int main(int argc, char* argv[]) -{ - if( argc >= 2 && std::strcmp( argv[1], "-h" ) == 0 ){ - std::cout << "usage: OscReceiveTest [port]\n"; - return 0; - } - - int port = 7000; - - if( argc >= 2 ) - port = std::atoi( argv[1] ); - - osc::RunReceiveTest( port ); - - return 0; -} - -#endif /* NO_OSC_TEST_MAIN */ - diff --git a/tests/OscReceiveTest.h b/tests/OscReceiveTest.h deleted file mode 100644 index c5effa5..0000000 --- a/tests/OscReceiveTest.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - oscpack -- Open Sound Control packet manipulation library - http://www.audiomulch.com/~rossb/oscpack - - Copyright (c) 2004-2005 Ross Bencina - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - Any person wishing to distribute modifications to the Software is - requested to send the modifications to the original developer so that - they can be incorporated into the canonical version. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -#ifndef INCLUDED_OSCRECEIVETEST_H -#define INCLUDED_OSCRECEIVETEST_H - -namespace osc{ - -void RunReceiveTest( int port ); - -} // namespace osc - -#endif /* INCLUDED_OSCSENDTESTS_H */ diff --git a/tests/OscSendTests.cpp b/tests/OscSendTests.cpp deleted file mode 100644 index f5ab0e3..0000000 --- a/tests/OscSendTests.cpp +++ /dev/null @@ -1,230 +0,0 @@ -/* - oscpack -- Open Sound Control (OSC) packet manipulation library - http://www.rossbencina.com/code/oscpack - - Copyright (c) 2004-2013 Ross Bencina - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -/* - The text above constitutes the entire oscpack license; however, - the oscpack developer(s) also make the following non-binding requests: - - Any person wishing to distribute modifications to the Software is - requested to send the modifications to the original developer so that - they can be incorporated into the canonical version. It is also - requested that these non-binding requests be included whenever the - above license is reproduced. -*/ -#include "OscSendTests.h" - -#include -#include -#include - -#if defined(__BORLANDC__) // workaround for BCB4 release build intrinsics bug -namespace std { -using ::__strcmp__; // avoid error: E2316 '__strcmp__' is not a member of 'std'. -} -#endif - -#include "osc/OscOutboundPacketStream.h" - -#include "ip/UdpSocket.h" -#include "ip/IpEndpointName.h" - -#define IP_MTU_SIZE 1536 - -namespace osc{ - -void RunSendTests( const IpEndpointName& host ) -{ - char buffer[IP_MTU_SIZE]; - osc::OutboundPacketStream p( buffer, IP_MTU_SIZE ); - UdpTransmitSocket socket( host ); - - p.Clear(); - p << osc::BeginMessage( "/test1" ) - << true << 23 << (float)3.1415 << "hello" << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - std::cout << "NOTE: sending /test1 message with too few arguments\n"\ - "(expect an exception if receiving with OscReceiveTest)\n\n"; - p.Clear(); - p << osc::BeginMessage( "/test1" ) - << true << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - std::cout << "NOTE: sending /test1 message with too many arguments\n"\ - "(expect an exception if receiving with OscReceiveTest)\n\n"; - p.Clear(); - p << osc::BeginMessage( "/test1" ) - << true << 23 << (float)3.1415 << "hello" << 42 << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - std::cout << "NOTE: sending /test1 message with wrong argument type\n"\ - "(expect an exception if receiving with OscReceiveTest)\n\n"; - p.Clear(); - p << osc::BeginMessage( "/test1" ) - << true << 1.0 << (float)3.1415 << "hello" << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - p.Clear(); - p << osc::BeginMessage( "/test2" ) - << true << 23 << (float)3.1415 << "hello" << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - // send four /test3 messages, each with a different type of argument - p.Clear(); - p << osc::BeginMessage( "/test3" ) - << true << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - p.Clear(); - p << osc::BeginMessage( "/test3" ) - << 23 << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - p.Clear(); - p << osc::BeginMessage( "/test3" ) - << (float)3.1415 << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - p.Clear(); - p << osc::BeginMessage( "/test3" ) - << "hello" << osc::EndMessage; - socket.Send( p.Data(), p.Size() ); - - - // send a bundle - p.Clear(); - p << osc::BeginBundle(); - - p << osc::BeginMessage( "/no_arguments" ) - << osc::EndMessage; - - p << osc::BeginMessage( "/a_bool" ) - << true << osc::EndMessage; - - p << osc::BeginMessage( "/a_bool" ) - << false << osc::EndMessage; - - p << osc::BeginMessage( "/a_bool" ) - << (bool)1234 << osc::EndMessage; - - p << osc::BeginMessage( "/nil" ) - << osc::Nil << osc::EndMessage; - - p << osc::BeginMessage( "/inf" ) - << osc::Infinitum << osc::EndMessage; - - p << osc::BeginMessage( "/an_int" ) << 1234 << osc::EndMessage; - - p << osc::BeginMessage( "/a_float" ) - << 3.1415926f << osc::EndMessage; - - p << osc::BeginMessage( "/a_char" ) - << 'c' << osc::EndMessage; - - p << osc::BeginMessage( "/an_rgba_color" ) - << osc::RgbaColor(0x22334455) << osc::EndMessage; - - p << osc::BeginMessage( "/a_midi_message" ) - << MidiMessage(0x7F) << osc::EndMessage; - - p << osc::BeginMessage( "/an_int64_t" ) - << (int64_t)(0xFFFFFFF) << osc::EndMessage; - - p << osc::BeginMessage( "/a_time_tag" ) - << osc::TimeTag(0xFFFFFFFUL) << osc::EndMessage; - - p << osc::BeginMessage( "/a_double" ) - << (double)3.1415926 << osc::EndMessage; - - p << osc::BeginMessage( "/a_string" ) - << "hello world" << osc::EndMessage; - - p << osc::BeginMessage( "/a_symbol" ) - << osc::Symbol("foobar") << osc::EndMessage; - - // blob - { - char blobData[] = "abcd"; - - p << osc::BeginMessage( "/a_blob" ) - << osc::Blob( blobData, 4 ) - << osc::EndMessage; - } - - p << osc::EndBundle; - socket.Send( p.Data(), p.Size() ); - - - - // nested bundles, and multiple messages in bundles... - p.Clear(); - p << osc::BeginBundle( 1234 ) - << osc::BeginMessage( "/an_int" ) << 1 << osc::EndMessage - << osc::BeginMessage( "/an_int" ) << 2 << osc::EndMessage - << osc::BeginMessage( "/an_int" ) << 3 << osc::EndMessage - << osc::BeginMessage( "/an_int" ) << 4 << osc::EndMessage - << osc::BeginBundle( 12345 ) - << osc::BeginMessage( "/an_int" ) << 5 << osc::EndMessage - << osc::BeginMessage( "/an_int" ) << 6 << osc::EndMessage - << osc::EndBundle - << osc::EndBundle; - - socket.Send( p.Data(), p.Size() ); -} - -} // namespace osc - -#ifndef NO_OSC_TEST_MAIN - -int main(int argc, char* argv[]) -{ - if( argc >= 2 && std::strcmp( argv[1], "-h" ) == 0 ){ - std::cout << "usage: OscSendTests [hostname [port]]\n"; - return 0; - } - - const char *hostName = "localhost"; - int port = 7000; - - if( argc >= 2 ) - hostName = argv[1]; - - if( argc >= 3 ) - port = std::atoi( argv[2] ); - - - IpEndpointName host( hostName, port ); - - char hostIpAddress[ IpEndpointName::ADDRESS_STRING_LENGTH ]; - host.AddressAsString( hostIpAddress ); - - std::cout << "sending test messages to " << hostName - << " (" << hostIpAddress << ") on port " << port << "...\n\n"; - - osc::RunSendTests( host ); -} - -#endif /* NO_OSC_TEST_MAIN */ diff --git a/tests/OscSendTests.h b/tests/OscSendTests.h deleted file mode 100644 index 7a564b2..0000000 --- a/tests/OscSendTests.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - oscpack -- Open Sound Control packet manipulation library - http://www.audiomulch.com/~rossb/oscpack - - Copyright (c) 2004-2005 Ross Bencina - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - Any person wishing to distribute modifications to the Software is - requested to send the modifications to the original developer so that - they can be incorporated into the canonical version. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR - ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -#ifndef INCLUDED_OSCSENDTESTS_H -#define INCLUDED_OSCSENDTESTS_H - -namespace osc{ - -void RunSendTests( unsigned long address, int port ); - -} // namespace osc - -#endif /* INCLUDED_OSCSENDTESTS_H */ diff --git a/tests/OscStreamFramingTest.cpp b/tests/OscStreamFramingTest.cpp new file mode 100644 index 0000000..0314566 --- /dev/null +++ b/tests/OscStreamFramingTest.cpp @@ -0,0 +1,124 @@ +/* + OscTap length-prefix stream-framing test. + + Exercises OscStreamDeframer's reassembly across every chunk boundary a byte + stream (TCP) can impose: coalesced packets, packets split across reads, the + 4-byte header itself split across reads, empty payloads, and the oversized- + frame DoS guard. This is the security-critical half of OSC-over-TCP, so it is + tested independently of any socket. +*/ + +#include "osc/OscStreamFraming.h" + +#include +#include +#include +#include + +static int failures = 0; +#define CHECK(cond) do{ if(!(cond)){ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++failures; } }while(0) + +// Collects emitted packets so we can compare against what was framed. +struct Collector { + std::vector packets; + void operator()( const char* p, uint32_t n ) { packets.emplace_back( p, p + n ); } +}; + +// Build a wire buffer: each input packet length-prefixed and concatenated. +static std::vector Wire( const std::vector& packets ) +{ + std::vector w; + for( const auto& pkt : packets ){ + char hdr[4]; + osctap::WriteOscStreamFrameHeader( hdr, (uint32_t)pkt.size() ); + w.insert( w.end(), hdr, hdr + 4 ); + w.insert( w.end(), pkt.begin(), pkt.end() ); + } + return w; +} + +// Feed `wire` to a fresh deframer in fixed-size chunks; return the emitted packets. +static std::vector DeframeInChunks( const std::vector& wire, std::size_t chunk ) +{ + osctap::OscStreamDeframer d; + Collector c; + bool ok = true; + for( std::size_t i = 0; i < wire.size(); i += chunk ){ + std::size_t n = (i + chunk <= wire.size()) ? chunk : (wire.size() - i); + ok = d.Consume( wire.data() + i, n, c ) && ok; + } + if( !ok ) c.packets.clear(); // signal protocol error to the caller's check + return c.packets; +} + +int main() +{ + const std::vector packets = { + std::string( "/a\0\0,i\0\0\0\0\0\1", 12 ), // a small "message-ish" blob + std::string( "hello" ), + std::string( 1000, 'x' ), // spans typical read sizes + std::string( "" ), // empty payload (valid frame) + std::string( "/z" ), + }; + const std::vector wire = Wire( packets ); + + // Feed the same stream at every chunk size from 1 byte up to the whole buffer: + // reassembly must be invariant to how the bytes are split. + for( std::size_t chunk = 1; chunk <= wire.size(); ++chunk ){ + const std::vector got = DeframeInChunks( wire, chunk ); + bool same = ( got.size() == packets.size() ); + for( std::size_t i = 0; same && i < got.size(); ++i ) + same = ( got[i] == packets[i] ); + if( !same ){ + std::printf( "FAIL: mismatch at chunk size %zu (got %zu packets)\n", chunk, got.size() ); + ++failures; + } + } + + // Coalesced: the whole stream in one Consume() yields every packet in order. + { + osctap::OscStreamDeframer d; Collector c; + CHECK( d.Consume( wire.data(), wire.size(), c ) ); + CHECK( c.packets.size() == packets.size() ); + } + + // Two deframers fed the same split stream agree (no shared/static state). + { + CHECK( DeframeInChunks( wire, 3 ) == DeframeInChunks( wire, 5 ) ); + } + + // Oversized-frame DoS guard: a header announcing > maxFrameSize -> Consume + // returns false, and does so even when only the header has arrived. + { + osctap::OscStreamDeframer d( 16 ); // tiny cap + char hdr[4]; + osctap::WriteOscStreamFrameHeader( hdr, 1u << 20 ); // 1 MiB announced + Collector c; + CHECK( d.Consume( hdr, 4, c ) == false ); + CHECK( c.packets.empty() ); + } + + // A frame exactly at the cap is accepted; one byte over is rejected. + { + osctap::OscStreamDeframer ok( 8 ), over( 8 ); + Collector c1, c2; + const std::vector w8 = Wire( { std::string( 8, 'a' ) } ); + const std::vector w9 = Wire( { std::string( 9, 'b' ) } ); + CHECK( ok.Consume( w8.data(), w8.size(), c1 ) == true ); + CHECK( c1.packets.size() == 1 && c1.packets[0].size() == 8 ); + CHECK( over.Consume( w9.data(), w9.size(), c2 ) == false ); + } + + // FrameOscPacket convenience: correct framing + capacity check. + { + char out[16]; + std::size_t n = osctap::FrameOscPacket( "hey", 3, out, sizeof(out) ); + CHECK( n == 7 ); + CHECK( osctap::ToUInt32( out ) == 3 ); + CHECK( std::memcmp( out + 4, "hey", 3 ) == 0 ); + CHECK( osctap::FrameOscPacket( "hey", 3, out, 6 ) == 0 ); // 4+3 > 6 -> no fit + } + + if( failures == 0 ) std::printf( "OscStreamFramingTest: OK\n" ); + return failures == 0 ? 0 : 1; +} diff --git a/tests/OscTcpTest.cpp b/tests/OscTcpTest.cpp new file mode 100644 index 0000000..915b5a4 --- /dev/null +++ b/tests/OscTcpTest.cpp @@ -0,0 +1,131 @@ +/* + OscTap OSC-over-TCP loopback test (POSIX). + + Drives the real TcpListeningReceiveSocket / TcpTransmitSocket over loopback: + a server runs on one thread; a client connects and sends several OSC messages, + including a large one that spans multiple TCP segments (exercising the + per-connection deframer's reassembly through actual sockets, not just a unit + test). Verifies every packet arrives intact and in order, then stops the + server with AsynchronousBreak() from the main thread. +*/ + +#include "ip/TcpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscPacketListener.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class RecordingListener : public osctap::OscPacketListener { +public: + std::atomic count{ 0 }; + std::vector addresses; // written by server thread, read after join + std::vector sizes; // payload size hint per message + +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, const osctap::IpEndpointName& ) override + { + addresses.emplace_back( m.AddressPattern() ); + int sz = 0; + auto a = m.ArgumentsBegin(); + if( a != m.ArgumentsEnd() ){ + if( a->IsInt32() ) sz = a->AsInt32Unchecked(); + else if( a->IsString() ) sz = (int)std::strlen( a->AsStringUnchecked() ); + } + sizes.push_back( sz ); + count.fetch_add( 1, std::memory_order_relaxed ); + } +}; + +int failures = 0; +#define CHECK(c) do{ if(!(c)){ std::printf("FAIL line %d: %s\n", __LINE__, #c); ++failures; } }while(0) + +} // namespace + +int main() +{ + RecordingListener listener; + + // Bind the server to an OS-assigned loopback port, then discover it. A bind + // failure means the environment forbids loopback networking (some sandboxed + // CI) -> skip rather than fail, matching OscUdpTest / OscConcurrencyTest. + osctap::TcpListeningReceiveSocket* serverPtr = nullptr; + try { + serverPtr = new osctap::TcpListeningReceiveSocket( + osctap::IpEndpointName( 127, 0, 0, 1, 0 ), &listener ); + } catch( const std::exception& e ) { + std::printf( "OscTcpTest: SKIP (cannot bind loopback TCP: %s)\n", e.what() ); + return 0; + } + osctap::TcpListeningReceiveSocket& server = *serverPtr; + const int port = server.LocalEndpointFor( osctap::IpEndpointName( 127, 0, 0, 1, 0 ) ).port; + CHECK( port > 0 ); + + std::thread serverThread( [&]{ server.Run(); } ); + + // Client: connect and send four messages. The last is large enough to be + // split across TCP segments, forcing the server-side deframer to reassemble. + const std::string big( 4000, 'x' ); + bool sent = false; + try { + osctap::TcpTransmitSocket client( osctap::IpEndpointName( 127, 0, 0, 1, port ) ); + char buf[8192]; + { + osctap::OutboundPacketStream p( buf, sizeof(buf) ); + p << osctap::BeginMessage( "/m1" ) << (int32_t)11 << osctap::EndMessage(); + client.Send( p.Data(), p.Size() ); + } + { + osctap::OutboundPacketStream p( buf, sizeof(buf) ); + p << osctap::BeginMessage( "/m2" ) << (int32_t)22 << osctap::EndMessage(); + client.Send( p.Data(), p.Size() ); + } + { + osctap::OutboundPacketStream p( buf, sizeof(buf) ); + p << osctap::BeginMessage( "/m3" ) << "hello" << osctap::EndMessage(); + client.Send( p.Data(), p.Size() ); + } + { + osctap::OutboundPacketStream p( buf, sizeof(buf) ); + p << osctap::BeginMessage( "/big" ) << big.c_str() << osctap::EndMessage(); + client.Send( p.Data(), p.Size() ); + } + sent = true; + } catch( const std::exception& e ) { + // Connect/send denied by the environment -> skip (not a library failure). + std::printf( "OscTcpTest: SKIP (loopback TCP send unavailable: %s)\n", e.what() ); + } + + // Wait (bounded) for all four to arrive, then stop the server. + if( sent ){ + for( int i = 0; i < 500 && listener.count.load() < 4; ++i ) + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + + server.AsynchronousBreak(); + serverThread.join(); + delete serverPtr; + + if( !sent ) + return 0; // skipped: send path unavailable in this environment + + CHECK( listener.count.load() == 4 ); + if( listener.addresses.size() == 4 ){ + CHECK( listener.addresses[0] == "/m1" && listener.sizes[0] == 11 ); + CHECK( listener.addresses[1] == "/m2" && listener.sizes[1] == 22 ); + CHECK( listener.addresses[2] == "/m3" && listener.sizes[2] == 5 ); // strlen("hello") + CHECK( listener.addresses[3] == "/big" && listener.sizes[3] == 4000 ); // reassembled + } + + if( failures == 0 ) + std::printf( "OscTcpTest: OK (4 packets over TCP, incl. a reassembled 4000-byte message)\n" ); + return failures == 0 ? 0 : 1; +} diff --git a/tests/OscUdpTest.cpp b/tests/OscUdpTest.cpp new file mode 100644 index 0000000..12224de --- /dev/null +++ b/tests/OscUdpTest.cpp @@ -0,0 +1,112 @@ +/* + OscTap OSC-over-UDP loopback test (POSIX). + + Asserting counterpart to the best-effort packet in OscConcurrencyTest: it binds + a real UdpListeningReceiveSocket on loopback, sends several OSC messages through + a UdpTransmitSocket, and verifies each arrives and decodes correctly. The TCP + path already had such a test (OscTcpTest); this gives the UDP socket path the + same asserting coverage. + + Resilient to sandboxed CI that forbids loopback UDP (some macOS runners throw + on connect/send): if the environment denies networking before any data flows, + the test SKIPs (prints a notice, returns success) rather than failing. +*/ + +#include "ip/UdpSocket.h" +#include "ip/IpEndpointName.h" +#include "osc/OscPacketListener.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class RecordingListener : public osctap::OscPacketListener { +public: + std::atomic count{ 0 }; + std::vector addresses; // written by receive thread, read after join + std::vector values; + +protected: + void ProcessMessage( const osctap::ReceivedMessage& m, const osctap::IpEndpointName& ) override + { + addresses.emplace_back( m.AddressPattern() ); + int v = 0; + auto a = m.ArgumentsBegin(); + if( a != m.ArgumentsEnd() ){ + if( a->IsInt32() ) v = a->AsInt32Unchecked(); + else if( a->IsString() ) v = (int)std::strlen( a->AsStringUnchecked() ); + } + values.push_back( v ); + count.fetch_add( 1, std::memory_order_relaxed ); + } +}; + +int failures = 0; +#define CHECK(c) do{ if(!(c)){ std::printf("FAIL line %d: %s\n", __LINE__, #c); ++failures; } }while(0) + +} // namespace + +int main() +{ + RecordingListener listener; + + // Bind the receiver to an OS-assigned loopback port. A bind failure here means + // the environment forbids loopback networking -> skip. + osctap::UdpListeningReceiveSocket* receiver = nullptr; + try { + receiver = new osctap::UdpListeningReceiveSocket( + osctap::IpEndpointName( 127, 0, 0, 1, 0 ), &listener ); + } catch( const std::exception& e ) { + std::printf( "OscUdpTest: SKIP (cannot bind loopback UDP: %s)\n", e.what() ); + return 0; + } + const int port = receiver->LocalPort(); + + std::thread runner( [&]{ receiver->Run(); } ); + + bool sent = false; + try { + osctap::UdpTransmitSocket sender( osctap::IpEndpointName( 127, 0, 0, 1, port ) ); + char buf[256]; + const char* addrs[] = { "/u1", "/u2", "/u3" }; + const int ints[] = { 11, 22, 33 }; + for( int i = 0; i < 3; ++i ){ + osctap::OutboundPacketStream p( buf, sizeof(buf) ); + p << osctap::BeginMessage( addrs[i] ) << (int32_t)ints[i] << osctap::EndMessage(); + sender.Send( p.Data(), p.Size() ); + } + sent = true; + } catch( const std::exception& e ) { + std::printf( "OscUdpTest: SKIP (loopback UDP send unavailable: %s)\n", e.what() ); + } + + if( sent ){ + for( int i = 0; i < 500 && listener.count.load() < 3; ++i ) + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + + receiver->AsynchronousBreak(); + runner.join(); + delete receiver; + + if( !sent ) + return 0; // skipped: send path unavailable in this environment + + CHECK( listener.count.load() == 3 ); + if( listener.addresses.size() == 3 ){ + CHECK( listener.addresses[0] == "/u1" && listener.values[0] == 11 ); + CHECK( listener.addresses[1] == "/u2" && listener.values[1] == 22 ); + CHECK( listener.addresses[2] == "/u3" && listener.values[2] == 33 ); + } + + if( failures == 0 ) + std::printf( "OscUdpTest: OK (3 packets over UDP loopback)\n" ); + return failures == 0 ? 0 : 1; +} diff --git a/tests/OscValidateTest.cpp b/tests/OscValidateTest.cpp new file mode 100644 index 0000000..d52806c --- /dev/null +++ b/tests/OscValidateTest.cpp @@ -0,0 +1,129 @@ +/* + OscTap non-throwing-validation test. + + Verifies that osctap::TryValidatePacket() (the non-throwing gate for untrusted + input on no-exceptions / freestanding builds) agrees with the throwing parse + path: for any input, TryValidatePacket() returns nullptr (valid) iff a full + recursive read of that packet would NOT throw. This differential check is what + makes the single-source refactor trustworthy -- the validator and the throwing + constructors share one implementation, and this proves they stay in lock-step. + + Built on the hosted matrix (exceptions on) so it can use try/catch as the + oracle. The freestanding harness separately proves TryValidatePacket() rejects + malformed input by *returning* instead of aborting. +*/ + +#include "osc/OscReceivedElements.h" +#include "osc/OscOutboundPacketStream.h" + +#include +#include +#include + +using osctap::osc_bundle_element_size_t; + +static int failures = 0; +#define CHECK(cond) do{ if(!(cond)){ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++failures; } }while(0) + +// Oracle: does a *full recursive read* of this packet throw? Mirrors exactly what +// TryValidatePacket promises is safe -- construct, and recurse through bundles and +// messages, touching everything the validator walks. +static void FullRead( const char* data, osc_bundle_element_size_t size ) +{ + osctap::ReceivedPacket p( data, size ); + if( p.IsBundle() ){ + osctap::ReceivedBundle b( p ); + for( auto i = b.ElementsBegin(); i != b.ElementsEnd(); ++i ){ + if( i->IsBundle() ) { + osctap::ReceivedBundle nested( *i ); + (void)nested.ElementCount(); + for( auto j = nested.ElementsBegin(); j != nested.ElementsEnd(); ++j ){ + if( !j->IsBundle() ){ osctap::ReceivedMessage m(*j); (void)m.ArgumentCount(); } + } + } else { + osctap::ReceivedMessage m( *i ); + (void)m.ArgumentCount(); + } + } + } else { + osctap::ReceivedMessage m( p ); + (void)m.ArgumentCount(); + } +} + +static bool ThrowingPathRejects( const char* data, osc_bundle_element_size_t size ) +{ + try { FullRead( data, size ); return false; } + catch( const osctap::Exception& ) { return true; } +} + +// Assert the non-throwing gate and the throwing path agree on this input. +static void Differential( const char* label, const std::vector& buf ) +{ + const osc_bundle_element_size_t n = (osc_bundle_element_size_t)buf.size(); + const bool rejects = ThrowingPathRejects( buf.data(), n ); + const bool gateRejects = ( osctap::TryValidatePacket( buf.data(), n ) != nullptr ); + if( rejects != gateRejects ) + std::printf( "FAIL [%s]: throwing-path rejects=%d but gate rejects=%d\n", + label, (int)rejects, (int)gateRejects ), ++failures; +} + +static std::vector BuildMessage() +{ + char tmp[256]; + osctap::OutboundPacketStream p( tmp, sizeof(tmp) ); + p << osctap::BeginMessage( "/x" ) << (int32_t)1 << (float)2.5f << "hi" << true + << osctap::EndMessage(); + return std::vector( p.Data(), p.Data() + p.Size() ); +} + +static std::vector BuildBundle() +{ + char tmp[256]; + osctap::OutboundPacketStream p( tmp, sizeof(tmp) ); + p << osctap::BeginBundleImmediate() + << osctap::BeginMessage( "/a" ) << (int32_t)7 << osctap::EndMessage() + << osctap::BeginMessage( "/b" ) << "yo" << osctap::EndMessage() + << osctap::EndBundle(); + return std::vector( p.Data(), p.Data() + p.Size() ); +} + +int main() +{ + // --- valid inputs: both paths accept --- + const std::vector msg = BuildMessage(); + const std::vector bun = BuildBundle(); + CHECK( osctap::TryValidatePacket( msg.data(), (osc_bundle_element_size_t)msg.size() ) == nullptr ); + CHECK( osctap::TryValidatePacket( bun.data(), (osc_bundle_element_size_t)bun.size() ) == nullptr ); + Differential( "valid message", msg ); + Differential( "valid bundle", bun ); + + // --- malformed inputs: both paths must reject, identically --- + // truncations (every prefix that isn't the whole thing) + for( std::size_t k = 1; k < msg.size(); ++k ) + Differential( "msg prefix", std::vector( msg.begin(), msg.begin() + k ) ); + for( std::size_t k = 1; k < bun.size(); ++k ) + Differential( "bun prefix", std::vector( bun.begin(), bun.begin() + k ) ); + + // corrupt the type tag to an unknown tag + { auto b = msg; // find the ',' type-tag start and poke a bogus tag after it + for( std::size_t i = 0; i + 1 < b.size(); ++i ) if( b[i] == ',' ){ b[i+1] = 'Q'; break; } + Differential( "unknown type tag", b ); } + + // claim a huge blob/forge: flip a size-ish word in the bundle's element size + { auto b = bun; if( b.size() > 19 ){ b[16] = '\x7F'; b[17] = '\xFF'; } // element size huge + Differential( "bundle element size overflow", b ); } + + // non-multiple-of-4 total size + { auto b = msg; b.push_back( 'x' ); Differential( "size not mult of 4", b ); } + + // --- explicit checks: trivial bad size + the nesting bound --- + CHECK( osctap::TryValidatePacket( "\0\0\0", 3 ) != nullptr ); // size 3: rejected + // The top-level bundle counts as the first nesting level, so maxDepth 0 rejects + // it outright; the default depth accepts the same (shallow) bundle. + CHECK( osctap::TryValidatePacket( bun.data(), (osc_bundle_element_size_t)bun.size(), 0 ) != nullptr ); + CHECK( osctap::TryValidatePacket( bun.data(), (osc_bundle_element_size_t)bun.size() ) == nullptr ); + + if( failures == 0 ) std::printf( "OscValidateTest: OK (gate agrees with throwing path)\n" ); + return failures == 0 ? 0 : 1; +} diff --git a/tests/Win32SocketSmoke.cpp b/tests/Win32SocketSmoke.cpp new file mode 100644 index 0000000..764571b --- /dev/null +++ b/tests/Win32SocketSmoke.cpp @@ -0,0 +1,74 @@ +/* + OscTap win32 socket-backend compile/link smoke (Windows-only). + + The win32 socket backend (ip/win32/UdpSocket.h, NetworkingUtils.h) had no compiled + coverage: the POSIX demos/tests are gated off Windows, and nothing else pulled + the win32 sockets into a build. This TU closes that gap. It is built by the + existing windows-latest CI legs via the WIN32-gated CMake target, so the + getaddrinfo port and the rest of the win32 backend now actually compile + link + on every push. + + It is a *compile/link* smoke, not a runtime network test: the socket paths are + instantiated and linked (so a hard error or missing symbol fails the build) but + are guarded behind a condition that is never taken, so CI needs no live network. + Only IpEndpointName string formatting actually runs. +*/ + +#include "ip/UdpSocket.h" +#include "ip/TcpSocket.h" +#include "ip/IpEndpointName.h" +#include "ip/PacketListener.h" +#include "osc/OscOutboundPacketStream.h" + +#include + +namespace { + +class NullListener : public osctap::PacketListener { +public: + void ProcessPacket( const char *, int, const osctap::IpEndpointName & ) override {} +}; + +// Defined and ODR-used (its address is taken below) but never executed on CI. +// Forces the win32 UdpSocket / SocketReceiveMultiplexer template members -- the +// ctor, Bind/SendTo/Send, Run/AsynchronousBreak, and the getaddrinfo-based +// GetHostByName -- to compile and link. +void exercise_win32_backend() +{ + osctap::IpEndpointName ep( "127.0.0.1", 9000 ); // -> GetHostByName -> getaddrinfo + + osctap::UdpTransmitSocket tx( ep ); + char buf[8] = { 0 }; + tx.Send( buf, sizeof(buf) ); + + NullListener listener; + osctap::UdpListeningReceiveSocket rx( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, 9000 ), &listener ); + rx.AsynchronousBreak(); + + // TCP backend (ip/win32/TcpSocket.h): client Send + connection-aware server. + osctap::TcpTransmitSocket tcpTx( ep ); + tcpTx.Send( buf, sizeof(buf) ); + + osctap::TcpListeningReceiveSocket tcpRx( + osctap::IpEndpointName( osctap::IpEndpointName::ANY_ADDRESS, 9001 ), &listener ); + tcpRx.Run(); + tcpRx.AsynchronousBreak(); +} + +} // namespace + +int main( int argc, char ** /*argv*/ ) +{ + // Actually runs (no network): exercise IpEndpointName formatting. + char s[ osctap::IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH ]; + osctap::IpEndpointName( 127, 0, 0, 1, 9000 ).AddressAndPortAsString( s ); + std::printf( "win32 socket smoke: %s\n", s ); + + // ODR-use the socket exercise so it links, but never call it on CI. + void (*fn)() = &exercise_win32_backend; + if( argc == 0x7fffffff ) // never true; opaque to the optimiser + fn(); + + return 0; +}