1 Commits
Author SHA1 Message Date
clawbot a77f07249e Store each hash as its raw 32 bytes in a BLOB column (closes #134)
check / check (push) Canceled after 0s
The head, tail and content columns of the files table are BLOB NOT
NULL and hold the raw 32-byte SHA-256, half the size of the hex text,
in the table and in the files_signature index. The hash phase, the
records, report and trees carry each hash as a [sha256.Size]byte and
compare it with ==; nothing is hex-encoded. Reading a record whose
hash column is not 32 bytes is an error naming its path.

In the tests, testHash is a stand-in hash: a string's bytes padded
with zeros, so fixtures keep the sort order their strings give.

Judgement call: a hash of the wrong length is refused on reading,
not copied silently.

Model: opus-5-5
2026-10-26 01:53:25 +00:00
+52 -27
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"crypto/sha256"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -473,40 +474,64 @@ func TestLoadRowsCountAgreeDuringScan(t *testing.T) {
} }
} }
// TestLoadRowsRefuseShortHash stores a record whose head is one byte, // TestLoadRowsRefuseWrongSizeHash stores a record whose head, tail or
// not the 32 bytes of a SHA-256: loadFileRows and loadSignatureRows // content is not the 32 bytes of a SHA-256: either one byte, or the 64
// must each fail on it with an error naming its path. // bytes of a SHA-256 written as hex text. loadFileRows and
func TestLoadRowsRefuseShortHash(t *testing.T) { // loadSignatureRows must each fail on it with an error naming its path.
func TestLoadRowsRefuseWrongSizeHash(t *testing.T) {
t.Parallel() t.Parallel()
db := openTestDB(t) hexText := strings.Repeat("ab", sha256.Size)
err := applyChanges(t.Context(), db, []scanRec{{path: "/a/f"}}, nil, nil) cases := []struct {
if err != nil { name string
t.Fatal(err) update string
value any
}{
{"one-byte head", "UPDATE files SET head = ?", []byte{0}},
{"hex-text head", "UPDATE files SET head = ?", hexText},
{"one-byte tail", "UPDATE files SET tail = ?", []byte{0}},
{"hex-text tail", "UPDATE files SET tail = ?", hexText},
{"one-byte content", "UPDATE files SET content = ?", []byte{0}},
{"hex-text content", "UPDATE files SET content = ?", hexText},
} }
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
_, err = db.ExecContext(t.Context(), "UPDATE files SET head = x'00'") db := openTestDB(t)
if err != nil {
t.Fatal(err)
}
loads := map[string]func() error{ err := applyChanges(t.Context(), db,
"loadFileRows": func() error { []scanRec{{path: "/a/f"}}, nil, nil)
return loadFileRows(t.Context(), db, func(int64) {}, if err != nil {
func(scanRec) {}) t.Fatal(err)
}, }
"loadSignatureRows": func() error {
return loadSignatureRows(t.Context(), db, func(int64) {},
func(scanRec) error { return nil })
},
}
for name, load := range loads { _, err = db.ExecContext(t.Context(), c.update, c.value)
err := load() if err != nil {
if !errors.Is(err, errHashSize) || !strings.Contains(err.Error(), "/a/f") { t.Fatal(err)
t.Errorf("%s: err = %v, want %v naming /a/f", name, err, errHashSize) }
}
loads := map[string]func() error{
"loadFileRows": func() error {
return loadFileRows(t.Context(), db, func(int64) {},
func(scanRec) {})
},
"loadSignatureRows": func() error {
return loadSignatureRows(t.Context(), db, func(int64) {},
func(scanRec) error { return nil })
},
}
for name, load := range loads {
err := load()
if !errors.Is(err, errHashSize) ||
!strings.Contains(err.Error(), "/a/f") {
t.Errorf("%s: err = %v, want %v naming /a/f",
name, err, errHashSize)
}
}
})
} }
} }