Merge branch 'worktree-agent-a79f3e32'
This commit is contained in:
43
cmd/sgard/verify.go
Normal file
43
cmd/sgard/verify.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/kisom/sgard/garden"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verifyCmd = &cobra.Command{
|
||||||
|
Use: "verify",
|
||||||
|
Short: "Check all blobs against manifest hashes",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
g, err := garden.Open(repoFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := g.Verify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allOK := true
|
||||||
|
for _, r := range results {
|
||||||
|
fmt.Printf("%-14s %s\n", r.Detail, r.Path)
|
||||||
|
if !r.OK {
|
||||||
|
allOK = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allOK {
|
||||||
|
return errors.New("verification failed: one or more blobs are corrupt or missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(verifyCmd)
|
||||||
|
}
|
||||||
61
garden/verify.go
Normal file
61
garden/verify.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package garden
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifyResult reports the integrity of a single tracked blob.
|
||||||
|
type VerifyResult struct {
|
||||||
|
Path string // tilde path from manifest
|
||||||
|
OK bool
|
||||||
|
Detail string // e.g. "ok", "hash mismatch", "blob missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks every file entry in the manifest against the blob store.
|
||||||
|
// It confirms that the blob exists and that its content still matches
|
||||||
|
// the recorded hash. Directories and symlinks are skipped because they
|
||||||
|
// have no blobs.
|
||||||
|
func (g *Garden) Verify() ([]VerifyResult, error) {
|
||||||
|
var results []VerifyResult
|
||||||
|
|
||||||
|
for _, entry := range g.manifest.Files {
|
||||||
|
if entry.Type != "file" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !g.store.Exists(entry.Hash) {
|
||||||
|
results = append(results, VerifyResult{
|
||||||
|
Path: entry.Path,
|
||||||
|
OK: false,
|
||||||
|
Detail: "blob missing",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := g.store.Read(entry.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading blob for %s: %w", entry.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
got := hex.EncodeToString(sum[:])
|
||||||
|
|
||||||
|
if got != entry.Hash {
|
||||||
|
results = append(results, VerifyResult{
|
||||||
|
Path: entry.Path,
|
||||||
|
OK: false,
|
||||||
|
Detail: "hash mismatch",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results = append(results, VerifyResult{
|
||||||
|
Path: entry.Path,
|
||||||
|
OK: true,
|
||||||
|
Detail: "ok",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
126
garden/verify_test.go
Normal file
126
garden/verify_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package garden
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyAllOK(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, []byte("hello world\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := g.Verify()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !results[0].OK {
|
||||||
|
t.Errorf("expected OK, got %s", results[0].Detail)
|
||||||
|
}
|
||||||
|
if results[0].Detail != "ok" {
|
||||||
|
t.Errorf("expected detail 'ok', got %q", results[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyHashMismatch(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, []byte("hello world\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrupt the blob on disk.
|
||||||
|
hash := g.manifest.Files[0].Hash
|
||||||
|
blobPath := filepath.Join(repoDir, "blobs", hash[:2], hash[2:4], hash)
|
||||||
|
if err := os.WriteFile(blobPath, []byte("corrupted data"), 0o644); err != nil {
|
||||||
|
t.Fatalf("corrupting blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := g.Verify()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].OK {
|
||||||
|
t.Error("expected not OK for corrupted blob")
|
||||||
|
}
|
||||||
|
if results[0].Detail != "hash mismatch" {
|
||||||
|
t.Errorf("expected detail 'hash mismatch', got %q", results[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyBlobMissing(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, []byte("hello world\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the blob from disk.
|
||||||
|
hash := g.manifest.Files[0].Hash
|
||||||
|
blobPath := filepath.Join(repoDir, "blobs", hash[:2], hash[2:4], hash)
|
||||||
|
if err := os.Remove(blobPath); err != nil {
|
||||||
|
t.Fatalf("removing blob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := g.Verify()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Verify: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0].OK {
|
||||||
|
t.Error("expected not OK for missing blob")
|
||||||
|
}
|
||||||
|
if results[0].Detail != "blob missing" {
|
||||||
|
t.Errorf("expected detail 'blob missing', got %q", results[0].Detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user