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/mbr.go b/metropolis/pkg/gpt/mbr.go
new file mode 100644
index 0000000..46f561f
--- /dev/null
+++ b/metropolis/pkg/gpt/mbr.go
@@ -0,0 +1,82 @@
+package gpt
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"math"
+)
+
+// See UEFI Specification 2.9 Table 5-3
+type mbr struct {
+	BootCode         [440]byte
+	DiskSignature    [4]byte
+	_                [2]byte
+	PartitionRecords [4]mbrPartitionRecord
+	Signature        [2]byte
+}
+
+// See UEFI Specification 2.9 Table 5-4
+type mbrPartitionRecord struct {
+	BootIndicator byte
+	StartingCHS   [3]byte
+	Type          byte
+	EndingCHS     [3]byte
+	StartingBlock uint32
+	SizeInBlocks  uint32
+}
+
+var mbrSignature = [2]byte{0x55, 0xaa}
+
+func makeProtectiveMBR(w io.Writer, blockCount int64, bootCode []byte) error {
+	var representedBlockCount = uint32(math.MaxUint32)
+	if blockCount < math.MaxUint32 {
+		representedBlockCount = uint32(blockCount)
+	}
+	m := mbr{
+		DiskSignature: [4]byte{0, 0, 0, 0},
+		PartitionRecords: [4]mbrPartitionRecord{
+			{
+				StartingCHS:   toCHS(1),
+				Type:          0xEE, // Table/Protective MBR
+				StartingBlock: 1,
+				SizeInBlocks:  representedBlockCount,
+				EndingCHS:     toCHS(blockCount + 1),
+			},
+			{},
+			{},
+			{},
+		},
+		Signature: mbrSignature,
+	}
+	if len(bootCode) > len(m.BootCode) {
+		return fmt.Errorf("BootCode is %d bytes, can only store %d", len(bootCode), len(m.BootCode))
+	}
+	copy(m.BootCode[:], bootCode)
+	if err := binary.Write(w, binary.LittleEndian, &m); err != nil {
+		return fmt.Errorf("failed to write MBR: %w", err)
+	}
+	return nil
+}
+
+// toCHS converts a LBA to a "logical" CHS, i.e. what a legacy BIOS 13h
+// interface would use. This has nothing to do with the actual CHS geometry
+// which depends on the disk and interface used.
+func toCHS(lba int64) (chs [3]byte) {
+	const maxCylinders = (1 << 10) - 1
+	const maxHeadsPerCylinder = (1 << 8) - 1
+	const maxSectorsPerTrack = (1 << 6) - 2 // Sector is 1-based
+	cylinder := lba / (maxHeadsPerCylinder * maxSectorsPerTrack)
+	head := (lba / maxSectorsPerTrack) % maxHeadsPerCylinder
+	sector := (lba % maxSectorsPerTrack) + 1
+	if cylinder > maxCylinders {
+		cylinder = maxCylinders
+		head = maxHeadsPerCylinder
+		sector = maxSectorsPerTrack + 1
+	}
+	chs[0] = uint8(head)
+	chs[1] = uint8(sector)
+	chs[1] |= uint8(cylinder>>2) & 0xc0
+	chs[2] = uint8(cylinder)
+	return
+}