m/p/gpt: add GPT package
This introduces our own GPT package. It will be used for provisioning
and Metropolis images.
Change-Id: I905cd5d540673fd4b69c01d8975f98b88e24edd4
Reviewed-on: https://review.monogon.dev/c/monogon/+/956
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/gpt/gpt_test.go b/metropolis/pkg/gpt/gpt_test.go
new file mode 100644
index 0000000..df2970b
--- /dev/null
+++ b/metropolis/pkg/gpt/gpt_test.go
@@ -0,0 +1,152 @@
+package gpt
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/google/uuid"
+)
+
+func TestUUIDTranspose(t *testing.T) {
+ testUUID := uuid.MustParse("00112233-4455-6677-c899-aabbccddeeff")
+ mixedEndianUUID := mangleUUID(testUUID)
+ expectedMixedEndianUUID := [16]byte{0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0xc8, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
+ if mixedEndianUUID != expectedMixedEndianUUID {
+ t.Errorf("mangleUUID(%s) = %x, expected %x", testUUID, mixedEndianUUID, expectedMixedEndianUUID)
+ }
+ roundTrippedUUID := unmangleUUID(mixedEndianUUID)
+ if testUUID != roundTrippedUUID {
+ t.Errorf("unmangleUUID(mangleUUID(%s)) = %s, expected input", testUUID, roundTrippedUUID)
+ }
+}
+
+func TestFreeSpaces(t *testing.T) {
+ cases := []struct {
+ name string
+ parts []*Partition
+ expected [][2]int64
+ expectedOverlap bool
+ }{
+ {"Empty", []*Partition{}, [][2]int64{{34, 2015}}, false},
+ {"OnePart", []*Partition{
+ {Type: PartitionTypeEFISystem, FirstBlock: 200, LastBlock: 1499},
+ }, [][2]int64{
+ {34, 200},
+ {1500, 2015},
+ }, false},
+ {"TwoOverlappingParts", []*Partition{
+ {Type: PartitionTypeEFISystem, FirstBlock: 200, LastBlock: 1499},
+ {Type: PartitionTypeEFISystem, FirstBlock: 1000, LastBlock: 1999},
+ }, [][2]int64{
+ {34, 200},
+ {2000, 2015},
+ }, true},
+ {"Full", []*Partition{
+ {Type: PartitionTypeEFISystem, FirstBlock: 34, LastBlock: 999},
+ {Type: PartitionTypeEFISystem, FirstBlock: 1000, LastBlock: 2014},
+ }, [][2]int64{}, false},
+ {"TwoSpacedParts", []*Partition{
+ {Type: PartitionTypeEFISystem, FirstBlock: 500, LastBlock: 899},
+ {Type: PartitionTypeEFISystem, FirstBlock: 1200, LastBlock: 1799},
+ }, [][2]int64{
+ {34, 500},
+ {900, 1200},
+ {1800, 2015},
+ }, false},
+ }
+
+ // Partitions are created manually as AddPartition calls FreeSpaces itself,
+ // which makes the test unreliable as well as making failures very hard to
+ // debug.
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ g := Table{
+ BlockSize: 512,
+ BlockCount: 2048, // 0.5KiB * 2048 = 1MiB
+ Partitions: c.parts,
+ }
+ fs, overlap, err := g.GetFreeSpaces()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if overlap != c.expectedOverlap {
+ t.Errorf("expected overlap %v, got %v", c.expectedOverlap, overlap)
+ }
+ if len(fs) != len(c.expected) {
+ t.Fatalf("expected %v, got %v", c.expected, fs)
+ }
+ for i := range fs {
+ if fs[i] != c.expected[i] {
+ t.Errorf("free space mismatch at pos %d: got [%d, %d), expected [%d, %d)", i, fs[i][0], fs[i][1], c.expected[i][0], c.expected[i][1])
+ }
+ }
+ })
+ }
+}
+
+func TestRoundTrip(t *testing.T) {
+ if os.Getenv("IN_KTEST") == "true" {
+ t.Skip("In ktest")
+ }
+ g := Table{
+ ID: uuid.NewSHA1(zeroUUID, []byte("test")),
+ BlockSize: 512,
+ BlockCount: 2048,
+ BootCode: []byte("just some test code"),
+ Partitions: []*Partition{
+ nil,
+ // This emoji is very complex and exercises UTF16 surrogate encoding
+ // and composing.
+ {Name: "Test 🏃♂️", FirstBlock: 10, LastBlock: 19, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(zeroUUID, []byte("test1")), Attributes: AttrNoBlockIOProto},
+ nil,
+ {Name: "Test2", FirstBlock: 20, LastBlock: 49, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(zeroUUID, []byte("test2")), Attributes: 0},
+ },
+ }
+ f, err := os.CreateTemp("", "")
+ if err != nil {
+ t.Fatalf("Failed to create temporary file: %v", err)
+ }
+ defer os.Remove(f.Name())
+ if err := Write(f, &g); err != nil {
+ t.Fatalf("Error while wrinting Table: %v", err)
+ }
+ f.Seek(0, io.SeekStart)
+ originalHash := sha256.New()
+ if _, err := io.Copy(originalHash, f); err != nil {
+ panic(err)
+ }
+
+ g2, err := Read(f, 512, 2048)
+ if err != nil {
+ t.Fatalf("Failed to read back GPT: %v", err)
+ }
+ if g2.ID != g.ID {
+ t.Errorf("Disk UUID changed when reading back: %v", err)
+ }
+ // Destroy primary GPT
+ f.Seek(1*g.BlockSize, io.SeekStart)
+ f.Write(make([]byte, 512))
+ f.Seek(0, io.SeekStart)
+ g3, err := Read(f, 512, 2048)
+ if err != nil {
+ t.Fatalf("Failed to read back GPT with primary GPT destroyed: %v", err)
+ }
+ if g3.ID != g.ID {
+ t.Errorf("Disk UUID changed when reading back: %v", err)
+ }
+ f.Seek(0, io.SeekStart)
+ if err := Write(f, g3); err != nil {
+ t.Fatalf("Error while writing back GPT: %v", err)
+ }
+ f.Seek(0, io.SeekStart)
+ rewrittenHash := sha256.New()
+ if _, err := io.Copy(rewrittenHash, f); err != nil {
+ panic(err)
+ }
+ if !bytes.Equal(originalHash.Sum([]byte{}), rewrittenHash.Sum([]byte{})) {
+ t.Errorf("Write/Read/Write test was not reproducible: %x != %x", originalHash.Sum([]byte{}), rewrittenHash.Sum([]byte{}))
+ }
+}