Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
209
internal/erasure/service.go
Normal file
209
internal/erasure/service.go
Normal file
@@ -0,0 +1,209 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user