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:
		
							parent
							
								
									7de593e816
								
							
						
					
					
						commit
						4f41afee0f
					
				
							
								
								
									
										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") } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user