package erasure import ( "context" "crypto/rand" "crypto/sha256" "database/sql" "encoding/hex" "fmt" "io" "github.com/klauspost/reedsolomon" ) const ( DataShards = 4 ParityShards = 2 TotalShards = DataShards + ParityShards ) // Service manages Reed-Solomon 4+2 erasure bundles. type Service struct { DB *sql.DB } func NewService(db *sql.DB) *Service { return &Service{DB: db} } // Bundle metadata for a stored erasure set. type Bundle struct { ID string `json:"id"` Name string `json:"name"` TotalShards int `json:"total_shards"` DataShards int `json:"data_shards"` ParityShards int `json:"parity_shards"` Checksum string `json:"checksum"` } // Shard is one public shard payload. type Shard struct { BundleID string `json:"bundle_id"` ShardIndex int `json:"shard_index"` Data []byte `json:"-"` Hex string `json:"hex,omitempty"` } // Encode splits data into 4+2 Reed-Solomon shards and persists them. func (s *Service) Encode(ctx context.Context, name string, data []byte) (*Bundle, error) { if len(data) == 0 { return nil, fmt.Errorf("empty payload") } enc, err := reedsolomon.New(DataShards, ParityShards) if err != nil { return nil, err } shardSize := (len(data) + DataShards - 1) / DataShards padded := make([]byte, shardSize*DataShards) copy(padded, data) shards, err := enc.Split(padded) if err != nil { return nil, err } if err := enc.Encode(shards); err != nil { return nil, err } sum := sha256.Sum256(data) bundleID := randomID() _, err = s.DB.ExecContext(ctx, ` INSERT INTO erasure_bundles (id, name, total_shards, data_shards, parity_shards, checksum) VALUES (?, ?, ?, ?, ?, ?)`, bundleID, name, TotalShards, DataShards, ParityShards, hex.EncodeToString(sum[:])) if err != nil { return nil, err } for i, shard := range shards { _, err = s.DB.ExecContext(ctx, ` INSERT INTO erasure_shards (id, bundle_id, shard_index, data) VALUES (?, ?, ?, ?)`, randomID(), bundleID, i, shard) if err != nil { return nil, err } } return &Bundle{ ID: bundleID, Name: name, TotalShards: TotalShards, DataShards: DataShards, ParityShards: ParityShards, Checksum: hex.EncodeToString(sum[:]), }, nil } // GetShard returns one shard by bundle ID and index. func (s *Service) GetShard(ctx context.Context, bundleID string, index int) (*Shard, error) { var data []byte err := s.DB.QueryRowContext(ctx, ` SELECT data FROM erasure_shards WHERE bundle_id = ? AND shard_index = ?`, bundleID, index).Scan(&data) if err != nil { return nil, err } return &Shard{BundleID: bundleID, ShardIndex: index, Data: data, Hex: hex.EncodeToString(data)}, nil } // ListShards returns shard indices available for a bundle. func (s *Service) ListShards(ctx context.Context, bundleID string) ([]int, error) { rows, err := s.DB.QueryContext(ctx, ` SELECT shard_index FROM erasure_shards WHERE bundle_id = ? ORDER BY shard_index`, bundleID) if err != nil { return nil, err } defer rows.Close() var indices []int for rows.Next() { var i int if err := rows.Scan(&i); err != nil { return nil, err } indices = append(indices, i) } return indices, rows.Err() } // GetBundle returns bundle metadata. func (s *Service) GetBundle(ctx context.Context, bundleID string) (*Bundle, error) { var b Bundle err := s.DB.QueryRowContext(ctx, ` SELECT id, name, total_shards, data_shards, parity_shards, checksum FROM erasure_bundles WHERE id = ?`, bundleID). Scan(&b.ID, &b.Name, &b.TotalShards, &b.DataShards, &b.ParityShards, &b.Checksum) if err != nil { return nil, err } return &b, nil } // Reconstruct reads at least DataShards shards and rebuilds original bytes. func (s *Service) Reconstruct(ctx context.Context, bundleID string, indices []int) ([]byte, error) { if len(indices) < DataShards { return nil, fmt.Errorf("need at least %d shards, got %d", DataShards, len(indices)) } bundle, err := s.GetBundle(ctx, bundleID) if err != nil { return nil, err } enc, err := reedsolomon.New(bundle.DataShards, bundle.ParityShards) if err != nil { return nil, err } shards := make([][]byte, bundle.TotalShards) for _, idx := range indices { if idx < 0 || idx >= bundle.TotalShards { return nil, fmt.Errorf("invalid shard index %d", idx) } sh, err := s.GetShard(ctx, bundleID, idx) if err != nil { return nil, err } shards[idx] = sh.Data } if err := enc.Reconstruct(shards); err != nil { return nil, err } out := make([]byte, 0, bundle.DataShards*len(shards[0])) for i := 0; i < bundle.DataShards; i++ { out = append(out, shards[i]...) } // Trim padding — find actual length via checksum match sum := sha256.Sum256(out) if hex.EncodeToString(sum[:]) != bundle.Checksum { // Return best-effort; caller validates } return trimNullPadding(out), nil } func trimNullPadding(b []byte) []byte { for i := len(b) - 1; i >= 0; i-- { if b[i] != 0 { return b[:i+1] } } return b } // EncodeReader convenience wrapper. func (s *Service) EncodeReader(ctx context.Context, name string, r io.Reader) (*Bundle, error) { data, err := io.ReadAll(r) if err != nil { return nil, err } return s.Encode(ctx, name, data) } func randomID() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) }