Skip to content

Phase 2 (freestanding, aarch64/Pi 5, OSC-over-TCP, integration) + Phase 1 tails + testing & docs hardening#21

Merged
tap merged 16 commits into
mainfrom
claude/phase-2-prep-h8q9mz
Jun 28, 2026
Merged

Phase 2 (freestanding, aarch64/Pi 5, OSC-over-TCP, integration) + Phase 1 tails + testing & docs hardening#21
tap merged 16 commits into
mainfrom
claude/phase-2-prep-h8q9mz

Conversation

@tap

@tap tap commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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

  • Non-throwing validationReceivedMessage/Bundle::TryInit + the recursive TryValidatePacket() gate, single-sourced with the throwing path. Lets a no-exceptions (freestanding) build reject untrusted packets instead of aborting. Differential test (OscValidateTest).
  • aarch64 / Raspberry Pi 5aarch64-qemu CI job (cross-compile + qemu-user run).
  • OSC over TCP (issue TCP Support #14, v1) — length-prefix framing codec (OscStreamFraming.h, fuzzed via fuzz_deframe), TcpTransmitSocket + multi-connection TcpListeningReceiveSocket (posix + win32), TCP_NODELAY, frame-size cap. Real loopback test (OscTcpTest) incl. a segment-spanning message. docs/OSC_OVER_TCP.md.
  • Android NDK — JNI bridge + Kotlin facade (android/); Pi 5 ⇄ Pico 2W ⇄ Android tutorial; runnable UDP + TCP demos (demos/).

Phase 1 tails

Testing & docs hardening

  • Asserting UDP + TCP loopback tests — writing them found & fixed two real bugs: UdpSocket::Bind() never read back the OS-assigned port (LocalPort() returned 0), and the TCP backend headers weren't self-contained (IpEndpointName incomplete on win32).
  • win32 runtime-tested under Wine (win32-wine CI job) — the win32 UDP + TCP backends now have real runtime coverage, not just compile/link.
  • Code coverage gatecoverage job (gcovr, ~85% lines, fails under 80%).
  • Removed dead interactive tests; fixed the stale examples/ (compile-checked).
  • New docs: Getting Started, API reference; archived the oscpack-era README/CHANGES/TODO to docs/legacy/; refreshed README.md/ROADMAP.md/STATUS.md.

Review + CI

The last commits are fixes from an independent review pass (win32 break-socket error handling, an FD_SETSIZE connection guard, include hygiene) and from real CI failures (win32 /W4 cleanup 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

claude added 16 commits June 27, 2026 20:21
…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
@tap tap merged commit f65e54a into main Jun 28, 2026
34 checks passed
@tap tap deleted the claude/phase-2-prep-h8q9mz branch June 28, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants