Skip to content

rmi: unify sentinel arg reading + add frame-by-frame Decoder - #19

Closed
phith0n wants to merge 1 commit into
masterfrom
rmi-decoder-frame-by-frame
Closed

phith0n wants to merge 1 commit into
masterfrom
rmi-decoder-frame-by-frame

Conversation

@phith0n

@phith0n phith0n commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Drop FromStream's Registry-only restriction: non-Registry CallMessage and ReturnMessage now parse in streaming mode via sentinel fallback. Registry Calls keep their exact-count fast path, so the common case still returns without peeking past the last arg.
  • Add rmi.Decoder — an idiomatic frame-by-frame API (NewDecoder + Opening + Next) for live net.Conn consumers who want to apply SetReadDeadline between frames.
  • Delete the streaming bool parameter chain (parseTransmission → readMessage → readCall / readReturn / readCallArgs). Each frame picks its arg strategy from its own header, not from the input source.

Why

Before this change, any live-TCP consumer of a non-Registry remote — or any consumer of ReturnMessage — had to buffer the whole conversation and call FromBytes, which defeats the point of a streaming parser. The unified sentinel approach + per-frame Decoder API solves that without losing the Registry non-blocking fast path.

The one genuinely new tradeoff is documented up front: non-Registry Calls and Returns block on a terminating peek until the next frame arrives or the reader returns an error. That's protocol-inherent (JRMP has no length prefix inside Call/Return), and the Decoder's contract is to push deadline management up to the caller via conn.SetReadDeadline, same pattern mature HTTP/WebSocket servers use.

Test plan

  • go test ./... — all 80+ existing tests pass, plus new coverage:
    • TestStream{NonRegistryCall,WrongInterfaceHash,UnknownRegistryOp,ReturnWithPayload,ReturnVoid} — replacements for the four old TestStreamRejects* tests
    • TestDecoderFrameByFrame, TestDecoderSkipsOpeningWhenOmitted, TestDecoderOpeningTwiceErrors, TestDecoderOpeningAfterNextErrors, TestDecoderBareCaptureNoOpening
    • TestDecoderRegistryCallReturnsWithoutPeek — feeds exact Registry Call bytes into an io.Pipe that is never closed; if the parser peeks past the last arg it deadlocks and a 2s timeout fires the failure. Proves the exact-count fast path.
    • TestDecoderNonRegistryCallBlocksOnSentinelPeek — asserts the sentinel peek does block while the pipe is open (150ms budget) and unblocks correctly once the writer closes. Documents the blocking contract.
  • go vet ./...
  • golangci-lint run (local version too new for current config; CI will validate)
  • go build ./...

🤖 Generated with Claude Code

The previous FromStream refused non-Registry Calls and all ReturnData
because it needed exact arg counts to avoid blocking at frame boundaries.
That worked, but left a usability gap: any live-TCP consumer of non-
Registry remotes had to buffer the whole conversation and fall back to
FromBytes, which defeats the point of a streaming parser.

This change unifies the arg/payload strategy and adds a proper frame-by-
frame API:

- readCallArgs now picks strategy from the frame's own header, not from
  the input source. Registry fast path (ObjID + interface hash + known
  op) still uses exact-count — no peek past the last arg, so Registry
  Calls over a live net.Conn return as soon as their own bytes arrive.
  Every other Call falls back to the sentinel (peek next byte, stop on
  non-TC_* / EOF).
- readReturn drops the streaming-refusal and always uses the sentinel
  for its 0/1-payload count. ReturnData now parses in streaming mode.
- The `streaming bool` parameter is removed from parseTransmission,
  readMessage, readCall, readReturn, readCallArgs. FromStream is a thin
  wrapper over the same parseTransmission that FromBytes uses.

- New rmi.Decoder (rmi/decoder.go) reads one frame per Next() call, with
  an optional Opening() to consume the handshake prefix. This is the
  idiomatic API for live net.Conn consumers: the caller applies
  SetReadDeadline between Next() calls to bound how long to wait for
  the next frame. Non-Registry Calls block on the sentinel's terminating
  peek — documented as a protocol-inherent tradeoff, not a bug.

Tests:
- Four TestStreamRejects* tests replaced with positive counterparts that
  exercise the new sentinel path on bytes.Reader input.
- decoder_test.go adds 7 tests including two blocking-contract tests:
  TestDecoderRegistryCallReturnsWithoutPeek proves Registry Calls return
  without peeking past the last arg (uses a never-closed io.Pipe with a
  2s deadlock guard); TestDecoderNonRegistryCallBlocksOnSentinelPeek
  asserts the expected blocking behavior for non-Registry Calls and that
  the parser unblocks correctly on io.EOF.

CLAUDE.md's rmi section rewritten: "Streaming scope" (the list of what
FromStream rejected) is replaced by "Arg-reading strategy" (how each
frame picks its own reader) and "Live-TCP ergonomics" (Decoder usage
with SetReadDeadline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@phith0n

phith0n commented Apr 18, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #NEW — this PR's direction ('support all frame types in streaming mode') turned out to be the wrong call. The new PR takes the opposite tack: restrict the module to java.rmi.registry.Registry traffic for clarity, and ship the frame-by-frame Decoder API without the non-Registry sentinel baggage.

@phith0n phith0n closed this Apr 18, 2026
@phith0n
phith0n deleted the rmi-decoder-frame-by-frame branch April 18, 2026 15:54
@phith0n

phith0n commented Apr 18, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #20.

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.

1 participant