package erasure_coding import ( "os" "path/filepath" "strings" "testing" "github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" "github.com/seaweedfs/seaweedfs/weed/storage/needle" "github.com/seaweedfs/seaweedfs/weed/storage/types" "github.com/seaweedfs/seaweedfs/weed/storage/volume_info" ) // A present-but-malformed .vif must FAIL the mount: every new encode // records a positive uniform block size there, and silently defaulting // to the legacy layout would serve those shards with the wrong offset // math. Absence stays legal — legacy volumes predate the sidecar. func TestNewEcVolumeFailsOnMalformedVif(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("seed .ecx: %v", err) } if err := os.WriteFile(base+".vif", []byte("not json"), 0644); err != nil { t.Fatalf("seed .vif: %v", err) } _, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)) if err == nil { t.Fatal("mount over a malformed .vif must fail") } if !strings.Contains(err.Error(), ".vif") { t.Errorf("error should name the vif: %v", err) } } func TestNewEcVolumeAbsentVifMountsWithDefaults(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("seed .ecx: %v", err) } ev, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)) if err != nil { t.Fatalf("legacy mount without .vif must succeed: %v", err) } defer ev.Close() if ev.ECContext.BlockSize != 0 { t.Errorf("legacy mount must use the legacy layout: BlockSize=%d", ev.ECContext.BlockSize) } } // A block size no encoder could have produced (negative, or not a whole // number of small blocks) maps every read to the wrong shard offset, so the // mount refuses it rather than serving those bytes. func TestNewEcVolumeRejectsImplausibleBlockSize(t *testing.T) { for _, blockSize := range []int64{1, -1, 3*1024*1024 + 1} { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("seed .ecx: %v", err) } if err := volume_info.SaveVolumeInfo(base+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.Version3), EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: 10, ParityShards: 4, BlockSize: blockSize, }, }); err != nil { t.Fatalf("seed .vif: %v", err) } if _, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)); err == nil { t.Errorf("block size %d must fail the mount", blockSize) } } } func TestNewEcVolumeAcceptsAlignedBlockSize(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("seed .ecx: %v", err) } if err := volume_info.SaveVolumeInfo(base+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.Version3), EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: 10, ParityShards: 4, BlockSize: 3 * 1024 * 1024, }, }); err != nil { t.Fatalf("seed .vif: %v", err) } ev, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)) if err != nil { t.Fatalf("aligned block size must mount: %v", err) } defer ev.Close() if ev.ECContext.BlockSize != 3*1024*1024 { t.Errorf("BlockSize = %d, want 3MiB", ev.ECContext.BlockSize) } } // The .vif and the .ecsum both record the layout their generation was encoded // with. When a generation-matching sidecar disagrees, one of them is wrong and // reads through the other land at the wrong shard offsets — so the mount must // refuse rather than quietly drop to unprotected reads. func TestNewEcVolumeRejectsSidecarGeometryDisagreement(t *testing.T) { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("seed .ecx: %v", err) } if err := volume_info.SaveVolumeInfo(base+".vif", &volume_server_pb.VolumeInfo{ Version: uint32(needle.Version3), EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: 10, ParityShards: 4, BlockSize: 0, // says legacy }, }); err != nil { t.Fatalf("seed .vif: %v", err) } // The sidecar for the same generation says uniform. prot := &volume_server_pb.EcBitrotProtection{ Algorithm: volume_server_pb.ChecksumAlgorithm_CHECKSUM_CRC32C, BlockSize: uint32(BitrotBlockSize), Generation: 0, EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: 10, ParityShards: 4, BlockSize: 3 * 1024 * 1024, }, } if err := SaveBitrotSidecar(BitrotSidecarPath(base, 0), prot); err != nil { t.Fatalf("seed .ecsum: %v", err) } _, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)) if err == nil { t.Fatal("a generation-matching layout disagreement must fail the mount") } if !strings.Contains(err.Error(), "block") { t.Errorf("the error should name the disagreement: %v", err) } } // With no .vif the bitrot sidecar is the ONLY record of the volume's layout. // Present but unusable — unreadable, stamped for another generation, or // carrying an incomplete ratio — must fail the mount: answering reads from a // guessed layout returns wrong bytes rather than none. Genuine absence stays // legal, and is covered by TestNewEcVolumeAbsentVifMountsWithDefaults. func TestNewEcVolumeRejectsUnusableSoleSidecar(t *testing.T) { seed := map[string]*volume_server_pb.EcBitrotProtection{ "another generation": { Algorithm: volume_server_pb.ChecksumAlgorithm_CHECKSUM_CRC32C, BlockSize: uint32(BitrotBlockSize), Generation: 5, EcShardConfig: &volume_server_pb.EcShardConfig{DataShards: 10, ParityShards: 4}, }, "no ec config": { Algorithm: volume_server_pb.ChecksumAlgorithm_CHECKSUM_CRC32C, BlockSize: uint32(BitrotBlockSize), Generation: 0, }, "incomplete ratio": { Algorithm: volume_server_pb.ChecksumAlgorithm_CHECKSUM_CRC32C, BlockSize: uint32(BitrotBlockSize), Generation: 0, EcShardConfig: &volume_server_pb.EcShardConfig{DataShards: 10}, }, "invalid block size": { Algorithm: volume_server_pb.ChecksumAlgorithm_CHECKSUM_CRC32C, BlockSize: uint32(BitrotBlockSize), Generation: 0, EcShardConfig: &volume_server_pb.EcShardConfig{ DataShards: 10, ParityShards: 4, BlockSize: 3*1024*1024 + 1, }, }, } for name, prot := range seed { dir := t.TempDir() base := filepath.Join(dir, "1") if err := os.WriteFile(base+".ecx", []byte{}, 0644); err != nil { t.Fatalf("%s: seed .ecx: %v", name, err) } if err := SaveBitrotSidecar(BitrotSidecarPath(base, 0), prot); err != nil { t.Fatalf("%s: seed .ecsum: %v", name, err) } if _, err := NewEcVolume(types.HardDriveType, dir, dir, "", needle.VolumeId(1)); err == nil { t.Errorf("%s: an unusable sole layout record must fail the mount", name) } } }