Store each hash as its raw 32 bytes in a BLOB column (closes #134)
check / check (push) Canceled after 0s
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
This commit is contained in:
+71
-28
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -43,13 +42,6 @@ func writeFile(t *testing.T, dir, name string, data []byte) string {
|
||||
return p
|
||||
}
|
||||
|
||||
// hexSum returns the lowercase-hex SHA-256 of data.
|
||||
func hexSum(data []byte) string {
|
||||
s := sha256.Sum256(data)
|
||||
|
||||
return hex.EncodeToString(s[:])
|
||||
}
|
||||
|
||||
// pattern returns n bytes of deterministic content seeded by tag.
|
||||
func pattern(tag byte, n int) []byte {
|
||||
data := make([]byte, n)
|
||||
@@ -62,7 +54,9 @@ func pattern(tag byte, n int) []byte {
|
||||
|
||||
// sig returns a file's full signature (head, tail, content), failing the
|
||||
// test on any error.
|
||||
func sig(t *testing.T, path string, size int64) (string, string, string) {
|
||||
func sig(t *testing.T, path string, size int64) (
|
||||
[sha256.Size]byte, [sha256.Size]byte, [sha256.Size]byte,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
head, tail, content, err := hashSignature(path, size)
|
||||
@@ -100,12 +94,12 @@ func TestHashSignature(t *testing.T) {
|
||||
head, tail, content := sig(t, p, int64(len(c.data)))
|
||||
|
||||
window := min(len(c.data), headTailWindow)
|
||||
wantHead := hexSum(c.data[:window])
|
||||
wantTail := hexSum(c.data[len(c.data)-window:])
|
||||
wantContent := hexSum(c.data)
|
||||
wantHead := sha256.Sum256(c.data[:window])
|
||||
wantTail := sha256.Sum256(c.data[len(c.data)-window:])
|
||||
wantContent := sha256.Sum256(c.data)
|
||||
|
||||
if head != wantHead || tail != wantTail || content != wantContent {
|
||||
t.Errorf("head=%s tail=%s content=%s, want %s %s %s",
|
||||
t.Errorf("head=%x tail=%x content=%x, want %x %x %x",
|
||||
head, tail, content, wantHead, wantTail, wantContent)
|
||||
}
|
||||
})
|
||||
@@ -170,22 +164,25 @@ func TestHashSignatureErrors(t *testing.T) {
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
// A missing file: an error, and every hash left empty.
|
||||
var zero [sha256.Size]byte
|
||||
|
||||
// A missing file: an error, and every hash left zero.
|
||||
head, tail, content, err := hashSignature(filepath.Join(dir, "missing"), 1)
|
||||
if err == nil {
|
||||
t.Error("no error for a missing file")
|
||||
}
|
||||
|
||||
if head != "" || tail != "" || content != "" {
|
||||
t.Errorf("missing file returned hashes: %q %q %q", head, tail, content)
|
||||
if head != zero || tail != zero || content != zero {
|
||||
t.Errorf("missing file returned hashes: %x %x %x", head, tail, content)
|
||||
}
|
||||
|
||||
// A zero-length file has constant hashes and is never opened: even
|
||||
// a missing path succeeds.
|
||||
empty := sha256.Sum256(nil)
|
||||
|
||||
head, tail, content, err = hashSignature(filepath.Join(dir, "missing"), 0)
|
||||
if err != nil ||
|
||||
head != emptyHash || tail != emptyHash || content != emptyHash {
|
||||
t.Errorf("empty: head=%q tail=%q content=%q err=%v, "+
|
||||
if err != nil || head != empty || tail != empty || content != empty {
|
||||
t.Errorf("empty: head=%x tail=%x content=%x err=%v, "+
|
||||
"want constant hashes", head, tail, content, err)
|
||||
}
|
||||
|
||||
@@ -198,8 +195,8 @@ func TestHashSignatureErrors(t *testing.T) {
|
||||
t.Error("no error when the stat size exceeds the file size")
|
||||
}
|
||||
|
||||
if head != "" || tail != "" || content != "" {
|
||||
t.Errorf("shrunk file returned hashes: %q %q %q", head, tail, content)
|
||||
if head != zero || tail != zero || content != zero {
|
||||
t.Errorf("shrunk file returned hashes: %x %x %x", head, tail, content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1211,8 +1208,10 @@ func TestSyncScanUnchangedReuse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sentinel := sha256.Sum256([]byte("sentinel"))
|
||||
|
||||
_, err := db.ExecContext(context.Background(),
|
||||
"UPDATE files SET head = 'sentinel' WHERE path = ?", []byte(a))
|
||||
"UPDATE files SET head = ? WHERE path = ?", sentinel[:], []byte(a))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1223,9 +1222,9 @@ func TestSyncScanUnchangedReuse(t *testing.T) {
|
||||
st)
|
||||
}
|
||||
|
||||
if r := recordByPath(t, dbRecords(t, db), a); r.head != "sentinel" {
|
||||
t.Fatalf("head = %q, want sentinel (file must not be re-read)",
|
||||
r.head)
|
||||
if r := recordByPath(t, dbRecords(t, db), a); r.head != sentinel {
|
||||
t.Fatalf("head = %x, want the sentinel %x (file must not be re-read)",
|
||||
r.head, sentinel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,13 +1264,56 @@ func assertWholeFileHashed(t *testing.T, db *sql.DB, path string,
|
||||
t.Helper()
|
||||
|
||||
r := recordByPath(t, dbRecords(t, db), path)
|
||||
if want := hexSum(data); r.head != want || r.tail != want ||
|
||||
if want := sha256.Sum256(data); r.head != want || r.tail != want ||
|
||||
r.content != want {
|
||||
t.Fatalf("head, tail, content = %q, %q, %q, want %q for each",
|
||||
t.Fatalf("head, tail, content = %x, %x, %x, want %x for each",
|
||||
r.head, r.tail, r.content, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanStoresRawHashes checks what the database holds for a file
|
||||
// larger than both end windows, whose three hashes all differ: head,
|
||||
// tail and content are each a BLOB of the 32 bytes of the SHA-256 the
|
||||
// scan computed.
|
||||
func TestScanStoresRawHashes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
db := openTestDB(t)
|
||||
data := pattern(3, 3*headTailWindow)
|
||||
p := writeFile(t, dir, "f.bin", data)
|
||||
|
||||
syncTree(t, db, dir)
|
||||
|
||||
var (
|
||||
blobs bool
|
||||
head, tail, content []byte
|
||||
)
|
||||
|
||||
err := db.QueryRowContext(t.Context(),
|
||||
"SELECT typeof(head) = 'blob' AND typeof(tail) = 'blob' AND "+
|
||||
"typeof(content) = 'blob', head, tail, content "+
|
||||
"FROM files WHERE path = ?", []byte(p)).
|
||||
Scan(&blobs, &head, &tail, &content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !blobs {
|
||||
t.Error("head, tail and content are not all BLOBs")
|
||||
}
|
||||
|
||||
wantHead := sha256.Sum256(data[:headTailWindow])
|
||||
wantTail := sha256.Sum256(data[len(data)-headTailWindow:])
|
||||
wantContent := sha256.Sum256(data)
|
||||
|
||||
if !bytes.Equal(head, wantHead[:]) || !bytes.Equal(tail, wantTail[:]) ||
|
||||
!bytes.Equal(content, wantContent[:]) {
|
||||
t.Errorf("head, tail, content = %x, %x, %x, want %x, %x, %x",
|
||||
head, tail, content, wantHead, wantTail, wantContent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSyncScanSameSecondRewrite rewrites a file in place at the same
|
||||
// size with an mtime later in the same second as the recorded one: the
|
||||
// next scan must notice the change and re-hash the file. The operand is
|
||||
@@ -1567,7 +1609,8 @@ func TestScanHardlinksReadOnce(t *testing.T) {
|
||||
ra := recordByPath(t, recs, a)
|
||||
rb := recordByPath(t, recs, b)
|
||||
|
||||
if ra.head == "" || ra.head != rb.head || ra.tail != rb.tail {
|
||||
if ra.head == [sha256.Size]byte{} || ra.head != rb.head ||
|
||||
ra.tail != rb.tail {
|
||||
t.Fatalf("hardlink hashes differ: %+v vs %+v", ra, rb)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user