internal/app/state: fixed size et-only header
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m26s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m21s
All checks were successful
Test / Create distribution (push) Successful in 46s
Test / Sandbox (push) Successful in 2m29s
Test / Hakurei (push) Successful in 3m26s
Test / Sandbox (race detector) (push) Successful in 4m15s
Test / Hpkg (push) Successful in 4m14s
Test / Hakurei (race detector) (push) Successful in 5m3s
Test / Flake checks (push) Successful in 1m21s
This header improves the robustness of the format and significantly reduces cleanup overhead. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
86
internal/app/state/header.go
Normal file
86
internal/app/state/header.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
const (
|
||||
// entryHeaderMagic are magic bytes at the beginning of the state entry file.
|
||||
entryHeaderMagic = "\x00\xff\xca\xfe"
|
||||
// entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format.
|
||||
entryHeaderRevision = "\x00\x00"
|
||||
// entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement.
|
||||
entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
|
||||
)
|
||||
|
||||
// entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte.
|
||||
func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte {
|
||||
data := [entryHeaderSize]byte([]byte(
|
||||
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}),
|
||||
))
|
||||
return &data
|
||||
}
|
||||
|
||||
// entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte.
|
||||
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
|
||||
if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
|
||||
return 0, errors.New("invalid header " + hex.EncodeToString(magic))
|
||||
}
|
||||
if revision := data[len(entryHeaderMagic):len(entryHeaderMagic+entryHeaderRevision)]; string(revision) != entryHeaderRevision {
|
||||
return 0, errors.New("unexpected revision " + hex.EncodeToString(revision))
|
||||
}
|
||||
|
||||
et := data[len(entryHeaderMagic+entryHeaderRevision)]
|
||||
if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
|
||||
return 0, errors.New("header enablement value is inconsistent")
|
||||
}
|
||||
return hst.Enablement(et), nil
|
||||
}
|
||||
|
||||
// EntrySizeError is returned for a file too small to hold a state entry header.
|
||||
type EntrySizeError struct {
|
||||
Name string
|
||||
Size int64
|
||||
}
|
||||
|
||||
func (e *EntrySizeError) Error() string {
|
||||
if e.Name == "" {
|
||||
return "state entry file is too short"
|
||||
}
|
||||
return "state entry file " + strconv.Quote(e.Name) + " is too short"
|
||||
}
|
||||
|
||||
// entryCheckFile checks whether [os.FileInfo] refers to a file that might hold [hst.State].
|
||||
func entryCheckFile(fi os.FileInfo) error {
|
||||
if fi.IsDir() {
|
||||
return syscall.EISDIR
|
||||
}
|
||||
if s := fi.Size(); s <= int64(entryHeaderSize) {
|
||||
return &EntrySizeError{Name: fi.Name(), Size: s}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// entryReadHeader reads [hst.Enablement] from an [io.Reader].
|
||||
func entryReadHeader(r io.Reader) (hst.Enablement, error) {
|
||||
var data [entryHeaderSize]byte
|
||||
if n, err := r.Read(data[:]); err != nil {
|
||||
return 0, err
|
||||
} else if n != entryHeaderSize {
|
||||
return 0, &EntrySizeError{Size: int64(n)}
|
||||
}
|
||||
return entryHeaderDecode(&data)
|
||||
}
|
||||
|
||||
// entryWriteHeader writes [hst.Enablement] header to an [io.Writer].
|
||||
func entryWriteHeader(w io.Writer, et hst.Enablement) error {
|
||||
_, err := w.Write(entryHeaderEncode(et)[:])
|
||||
return err
|
||||
}
|
||||
184
internal/app/state/header_test.go
Normal file
184
internal/app/state/header_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func TestEntryHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data [entryHeaderSize]byte
|
||||
et hst.Enablement
|
||||
err error
|
||||
}{
|
||||
{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
|
||||
0x0a, 0xf6}, 0,
|
||||
errors.New("header enablement value is inconsistent")},
|
||||
{"unexpected revision", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0xff, 0xff}, 0,
|
||||
errors.New("unexpected revision ffff")},
|
||||
{"invalid header", [entryHeaderSize]byte{0x00, 0xfe, 0xca, 0xfe}, 0,
|
||||
errors.New("invalid header 00fecafe")},
|
||||
|
||||
{"success high", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
|
||||
0xff, 0x00}, 0xff, nil},
|
||||
{"success", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,
|
||||
0x09, 0xf6}, hst.EWayland | hst.EPulse, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
if tc.err != nil {
|
||||
return
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
if got := entryHeaderEncode(tc.et); *got != tc.data {
|
||||
t.Errorf("entryHeaderEncode: %x, want %x", *got, tc.data)
|
||||
}
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := entryWriteHeader(&buf, tc.et); err != nil {
|
||||
t.Fatalf("entryWriteHeader: error = %v", err)
|
||||
}
|
||||
if got := ([entryHeaderSize]byte)(buf.Bytes()); got != tc.data {
|
||||
t.Errorf("entryWriteHeader: %x, want %x", got, tc.data)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := entryHeaderDecode(&tc.data)
|
||||
if !reflect.DeepEqual(err, tc.err) {
|
||||
t.Fatalf("entryHeaderDecode: error = %#v, want %#v", err, tc.err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if got != tc.et {
|
||||
t.Errorf("entryHeaderDecode: et = %q, want %q", got, tc.et)
|
||||
}
|
||||
|
||||
if got, err = entryReadHeader(bytes.NewReader(tc.data[:])); err != nil {
|
||||
t.Fatalf("entryReadHeader: error = %#v", err)
|
||||
} else if got != tc.et {
|
||||
t.Errorf("entryReadHeader: et = %q, want %q", got, tc.et)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntrySizeError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{"size only", &EntrySizeError{Size: 0xdeadbeef},
|
||||
`state entry file is too short`},
|
||||
{"full", &EntrySizeError{Name: "nonexistent", Size: 0xdeadbeef},
|
||||
`state entry file "nonexistent" is too short`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.err.Error(); got != tc.want {
|
||||
t.Errorf("Error: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntryCheckFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
fi os.FileInfo
|
||||
err error
|
||||
}{
|
||||
{"dir", &stubFi{name: "dir", isDir: true},
|
||||
syscall.EISDIR},
|
||||
{"short", stubFi{name: "short", size: 8},
|
||||
&EntrySizeError{Name: "short", Size: 8}},
|
||||
{"success", stubFi{size: 9}, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if err := entryCheckFile(tc.fi); !reflect.DeepEqual(err, tc.err) {
|
||||
t.Errorf("entryCheckFile: error = %#v, want %#v", err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntryReadHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
newR func() io.Reader
|
||||
err error
|
||||
}{
|
||||
{"eof", func() io.Reader { return bytes.NewReader([]byte{}) }, io.EOF},
|
||||
{"short", func() io.Reader { return bytes.NewReader([]byte{0}) }, &EntrySizeError{Size: 1}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := entryReadHeader(tc.newR()); !reflect.DeepEqual(err, tc.err) {
|
||||
t.Errorf("entryReadHeader: error = %#v, want %#v", err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// stubFi partially implements [os.FileInfo] using hardcoded values.
|
||||
type stubFi struct {
|
||||
name string
|
||||
size int64
|
||||
isDir bool
|
||||
}
|
||||
|
||||
func (fi stubFi) Name() string {
|
||||
if fi.name == "" {
|
||||
panic("unreachable")
|
||||
}
|
||||
return fi.name
|
||||
}
|
||||
|
||||
func (fi stubFi) Size() int64 {
|
||||
if fi.size < 0 {
|
||||
panic("unreachable")
|
||||
}
|
||||
return fi.size
|
||||
}
|
||||
|
||||
func (fi stubFi) IsDir() bool { return fi.isDir }
|
||||
|
||||
func (fi stubFi) Mode() fs.FileMode { panic("unreachable") }
|
||||
func (fi stubFi) ModTime() time.Time { panic("unreachable") }
|
||||
func (fi stubFi) Sys() any { panic("unreachable") }
|
||||
Reference in New Issue
Block a user