add content-addressed hak source mode (#7)
test-image / build-image (push) Successful in 46s
test / test (push) Successful in 1m26s
build-binaries / build-binaries (push) Successful in 2m20s
build-image / publish (push) Successful in 31s

Reviewed-on: #7
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #7.
This commit is contained in:
2026-06-19 20:56:36 +00:00
committed by archvillainette
parent 223b9831c5
commit cdbbba3181
11 changed files with 975 additions and 61 deletions
+20 -1
View File
@@ -2,14 +2,19 @@ package erf
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"regexp"
"sort"
"strings"
)
var expectedSHA256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`)
const (
headerSize = 160
versionV10 = "V1.0"
@@ -30,6 +35,9 @@ type Resource struct {
Data []byte
SourcePath string
Size int64
// ExpectedSHA256, when set, is the lowercase hex SHA-256 the streamed
// SourcePath payload must hash to. A mismatch fails the write.
ExpectedSHA256 string
}
type header struct {
@@ -407,18 +415,29 @@ func writeResourceData(w io.Writer, resource Resource) error {
return err
}
if resource.ExpectedSHA256 != "" && !expectedSHA256Pattern.MatchString(resource.ExpectedSHA256) {
return fmt.Errorf("resource %q has invalid expected sha256", resource.SourcePath)
}
file, err := os.Open(resource.SourcePath)
if err != nil {
return fmt.Errorf("open resource %q: %w", resource.SourcePath, err)
}
defer file.Close()
written, err := io.Copy(w, file)
hash := sha256.New()
written, err := io.Copy(io.MultiWriter(w, hash), file)
if err != nil {
return fmt.Errorf("copy resource %q: %w", resource.SourcePath, err)
}
if resource.Size > 0 && written != resource.Size {
return fmt.Errorf("copy resource %q: expected %d bytes, wrote %d", resource.SourcePath, resource.Size, written)
}
if resource.ExpectedSHA256 != "" {
got := hex.EncodeToString(hash.Sum(nil))
if got != resource.ExpectedSHA256 {
return fmt.Errorf("resource %q sha256 mismatch: expected %s, got %s", resource.SourcePath, resource.ExpectedSHA256, got)
}
}
return nil
}
+63
View File
@@ -2,9 +2,72 @@ package erf
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
func writeTempPayload(t *testing.T, payload []byte) string {
t.Helper()
path := filepath.Join(t.TempDir(), "payload.bin")
if err := os.WriteFile(path, payload, 0o644); err != nil {
t.Fatalf("write payload: %v", err)
}
return path
}
func TestWriteRejectsSourcePathSHA256Mismatch(t *testing.T) {
payload := []byte("hello world")
path := writeTempPayload(t, payload)
archive := New("HAK ", []Resource{{
Name: "asset",
Type: 0x0003,
SourcePath: path,
Size: int64(len(payload)),
ExpectedSHA256: strings.Repeat("a", 64),
}})
var buf bytes.Buffer
err := Write(&buf, archive)
if err == nil {
t.Fatal("expected sha256 mismatch error")
}
if !strings.Contains(err.Error(), "sha256 mismatch") {
t.Fatalf("error = %v, want sha256 mismatch", err)
}
}
func TestWriteAcceptsMatchingSourcePathSHA256(t *testing.T) {
payload := []byte("hello world")
path := writeTempPayload(t, payload)
sum := sha256.Sum256(payload)
archive := New("HAK ", []Resource{{
Name: "asset",
Type: 0x0003,
SourcePath: path,
Size: int64(len(payload)),
ExpectedSHA256: hex.EncodeToString(sum[:]),
}})
var buf bytes.Buffer
if err := Write(&buf, archive); err != nil {
t.Fatalf("write: %v", err)
}
decoded, err := Read(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("read: %v", err)
}
if len(decoded.Resources) != 1 || string(decoded.Resources[0].Data) != string(payload) {
t.Fatalf("unexpected payload: %#v", decoded.Resources)
}
}
func TestArchiveRoundTrip(t *testing.T) {
archive := New("MOD ", []Resource{
{Name: "module", Type: 0x07DE, Data: []byte("ifo")},