Phase 2 (freestanding, aarch64/Pi 5, OSC-over-TCP, integration) + Phase 1 tails + testing & docs hardening#21
Merged
Merged
Conversation
…orial Build out the Pi 5 <-> Pico 2W <-> Android OSC integration on top of the freestanding groundwork. Raspberry Pi 5 (aarch64 Linux): - demos/pi5_hub.cpp + demos/osc_send.cpp: a runnable OSC hub/router and a CLI sender over the POSIX UDP backend (OSCTAP_BUILD_DEMOS, POSIX-only). First compiled coverage of the ip/ networking layer. Verified end-to-end on loopback. - aarch64-qemu CI job: cross-compile with aarch64-linux-gnu-g++ and run the suite under qemu-user (CMAKE_CROSSCOMPILING_EMULATOR), proving the Pi-5-class build. Excludes OscConcurrencyTest (emulated threads/sockets are flaky). Verified locally: 98/98 unit tests + freestanding pass under qemu-aarch64. Android: - android/osctap_jni.cpp: JNI bridge (buildMessage/describe) exposing the core to Kotlin; compile-verified against jni.h. android/CMakeLists.txt (NDK) and android/OscTap.kt (facade + JVM UDP transport). Plus a JVM-OSC-library alternative in the tutorial. Library fix (found via the hub): - OutboundPacketStream now has operator<<(const char*) -> OSC string. Previously a runtime const char* bound to operator<<(bool) (pointer->bool standard conversion outranks the user-defined string_view conversion) and was silently serialized as a boolean; only string literals worked. Freestanding-safe; OscFreestandingTest sends a runtime const char* to guard it. Docs: - docs/INTEGRATION_PI5_PICO_ANDROID.md: end-to-end tutorial (OSC contract, ports, per-node code, bring-up order, troubleshooting). - ROADMAP.md / docs/STATUS.md updated; new landmines for the const char* overload, the now-compiled ip/ layer, and the aarch64/demos/Android surfaces. Verified: hosted suite (C++17/20, GCC/Clang/MSVC, ASan/UBSan) green; freestanding green (GCC+Clang); aarch64 suite green under qemu; demos run on loopback; JNI bridge compiles against jni.h. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
…allString.h Knock out the cosmetic Phase 1 tails (OSS-Fuzz #7 excluded): - Rename the internal include guards INCLUDED_OSCPACK_* -> INCLUDED_OSCTAP_* across the 13 osctap/ headers. The <oscpack/...> include *paths* are unchanged (redirect shim tree); only the guard macro names moved. CompatIncludeShim still green. - Close audit finding #6's "empty SmallString.h": the outbound stream no longer needs it (uses std::string_view + the const char* overload), so drop the dead include and replace the zero-byte file with a guarded, documented no-op. The file and its <oscpack/...> shim are retained so the public path keeps resolving. Verified: hosted suite + CompatIncludeShim green; freestanding green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
Make the freestanding/no-exceptions build safe against malformed packets. The parser validates by throwing, and with exceptions off OSCTAP_THROW aborts, so a single bad datagram was a remote DoS. Add a non-throwing validation path: - ReceivedMessage::TryInit / ReceivedBundle::TryInit: the existing Init() logic refactored to RETURN a static error string (nullptr == ok) instead of throwing, setting the boundary members on success. The throwing Init() now delegates (Init = TryInit + OSCTAP_THROW on error), and ReceivedPacket grows a matching ValidateSizeNoThrow(). One source of truth -> the throwing and non-throwing paths cannot drift. - ReceivedMessage/ReceivedBundle gain a default ctor + static Validate() for the default-construct-then-TryInit pattern. (ReceivedMessage::size_ is no longer const.) - osctap::TryValidatePacket(data, size): recursive, non-throwing, non-allocating whole-packet gate (message, or bundle whose elements are all well-formed), bounded against deep bundle nesting. Returns nullptr iff the packet can be constructed AND read in full without any OSCTAP_THROW firing. Tests: - tests/OscValidateTest.cpp (hosted): differential check that the gate agrees with the throwing path across a valid message/bundle, every truncation, and several corruptions. Wired into ctest. - OscFreestandingTest extended: under -fno-exceptions, malformed input returns an error from TryValidatePacket instead of aborting; default-ctor + TryInit parses a valid message. Verified: hosted suite + new differential test green (incl. ASan/UBSan); test4/ test5 unchanged (throwing behaviour identical); freestanding green on GCC+Clang; aarch64 suite green under qemu. Pico guide / ROADMAP / STATUS updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
The ip/win32 socket backend had no compiled coverage (the POSIX demos/tests are gated off Windows), so the getaddrinfo port and the rest only ever existed on paper. Add tests/Win32SocketSmoke.cpp, a WIN32-gated target the existing windows-latest CI legs build: it instantiates and links the win32 UdpSocket/SocketReceiveMultiplexer templates and the getaddrinfo-based GetHostByName (so a hard error or missing symbol fails the build), while guarding the actual socket calls behind a never-taken branch so CI needs no live network. Built with /WX- (warnings-as-errors off for this TU only): it brings the win32 sockets into the build for the first time, and gating CI on their previously unexercised /W4 surface is a separate cleanup. Verified locally by cross-compiling with MinGW-w64 (-Wall -Wextra, clean): the backend compiles and links to a PE32+ executable (winsock2 / getaddrinfo / timeGetTime all resolved). STATUS.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
The transport-agnostic foundation for OSC over TCP. A byte stream has no message
boundaries, so the receiver must reassemble packets from arbitrarily chunked
reads; this is the one genuinely new algorithm, and it is security-critical
(the length prefix is attacker-controlled).
- osc/OscStreamFraming.h (header-only, + oscpack shim):
* WriteOscStreamFrameHeader / FrameOscPacket: trivial length-prefix encoder.
* OscStreamDeframer: streaming decoder. Reassembles complete packets across
any read boundary, dispatches whole-in-a-chunk packets in place (no copy),
and caps the frame size (default 64 KiB) so a hostile prefix can't make it
buffer unbounded data (cf. the blob-size discipline from #1/#4). Non-throwing.
- tests/OscStreamFramingTest.cpp: reassembly is invariant across every chunk size
from 1 byte to the whole stream; coalesced packets; header split across reads;
empty payloads; the oversized-frame guard and the exact cap boundary.
- fuzz/fuzz_deframe.cpp + fuzz/corpus_deframe/: feeds bytes through the deframer in
pseudo-random chunks and runs each frame through the parser (the full TCP receive
path). Wired into CMake (both fuzzers via a foreach), the CI fuzz-smoke job, and
ClusterFuzzLite. 200k standalone ASan/UBSan mutations: no crash.
SLIP framing is intentionally deferred (length-prefix is the de-facto standard).
Sockets land next.
Verified: hosted suite 6/6; framing test green; deframer fuzzer ASan/UBSan-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
…ion server)
The OSC-over-TCP socket layer on top of the framing codec.
- ip/posix/TcpSocket.h:
* TcpTransmitSocket (client): connect + framed Send() that loops over partial
writes; TCP_NODELAY on (Nagle off); SIGPIPE suppressed (MSG_NOSIGNAL /
SO_NOSIGPIPE).
* TcpListeningReceiveSocket (server): single-threaded, select()-based, and
connection-aware -- accept()s any number of clients, keeps a per-connection
OscStreamDeframer, and dispatches each complete packet to a PacketListener,
with the peer endpoint. Self-pipe Break()/AsynchronousBreak() mirroring the
UDP multiplexer; closes + reaps connections on EOF/error and drops any peer
that sends an over-cap frame.
- ip/TcpSocket.h facade (+ oscpack shims for it and OscStreamFraming.h): selects
the per-platform backend and exposes osctap::TcpTransmitSocket /
TcpListeningReceiveSocket. (Concrete per-platform classes + `using`, rather than
the UDP facade's template-over-Implementation -- lighter for v1.)
- tests/OscTcpTest.cpp: real loopback -- server on a thread, client sends four
messages incl. a 4000-byte one that spans TCP segments, so the deframer
reassembles through actual sockets. POSIX-only (threads+sockets), and also built
under the TSan job (Run() vs AsynchronousBreak() on the TCP loop).
Verified: TCP test green, and clean under ASan/UBSan and TSan; full hosted suite
7/7; cross-compiles for aarch64.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
Mirror the posix TCP backend on Winsock so OSC-over-TCP works on Windows too. - ip/win32/TcpSocket.h: TcpTransmitSocket + TcpListeningReceiveSocket using SOCKET/closesocket/WSAStartup (via NetworkInitializer). The connection-aware server uses select() (Winsock supports select() over SOCKETs) with a self-connected loopback UDP "break" socket standing in for the posix self-pipe, so AsynchronousBreak() works cross-thread. Structurally identical to the posix backend (same deframer, same per-connection dispatch, same frame-size cap). - ip/TcpSocket.h facade already selects win32 vs posix. - tests/Win32SocketSmoke.cpp extended to instantiate + link the TCP types. Verified by cross-compiling the win32 smoke with MinGW-w64 (-Wall -Wextra, clean) to a PE32+ executable: the win32 UDP+TCP backends compile and link (winsock2 / TCP_NODELAY / select all resolved). Also built by the windows-latest MSVC CI legs. Caveat: not runtime-tested on Windows (no Windows runner / no Wine here) -- the posix backend is the runtime-verified reference. Linux suite still 7/7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
- docs/OSC_OVER_TCP.md: usage guide -- framing, server/client snippets, the frame-size cap / DoS note, the realtime split, interop (SuperCollider/Max/JUCE/ liblo osc.tcp), and what's deferred. - ROADMAP.md: TCP v1 marked landed under Phase 2. - docs/STATUS.md: landmines for the framing single-source, the cap, the concrete per-platform TCP facade, and the posix-runtime-tested / win32-compile-only split. Final verification across surfaces: hosted suite 7/7; freestanding green (GCC+ Clang); win32 UDP+TCP compile/link clean (MinGW); deframer fuzzer 200k mutations ASan/UBSan-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
OSC-over-TCP counterparts to the UDP pi5_hub / osc_send demos: - demos/tcp_server.cpp: listens, accepts multiple clients, prints each received message (address + typed args + peer); drops malformed frames instead of dying. - demos/tcp_send.cpp: connects and sends one message, typed-arg CLI mirroring osc_send (i:/f:/s:/T/F). Wired into OSCTAP_BUILD_DEMOS (POSIX); docs/OSC_OVER_TCP.md gains a "try it" section. Verified on loopback: three connections, all messages received and decoded correctly. Built -Werror-clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
…ples, drop dead tests
Addresses 3 of the 5 testing-gap items:
- Item 2 (UDP loopback): tests/OscUdpTest.cpp -- asserting client+server over UDP
loopback, the counterpart to OscTcpTest. Writing it surfaced a real bug:
UdpSocketImplementation::Bind() never read the OS-assigned port back, so
LocalPort() returned 0 after a port-0 bind (sender then targeted port 0 ->
nothing delivered; the old best-effort, never-asserted OscConcurrencyTest packet
hid it). Fixed in posix and win32 via getsockname() after bind. OscTcpTest +
OscUdpTest now SKIP gracefully if the environment forbids loopback networking.
- Item 1 (dead tests): delete tests/OscSendTests.{cpp,h} and OscReceiveTest.{cpp,h}
-- interactive, dead osc:: namespace, superseded by demos/ + the loopback tests.
- Item 5 (stale examples): fix examples/SimpleSend.cpp + SimpleReceive.cpp to
compile against the current API (and the oscpack:: alias); OscDump.cpp was
already current. All three are now compile-checked in CMake (not run).
Verified: full suite 8/8 (incl. OscUdpTest); win32 compiles via MinGW.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
… headers) The win32 socket backends were compile-only. Add a win32-wine CI job that cross-compiles OscUdpTest/OscTcpTest with MinGW-w64 and runs them under Wine, so the win32 UDP and TCP backends get real runtime coverage on every push. Doing this locally caught a latent bug: ip/win32/TcpSocket.h (and the posix one) used IpEndpointName before it was a complete type -- it only compiled when UdpSocket.h happened to be included first (as in Win32SocketSmoke). Including TcpSocket.h on its own (as OscTcpTest does) failed on win32. Fixed by making both TCP backend headers self-contained (explicit <osctap/ip/IpEndpointName.h>); the Wine job now guards against the regression. Verified locally under Wine 9.0: OscUdpTest and OscTcpTest both pass on win32 (incl. the reassembled 4000-byte TCP message). Linux suite still 8/8. Docs updated (win32 is now runtime-tested, not compile-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
Add a coverage CI job: instrument with gcov (-O0 --coverage), run the suite, and report library-header coverage with gcovr. Uses gcovr rather than lcov because lcov 2.0 mis-merges header-only inline functions across the many test binaries (>100% / 0% artifacts); gcovr handles it cleanly. Baseline ~85% lines / ~94% functions over osctap/. The job fails under 80% lines so coverage regressions are caught, and uploads a Cobertura XML for trend tooling. Known-uncovered noted in STATUS (hostname-resolution path; some socket error branches). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
Closes the documentation gaps from the testing/docs briefing: - docs/GETTING_STARTED.md: the missing "OSC 101 over UDP" guide -- build, send, receive, parse arguments (three styles), bundles, and untrusted-input handling. - docs/API.md: a curated reference of the public surface, grouped by header (outbound stream + manipulators, received elements + accessors, framing, dispatch, networking, the config seam). Hand-maintained. - docs/legacy/: moved the original oscpack README/CHANGES/TODO here (renamed oscpack-*.txt) with an explainer, de-cluttering the repo root while preserving the heritage files. - README.md: refreshed -- links the new guides, fixes the now-stale Layout table (adds demos/, android/, docs/; drops the deleted in-tree send/receive tests), updates the status blurb (TCP, validation gate, aarch64, Wine, ~85% coverage), and repoints the legacy link to docs/legacy/. Verified: all doc links resolve, no dangling references to the moved files; build + suite still 8/8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
…anup From an independent review pass of the branch: - win32/TcpSocket.h: CreateBreakSocket() now checks bind/getsockname/connect and throws on failure (an unchecked failure could leave AsynchronousBreak() unable to wake Run(), hanging the thread). [HIGH] - win32/TcpSocket.h: refuse new connections at the Winsock FD_SETSIZE (~64) limit instead of silently dropping them from the select() set (they'd stall forever). v1 targets a handful of connections; high counts are a future poll/epoll item. [HIGH] - OscReceivedElements.h: complete the truncated "bundle contents " error string. - posix/TcpSocket.h: include <cerrno>/<vector> explicitly (don't rely on transitive includes -- same class of issue as the win32 self-contained-header fix). - OscStreamFraming.h: drop a dead ternary; document that a zero-length frame is valid framing forwarded as an empty packet (the OSC layer rejects it). - ci.yml: drop the unnecessary i386 multiarch from the win32-wine job (the cross-build is 64-bit only). Re-verified: Linux suite 8/8; win32 OscUdpTest + OscTcpTest still pass under Wine; win32 smoke compiles clean (MinGW). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
…/WX-
The first real CI run on this branch (exercising the new jobs) flagged two issues:
- windows-latest MSVC build failed: wiring the examples in made the Windows legs
compile win32/UdpSocket.h with /WX, surfacing C4505 -- CompareScheduledTimerCalls
is `static` (internal linkage), so a transmit-only TU (SimpleSend, which never
instantiates the multiplexer's timer sort) defines it unreferenced. Made it
`inline` to match the posix backend (inline functions may be unused without
warning). Verified via MinGW -Wunused-function -Werror (the GCC analogue of
C4505), which now passes for SimpleSend. Also gate the three examples with /WX-
on MSVC (like Win32SocketSmoke), since they pull in the not-yet-/W4-audited
win32 socket backend; GCC/Clang keep -Werror.
- win32-wine job failed: Wine refuses WINEPREFIX under /tmp when /tmp isn't owned
by the runner user. Point it at ${{ runner.temp }}/wineprefix (runner-owned).
Re-verified locally: Linux suite 8/8; all three examples compile clean for win32
under MinGW -Wall -Wextra -Werror.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
The previous CI run got past C4505 but surfaced two more MSVC warnings-as-errors, and revealed that the per-target /WX- "suppression" never worked: CMake appends a linked INTERFACE target's options AFTER the target's own, so the INTERFACE /WX always wins (verified with a GCC -Werror/-Wno-error repro). So the win32 backend had to actually be cleaned (it's instantiated by Win32SocketSmoke regardless). - win32/UdpSocket.h: fix the C4456 shadow -- the inner `currentTimeMs` in the multiplexer Run() loop redeclared the outer; reuse it (assignment), matching the posix backend. - tests/OscValidateTest.cpp: fix C4310 -- `(char)0xFF` truncates a constant; use the `'\xFF'` char literal (the same fix STATUS notes was applied before; I'd reintroduced it). - CMakeLists: drop the ineffective /WX- from the examples and Win32SocketSmoke; the win32 surface now builds clean under the INTERFACE /W4 /WX. Verified: all win32 targets (smoke + 3 examples) compile clean under MinGW -Wall -Wextra -Wshadow -Werror (the MSVC /W4 proxy); Linux suite 8/8; win32 UDP+TCP still pass under Wine. STATUS updated (win32 is /W4-clean; the /WX- ordering trap documented). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A large, cohesive batch advancing OscTap through Phase 2 ("Reach"), finishing the Phase 1 tails, and hardening testing + docs. Builds on the merged Phase 2 prep (#20); 16 commits here, each independently green; all 17 CI jobs pass on the head commit. Kept as discrete commits — please Rebase and merge (don't squash).
Phase 2 — Reach
ReceivedMessage/Bundle::TryInit+ the recursiveTryValidatePacket()gate, single-sourced with the throwing path. Lets a no-exceptions (freestanding) build reject untrusted packets instead of aborting. Differential test (OscValidateTest).aarch64-qemuCI job (cross-compile + qemu-user run).OscStreamFraming.h, fuzzed viafuzz_deframe),TcpTransmitSocket+ multi-connectionTcpListeningReceiveSocket(posix + win32),TCP_NODELAY, frame-size cap. Real loopback test (OscTcpTest) incl. a segment-spanning message.docs/OSC_OVER_TCP.md.android/); Pi 5 ⇄ Pico 2W ⇄ Android tutorial; runnable UDP + TCP demos (demos/).Phase 1 tails
INCLUDED_OSCPACK_*→INCLUDED_OSCTAP_*; resolved the emptySmallString.h(audit Phase 1: TSan — concurrency test for SocketReceiveMultiplexer (Run vs AsynchronousBreak) #6); the win32 socket backend is now in the compiled surface and/W4-clean.Testing & docs hardening
UdpSocket::Bind()never read back the OS-assigned port (LocalPort()returned 0), and the TCP backend headers weren't self-contained (IpEndpointNameincomplete on win32).win32-wineCI job) — the win32 UDP + TCP backends now have real runtime coverage, not just compile/link.coveragejob (gcovr, ~85% lines, fails under 80%).examples/(compile-checked).README/CHANGES/TODOtodocs/legacy/; refreshedREADME.md/ROADMAP.md/STATUS.md.Review + CI
The last commits are fixes from an independent review pass (win32 break-socket error handling, an
FD_SETSIZEconnection guard, include hygiene) and from real CI failures (win32/W4cleanup once the backend entered the build; note the per-target/WX-override is ineffective — CMake appends INTERFACE flags last; Wine prefix ownership).CI
All 17 jobs green: Linux/macOS/Windows × GCC/Clang/MSVC × C++17/20, ASan/UBSan, RTSan, TSan, fuzz-smoke, freestanding (×2), aarch64-qemu, win32-wine, coverage.
Relates to issues #14, #15, #16, #17, #18.
🤖 Generated with Claude Code
https://claude.ai/code/session_01MMR6tmM7H3obaL7SX14bkJ
Generated by Claude Code