From 62f769ae9bb32989825f06f242aecc15e30fdab6 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sat, 5 Sep 2026 03:32:00 +0000 Subject: [PATCH] add SHA256File for checksumming a file SHA256File returns the SHA-256 digest of a file's contents as lowercase hexadecimal, copying the file through the hash in chunks so its size does not matter. Comes with a doc comment and table-driven tests. (closes #19) Model: opus-5 --- sha256file.go | 30 ++++++++++++++++++ sha256file_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 sha256file.go create mode 100644 sha256file_test.go diff --git a/sha256file.go b/sha256file.go new file mode 100644 index 0000000..1553426 --- /dev/null +++ b/sha256file.go @@ -0,0 +1,30 @@ +package util + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "os" +) + +// SHA256File returns the SHA-256 digest of the contents of the file at path, +// written as lowercase hexadecimal, in the same form as the sha256sum command +// prints. The file is copied through the hash in chunks rather than read into +// memory, so its size does not matter. +// +// The error is whatever went wrong opening or reading the file, passed along +// unchanged so a caller can test it with os.IsNotExist and the like. +func SHA256File(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + digest := sha256.New() + if _, err := io.Copy(digest, file); err != nil { + return "", err + } + + return hex.EncodeToString(digest.Sum(nil)), nil +} diff --git a/sha256file_test.go b/sha256file_test.go new file mode 100644 index 0000000..2dc69b3 --- /dev/null +++ b/sha256file_test.go @@ -0,0 +1,76 @@ +package util + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSHA256File(t *testing.T) { + tests := []struct { + name string + contents []byte + expected string + }{ + { + "an empty file", + []byte{}, + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + { + "a short file", + []byte("hello world\n"), + "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447", + }, + { + "binary contents", + []byte{0x00, 0x01, 0x02, 0x03}, + "054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8", + }, + { + "larger than one read buffer", + bytes.Repeat([]byte("a"), 100000), + "6d1cf22d7cc09b085dfc25ee1a1f3ae0265804c607bc2074ad253bcc82fd81ee", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "file") + if err := os.WriteFile(path, test.contents, 0644); err != nil { + t.Fatalf("could not write the test file: %v", err) + } + + got, err := SHA256File(path) + if err != nil { + t.Fatalf("did not expect an error, got %v", err) + } + if got != test.expected { + t.Errorf("expected %q got %q", test.expected, got) + } + if got != strings.ToLower(got) { + t.Errorf("expected lowercase hexadecimal, got %q", got) + } + }) + } +} + +func TestSHA256FileReportsAMissingFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "not-there") + + got, err := SHA256File(path) + if err == nil { + t.Fatalf("expected an error for a file that does not exist, got %q", got) + } + if !os.IsNotExist(err) { + t.Errorf("expected an error that os.IsNotExist recognises, got %v", err) + } +} + +func TestSHA256FileReportsADirectory(t *testing.T) { + if _, err := SHA256File(t.TempDir()); err == nil { + t.Errorf("expected an error for a directory") + } +}