starpc 0.52.0 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +37 -8
  2. package/dist/echo/client-test.d.ts +1 -1
  3. package/dist/echo/client-test.js +110 -2
  4. package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
  5. package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
  6. package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
  7. package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
  8. package/dist/integration/cross-language/ts-client.js +50 -37
  9. package/dist/integration/cross-language/ts-server.js +1 -36
  10. package/dist/rpcstream/rpcstream.d.ts +5 -1
  11. package/dist/rpcstream/rpcstream.js +75 -28
  12. package/dist/rpcstream/rpcstream.test.d.ts +1 -0
  13. package/dist/rpcstream/rpcstream.test.js +92 -0
  14. package/dist/srpc/client-rpc.d.ts +1 -0
  15. package/dist/srpc/client-rpc.js +9 -0
  16. package/dist/srpc/client-rpc.test.d.ts +1 -0
  17. package/dist/srpc/client-rpc.test.js +50 -0
  18. package/dist/srpc/client.js +18 -4
  19. package/dist/srpc/common-rpc.d.ts +5 -0
  20. package/dist/srpc/common-rpc.js +37 -8
  21. package/dist/srpc/common-rpc.test.js +2 -0
  22. package/dist/srpc/errors.d.ts +2 -0
  23. package/dist/srpc/errors.js +14 -0
  24. package/dist/srpc/index.d.ts +1 -1
  25. package/dist/srpc/index.js +1 -1
  26. package/dist/srpc/packet-codec.test.d.ts +1 -0
  27. package/dist/srpc/packet-codec.test.js +75 -0
  28. package/dist/srpc/packet.d.ts +1 -1
  29. package/dist/srpc/packet.js +11 -1
  30. package/dist/srpc/server.js +19 -6
  31. package/dist/srpc/server.test.js +43 -0
  32. package/dist/srpc/stream.d.ts +4 -1
  33. package/dist/srpc/stream.js +62 -3
  34. package/dist/srpc/stream.test.js +110 -1
  35. package/dist/srpc/termination.d.ts +27 -0
  36. package/dist/srpc/termination.js +56 -0
  37. package/dist/srpc/termination.test.d.ts +1 -0
  38. package/dist/srpc/termination.test.js +24 -0
  39. package/dist/testdata/packet-codec-vectors.json +64 -0
  40. package/echo/Cargo.toml +8 -0
  41. package/echo/client-test.ts +124 -2
  42. package/echo/echo_e2e_test.cpp +542 -985
  43. package/echo/echo_pb2.py +40 -0
  44. package/echo/echo_pb2.pyi +13 -0
  45. package/echo/echo_srpc.pb.cpp +3 -5
  46. package/echo/echo_srpc.pb.go +6 -9
  47. package/echo/echo_srpc.pb.hpp +23 -3
  48. package/echo/echo_srpc.pb.rs +61 -26
  49. package/echo/echo_srpc.py +309 -0
  50. package/echo/echo_srpc.pyi +85 -0
  51. package/echo/half_close_test.rs +178 -0
  52. package/echo/integration_server.rs +14 -21
  53. package/echo/main.rs +28 -48
  54. package/go.mod +3 -3
  55. package/go.sum +6 -8
  56. package/integration/cross-language/cpp-client.cpp +446 -402
  57. package/integration/cross-language/cpp-pipe-server.cpp +139 -0
  58. package/integration/cross-language/cpp-pipe_test.go +114 -0
  59. package/integration/cross-language/cpp-server.cpp +222 -186
  60. package/integration/cross-language/go-client/main.go +79 -3
  61. package/integration/cross-language/python-client.py +146 -0
  62. package/integration/cross-language/python-server.py +140 -0
  63. package/integration/cross-language/run.bash +190 -65
  64. package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
  65. package/integration/cross-language/tcp-packet-stream.ts +121 -0
  66. package/integration/cross-language/ts-client.ts +62 -40
  67. package/integration/cross-language/ts-server.ts +1 -45
  68. package/mock/mock_pb2.py +38 -0
  69. package/mock/mock_pb2.pyi +11 -0
  70. package/mock/mock_srpc.pb.cpp +1 -1
  71. package/mock/mock_srpc.pb.hpp +1 -1
  72. package/mock/mock_srpc.pb.rs +10 -9
  73. package/mock/mock_srpc.py +71 -0
  74. package/mock/mock_srpc.pyi +27 -0
  75. package/package.json +19 -5
  76. package/srpc/CMakeLists.txt +3 -58
  77. package/srpc/README-cpp.md +69 -0
  78. package/srpc/__init__.py +0 -0
  79. package/srpc/accept.go +10 -4
  80. package/srpc/accept_test.go +25 -0
  81. package/srpc/build.rs +129 -34
  82. package/srpc/client-half-close_test.go +152 -0
  83. package/srpc/client-invoker.go +32 -42
  84. package/srpc/client-invoker_test.go +53 -0
  85. package/srpc/client-rpc.cpp +65 -80
  86. package/srpc/client-rpc.go +32 -33
  87. package/srpc/client-rpc.hpp +52 -38
  88. package/srpc/client-rpc.test.ts +67 -0
  89. package/srpc/client-rpc.ts +10 -0
  90. package/srpc/client-rpc_test.go +76 -0
  91. package/srpc/client.cpp +99 -87
  92. package/srpc/client.hpp +46 -35
  93. package/srpc/client.rs +78 -38
  94. package/srpc/client.ts +20 -4
  95. package/srpc/client_half_close_test.cc +331 -0
  96. package/srpc/client_invoker.rs +75 -0
  97. package/srpc/codec.rs +6 -0
  98. package/srpc/common-rpc.cpp +186 -146
  99. package/srpc/common-rpc.go +5 -4
  100. package/srpc/common-rpc.hpp +119 -89
  101. package/srpc/common-rpc.test.ts +2 -0
  102. package/srpc/common-rpc.ts +43 -8
  103. package/srpc/common-rpc_test.go +23 -0
  104. package/srpc/error.rs +19 -2
  105. package/srpc/errors.hpp +80 -50
  106. package/srpc/errors.ts +16 -0
  107. package/srpc/handler.hpp +9 -7
  108. package/srpc/handler.rs +7 -45
  109. package/srpc/index.ts +2 -0
  110. package/srpc/invoker.hpp +82 -55
  111. package/srpc/invoker.rs +14 -52
  112. package/srpc/lib.rs +2 -0
  113. package/srpc/message.hpp +17 -12
  114. package/srpc/msg-stream.go +1 -0
  115. package/srpc/msg-stream.hpp +108 -61
  116. package/srpc/mux.cpp +84 -84
  117. package/srpc/mux.hpp +48 -38
  118. package/srpc/mux.rs +4 -4
  119. package/srpc/net.go +12 -3
  120. package/srpc/packet-codec-vectors_test.go +195 -0
  121. package/srpc/packet-codec.test.ts +139 -0
  122. package/srpc/packet-rw.go +9 -2
  123. package/srpc/packet.cpp +60 -58
  124. package/srpc/packet.hpp +35 -20
  125. package/srpc/packet.rs +8 -27
  126. package/srpc/packet.ts +15 -2
  127. package/srpc/py.typed +0 -0
  128. package/srpc/rpc.rs +332 -115
  129. package/srpc/rpc_lifecycle_test.cc +333 -0
  130. package/srpc/rpcproto_pb2.py +40 -0
  131. package/srpc/rpcproto_pb2.pyi +40 -0
  132. package/srpc/server-cancel_test.go +39 -0
  133. package/srpc/server-rpc.cpp +83 -73
  134. package/srpc/server-rpc.go +21 -19
  135. package/srpc/server-rpc.hpp +56 -37
  136. package/srpc/server.rs +76 -131
  137. package/srpc/server.test.ts +50 -0
  138. package/srpc/server.ts +22 -6
  139. package/srpc/starpc.hpp +4 -2
  140. package/srpc/stream.go +4 -0
  141. package/srpc/stream.hpp +95 -34
  142. package/srpc/stream.rs +7 -2
  143. package/srpc/stream.test.ts +132 -1
  144. package/srpc/stream.ts +65 -9
  145. package/srpc/termination.test.ts +30 -0
  146. package/srpc/termination.ts +70 -0
  147. package/srpc/testing.rs +11 -66
  148. package/srpc/transport.rs +21 -71
  149. package/srpc/websocket.rs +106 -57
  150. package/srpc/writer.hpp +46 -32
  151. package/srpc/yamux.rs +82 -26
  152. package/srpc/tests/integration_test.rs +0 -495
package/srpc/mux.rs CHANGED
@@ -90,9 +90,7 @@ impl Mux {
90
90
  let method_ids = handler.method_ids();
91
91
 
92
92
  let mut services = self.services.write().unwrap();
93
- let service_methods = services
94
- .entry(service_id.to_string())
95
- .or_insert_with(HashMap::new);
93
+ let service_methods = services.entry(service_id.to_string()).or_default();
96
94
 
97
95
  for method_id in method_ids {
98
96
  if !method_id.is_empty() {
@@ -346,7 +344,9 @@ mod tests {
346
344
  }
347
345
 
348
346
  // Should find the method even with empty service ID
349
- let (handled, result) = mux.invoke_method("", "TestMethod", Box::new(MockStream)).await;
347
+ let (handled, result) = mux
348
+ .invoke_method("", "TestMethod", Box::new(MockStream))
349
+ .await;
350
350
  assert!(handled);
351
351
  assert!(result.is_ok());
352
352
  }
package/srpc/net.go CHANGED
@@ -7,12 +7,14 @@ import (
7
7
 
8
8
  // Dial dials a remote server using TCP with the default muxed conn type.
9
9
  func Dial(addr string) (Client, error) {
10
+ // Connect the byte stream before attaching its stream multiplexer.
10
11
  nconn, err := net.Dial("tcp", addr)
11
12
  if err != nil {
12
13
  return nil, err
13
14
  }
14
15
  muxedConn, err := NewMuxedConn(nconn, false, nil)
15
16
  if err != nil {
17
+ _ = nconn.Close()
16
18
  return nil, err
17
19
  }
18
20
  return NewClientWithMuxedConn(muxedConn), nil
@@ -22,11 +24,14 @@ func Dial(addr string) (Client, error) {
22
24
  // Returns on any fatal error or if ctx was canceled.
23
25
  // errCh is an optional error channel (can be nil)
24
26
  func Listen(ctx context.Context, addr string, srv *Server, errCh <-chan error) error {
27
+ // The listener and its accept routine live until this function returns.
25
28
  lis, err := net.Listen("tcp", addr)
26
29
  if err != nil {
27
30
  return err
28
31
  }
32
+ defer lis.Close()
29
33
 
34
+ // Observe serving failure alongside the caller's stop conditions.
30
35
  listenErrCh := make(chan error, 1)
31
36
  go func() {
32
37
  listenErrCh <- AcceptMuxedListener(ctx, lis, srv, nil)
@@ -35,10 +40,14 @@ func Listen(ctx context.Context, addr string, srv *Server, errCh <-chan error) e
35
40
 
36
41
  select {
37
42
  case <-ctx.Done():
38
- return context.Canceled
39
- case err := <-errCh:
40
- return err
43
+ err = ctx.Err()
44
+ case err = <-errCh:
41
45
  case err := <-listenErrCh:
42
46
  return err
43
47
  }
48
+
49
+ // Closing the listener wakes Accept; wait for its cleanup before returning.
50
+ _ = lis.Close()
51
+ <-listenErrCh
52
+ return err
44
53
  }
@@ -0,0 +1,195 @@
1
+ package srpc
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/binary"
6
+ "encoding/hex"
7
+ "encoding/json"
8
+ "errors"
9
+ "io"
10
+ "os"
11
+ "testing"
12
+ )
13
+
14
+ type packetCodecVector struct {
15
+ Name string `json:"name"`
16
+ PacketHex string `json:"packet_hex"`
17
+ FrameHex string `json:"frame_hex"`
18
+ }
19
+
20
+ func TestPacketCodecGoldenVectors(t *testing.T) {
21
+ data, err := os.ReadFile("../testdata/packet-codec-vectors.json")
22
+ if err != nil {
23
+ t.Fatal(err)
24
+ }
25
+ var document struct {
26
+ Cases []packetCodecVector `json:"cases"`
27
+ }
28
+ if err := json.Unmarshal(data, &document); err != nil {
29
+ t.Fatal(err)
30
+ }
31
+ for _, tc := range document.Cases {
32
+ if tc.PacketHex == "" || tc.FrameHex == "" {
33
+ continue
34
+ }
35
+ t.Run(tc.Name, func(t *testing.T) {
36
+ packet := goldenPacket(t, tc.Name)
37
+ packetData, err := packet.MarshalVT()
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ if want := decodeHex(t, tc.PacketHex); !bytes.Equal(packetData, want) {
42
+ t.Fatalf("packet = %x, want %x", packetData, want)
43
+ }
44
+ stream := &packetTestStream{}
45
+ if err := NewPacketReadWriter(stream).WritePacket(packet); err != nil {
46
+ t.Fatal(err)
47
+ }
48
+ if want := decodeHex(t, tc.FrameHex); !bytes.Equal(stream.writes, want) {
49
+ t.Fatalf("frame = %x, want %x", stream.writes, want)
50
+ }
51
+ })
52
+ }
53
+ }
54
+
55
+ func goldenPacket(t *testing.T, name string) *Packet {
56
+ t.Helper()
57
+ switch name {
58
+ case "call_start_data":
59
+ return NewCallStartPacket("svc", "method", []byte("abc"), false)
60
+ case "call_start_absent_empty":
61
+ return NewCallStartPacket("svc", "method", nil, false)
62
+ case "call_start_present_empty":
63
+ return NewCallStartPacket("svc", "method", nil, true)
64
+ case "call_data_terminal":
65
+ return NewCallDataPacket([]byte("out"), false, true, nil)
66
+ case "call_data_error":
67
+ return NewCallDataPacket(nil, false, false, errors.New("failed"))
68
+ case "call_cancel":
69
+ return NewCallCancelPacket()
70
+ default:
71
+ t.Fatalf("unknown golden packet %q", name)
72
+ return nil
73
+ }
74
+ }
75
+
76
+ func decodeHex(t *testing.T, value string) []byte {
77
+ t.Helper()
78
+ data, err := hex.DecodeString(value)
79
+ if err != nil {
80
+ t.Fatal(err)
81
+ }
82
+ return data
83
+ }
84
+
85
+ func framePacket(t *testing.T, packet *Packet) []byte {
86
+ t.Helper()
87
+ packetSize := packet.SizeVT()
88
+ if packetSize > maxMessageSize {
89
+ t.Fatalf("packet size %d exceeds maximum %d", packetSize, maxMessageSize)
90
+ }
91
+ frame := make([]byte, 4+packetSize)
92
+ binary.LittleEndian.PutUint32(frame, uint32(packetSize)) //nolint:gosec // bounded by maxMessageSize
93
+ if _, err := packet.MarshalToSizedBufferVT(frame[4:]); err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ return frame
97
+ }
98
+
99
+ type packetTestStream struct {
100
+ reads bytes.Buffer
101
+ writes []byte
102
+ maxRead int
103
+ }
104
+
105
+ func (s *packetTestStream) Read(data []byte) (int, error) {
106
+ if s.maxRead > 0 && len(data) > s.maxRead {
107
+ data = data[:s.maxRead]
108
+ }
109
+ return s.reads.Read(data)
110
+ }
111
+
112
+ func (s *packetTestStream) Write(data []byte) (int, error) {
113
+ s.writes = append(s.writes, data...)
114
+ return len(data), nil
115
+ }
116
+
117
+ func (s *packetTestStream) Close() error { return nil }
118
+
119
+ func TestPacketCodecReadFragmentsAndCoalesces(t *testing.T) {
120
+ first := framePacket(t, NewCallCancelPacket())
121
+ second := framePacket(t, NewCallDataPacket([]byte("x"), false, true, nil))
122
+ input := append(first, second...)
123
+ stream := &packetTestStream{reads: *bytes.NewBuffer(input)}
124
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
125
+ var count int
126
+ err := NewPacketReadWriter(stream).ReadToHandler(func(data []byte) error {
127
+ count++
128
+ return decode(data)
129
+ })
130
+ if err != nil || count != 2 {
131
+ t.Fatalf("ReadToHandler() = %v, packets=%d", err, count)
132
+ }
133
+ }
134
+
135
+ func TestPacketCodecReadEveryFragmentBoundary(t *testing.T) {
136
+ frame := framePacket(t, NewCallCancelPacket())
137
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
138
+ for size := 1; size <= len(frame); size++ {
139
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame), maxRead: size}
140
+ var count int
141
+ err := NewPacketReadWriter(stream).ReadToHandler(func(data []byte) error {
142
+ count++
143
+ return decode(data)
144
+ })
145
+ if err != nil || count != 1 {
146
+ t.Fatalf("chunk %d: err=%v count=%d", size, err, count)
147
+ }
148
+ }
149
+ }
150
+
151
+ func TestPacketCodecRejectsInvalidPrefixesAndMalformedPacket(t *testing.T) {
152
+ for name, frame := range map[string][]byte{
153
+ "zero": {0, 0, 0, 0},
154
+ "oversized": {0x81, 0x96, 0x98, 0x00},
155
+ } {
156
+ t.Run(name, func(t *testing.T) {
157
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
158
+ if err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error { return nil }); err == nil {
159
+ t.Fatal("accepted invalid prefix")
160
+ }
161
+ })
162
+ }
163
+ frame := []byte{3, 0, 0, 0, 0x0a, 0x01, 0xff}
164
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
165
+ decode := NewPacketDataHandler(func(*Packet) error { return nil })
166
+ if err := NewPacketReadWriter(stream).ReadToHandler(decode); err == nil {
167
+ t.Fatal("accepted malformed protobuf")
168
+ }
169
+ }
170
+
171
+ func TestPacketCodecCleanEOFReturnsNil(t *testing.T) {
172
+ stream := &packetTestStream{reads: *bytes.NewBuffer(nil)}
173
+ if err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error { return nil }); err != nil {
174
+ t.Fatalf("clean EOF: %v", err)
175
+ }
176
+ }
177
+
178
+ func TestPacketCodecTruncatedBodyAtEOF(t *testing.T) {
179
+ frame := []byte{4, 0, 0, 0, 0x0a, 0x01}
180
+ stream := &packetTestStream{reads: *bytes.NewBuffer(frame)}
181
+ var count int
182
+ err := NewPacketReadWriter(stream).ReadToHandler(func([]byte) error {
183
+ count++
184
+ return nil
185
+ })
186
+ if err != io.ErrUnexpectedEOF || count != 0 {
187
+ t.Fatalf("err=%v count=%d", err, count)
188
+ }
189
+ }
190
+
191
+ func TestPacketCodecWriteRejectsZeroPacket(t *testing.T) {
192
+ if err := NewPacketReadWriter(&packetTestStream{}).WritePacket(&Packet{}); err == nil {
193
+ t.Fatal("accepted zero-size packet")
194
+ }
195
+ }
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import vectors from '../testdata/packet-codec-vectors.json'
3
+ import { Packet } from './rpcproto.pb.js'
4
+ import {
5
+ decodePacketSource,
6
+ encodePacketSource,
7
+ lengthPrefixDecode,
8
+ prependLengthPrefixTransform,
9
+ uint32LEDecode,
10
+ } from './packet.js'
11
+
12
+ const bytes = (hex: string) =>
13
+ Uint8Array.from(hex.match(/../g) ?? [], (b) => parseInt(b, 16))
14
+ const hex = (
15
+ data: Uint8Array | { subarray: (start?: number, end?: number) => Uint8Array },
16
+ ) => Buffer.from(data.subarray()).toString('hex')
17
+ const collect = async <T>(source: AsyncIterable<T> | Iterable<T>) => {
18
+ const out: T[] = []
19
+ for await (const value of source) out.push(value)
20
+ return out
21
+ }
22
+
23
+ type ValidVector = (typeof vectors.cases)[number] & {
24
+ packet_hex: string
25
+ frame_hex: string
26
+ }
27
+ const validCases: ValidVector[] = vectors.cases.filter(
28
+ (entry): entry is ValidVector => Boolean(entry.packet_hex && entry.frame_hex),
29
+ )
30
+
31
+ describe('packet codec golden vectors', () => {
32
+ it.each(validCases)(
33
+ '$name has exact protobuf and frame bytes',
34
+ async (entry) => {
35
+ const packet = Packet.fromBinary(bytes(entry.packet_hex))
36
+ const encoded = (
37
+ await collect(
38
+ encodePacketSource(
39
+ (async function* () {
40
+ yield packet
41
+ })(),
42
+ ),
43
+ )
44
+ )[0]
45
+ expect(hex(encoded)).toBe(entry.packet_hex)
46
+
47
+ const framed = (
48
+ await collect(
49
+ prependLengthPrefixTransform()(
50
+ (async function* () {
51
+ yield encoded
52
+ })(),
53
+ ),
54
+ )
55
+ )[0]
56
+ expect(hex(framed)).toBe(entry.frame_hex)
57
+ },
58
+ )
59
+
60
+ it('rejects zero and oversized encoded chunks', async () => {
61
+ const zero = (async function* () {
62
+ yield new Uint8Array()
63
+ })()
64
+ await expect(collect(prependLengthPrefixTransform()(zero))).rejects.toThrow(
65
+ 'invalid packet length',
66
+ )
67
+ const oversized = (async function* () {
68
+ yield new Uint8Array(10_000_001)
69
+ })()
70
+ await expect(
71
+ collect(prependLengthPrefixTransform()(oversized)),
72
+ ).rejects.toThrow('invalid packet length')
73
+ })
74
+
75
+ it('rejects zero and oversized lengths, and truncated bodies', async () => {
76
+ const zero = (async function* () {
77
+ yield bytes('00000000')
78
+ })()
79
+ await expect(
80
+ collect(lengthPrefixDecode(zero, uint32LEDecode)),
81
+ ).rejects.toThrow('invalid packet length')
82
+ const oversized = (async function* () {
83
+ yield bytes('81969800')
84
+ })()
85
+ await expect(
86
+ collect(lengthPrefixDecode(oversized, uint32LEDecode)),
87
+ ).rejects.toThrow('invalid packet length')
88
+ const truncated = (async function* () {
89
+ yield bytes('040000000a01')
90
+ })()
91
+ await expect(
92
+ collect(lengthPrefixDecode(truncated, uint32LEDecode)),
93
+ ).rejects.toThrow('truncated packet frame')
94
+ })
95
+
96
+ it('decodes fragmented and coalesced frames', async () => {
97
+ const frames = validCases.map((entry) => bytes(entry.frame_hex))
98
+ const combined = new Uint8Array(
99
+ frames.reduce((n, frame) => n + frame.length, 0),
100
+ )
101
+ let offset = 0
102
+ for (const frame of frames) {
103
+ combined.set(frame, offset)
104
+ offset += frame.length
105
+ }
106
+ const payloads = await collect(
107
+ lengthPrefixDecode(
108
+ (async function* () {
109
+ yield combined.subarray(0, 3)
110
+ yield combined.subarray(3, 11)
111
+ yield combined.subarray(11)
112
+ })(),
113
+ uint32LEDecode,
114
+ ),
115
+ )
116
+ expect(payloads.map(hex)).toEqual(
117
+ validCases.map((entry) => entry.packet_hex),
118
+ )
119
+ const decoded = await collect(
120
+ decodePacketSource(payloads.map((payload) => payload.slice())),
121
+ )
122
+ expect(decoded).toHaveLength(validCases.length)
123
+ })
124
+
125
+ it('rejects malformed protobuf and incomplete frame prefix', async () => {
126
+ const malformed = vectors.cases.find(
127
+ (entry) => entry.name === 'malformed_complete',
128
+ )!
129
+ expect(() =>
130
+ Packet.fromBinary(bytes(malformed.frame_hex!.slice(8))),
131
+ ).toThrow()
132
+ const incomplete = (async function* () {
133
+ yield bytes('010203')
134
+ })()
135
+ await expect(
136
+ collect(lengthPrefixDecode(incomplete, uint32LEDecode)),
137
+ ).rejects.toThrow('truncated packet frame')
138
+ })
139
+ })
package/srpc/packet-rw.go CHANGED
@@ -65,7 +65,10 @@ func (r *PacketReadWriter) WritePacket(p *Packet) error {
65
65
  defer r.writeMtx.Unlock()
66
66
 
67
67
  msgSize := p.SizeVT()
68
- if msgSize < 0 || msgSize > maxMessageSize {
68
+ if msgSize <= 0 {
69
+ return errors.New("unexpected zero message size")
70
+ }
71
+ if msgSize > maxMessageSize {
69
72
  return errors.Errorf("message size %v greater than maximum %v", msgSize, maxMessageSize)
70
73
  }
71
74
 
@@ -163,7 +166,11 @@ func (r *PacketReadWriter) ReadToHandler(cb PacketDataHandler) error {
163
166
  }
164
167
  }
165
168
 
166
- // closed
169
+ // closed: a clean frame boundary has no buffered bytes; otherwise EOF
170
+ // ended a prefix or packet body before completion.
171
+ if currLen != 0 || r.buf.Len() != 0 {
172
+ return io.ErrUnexpectedEOF
173
+ }
167
174
  return nil
168
175
  }
169
176
 
package/srpc/packet.cpp CHANGED
@@ -6,78 +6,80 @@
6
6
 
7
7
  namespace starpc {
8
8
 
9
- PacketDataHandler NewPacketDataHandler(PacketHandler handler) {
10
- return [handler](const std::string &data) -> Error {
11
- srpc::Packet pkt;
12
- if (!pkt.ParseFromString(data)) {
13
- return Error::InvalidMessage;
14
- }
15
- return handler(pkt);
16
- };
9
+ PacketDataHandler NewPacketDataHandler(PacketHandler handler)
10
+ {
11
+ return [handler](const std::string &data) -> Error {
12
+ srpc::Packet pkt;
13
+ if (!pkt.ParseFromString(data))
14
+ return Error::InvalidMessage;
15
+ return handler(pkt);
16
+ };
17
17
  }
18
18
 
19
- Error ValidatePacket(const srpc::Packet &pkt) {
20
- switch (pkt.body_case()) {
21
- case srpc::Packet::kCallStart:
22
- return ValidateCallStart(pkt.call_start());
23
- case srpc::Packet::kCallData:
24
- return ValidateCallData(pkt.call_data());
25
- case srpc::Packet::kCallCancel:
26
- return Error::OK;
27
- default:
28
- return Error::UnrecognizedPacket;
29
- }
19
+ Error ValidatePacket(const srpc::Packet &pkt)
20
+ {
21
+ switch (pkt.body_case()) {
22
+ case srpc::Packet::kCallStart:
23
+ return ValidateCallStart(pkt.call_start());
24
+ case srpc::Packet::kCallData:
25
+ return ValidateCallData(pkt.call_data());
26
+ case srpc::Packet::kCallCancel:
27
+ return Error::OK;
28
+ default:
29
+ return Error::UnrecognizedPacket;
30
+ }
30
31
  }
31
32
 
32
- Error ValidateCallStart(const srpc::CallStart &pkt) {
33
- if (pkt.rpc_method().empty()) {
34
- return Error::EmptyMethodID;
35
- }
36
- if (pkt.rpc_service().empty()) {
37
- return Error::EmptyServiceID;
38
- }
39
- return Error::OK;
33
+ Error ValidateCallStart(const srpc::CallStart &pkt)
34
+ {
35
+ if (pkt.rpc_method().empty())
36
+ return Error::EmptyMethodID;
37
+ if (pkt.rpc_service().empty())
38
+ return Error::EmptyServiceID;
39
+ return Error::OK;
40
40
  }
41
41
 
42
- Error ValidateCallData(const srpc::CallData &pkt) {
43
- if (pkt.data().empty() && !pkt.complete() && pkt.error().empty() &&
44
- !pkt.data_is_zero()) {
45
- return Error::EmptyPacket;
46
- }
47
- return Error::OK;
42
+ Error ValidateCallData(const srpc::CallData &pkt)
43
+ {
44
+ if (pkt.data().empty() && !pkt.complete() && pkt.error().empty() &&
45
+ !pkt.data_is_zero())
46
+ return Error::EmptyPacket;
47
+ return Error::OK;
48
48
  }
49
49
 
50
50
  std::unique_ptr<srpc::Packet> NewCallStartPacket(const std::string &service,
51
- const std::string &method,
52
- const std::string &data,
53
- bool data_is_zero) {
54
- auto pkt = std::make_unique<srpc::Packet>();
55
- auto *call_start = pkt->mutable_call_start();
56
- call_start->set_rpc_service(service);
57
- call_start->set_rpc_method(method);
58
- call_start->set_data(data);
59
- call_start->set_data_is_zero(data_is_zero);
60
- return pkt;
51
+ const std::string &method,
52
+ const std::string &data,
53
+ bool data_is_zero)
54
+ {
55
+ auto pkt = std::make_unique<srpc::Packet>();
56
+ auto *call_start = pkt->mutable_call_start();
57
+ call_start->set_rpc_service(service);
58
+ call_start->set_rpc_method(method);
59
+ call_start->set_data(data);
60
+ call_start->set_data_is_zero(data_is_zero);
61
+ return pkt;
61
62
  }
62
63
 
63
64
  std::unique_ptr<srpc::Packet> NewCallDataPacket(const std::string &data,
64
- bool data_is_zero,
65
- bool complete, Error err) {
66
- auto pkt = std::make_unique<srpc::Packet>();
67
- auto *call_data = pkt->mutable_call_data();
68
- call_data->set_data(data);
69
- call_data->set_data_is_zero(data_is_zero);
70
- call_data->set_complete(err != Error::OK || complete);
71
- if (err != Error::OK) {
72
- call_data->set_error(ErrorString(err));
73
- }
74
- return pkt;
65
+ bool data_is_zero,
66
+ bool complete, Error err)
67
+ {
68
+ auto pkt = std::make_unique<srpc::Packet>();
69
+ auto *call_data = pkt->mutable_call_data();
70
+ call_data->set_data(data);
71
+ call_data->set_data_is_zero(data_is_zero);
72
+ call_data->set_complete(err != Error::OK || complete);
73
+ if (err != Error::OK)
74
+ call_data->set_error(ErrorString(err));
75
+ return pkt;
75
76
  }
76
77
 
77
- std::unique_ptr<srpc::Packet> NewCallCancelPacket() {
78
- auto pkt = std::make_unique<srpc::Packet>();
79
- pkt->set_call_cancel(true);
80
- return pkt;
78
+ std::unique_ptr<srpc::Packet> NewCallCancelPacket()
79
+ {
80
+ auto pkt = std::make_unique<srpc::Packet>();
81
+ pkt->set_call_cancel(true);
82
+ return pkt;
81
83
  }
82
84
 
83
85
  } // namespace starpc
package/srpc/packet.hpp CHANGED
@@ -1,12 +1,12 @@
1
1
  #pragma once
2
2
 
3
+ #include "errors.hpp"
4
+
3
5
  #include <cstdint>
4
6
  #include <functional>
5
7
  #include <memory>
6
8
  #include <string>
7
9
 
8
- #include "errors.hpp"
9
-
10
10
  namespace srpc {
11
11
  class Packet;
12
12
  class CallStart;
@@ -15,42 +15,57 @@ class CallData;
15
15
 
16
16
  namespace starpc {
17
17
 
18
- // CloseHandler handles the stream closing with an optional error.
19
- // Matches Go CloseHandler in packet.go
18
+ /*
19
+ * CloseHandler receives the transport close outcome for a stream.
20
+ * Matches Go CloseHandler in packet.go.
21
+ */
20
22
  using CloseHandler = std::function<void(Error close_err)>;
21
23
 
22
- // PacketHandler handles a packet.
23
- // Matches Go PacketHandler in packet.go
24
+ /* PacketHandler receives one parsed packet. Matches Go PacketHandler. */
24
25
  using PacketHandler = std::function<Error(const srpc::Packet &pkt)>;
25
26
 
26
- // PacketDataHandler handles a packet before it is parsed.
27
- // Matches Go PacketDataHandler in packet.go
27
+ /*
28
+ * PacketDataHandler receives one packet before parsing, as its serialized
29
+ * bytes. Matches Go PacketDataHandler in packet.go.
30
+ */
28
31
  using PacketDataHandler = std::function<Error(const std::string &data)>;
29
32
 
30
- // NewPacketDataHandler wraps a PacketHandler with a decoding step.
33
+ /* NewPacketDataHandler parses each payload and forwards it to handler. */
31
34
  PacketDataHandler NewPacketDataHandler(PacketHandler handler);
32
35
 
33
- // Validate performs cursory validation of a Packet.
36
+ /*
37
+ * ValidatePacket checks a packet's shape: its body is one of the known types
38
+ * and the body's required fields are present.
39
+ */
34
40
  Error ValidatePacket(const srpc::Packet &pkt);
35
41
 
36
- // Validate performs cursory validation of a CallStart.
42
+ /* ValidateCallStart requires the service and method IDs to be present. */
37
43
  Error ValidateCallStart(const srpc::CallStart &pkt);
38
44
 
39
- // Validate performs cursory validation of a CallData.
45
+ /*
46
+ * ValidateCallData rejects a packet that carries no data, no completion, no
47
+ * error, and no zero-length marker.
48
+ */
40
49
  Error ValidateCallData(const srpc::CallData &pkt);
41
50
 
42
- // NewCallStartPacket constructs a new CallStart packet.
51
+ /*
52
+ * NewCallStartPacket builds the CallStart packet that opens a call.
53
+ * data_is_zero marks an intentionally empty first message.
54
+ */
43
55
  std::unique_ptr<srpc::Packet> NewCallStartPacket(const std::string &service,
44
- const std::string &method,
45
- const std::string &data,
46
- bool data_is_zero);
56
+ const std::string &method,
57
+ const std::string &data,
58
+ bool data_is_zero);
47
59
 
48
- // NewCallDataPacket constructs a new CallData packet.
60
+ /*
61
+ * NewCallDataPacket builds one CallData packet. err carries the call's
62
+ * terminal verdict; complete ends the sending side.
63
+ */
49
64
  std::unique_ptr<srpc::Packet> NewCallDataPacket(const std::string &data,
50
- bool data_is_zero,
51
- bool complete, Error err);
65
+ bool data_is_zero,
66
+ bool complete, Error err);
52
67
 
53
- // NewCallCancelPacket constructs a new CallCancel packet with cancel.
68
+ /* NewCallCancelPacket builds the packet that cancels the call. */
54
69
  std::unique_ptr<srpc::Packet> NewCallCancelPacket();
55
70
 
56
71
  } // namespace starpc