package ipc import ( "bytes" "encoding/binary" "errors" "io" "testing" ) // The framing is hand-rolled, and it is the protocol all nine daemons depend // on, so a bug in it is a bug everywhere (Vikunja #410). The review asked // whether to replace it with something standard. It stays: length-prefixed // JSON over a unix socket is ninety lines, it is readable with socat, and the // alternatives (net/rpc, gRPC, a codec library) all buy schema machinery this // boundary does not want. What was wrong was inheriting it untested. These // are the paths a real socket produces that the round-trip test never does. // A short header is not EOF. EOF means the peer closed cleanly between // frames, which the server treats as a normal disconnect; a header that stops // halfway is a truncated frame and must be reported as an error, or a peer // that dies mid-write looks like one that hung up politely. func TestReadFrameTruncatedHeader(t *testing.T) { var v any err := readFrame(bytes.NewReader([]byte{0, 0, 4}), &v) if err == nil { t.Fatal("a three-byte header must fail") } if errors.Is(err, io.EOF) { t.Fatalf("err = %v, want a truncation error, not EOF", err) } } // A header promising more body than follows. Same reasoning: the frame never // arrived, so it must not decode into a zero value the caller then trusts. func TestReadFrameTruncatedBody(t *testing.T) { var buf bytes.Buffer var hdr [4]byte binary.BigEndian.PutUint32(hdr[:], 32) buf.Write(hdr[:]) buf.WriteString(`{"m":"pi`) var got map[string]string if err := readFrame(&buf, &got); err == nil { t.Fatal("a body shorter than its prefix must fail") } if len(got) != 0 { t.Errorf("decoded %v from a truncated frame", got) } } // Nothing at all is EOF, and only this is. func TestReadFrameEmptyIsEOF(t *testing.T) { var v any if err := readFrame(bytes.NewReader(nil), &v); !errors.Is(err, io.EOF) { t.Fatalf("err = %v, want io.EOF", err) } } // byteAtATime returns one byte per Read, which is what a socket is allowed to // do and what a bytes.Reader never does. readFrame uses io.ReadFull for both // the header and the body; this is the test that would fail if either turned // into a bare Read. type byteAtATime struct { b []byte i int } func (r *byteAtATime) Read(p []byte) (int, error) { if r.i >= len(r.b) { return 0, io.EOF } if len(p) == 0 { return 0, nil } p[0] = r.b[r.i] r.i++ return 1, nil } func TestReadFrameReassemblesPartialReads(t *testing.T) { type payload struct { Msg string `json:"m"` N int `json:"n"` } want := payload{Msg: "привет", N: 7} var buf bytes.Buffer if err := writeFrame(&buf, want); err != nil { t.Fatalf("writeFrame: %v", err) } var got payload if err := readFrame(&byteAtATime{b: buf.Bytes()}, &got); err != nil { t.Fatalf("readFrame: %v", err) } if got != want { t.Fatalf("got %+v, want %+v", got, want) } } // Two frames written back to back must come back as two frames. A reader that // consumed more than one frame's body would desynchronize the connection, and // the symptom would be a reply attributed to the wrong request. func TestReadFrameStopsAtTheFrameBoundary(t *testing.T) { var buf bytes.Buffer for _, m := range []string{"first", "second"} { if err := writeFrame(&buf, map[string]string{"m": m}); err != nil { t.Fatalf("writeFrame: %v", err) } } r := &byteAtATime{b: buf.Bytes()} for _, want := range []string{"first", "second"} { var got map[string]string if err := readFrame(r, &got); err != nil { t.Fatalf("readFrame(%s): %v", want, err) } if got["m"] != want { t.Fatalf("got %q, want %q", got["m"], want) } } var extra map[string]string if err := readFrame(r, &extra); !errors.Is(err, io.EOF) { t.Fatalf("after two frames: err = %v, want io.EOF", err) } } // A body that is not JSON is an error, not a zero value. The peer is either // broken or not speaking this protocol; either way the caller must not read // on as though it decoded. func TestReadFrameRejectsNonJSONBody(t *testing.T) { var buf bytes.Buffer var hdr [4]byte body := []byte("not json at all") binary.BigEndian.PutUint32(hdr[:], uint32(len(body))) buf.Write(hdr[:]) buf.Write(body) var got map[string]string if err := readFrame(&buf, &got); err == nil { t.Fatal("a non-JSON body must fail") } }