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.
- package/README.md +37 -8
- package/dist/echo/client-test.d.ts +1 -1
- package/dist/echo/client-test.js +110 -2
- package/dist/integration/cross-language/tcp-packet-stream.d.ts +3 -0
- package/dist/integration/cross-language/tcp-packet-stream.js +112 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.d.ts +1 -0
- package/dist/integration/cross-language/tcp-packet-stream.test.js +121 -0
- package/dist/integration/cross-language/ts-client.js +50 -37
- package/dist/integration/cross-language/ts-server.js +1 -36
- package/dist/rpcstream/rpcstream.d.ts +5 -1
- package/dist/rpcstream/rpcstream.js +75 -28
- package/dist/rpcstream/rpcstream.test.d.ts +1 -0
- package/dist/rpcstream/rpcstream.test.js +92 -0
- package/dist/srpc/client-rpc.d.ts +1 -0
- package/dist/srpc/client-rpc.js +9 -0
- package/dist/srpc/client-rpc.test.d.ts +1 -0
- package/dist/srpc/client-rpc.test.js +50 -0
- package/dist/srpc/client.js +18 -4
- package/dist/srpc/common-rpc.d.ts +5 -0
- package/dist/srpc/common-rpc.js +37 -8
- package/dist/srpc/common-rpc.test.js +2 -0
- package/dist/srpc/errors.d.ts +2 -0
- package/dist/srpc/errors.js +14 -0
- package/dist/srpc/index.d.ts +1 -1
- package/dist/srpc/index.js +1 -1
- package/dist/srpc/packet-codec.test.d.ts +1 -0
- package/dist/srpc/packet-codec.test.js +75 -0
- package/dist/srpc/packet.d.ts +1 -1
- package/dist/srpc/packet.js +11 -1
- package/dist/srpc/server.js +19 -6
- package/dist/srpc/server.test.js +43 -0
- package/dist/srpc/stream.d.ts +4 -1
- package/dist/srpc/stream.js +62 -3
- package/dist/srpc/stream.test.js +110 -1
- package/dist/srpc/termination.d.ts +27 -0
- package/dist/srpc/termination.js +56 -0
- package/dist/srpc/termination.test.d.ts +1 -0
- package/dist/srpc/termination.test.js +24 -0
- package/dist/testdata/packet-codec-vectors.json +64 -0
- package/echo/Cargo.toml +8 -0
- package/echo/client-test.ts +124 -2
- package/echo/echo_e2e_test.cpp +542 -985
- package/echo/echo_pb2.py +40 -0
- package/echo/echo_pb2.pyi +13 -0
- package/echo/echo_srpc.pb.cpp +3 -5
- package/echo/echo_srpc.pb.go +6 -9
- package/echo/echo_srpc.pb.hpp +23 -3
- package/echo/echo_srpc.pb.rs +61 -26
- package/echo/echo_srpc.py +309 -0
- package/echo/echo_srpc.pyi +85 -0
- package/echo/half_close_test.rs +178 -0
- package/echo/integration_server.rs +14 -21
- package/echo/main.rs +28 -48
- package/go.mod +3 -3
- package/go.sum +6 -8
- package/integration/cross-language/cpp-client.cpp +446 -402
- package/integration/cross-language/cpp-pipe-server.cpp +139 -0
- package/integration/cross-language/cpp-pipe_test.go +114 -0
- package/integration/cross-language/cpp-server.cpp +222 -186
- package/integration/cross-language/go-client/main.go +79 -3
- package/integration/cross-language/python-client.py +146 -0
- package/integration/cross-language/python-server.py +140 -0
- package/integration/cross-language/run.bash +190 -65
- package/integration/cross-language/tcp-packet-stream.test.ts +154 -0
- package/integration/cross-language/tcp-packet-stream.ts +121 -0
- package/integration/cross-language/ts-client.ts +62 -40
- package/integration/cross-language/ts-server.ts +1 -45
- package/mock/mock_pb2.py +38 -0
- package/mock/mock_pb2.pyi +11 -0
- package/mock/mock_srpc.pb.cpp +1 -1
- package/mock/mock_srpc.pb.hpp +1 -1
- package/mock/mock_srpc.pb.rs +10 -9
- package/mock/mock_srpc.py +71 -0
- package/mock/mock_srpc.pyi +27 -0
- package/package.json +19 -5
- package/srpc/CMakeLists.txt +3 -58
- package/srpc/README-cpp.md +69 -0
- package/srpc/__init__.py +0 -0
- package/srpc/accept.go +10 -4
- package/srpc/accept_test.go +25 -0
- package/srpc/build.rs +129 -34
- package/srpc/client-half-close_test.go +152 -0
- package/srpc/client-invoker.go +32 -42
- package/srpc/client-invoker_test.go +53 -0
- package/srpc/client-rpc.cpp +65 -80
- package/srpc/client-rpc.go +32 -33
- package/srpc/client-rpc.hpp +52 -38
- package/srpc/client-rpc.test.ts +67 -0
- package/srpc/client-rpc.ts +10 -0
- package/srpc/client-rpc_test.go +76 -0
- package/srpc/client.cpp +99 -87
- package/srpc/client.hpp +46 -35
- package/srpc/client.rs +78 -38
- package/srpc/client.ts +20 -4
- package/srpc/client_half_close_test.cc +331 -0
- package/srpc/client_invoker.rs +75 -0
- package/srpc/codec.rs +6 -0
- package/srpc/common-rpc.cpp +186 -146
- package/srpc/common-rpc.go +5 -4
- package/srpc/common-rpc.hpp +119 -89
- package/srpc/common-rpc.test.ts +2 -0
- package/srpc/common-rpc.ts +43 -8
- package/srpc/common-rpc_test.go +23 -0
- package/srpc/error.rs +19 -2
- package/srpc/errors.hpp +80 -50
- package/srpc/errors.ts +16 -0
- package/srpc/handler.hpp +9 -7
- package/srpc/handler.rs +7 -45
- package/srpc/index.ts +2 -0
- package/srpc/invoker.hpp +82 -55
- package/srpc/invoker.rs +14 -52
- package/srpc/lib.rs +2 -0
- package/srpc/message.hpp +17 -12
- package/srpc/msg-stream.go +1 -0
- package/srpc/msg-stream.hpp +108 -61
- package/srpc/mux.cpp +84 -84
- package/srpc/mux.hpp +48 -38
- package/srpc/mux.rs +4 -4
- package/srpc/net.go +12 -3
- package/srpc/packet-codec-vectors_test.go +195 -0
- package/srpc/packet-codec.test.ts +139 -0
- package/srpc/packet-rw.go +9 -2
- package/srpc/packet.cpp +60 -58
- package/srpc/packet.hpp +35 -20
- package/srpc/packet.rs +8 -27
- package/srpc/packet.ts +15 -2
- package/srpc/py.typed +0 -0
- package/srpc/rpc.rs +332 -115
- package/srpc/rpc_lifecycle_test.cc +333 -0
- package/srpc/rpcproto_pb2.py +40 -0
- package/srpc/rpcproto_pb2.pyi +40 -0
- package/srpc/server-cancel_test.go +39 -0
- package/srpc/server-rpc.cpp +83 -73
- package/srpc/server-rpc.go +21 -19
- package/srpc/server-rpc.hpp +56 -37
- package/srpc/server.rs +76 -131
- package/srpc/server.test.ts +50 -0
- package/srpc/server.ts +22 -6
- package/srpc/starpc.hpp +4 -2
- package/srpc/stream.go +4 -0
- package/srpc/stream.hpp +95 -34
- package/srpc/stream.rs +7 -2
- package/srpc/stream.test.ts +132 -1
- package/srpc/stream.ts +65 -9
- package/srpc/termination.test.ts +30 -0
- package/srpc/termination.ts +70 -0
- package/srpc/testing.rs +11 -66
- package/srpc/transport.rs +21 -71
- package/srpc/websocket.rs +106 -57
- package/srpc/writer.hpp +46 -32
- package/srpc/yamux.rs +82 -26
- 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
|
|
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
|
-
|
|
39
|
-
case err
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
+
/* NewPacketDataHandler parses each payload and forwards it to handler. */
|
|
31
34
|
PacketDataHandler NewPacketDataHandler(PacketHandler handler);
|
|
32
35
|
|
|
33
|
-
|
|
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
|
-
|
|
42
|
+
/* ValidateCallStart requires the service and method IDs to be present. */
|
|
37
43
|
Error ValidateCallStart(const srpc::CallStart &pkt);
|
|
38
44
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const std::string &method,
|
|
57
|
+
const std::string &data,
|
|
58
|
+
bool data_is_zero);
|
|
47
59
|
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
65
|
+
bool data_is_zero,
|
|
66
|
+
bool complete, Error err);
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
/* NewCallCancelPacket builds the packet that cancels the call. */
|
|
54
69
|
std::unique_ptr<srpc::Packet> NewCallCancelPacket();
|
|
55
70
|
|
|
56
71
|
} // namespace starpc
|