Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
245 lines
7.0 KiB
Go
245 lines
7.0 KiB
Go
// Copyright 2026 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package archive
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
|
|
w := NewWriter(&buf)
|
|
|
|
want := []struct {
|
|
hdr Header
|
|
data string
|
|
}{
|
|
{
|
|
hdr: Header{
|
|
Name: "alpha.txt",
|
|
Timestamp: 100,
|
|
HeaderSize: 4,
|
|
PayloadSize: 7,
|
|
},
|
|
data: "hello alpha",
|
|
},
|
|
{
|
|
hdr: Header{
|
|
Name: "nested/beta.txt",
|
|
Timestamp: 100,
|
|
Sequence: 42,
|
|
HeaderSize: 3,
|
|
PayloadSize: 9,
|
|
},
|
|
data: "beta payload",
|
|
},
|
|
}
|
|
|
|
for i := range want {
|
|
if err := w.WriteHeader(&want[i].hdr); err != nil {
|
|
t.Fatalf("write header %d: %v", i, err)
|
|
}
|
|
if _, err := io.WriteString(w, want[i].data); err != nil {
|
|
t.Fatalf("write payload %d: %v", i, err)
|
|
}
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("close writer: %v", err)
|
|
}
|
|
|
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
|
|
|
for i := range want {
|
|
hdr, err := r.Next()
|
|
if err != nil {
|
|
t.Fatalf("next %d: %v", i, err)
|
|
}
|
|
if hdr.Name != want[i].hdr.Name {
|
|
t.Fatalf("header %d name mismatch: got %q want %q", i, hdr.Name, want[i].hdr.Name)
|
|
}
|
|
if hdr.Timestamp != want[i].hdr.Timestamp {
|
|
t.Fatalf("header %d timestamp mismatch: got %d want %d", i, hdr.Timestamp, want[i].hdr.Timestamp)
|
|
}
|
|
if hdr.Sequence != want[i].hdr.Sequence {
|
|
t.Fatalf("header %d sequence mismatch: got %d want %d", i, hdr.Sequence, want[i].hdr.Sequence)
|
|
}
|
|
if hdr.HeaderSize != want[i].hdr.HeaderSize {
|
|
t.Fatalf("header %d header size mismatch: got %d want %d", i, hdr.HeaderSize, want[i].hdr.HeaderSize)
|
|
}
|
|
if hdr.PayloadSize != want[i].hdr.PayloadSize {
|
|
t.Fatalf("header %d payload size mismatch: got %d want %d", i, hdr.PayloadSize, want[i].hdr.PayloadSize)
|
|
}
|
|
got, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatalf("read payload %d: %v", i, err)
|
|
}
|
|
if string(got) != want[i].data {
|
|
t.Fatalf("payload %d mismatch: got %q want %q", i, got, want[i].data)
|
|
}
|
|
}
|
|
|
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("expected EOF, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNextSkipsUnreadPayload(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
|
|
w := NewWriter(&buf)
|
|
first := Header{Name: "first", HeaderSize: 0, PayloadSize: int64(len("abcdef"))}
|
|
if err := w.WriteHeader(&first); err != nil {
|
|
t.Fatalf("write first header: %v", err)
|
|
}
|
|
if _, err := io.WriteString(w, "abcdef"); err != nil {
|
|
t.Fatalf("write first payload: %v", err)
|
|
}
|
|
|
|
second := Header{Name: "second", HeaderSize: 0, PayloadSize: int64(len("second-body"))}
|
|
if err := w.WriteHeader(&second); err != nil {
|
|
t.Fatalf("write second header: %v", err)
|
|
}
|
|
if _, err := io.WriteString(w, "second-body"); err != nil {
|
|
t.Fatalf("write second payload: %v", err)
|
|
}
|
|
|
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
|
|
|
hdr, err := r.Next()
|
|
if err != nil {
|
|
t.Fatalf("next first: %v", err)
|
|
}
|
|
if hdr.Name != "first" {
|
|
t.Fatalf("unexpected first header name %q", hdr.Name)
|
|
}
|
|
|
|
buf1 := make([]byte, 3)
|
|
if _, err := io.ReadFull(r, buf1); err != nil {
|
|
t.Fatalf("read partial first payload: %v", err)
|
|
}
|
|
if string(buf1) != "abc" {
|
|
t.Fatalf("unexpected partial payload %q", buf1)
|
|
}
|
|
|
|
hdr, err = r.Next()
|
|
if err != nil {
|
|
t.Fatalf("next second: %v", err)
|
|
}
|
|
if hdr.Name != "second" {
|
|
t.Fatalf("unexpected second header name %q", hdr.Name)
|
|
}
|
|
|
|
got, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatalf("read second payload: %v", err)
|
|
}
|
|
if string(got) != "second-body" {
|
|
t.Fatalf("unexpected second payload %q", got)
|
|
}
|
|
}
|
|
|
|
// validArchive returns a well-formed archive holding a single entry whose
|
|
// payload is the 6-byte string "abcdef" split as HeaderSize=2, PayloadSize=4.
|
|
func validArchive(t *testing.T) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
w := NewWriter(&buf)
|
|
hdr := Header{Name: "first", Timestamp: 7, Sequence: 3, HeaderSize: 2, PayloadSize: 4}
|
|
if err := w.WriteHeader(&hdr); err != nil {
|
|
t.Fatalf("write header: %v", err)
|
|
}
|
|
if _, err := io.WriteString(w, "abcdef"); err != nil {
|
|
t.Fatalf("write payload: %v", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func TestEmptyArchiveReturnsEOF(t *testing.T) {
|
|
r := NewReader(bytes.NewReader(nil))
|
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("expected io.EOF on empty archive, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInvalidMagicIsRejected(t *testing.T) {
|
|
r := NewReader(bytes.NewReader([]byte("not-a-valid-archive-stream")))
|
|
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
|
|
t.Fatalf("expected ErrInvalidArchive, got %v", err)
|
|
}
|
|
}
|
|
|
|
// A clean EOF on the first header field means end-of-stream, but an EOF
|
|
// partway through the header means the entry was truncated.
|
|
func TestTruncatedHeaderIsInvalid(t *testing.T) {
|
|
full := validArchive(t)
|
|
// Keep the magic plus the single name-length byte, dropping the rest.
|
|
r := NewReader(bytes.NewReader(full[:len(MagicBytes)+1]))
|
|
if _, err := r.Next(); !errors.Is(err, ErrInvalidArchive) {
|
|
t.Fatalf("expected ErrInvalidArchive, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTruncatedPayloadIsInvalid(t *testing.T) {
|
|
full := validArchive(t)
|
|
// Drop the last 3 payload bytes; the header still decodes cleanly.
|
|
r := NewReader(bytes.NewReader(full[:len(full)-3]))
|
|
hdr, err := r.Next()
|
|
if err != nil {
|
|
t.Fatalf("next: %v", err)
|
|
}
|
|
if hdr.HeaderSize+hdr.PayloadSize != 6 {
|
|
t.Fatalf("unexpected payload size %d", hdr.HeaderSize+hdr.PayloadSize)
|
|
}
|
|
if _, err := io.ReadAll(r); !errors.Is(err, ErrInvalidArchive) {
|
|
t.Fatalf("expected ErrInvalidArchive reading payload, got %v", err)
|
|
}
|
|
}
|
|
|
|
// The header round-trips when its fields are read directly from the stream,
|
|
// including a large Sequence (uvarint) and a negative Timestamp (varint).
|
|
func TestHeaderFieldEncoding(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
w := NewWriter(&buf)
|
|
want := Header{Name: "nested/x.dat", Timestamp: -123, Sequence: 1 << 60, HeaderSize: 0, PayloadSize: 0}
|
|
if err := w.WriteHeader(&want); err != nil {
|
|
t.Fatalf("write header: %v", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatalf("close: %v", err)
|
|
}
|
|
|
|
r := NewReader(bytes.NewReader(buf.Bytes()))
|
|
got, err := r.Next()
|
|
if err != nil {
|
|
t.Fatalf("next: %v", err)
|
|
}
|
|
if *got != want {
|
|
t.Fatalf("header round-trip mismatch: got %+v want %+v", *got, want)
|
|
}
|
|
// A zero-length payload yields immediate EOF, then a clean end-of-stream.
|
|
if n, err := r.Read(make([]byte, 4)); n != 0 || !errors.Is(err, io.EOF) {
|
|
t.Fatalf("expected (0, EOF) reading empty payload, got (%d, %v)", n, err)
|
|
}
|
|
if _, err := r.Next(); !errors.Is(err, io.EOF) {
|
|
t.Fatalf("expected io.EOF after last entry, got %v", err)
|
|
}
|
|
}
|