mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +00:00
* erasure_coding: WriteDatFile takes the encode-time dat size for the shard block layout * volume server: derive EC decode layout from the encode-time dat size, not the live extent * erasure_coding: test decode after tail deletions shrink the live extent below a large-block row * seaweed-volume: write_dat_file_from_shards takes the encode-time dat size for the shard block layout * seaweed-volume: derive EC decode layout from the encode-time dat size, not the live extent * seaweed-volume: test decode after tail deletions shrink the live extent below a large-block row * erasure_coding: reject decoding with no data shards * worker: record the encode-time dat size in the .vif * erasure_coding: fall back to the shard-derived layout only when the encode-time dat size is missing * erasure_coding: reject an ambiguous shard-derived block layout * seaweed-volume: fall back to the shard-derived layout only when the encode-time dat size is missing * seaweed-volume: reject an ambiguous shard-derived block layout
510 lines
19 KiB
Go
510 lines
19 KiB
Go
package erasure_coding
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/rand"
|
||
"fmt"
|
||
"os"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
||
)
|
||
|
||
// TestEcReadRoundTrip tests the EC encode→read cycle via LocateData for various
|
||
// .dat file sizes, paying special attention to the large/small block boundary.
|
||
//
|
||
// The nLargeBlockRows calculation must correctly distinguish between large and small
|
||
// blocks. A previous bug (issue #8947) caused an off-by-one error when
|
||
// shardDatSize was an exact multiple of largeBlockSize, leading to data corruption.
|
||
func TestEcReadRoundTrip(t *testing.T) {
|
||
const (
|
||
large = largeBlockSize // 10000
|
||
small = smallBlockSize // 100
|
||
)
|
||
|
||
largeRowSize := large * DataShardsCount // 100000
|
||
smallRowSize := small * DataShardsCount // 1000
|
||
|
||
testCases := []struct {
|
||
name string
|
||
datSize int64
|
||
}{
|
||
// Exact multiples of largeRowSize — triggers the nLargeBlockRows off-by-one bug
|
||
{"1_large_row_exact", int64(largeRowSize)},
|
||
{"2_large_rows_exact", int64(2 * largeRowSize)},
|
||
{"3_large_rows_exact", int64(3 * largeRowSize)},
|
||
|
||
// Just over a large row boundary — has small blocks
|
||
{"1_large_row_plus_1", int64(largeRowSize + 1)},
|
||
{"2_large_rows_plus_small", int64(2*largeRowSize + smallRowSize)},
|
||
{"1_large_row_plus_half_small", int64(largeRowSize + smallRowSize/2)},
|
||
|
||
// Just under a large row boundary — all small blocks
|
||
{"just_under_1_large_row", int64(largeRowSize - 1)},
|
||
{"just_under_2_large_rows", int64(2*largeRowSize - 1)},
|
||
|
||
// Small data — no large blocks at all
|
||
{"small_only", int64(smallRowSize * 3)},
|
||
{"small_single_row", int64(smallRowSize)},
|
||
|
||
// Boundary with mixed large and small
|
||
{"boundary_spanning", int64(largeRowSize + smallRowSize*5 + 50)},
|
||
}
|
||
|
||
for _, tc := range testCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
testEcRead(t, large, small, tc.datSize)
|
||
})
|
||
}
|
||
}
|
||
|
||
// testEcRead creates a .dat file, EC-encodes it, then verifies LocateData-based reads
|
||
// return correct data at positions throughout the file (especially near the large/small
|
||
// block boundary).
|
||
func testEcRead(t *testing.T, large, small, datSize int64) {
|
||
t.Helper()
|
||
|
||
dir := t.TempDir()
|
||
baseFileName := fmt.Sprintf("%s/rt_%d", dir, datSize)
|
||
|
||
// 1. Create a .dat file with deterministic random data
|
||
originalData := make([]byte, datSize)
|
||
_, err := rand.Read(originalData)
|
||
require.NoError(t, err, "generating random data")
|
||
|
||
err = os.WriteFile(baseFileName+".dat", originalData, 0644)
|
||
require.NoError(t, err, "writing .dat file")
|
||
|
||
ctx := NewDefaultECContext("", 0)
|
||
|
||
// 2. EC encode with test block sizes
|
||
_, err = generateEcFiles(baseFileName, int(small), large, small, ctx)
|
||
require.NoError(t, err, "EC encoding")
|
||
|
||
// 3. Open EC shard files for reading
|
||
ecFiles, err := openEcFiles(baseFileName, true, ctx)
|
||
require.NoError(t, err, "opening EC files")
|
||
defer closeEcFiles(ecFiles)
|
||
|
||
ecStat, err := ecFiles[0].Stat()
|
||
require.NoError(t, err)
|
||
shardFileSize := ecStat.Size()
|
||
|
||
// Compute shardDatSize as the production code does when datFileSize is known
|
||
shardDatSizeFromDat := datSize / int64(ctx.DataShards)
|
||
|
||
// 4. Verify EC reads at various positions
|
||
largeRowSize := large * DataShardsCount
|
||
encoderLargeRows := datSize / int64(largeRowSize)
|
||
boundaryOffset := encoderLargeRows * int64(largeRowSize)
|
||
|
||
readSize := types.Size(small / 2) // read half a small block
|
||
testOffsets := collectTestOffsets(datSize, int64(readSize), boundaryOffset, large, small)
|
||
|
||
for _, offset := range testOffsets {
|
||
// Test with shardDatSize from datFileSize (the production path with fix)
|
||
intervals := LocateData(large, small, shardDatSizeFromDat, offset, readSize)
|
||
ecData, err := assembleFromIntervals(ecFiles, intervals, large, small)
|
||
require.NoError(t, err, "reading EC data at offset %d (datFileSize path)", offset)
|
||
|
||
expected := originalData[offset : offset+int64(readSize)]
|
||
if !bytes.Equal(expected, ecData) {
|
||
t.Errorf("EC read mismatch at offset %d (datFileSize path, shardDatSize=%d, nLargeBlockRows=%d)",
|
||
offset, shardDatSizeFromDat, shardDatSizeFromDat/large)
|
||
}
|
||
|
||
// Test with shardDatSize from ecdFileSize-1 (the fallback path for old volumes)
|
||
intervalsFallback := LocateData(large, small, shardFileSize-1, offset, readSize)
|
||
ecDataFallback, err := assembleFromIntervals(ecFiles, intervalsFallback, large, small)
|
||
if err == nil && !bytes.Equal(expected, ecDataFallback) {
|
||
// The fallback path may fail for exact multiples — log as warning
|
||
t.Logf("WARN: EC read mismatch at offset %d (fallback path, shardFileSize=%d)",
|
||
offset, shardFileSize)
|
||
}
|
||
}
|
||
}
|
||
|
||
// locateOffsetBuggy reimplements locateOffset with the old buggy formula:
|
||
//
|
||
// nLargeBlockRows = (shardDatSize - 1) / largeBlockLength
|
||
//
|
||
// This caused an off-by-one error when shardDatSize was an exact multiple of
|
||
// largeBlockLength, miscounting the number of large block rows.
|
||
func locateOffsetBuggy(largeBlockLength, smallBlockLength int64, shardDatSize int64, offset int64) (blockIndex int, isLargeBlock bool, nLargeBlockRows int64, innerBlockOffset int64) {
|
||
largeRowSize := largeBlockLength * DataShardsCount
|
||
nLargeBlockRows = (shardDatSize - 1) / largeBlockLength // THE BUG
|
||
|
||
if offset < nLargeBlockRows*largeRowSize {
|
||
isLargeBlock = true
|
||
blockIndex = int(offset / largeBlockLength)
|
||
innerBlockOffset = offset % largeBlockLength
|
||
return
|
||
}
|
||
|
||
isLargeBlock = false
|
||
offset -= nLargeBlockRows * largeRowSize
|
||
blockIndex = int(offset / smallBlockLength)
|
||
innerBlockOffset = offset % smallBlockLength
|
||
return
|
||
}
|
||
|
||
// locateDataBuggy is LocateData using the old buggy locateOffset.
|
||
func locateDataBuggy(largeBlockLength, smallBlockLength int64, shardDatSize int64, offset int64, size types.Size) []Interval {
|
||
blockIndex, isLargeBlock, nLargeBlockRows, innerBlockOffset := locateOffsetBuggy(largeBlockLength, smallBlockLength, shardDatSize, offset)
|
||
|
||
var intervals []Interval
|
||
for size > 0 {
|
||
blockRemaining := largeBlockLength - innerBlockOffset
|
||
if !isLargeBlock {
|
||
blockRemaining = smallBlockLength - innerBlockOffset
|
||
}
|
||
if blockRemaining <= 0 {
|
||
blockIndex, isLargeBlock = moveToNextBlock(blockIndex, isLargeBlock, nLargeBlockRows)
|
||
innerBlockOffset = 0
|
||
continue
|
||
}
|
||
interval := Interval{
|
||
BlockIndex: blockIndex,
|
||
InnerBlockOffset: innerBlockOffset,
|
||
IsLargeBlock: isLargeBlock,
|
||
LargeBlockRowsCount: int(nLargeBlockRows),
|
||
}
|
||
if int64(size) <= blockRemaining {
|
||
interval.Size = size
|
||
intervals = append(intervals, interval)
|
||
return intervals
|
||
}
|
||
interval.Size = types.Size(blockRemaining)
|
||
intervals = append(intervals, interval)
|
||
size -= interval.Size
|
||
blockIndex, isLargeBlock = moveToNextBlock(blockIndex, isLargeBlock, nLargeBlockRows)
|
||
innerBlockOffset = 0
|
||
}
|
||
return intervals
|
||
}
|
||
|
||
// TestEcOffByOneBug_Issue8947 directly demonstrates the off-by-one bug.
|
||
//
|
||
// It creates a .dat file whose size is an exact multiple of (largeBlockSize * DataShards),
|
||
// EC-encodes it, then shows that:
|
||
// - The OLD buggy formula produces WRONG data (data corruption)
|
||
// - The FIXED formula produces CORRECT data
|
||
func TestEcOffByOneBug_Issue8947(t *testing.T) {
|
||
const (
|
||
large = largeBlockSize // 10000
|
||
small = smallBlockSize // 100
|
||
)
|
||
|
||
// datSize is exactly 2 large rows — each shard gets exactly 2*largeBlockSize bytes.
|
||
// The encoder produces 2 large block rows and 0 small block rows.
|
||
datSize := int64(2 * large * DataShardsCount) // 200000
|
||
|
||
dir := t.TempDir()
|
||
baseFileName := fmt.Sprintf("%s/bug_%d", dir, datSize)
|
||
|
||
originalData := make([]byte, datSize)
|
||
_, err := rand.Read(originalData)
|
||
require.NoError(t, err)
|
||
|
||
err = os.WriteFile(baseFileName+".dat", originalData, 0644)
|
||
require.NoError(t, err)
|
||
|
||
ctx := NewDefaultECContext("", 0)
|
||
_, err = generateEcFiles(baseFileName, int(small), large, small, ctx)
|
||
require.NoError(t, err, "EC encoding")
|
||
|
||
ecFiles, err := openEcFiles(baseFileName, true, ctx)
|
||
require.NoError(t, err)
|
||
defer closeEcFiles(ecFiles)
|
||
|
||
// shardDatSize = datFileSize / DataShards = 2 * largeBlockSize
|
||
// This is an EXACT multiple of largeBlockSize.
|
||
shardDatSize := datSize / int64(ctx.DataShards) // = 2 * large = 20000
|
||
|
||
// The encoder used 2 large block rows, 0 small block rows.
|
||
// Correct: nLargeBlockRows = 20000 / 10000 = 2
|
||
// Buggy: nLargeBlockRows = (20000 - 1) / 10000 = 1 ← OFF BY ONE
|
||
fixedRows := shardDatSize / large
|
||
buggyRows := (shardDatSize - 1) / large
|
||
assert.Equal(t, int64(2), fixedRows, "fixed formula should give 2 large block rows")
|
||
assert.Equal(t, int64(1), buggyRows, "buggy formula gives only 1 (the bug)")
|
||
|
||
// Test reading from the 2nd large block row (offsets 100000–199999).
|
||
// With the buggy formula (nLargeBlockRows=1), this region is incorrectly
|
||
// treated as small blocks, causing reads from the WRONG shard positions.
|
||
readSize := types.Size(small / 2)
|
||
|
||
// Pick an offset well into the 2nd large block row so that the buggy formula
|
||
// computes a different (shard, offset) than the correct formula.
|
||
// At the very start of the 2nd row, both formulas coincidentally hit the same
|
||
// shard position. But further in, the small-block vs large-block addressing diverges.
|
||
offset := int64(large*DataShardsCount) + large + 50 // 110050: in 2nd large row, shard 1
|
||
|
||
// --- Fixed formula: reads correct data ---
|
||
fixedIntervals := LocateData(large, small, shardDatSize, offset, readSize)
|
||
fixedData, err := assembleFromIntervals(ecFiles, fixedIntervals, large, small)
|
||
require.NoError(t, err, "fixed LocateData read")
|
||
|
||
expected := originalData[offset : offset+int64(readSize)]
|
||
assert.True(t, bytes.Equal(expected, fixedData),
|
||
"FIXED formula should read correct data from 2nd large block row")
|
||
|
||
// --- Buggy formula: reads WRONG data ---
|
||
buggyIntervals := locateDataBuggy(large, small, shardDatSize, offset, readSize)
|
||
buggyData, err := assembleFromIntervals(ecFiles, buggyIntervals, large, small)
|
||
// The buggy formula might read from wrong offsets (possibly out of bounds),
|
||
// so an error is also evidence of the bug.
|
||
if err != nil {
|
||
t.Logf("Buggy formula caused read error (expected): %v", err)
|
||
} else {
|
||
assert.False(t, bytes.Equal(expected, buggyData),
|
||
"BUGGY formula should return WRONG data from 2nd large block row (demonstrating the corruption)")
|
||
n := 8
|
||
if len(expected) < n {
|
||
n = len(expected)
|
||
}
|
||
t.Logf("Buggy formula returned wrong data: expected first bytes %x, got %x",
|
||
expected[:n], buggyData[:n])
|
||
}
|
||
|
||
// Verify the bug mechanism: buggy formula misclassifies the 2nd large row as small blocks
|
||
assert.True(t, fixedIntervals[0].IsLargeBlock,
|
||
"fixed: offset %d should be in large blocks", offset)
|
||
assert.False(t, buggyIntervals[0].IsLargeBlock,
|
||
"buggy: offset %d is incorrectly classified as small blocks (the bug)", offset)
|
||
|
||
t.Logf("Fixed: nLargeBlockRows=%d, interval=%+v", fixedRows, fixedIntervals[0])
|
||
t.Logf("Buggy: nLargeBlockRows=%d, interval=%+v", buggyRows, buggyIntervals[0])
|
||
}
|
||
|
||
// TestEcDecodeDatRoundTrip tests the full WriteDatFile decode path using the production
|
||
// block sizes with a small .dat file that fits within the small block region.
|
||
func TestEcDecodeDatRoundTrip(t *testing.T) {
|
||
// With production sizes, datFileSize must be < DataShardsCount * ErasureCodingLargeBlockSize (10GB)
|
||
// to avoid needing huge test files. We test small block decode only.
|
||
// Each shard gets datSize/DataShards bytes in small 1MB blocks.
|
||
datSizes := []int64{
|
||
1000, // tiny
|
||
int64(DataShardsCount) * ErasureCodingSmallBlockSize, // exactly 1 small row (10MB)
|
||
int64(DataShardsCount)*ErasureCodingSmallBlockSize + 500, // 1 small row + partial
|
||
}
|
||
|
||
for _, datSize := range datSizes {
|
||
t.Run(fmt.Sprintf("size_%d", datSize), func(t *testing.T) {
|
||
testDecodeDat(t, datSize)
|
||
})
|
||
}
|
||
}
|
||
|
||
func testDecodeDat(t *testing.T, datSize int64) {
|
||
t.Helper()
|
||
|
||
dir := t.TempDir()
|
||
baseFileName := fmt.Sprintf("%s/dec_%d", dir, datSize)
|
||
|
||
// 1. Create .dat with random data
|
||
originalData := make([]byte, datSize)
|
||
_, err := rand.Read(originalData)
|
||
require.NoError(t, err)
|
||
|
||
err = os.WriteFile(baseFileName+".dat", originalData, 0644)
|
||
require.NoError(t, err)
|
||
|
||
ctx := NewDefaultECContext("", 0)
|
||
|
||
// 2. EC encode with PRODUCTION block sizes
|
||
_, err = generateEcFiles(baseFileName, 256*1024, ErasureCodingLargeBlockSize, ErasureCodingSmallBlockSize, ctx)
|
||
require.NoError(t, err, "EC encoding")
|
||
|
||
// 3. Decode via WriteDatFile
|
||
decodedBase := baseFileName + "_decoded"
|
||
shardFileNames := make([]string, DataShardsCount)
|
||
for i := 0; i < DataShardsCount; i++ {
|
||
shardFileNames[i] = fmt.Sprintf("%s%s", baseFileName, ctx.ToExt(i))
|
||
}
|
||
|
||
err = WriteDatFile(decodedBase, datSize, datSize, shardFileNames)
|
||
require.NoError(t, err, "WriteDatFile")
|
||
|
||
// The atomic publish must rename the temp file away, never leaving it behind.
|
||
_, statErr := os.Stat(decodedBase + ".dat.tmp")
|
||
require.True(t, os.IsNotExist(statErr), "WriteDatFile must not leave a .dat.tmp")
|
||
|
||
// 4. Verify decoded .dat matches original
|
||
decodedData, err := os.ReadFile(decodedBase + ".dat")
|
||
require.NoError(t, err)
|
||
|
||
assert.Equal(t, len(originalData), len(decodedData), "decoded .dat size mismatch")
|
||
if !bytes.Equal(originalData, decodedData) {
|
||
for i := 0; i < len(originalData) && i < len(decodedData); i++ {
|
||
if originalData[i] != decodedData[i] {
|
||
t.Fatalf("decoded .dat mismatch at byte %d (datSize=%d)", i, datSize)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// collectTestOffsets generates offsets to test, focusing on the large/small block boundary.
|
||
func collectTestOffsets(datSize, readSize, boundaryOffset, large, small int64) []int64 {
|
||
offsets := []int64{0}
|
||
|
||
if datSize > readSize {
|
||
offsets = append(offsets, datSize/2)
|
||
}
|
||
|
||
// Near the large/small block boundary
|
||
if boundaryOffset > 0 && boundaryOffset < datSize {
|
||
for _, delta := range []int64{-large, -small, -1, 0, 1, small, large} {
|
||
off := boundaryOffset + delta
|
||
if off >= 0 && off+readSize <= datSize {
|
||
offsets = append(offsets, off)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Near end of file
|
||
if datSize > readSize {
|
||
offsets = append(offsets, datSize-readSize)
|
||
}
|
||
|
||
return offsets
|
||
}
|
||
|
||
// assembleFromIntervals reads data from EC shard files according to the given intervals.
|
||
func assembleFromIntervals(ecFiles []*os.File, intervals []Interval, large, small int64) ([]byte, error) {
|
||
var data []byte
|
||
for _, interval := range intervals {
|
||
shardId, shardOffset := interval.ToShardIdAndOffset(large, small)
|
||
chunk := make([]byte, interval.Size)
|
||
n, err := ecFiles[shardId].ReadAt(chunk, shardOffset)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read shard %d offset %d size %d: %v", shardId, shardOffset, interval.Size, err)
|
||
}
|
||
if n != int(interval.Size) {
|
||
return nil, fmt.Errorf("short read from shard %d: got %d, want %d", shardId, n, interval.Size)
|
||
}
|
||
data = append(data, chunk...)
|
||
}
|
||
return data, nil
|
||
}
|
||
|
||
// TestWriteDatFileAfterTailDeletion decodes after deletions moved the live
|
||
// extent below the large-block row boundary. The shard block layout is fixed
|
||
// by the encode-time .dat size, so de-striping must not derive it from the
|
||
// shrunk live extent.
|
||
func TestWriteDatFileAfterTailDeletion(t *testing.T) {
|
||
const (
|
||
large = int64(largeBlockSize) // 10000
|
||
small = int64(smallBlockSize) // 100
|
||
)
|
||
largeRowSize := large * DataShardsCount
|
||
smallRowSize := small * DataShardsCount
|
||
|
||
// one full large-block row plus a small-block tail
|
||
datSize := largeRowSize + 2*smallRowSize + 530
|
||
|
||
dir := t.TempDir()
|
||
baseFileName := fmt.Sprintf("%s/tail_del", dir)
|
||
|
||
originalData := make([]byte, datSize)
|
||
_, err := rand.Read(originalData)
|
||
require.NoError(t, err)
|
||
require.NoError(t, os.WriteFile(baseFileName+".dat", originalData, 0644))
|
||
|
||
ctx := NewDefaultECContext("", 0)
|
||
_, err = generateEcFiles(baseFileName, 50, large, small, ctx)
|
||
require.NoError(t, err, "EC encoding")
|
||
|
||
shardFileNames := make([]string, DataShardsCount)
|
||
for i := 0; i < DataShardsCount; i++ {
|
||
shardFileNames[i] = baseFileName + ctx.ToExt(i)
|
||
}
|
||
shardFileInfo, err := os.Stat(shardFileNames[0])
|
||
require.NoError(t, err)
|
||
paddedSize := int64(DataShardsCount) * shardFileInfo.Size()
|
||
|
||
decodedBase := baseFileName + "_decoded"
|
||
decode := func(liveSize, encodedSize int64) []byte {
|
||
require.NoError(t, writeDatFile(decodedBase, liveSize, encodedSize, shardFileNames, large, small))
|
||
decoded, readErr := os.ReadFile(decodedBase + ".dat")
|
||
require.NoError(t, readErr)
|
||
require.NoError(t, os.Remove(decodedBase+".dat"))
|
||
return decoded
|
||
}
|
||
|
||
liveSizes := []int64{
|
||
large - 1, // within the first large block
|
||
large + 42, // partial second large block
|
||
largeRowSize / 2, // mid large row
|
||
largeRowSize, // exactly the large row
|
||
largeRowSize + 5*small, // into the small-block tail
|
||
datSize, // nothing deleted
|
||
}
|
||
for _, liveSize := range liveSizes {
|
||
assert.Equal(t, originalData[:liveSize], decode(liveSize, datSize), "live size %d, encode-time layout", liveSize)
|
||
assert.Equal(t, originalData[:liveSize], decode(liveSize, paddedSize), "live size %d, padded layout", liveSize)
|
||
}
|
||
|
||
// deriving the layout from the shrunk live extent reorders the data
|
||
assert.NotEqual(t, originalData[:largeRowSize/2], decode(largeRowSize/2, largeRowSize/2))
|
||
|
||
// the live extent can never exceed the encode-time size
|
||
require.Error(t, writeDatFile(decodedBase, datSize+1, datSize, shardFileNames, large, small))
|
||
}
|
||
|
||
// TestWriteDatFileFallbackLayout covers decoding when .vif does not record the
|
||
// encode-time size: the layout is inferred from the shard size, except when
|
||
// that is an exact large-block multiple and the live extent reaches the
|
||
// ambiguous region.
|
||
func TestWriteDatFileFallbackLayout(t *testing.T) {
|
||
const (
|
||
large = int64(largeBlockSize) // 10000
|
||
small = int64(smallBlockSize) // 100
|
||
)
|
||
largeRowSize := large * DataShardsCount
|
||
|
||
dir := t.TempDir()
|
||
|
||
encode := func(name string, datSize int64) (string, []string, []byte) {
|
||
baseFileName := fmt.Sprintf("%s/%s", dir, name)
|
||
originalData := make([]byte, datSize)
|
||
_, err := rand.Read(originalData)
|
||
require.NoError(t, err)
|
||
require.NoError(t, os.WriteFile(baseFileName+".dat", originalData, 0644))
|
||
ctx := NewDefaultECContext("", 0)
|
||
_, err = generateEcFiles(baseFileName, 50, large, small, ctx)
|
||
require.NoError(t, err, "EC encoding")
|
||
shardFileNames := make([]string, DataShardsCount)
|
||
for i := 0; i < DataShardsCount; i++ {
|
||
shardFileNames[i] = baseFileName + ctx.ToExt(i)
|
||
}
|
||
return baseFileName, shardFileNames, originalData
|
||
}
|
||
|
||
// a small-block tail that is not a large-block multiple is unambiguous
|
||
base, shards, original := encode("plain", largeRowSize+2530)
|
||
liveSize := largeRowSize / 2
|
||
require.NoError(t, writeDatFile(base+"_d", liveSize, 0, shards, large, small))
|
||
decoded, err := os.ReadFile(base + "_d.dat")
|
||
require.NoError(t, err)
|
||
assert.Equal(t, original[:liveSize], decoded)
|
||
|
||
// datSize just under one large row: the full small-block region makes each
|
||
// shard exactly largeBlockSize, indistinguishable from one large row
|
||
base, shards, _ = encode("ambiguous1", largeRowSize-1)
|
||
err = writeDatFile(base+"_d", largeRowSize/2, 0, shards, large, small)
|
||
require.ErrorContains(t, err, "does not identify the block layout")
|
||
|
||
// two-row equivalent: decoding within the agreed prefix still works
|
||
base, shards, original = encode("ambiguous2", 2*largeRowSize-1)
|
||
require.NoError(t, writeDatFile(base+"_d", largeRowSize, 0, shards, large, small))
|
||
decoded, err = os.ReadFile(base + "_d.dat")
|
||
require.NoError(t, err)
|
||
assert.Equal(t, original[:largeRowSize], decoded)
|
||
err = writeDatFile(base+"_d2", largeRowSize+1, 0, shards, large, small)
|
||
require.ErrorContains(t, err, "does not identify the block layout")
|
||
}
|