m/p/efivarfs: refactor

This accomplishes three things:

First, split out the variable access layer from the rest of the code.
This cleans up the attribute handling, which is now done centrally as
well as making the high-level functions very short and clean. They now
also return better errors.

Second this introduces proper types for LoadOption, which can now also
be unmarshaled which was a requirement for A/B updates. This required
implementation of EFI's DevicePath structure.
While refactoring the higher-level functions for this, this also
fixes a bug where the variable index (the 4 hex nibbles at the end) were
improperly generated as lowercase hex.

Third, this adds new high-level functions for interacting with more
boot-related variables needed for the A/B effort.

Change-Id: I53490fa4898a5e7a5498ecc05a9078bd2d66c26e
Reviewed-on: https://review.monogon.dev/c/monogon/+/1855
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/efivarfs/boot_test.go b/metropolis/pkg/efivarfs/boot_test.go
new file mode 100644
index 0000000..9abd8f9
--- /dev/null
+++ b/metropolis/pkg/efivarfs/boot_test.go
@@ -0,0 +1,58 @@
+package efivarfs
+
+import (
+	"bytes"
+	"encoding/hex"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/uuid"
+)
+
+// Generated with old working marshaler and manually double-checked
+var ref, _ = hex.DecodeString(
+	"010000004a004500780061006d0070006c006500000004012a00010000000" +
+		"500000000000000080000000000000014b8a76bad9dd11180b400c04fd430" +
+		"c8020204041c005c0074006500730074005c0061002e00650066006900000" +
+		"07fff0400",
+)
+
+func TestEncoding(t *testing.T) {
+	opt := LoadOption{
+		Description: "Example",
+		FilePath: DevicePath{
+			&HardDrivePath{
+				PartitionNumber:     1,
+				PartitionStartBlock: 5,
+				PartitionSizeBlocks: 8,
+				PartitionMatch: PartitionGPT{
+					PartitionUUID: uuid.NameSpaceX500,
+				},
+			},
+			FilePath("/test/a.efi"),
+		},
+	}
+	got, err := opt.Marshal()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(ref, got) {
+		t.Fatalf("expected %x, got %x", ref, got)
+	}
+	got2, err := UnmarshalLoadOption(got)
+	if err != nil {
+		t.Fatalf("failed to unmarshal marshaled LoadOption: %v", err)
+	}
+	diff := cmp.Diff(&opt, got2)
+	if diff != "" {
+		t.Errorf("marshal/unmarshal wasn't transparent: %v", diff)
+	}
+}
+
+func FuzzDecode(f *testing.F) {
+	f.Add(ref)
+	f.Fuzz(func(t *testing.T, a []byte) {
+		// Just try to see if it crashes
+		_, _ = UnmarshalLoadOption(a)
+	})
+}