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/devicepath_test.go b/metropolis/pkg/efivarfs/devicepath_test.go
new file mode 100644
index 0000000..b5823ac
--- /dev/null
+++ b/metropolis/pkg/efivarfs/devicepath_test.go
@@ -0,0 +1,89 @@
+package efivarfs
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/google/uuid"
+)
+
+func TestMarshalExamples(t *testing.T) {
+	cases := []struct {
+		name        string
+		path        DevicePath
+		expected    []byte
+		expectError bool
+	}{
+		{
+			name: "TestNone",
+			path: DevicePath{},
+			expected: []byte{
+				0x7f, 0xff, // End of HW device path
+				0x04, 0x00, // Length: 4 bytes
+			},
+		},
+		{
+			// From UEFI Device Path Examples, extracted single entry
+			name: "TestHD",
+			path: DevicePath{
+				&HardDrivePath{
+					PartitionNumber:     1,
+					PartitionStartBlock: 0x22,
+					PartitionSizeBlocks: 0x2710000,
+					PartitionMatch: PartitionGPT{
+						PartitionUUID: uuid.MustParse("15E39A00-1DD2-1000-8D7F-00A0C92408FC"),
+					},
+				},
+			},
+			expected: []byte{
+				0x04, 0x01, // Hard Disk type
+				0x2a, 0x00, // Length
+				0x01, 0x00, 0x00, 0x00, // Partition Number
+				0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Part Start
+				0x00, 0x00, 0x71, 0x02, 0x00, 0x00, 0x00, 0x00, // Part Size
+				0x00, 0x9a, 0xe3, 0x15, 0xd2, 0x1d, 0x00, 0x10,
+				0x8d, 0x7f, 0x00, 0xa0, 0xc9, 0x24, 0x08, 0xfc, // Signature
+				0x02,       // Part Format GPT
+				0x02,       // Signature GPT
+				0x7f, 0xff, // End of HW device path
+				0x04, 0x00, // Length: 4 bytes
+			},
+		},
+		{
+			name: "TestFilePath",
+			path: DevicePath{
+				FilePath("asdf"),
+			},
+			expected: []byte{
+				0x04, 0x04, // File Path type
+				0x0e, 0x00, // Length
+				'a', 0x00, 's', 0x00, 'd', 0x00, 'f', 0x00,
+				0x00, 0x00,
+				0x7f, 0xff, // End of HW device path
+				0x04, 0x00, // Length: 4 bytes
+			},
+		},
+	}
+
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			got, err := c.path.Marshal()
+			if err != nil && !c.expectError {
+				t.Fatalf("unexpected error: %v", err)
+			}
+			if err == nil && c.expectError {
+				t.Fatalf("expected error, got %x", got)
+			}
+			if err != nil && c.expectError {
+				// Do not compare result in case error is expected
+				return
+			}
+			if !bytes.Equal(got, c.expected) {
+				t.Fatalf("expected %x, got %x", c.expected, got)
+			}
+			if _, err := UnmarshalDevicePath(got); err != nil {
+				t.Errorf("failed to unmarshal value again: %v", err)
+			}
+		})
+	}
+}