aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/store/header.go
blob: a6e756f94f364364542873d37f83df15ad11ee49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package store

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
}