mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-10 00:20:42 +02:00
DecodeLayout rejected names over 256 bytes while Encode accepted them, so an oversized name encoded fine and then failed every decode. Enforce the bound in Validate, shared by both directions.
193 lines
6.4 KiB
Go
193 lines
6.4 KiB
Go
package format
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLayoutEncodeDecodeRoundTrip(t *testing.T) {
|
|
layout := &Layout{
|
|
Format: "hls-ts",
|
|
ExtentSizes: []int64{188 * 3, 188 * 2, 188 * 7},
|
|
Align: 188,
|
|
Payload: []byte{1, 2, 3},
|
|
}
|
|
encoded, err := layout.Encode()
|
|
if err != nil {
|
|
t.Fatalf("Encode() error = %v", err)
|
|
}
|
|
decoded, err := DecodeLayout(encoded)
|
|
if err != nil {
|
|
t.Fatalf("DecodeLayout() error = %v", err)
|
|
}
|
|
if decoded.Format != layout.Format || decoded.Align != layout.Align {
|
|
t.Fatalf("decoded = %+v, want %+v", decoded, layout)
|
|
}
|
|
if len(decoded.ExtentSizes) != len(layout.ExtentSizes) {
|
|
t.Fatalf("extent count = %d, want %d", len(decoded.ExtentSizes), len(layout.ExtentSizes))
|
|
}
|
|
for i := range layout.ExtentSizes {
|
|
if decoded.ExtentSizes[i] != layout.ExtentSizes[i] {
|
|
t.Fatalf("extent %d = %d, want %d", i, decoded.ExtentSizes[i], layout.ExtentSizes[i])
|
|
}
|
|
}
|
|
if string(decoded.Payload) != string(layout.Payload) {
|
|
t.Fatalf("payload = %v, want %v", decoded.Payload, layout.Payload)
|
|
}
|
|
}
|
|
|
|
func TestDecodeLayoutRejectsCorruptInput(t *testing.T) {
|
|
layout := &Layout{Format: "parquet", ExtentSizes: []int64{10, 20}, Align: 1}
|
|
encoded, err := layout.Encode()
|
|
if err != nil {
|
|
t.Fatalf("Encode() error = %v", err)
|
|
}
|
|
for cut := 0; cut < len(encoded); cut++ {
|
|
if _, err := DecodeLayout(encoded[:cut]); err == nil {
|
|
t.Fatalf("DecodeLayout() accepted truncation at %d", cut)
|
|
}
|
|
}
|
|
if _, err := DecodeLayout(append(append([]byte{}, encoded...), 0)); err == nil {
|
|
t.Fatalf("DecodeLayout() accepted trailing bytes")
|
|
}
|
|
}
|
|
|
|
func TestLayoutValidate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
layout Layout
|
|
fileSize int64
|
|
wantErr string
|
|
}{
|
|
{"valid", Layout{Format: "x", ExtentSizes: []int64{5, 5}, Align: 1}, 10, ""},
|
|
{"skip size check", Layout{Format: "x", ExtentSizes: []int64{5}, Align: 1}, -1, ""},
|
|
{"wrong total", Layout{Format: "x", ExtentSizes: []int64{5, 5}, Align: 1}, 11, "but the file has"},
|
|
{"zero extent", Layout{Format: "x", ExtentSizes: []int64{5, 0}, Align: 1}, -1, "invalid size"},
|
|
{"no extents", Layout{Format: "x", Align: 1}, -1, "no extents"},
|
|
{"bad align", Layout{Format: "x", ExtentSizes: []int64{5}, Align: 0}, -1, "align"},
|
|
{"no name", Layout{ExtentSizes: []int64{5}, Align: 1}, -1, "format name"},
|
|
{"name too long", Layout{Format: strings.Repeat("x", MaxFormatNameBytes+1), ExtentSizes: []int64{5}, Align: 1}, -1, "too long"},
|
|
}
|
|
for _, test := range tests {
|
|
err := test.layout.Validate(test.fileSize)
|
|
if test.wantErr == "" {
|
|
if err != nil {
|
|
t.Fatalf("%s: Validate() error = %v", test.name, err)
|
|
}
|
|
continue
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
|
t.Fatalf("%s: Validate() error = %v, want %q", test.name, err, test.wantErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtentRange(t *testing.T) {
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 20, 30}, Align: 1}
|
|
offset, size, ok := layout.ExtentRange(1)
|
|
if !ok || offset != 10 || size != 20 {
|
|
t.Fatalf("ExtentRange(1) = (%d, %d, %v), want (10, 20, true)", offset, size, ok)
|
|
}
|
|
if _, _, ok := layout.ExtentRange(3); ok {
|
|
t.Fatalf("ExtentRange(3) accepted out-of-range index")
|
|
}
|
|
if _, _, ok := layout.ExtentRange(-1); ok {
|
|
t.Fatalf("ExtentRange(-1) accepted negative index")
|
|
}
|
|
}
|
|
|
|
// collectChunks walks the cutter the way the upload loop does.
|
|
func collectChunks(t *testing.T, cutter *Cutter) [][2]int64 {
|
|
t.Helper()
|
|
var chunks [][2]int64
|
|
var offset int64
|
|
for {
|
|
size := cutter.NextChunkSize(offset)
|
|
if size <= 0 {
|
|
return chunks
|
|
}
|
|
chunks = append(chunks, [2]int64{offset, size})
|
|
offset += size
|
|
}
|
|
}
|
|
|
|
func TestCutterKeepsExtentBoundaries(t *testing.T) {
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{5, 4}, Align: 1}
|
|
chunks := collectChunks(t, layout.Cutter(16))
|
|
want := [][2]int64{{0, 5}, {5, 4}}
|
|
if len(chunks) != len(want) {
|
|
t.Fatalf("chunks = %v, want %v", chunks, want)
|
|
}
|
|
for i := range want {
|
|
if chunks[i] != want[i] {
|
|
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCutterSplitsOversizedExtentsOnAlign(t *testing.T) {
|
|
// maxChunkSize 5 with align 2 quantizes down to 4-byte interior cuts.
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 3}, Align: 2}
|
|
chunks := collectChunks(t, layout.Cutter(5))
|
|
want := [][2]int64{{0, 4}, {4, 4}, {8, 2}, {10, 3}}
|
|
if len(chunks) != len(want) {
|
|
t.Fatalf("chunks = %v, want %v", chunks, want)
|
|
}
|
|
for i := range want {
|
|
if chunks[i] != want[i] {
|
|
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCutterAlignLargerThanChunkLimit(t *testing.T) {
|
|
// Align above maxChunkSize still cuts on whole atoms.
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{20}, Align: 8}
|
|
chunks := collectChunks(t, layout.Cutter(5))
|
|
want := [][2]int64{{0, 8}, {8, 8}, {16, 4}}
|
|
if len(chunks) != len(want) {
|
|
t.Fatalf("chunks = %v, want %v", chunks, want)
|
|
}
|
|
for i := range want {
|
|
if chunks[i] != want[i] {
|
|
t.Fatalf("chunk %d = %v, want %v", i, chunks[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCutterUnlimitedKeepsOneChunkPerExtent(t *testing.T) {
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{10, 3}, Align: 188}
|
|
chunks := collectChunks(t, layout.Cutter(0))
|
|
want := [][2]int64{{0, 10}, {10, 3}}
|
|
if len(chunks) != len(want) {
|
|
t.Fatalf("chunks = %v, want %v", chunks, want)
|
|
}
|
|
}
|
|
|
|
// A hostile layout may declare an enormous extent; the cutter must stay O(1)
|
|
// per query instead of materializing every interior cut.
|
|
func TestCutterHugeExtentStaysLazy(t *testing.T) {
|
|
const quantum = 4 << 20 // 4MiB, already a multiple of align 1
|
|
layout := &Layout{Format: "x", ExtentSizes: []int64{1 << 50, 188}, Align: 188}
|
|
cutter := layout.Cutter(quantum)
|
|
alignedQuantum := int64(quantum - quantum%188)
|
|
if got := cutter.NextChunkSize(0); got != alignedQuantum {
|
|
t.Fatalf("NextChunkSize(0) = %d, want %d", got, alignedQuantum)
|
|
}
|
|
if got := cutter.NextChunkSize(alignedQuantum * 1000); got != alignedQuantum {
|
|
t.Fatalf("mid-extent chunk = %d, want %d", got, alignedQuantum)
|
|
}
|
|
// the final interior chunk stops at the extent boundary
|
|
last := (int64(1<<50) / alignedQuantum) * alignedQuantum
|
|
if got := cutter.NextChunkSize(last); got != int64(1<<50)-last {
|
|
t.Fatalf("tail chunk = %d, want %d", got, int64(1<<50)-last)
|
|
}
|
|
// the next extent still cuts independently
|
|
if got := cutter.NextChunkSize(1 << 50); got != 188 {
|
|
t.Fatalf("second extent chunk = %d, want 188", got)
|
|
}
|
|
if got := cutter.NextChunkSize(1<<50 + 188); got != 0 {
|
|
t.Fatalf("past end = %d, want 0", got)
|
|
}
|
|
}
|