pkg/smbios: add SMBIOS package

This adds a new SMBIOS package which contains common structures from
SMBIOS as well as corresponding parsers.
I originally explored an approach where I manually designed optimized Go
types for each structure, but that would have led to a huge amount of
code that reading a structure of this type would cause if done
literally. I also considered code generation, but if the generated types
are to be close to the manually-designed ones it would be an incredibly
complex piece of code as well.
Finally I went with a design based on reflection which is much more
compact than the first two and consists of plain Go code at the expense
some niceness in the types.
I called the current types SomeTypeRaw in case I want to come back later
introduce a small layer mapping the current structures into nicer ones.
But for our current purposes the raw ones are good enough already.

This has been tested against our deployment targets, but as the SMBIOS
data contains uniquely identifying information these small tests are not
part of this CL. Sadly I haven't found any public SMBIOS test-cases.

Change-Id: I55d746ada0801de456f2a0eb961821abd9d58fa2
Reviewed-on: https://review.monogon.dev/c/monogon/+/983
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/smbios/BUILD.bazel b/metropolis/pkg/smbios/BUILD.bazel
new file mode 100644
index 0000000..f29f559
--- /dev/null
+++ b/metropolis/pkg/smbios/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "smbios",
+    srcs = [
+        "smbios.go",
+        "structures.go",
+    ],
+    importpath = "source.monogon.dev/metropolis/pkg/smbios",
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/pkg/smbios/smbios.go b/metropolis/pkg/smbios/smbios.go
new file mode 100644
index 0000000..0749110
--- /dev/null
+++ b/metropolis/pkg/smbios/smbios.go
@@ -0,0 +1,267 @@
+// Package smbios implements parsing of SMBIOS data structures.
+// SMBIOS data is commonly populated by platform firmware to convey various
+// metadata (including name, vendor, slots and serial numbers) about the
+// platform to the operating system.
+// The SMBIOS standard is maintained by DMTF and available at
+// https://www.dmtf.org/sites/default/files/standards/documents/
+// DSP0134_3.6.0.pdf. The rest of this package just refers to it as "the
+// standard".
+package smbios
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"reflect"
+	"strings"
+)
+
+// See spec section 6.1.2
+type structureHeader struct {
+	// Types 128 through 256 are reserved for OEM and system-specific use.
+	Type uint8
+	// Length of the structure including this header, excluding the string
+	// set.
+	Length uint8
+	// Unique handle for this structure.
+	Handle uint16
+}
+
+type Structure struct {
+	Type             uint8
+	Handle           uint16
+	FormattedSection []byte
+	Strings          []string
+}
+
+// Table represents a decoded SMBIOS table consisting of its structures.
+// A few known structures are parsed if present, the rest is put into
+// Structures unparsed.
+type Table struct {
+	BIOSInformationRaw       *BIOSInformationRaw
+	SystemInformationRaw     *SystemInformationRaw
+	BaseboardsInformationRaw []*BaseboardInformationRaw
+	SystemSlotsRaw           []*SystemSlotRaw
+	MemoryDevicesRaw         []*MemoryDeviceRaw
+
+	Structures []Structure
+}
+
+const (
+	structTypeInactive   = 126
+	structTypeEndOfTable = 127
+)
+
+func Unmarshal(table *bufio.Reader) (*Table, error) {
+	var tbl Table
+	for {
+		var structHdr structureHeader
+		if err := binary.Read(table, binary.LittleEndian, &structHdr); err != nil {
+			if err == io.EOF {
+				// Be tolerant of EOFs on structure boundaries even though
+				// the EOT marker is specified as a type 127 structure.
+				break
+			}
+			return nil, fmt.Errorf("unable to read structure header: %w", err)
+		}
+		if int(structHdr.Length) < binary.Size(structHdr) {
+			return nil, fmt.Errorf("invalid structure: header length (%d) smaller than header", structHdr.Length)
+		}
+		if structHdr.Type == structTypeEndOfTable {
+			break
+		}
+		var s Structure
+		s.Type = structHdr.Type
+		s.Handle = structHdr.Handle
+		s.FormattedSection = make([]byte, structHdr.Length-uint8(binary.Size(structHdr)))
+		if _, err := io.ReadFull(table, s.FormattedSection); err != nil {
+			return nil, fmt.Errorf("error while reading structure (handle %d) contents: %w", structHdr.Handle, err)
+		}
+		// Read string-set
+		for {
+			str, err := table.ReadString(0x00)
+			if err != nil {
+				return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err)
+			}
+			// Remove trailing null byte
+			str = strings.TrimSuffix(str, "\x00")
+			// Don't populate a zero-length first string if the string-set is
+			// empty.
+			if len(str) != 0 {
+				s.Strings = append(s.Strings, str)
+			}
+			maybeTerminator, err := table.ReadByte()
+			if err != nil {
+				return nil, fmt.Errorf("error while reading string table (handle %d): %w", structHdr.Handle, err)
+			}
+			if maybeTerminator == 0 {
+				// We have a valid string-set terminator, exit the loop
+				break
+			}
+			// The next byte was not a terminator, put it back
+			if err := table.UnreadByte(); err != nil {
+				panic(err) // Cannot happen operationally
+			}
+		}
+		switch structHdr.Type {
+		case structTypeInactive:
+			continue
+		case structTypeBIOSInformation:
+			var biosInfo BIOSInformationRaw
+			if err := UnmarshalStructureRaw(s, &biosInfo); err != nil {
+				return nil, fmt.Errorf("failed unmarshaling BIOS Information: %w", err)
+			}
+			tbl.BIOSInformationRaw = &biosInfo
+		case structTypeSystemInformation:
+			var systemInfo SystemInformationRaw
+			if err := UnmarshalStructureRaw(s, &systemInfo); err != nil {
+				return nil, fmt.Errorf("failed unmarshaling System Information: %w", err)
+			}
+			tbl.SystemInformationRaw = &systemInfo
+		case structTypeBaseboardInformation:
+			fmt.Println(s)
+			var baseboardInfo BaseboardInformationRaw
+			if err := UnmarshalStructureRaw(s, &baseboardInfo); err != nil {
+				return nil, fmt.Errorf("failed unmarshaling Baseboard Information: %w", err)
+			}
+			tbl.BaseboardsInformationRaw = append(tbl.BaseboardsInformationRaw, &baseboardInfo)
+		case structTypeSystemSlot:
+			var sysSlot SystemSlotRaw
+			if err := UnmarshalStructureRaw(s, &sysSlot); err != nil {
+				return nil, fmt.Errorf("failed unmarshaling System Slot: %w", err)
+			}
+			tbl.SystemSlotsRaw = append(tbl.SystemSlotsRaw, &sysSlot)
+		case structTypeMemoryDevice:
+			var memoryDev MemoryDeviceRaw
+			if err := UnmarshalStructureRaw(s, &memoryDev); err != nil {
+				return nil, fmt.Errorf("failed unmarshaling Memory Device: %w", err)
+			}
+			tbl.MemoryDevicesRaw = append(tbl.MemoryDevicesRaw, &memoryDev)
+		default:
+			// Just pass through the raw structure
+			tbl.Structures = append(tbl.Structures, s)
+		}
+	}
+	return &tbl, nil
+}
+
+// Version contains a two-part version number consisting of a major and minor
+// version. This is a common structure in SMBIOS.
+type Version struct {
+	Major uint8
+	Minor uint8
+}
+
+func (v *Version) String() string {
+	return fmt.Sprintf("%d.%d", v.Major, v.Minor)
+}
+
+// AtLeast returns true if the version in v is at least the given version.
+func (v *Version) AtLeast(major, minor uint8) bool {
+	return v.Major >= major && v.Minor >= minor
+}
+
+// UnmarshalStructureRaw unmarshals a SMBIOS structure into a Go struct which
+// has some constraints. The first two fields need to be a `uint16 handle` and
+// a `StructureVersion Version` field. After that any number of fields may
+// follow as long as they are either of type `string` (which will be looked up
+// in the string table) or readable by binary.Read. To determine the structure
+// version, the smbios_min_vers struct tag needs to be put on the first field
+// of a newer structure version. The version implicitly starts with 2.0.
+// The version determined is written to the second target struct field.
+// Fields which do not have a fixed size need to be typed as a slice and tagged
+// with smbios_repeat set to the name of the field containing the count. The
+// count field itself needs to be some width of uint.
+func UnmarshalStructureRaw(rawStruct Structure, target any) error {
+	v := reflect.ValueOf(target)
+	if v.Kind() != reflect.Pointer {
+		return errors.New("target needs to be a pointer")
+	}
+	v = v.Elem()
+	if v.Kind() != reflect.Struct {
+		return errors.New("target needs to be a pointer to a struct")
+	}
+	v.Field(0).SetUint(uint64(rawStruct.Handle))
+	r := bytes.NewReader(rawStruct.FormattedSection)
+	completedVersion := Version{Major: 0, Minor: 0}
+	parsingVersion := Version{Major: 2, Minor: 0}
+	numFields := v.NumField()
+	hasAborted := false
+	for i := 2; i < numFields; i++ {
+		fieldType := v.Type().Field(i)
+		if min := fieldType.Tag.Get("smbios_min_ver"); min != "" {
+			var ver Version
+			if _, err := fmt.Sscanf(min, "%d.%d", &ver.Major, &ver.Minor); err != nil {
+				panic(fmt.Sprintf("invalid smbios_min_ver tag in %v: %v", fieldType.Name, err))
+			}
+			completedVersion = parsingVersion
+			parsingVersion = ver
+		}
+		f := v.Field(i)
+
+		if repeat := fieldType.Tag.Get("smbios_repeat"); repeat != "" {
+			repeatCountField := v.FieldByName(repeat)
+			if !repeatCountField.IsValid() {
+				panic(fmt.Sprintf("invalid smbios_repeat tag in %v: no such field %q", fieldType.Name, repeat))
+			}
+			if !repeatCountField.CanUint() {
+				panic(fmt.Sprintf("invalid smbios_repeat tag in %v: referenced field %q is not uint-compatible", fieldType.Name, repeat))
+			}
+			if f.Kind() != reflect.Slice {
+				panic(fmt.Sprintf("cannot repeat a field (%q) which is not a slice", fieldType.Name))
+			}
+			if repeatCountField.Uint() > 65536 {
+				return fmt.Errorf("refusing to read a field repeated more than 65536 times (given %d times)", repeatCountField.Uint())
+			}
+			repeatCount := int(repeatCountField.Uint())
+			f.Set(reflect.MakeSlice(f.Type(), repeatCount, repeatCount))
+			for j := 0; j < repeatCount; j++ {
+				fs := f.Index(j)
+				err := unmarshalField(&rawStruct, fs, r)
+				if errors.Is(err, io.EOF) {
+					hasAborted = true
+					break
+				} else if err != nil {
+					return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
+				}
+			}
+		}
+		err := unmarshalField(&rawStruct, f, r)
+		if errors.Is(err, io.EOF) {
+			hasAborted = true
+			break
+		} else if err != nil {
+			return fmt.Errorf("error unmarshaling field %q: %w", fieldType.Name, err)
+		}
+	}
+	if !hasAborted {
+		completedVersion = parsingVersion
+	}
+	if completedVersion.Major == 0 {
+		return fmt.Errorf("structure's formatted section (%d bytes) is smaller than its minimal size", len(rawStruct.FormattedSection))
+	}
+	v.Field(1).Set(reflect.ValueOf(completedVersion))
+	return nil
+}
+
+func unmarshalField(rawStruct *Structure, field reflect.Value, r *bytes.Reader) error {
+	if field.Kind() == reflect.String {
+		var stringTableIdx uint8
+		err := binary.Read(r, binary.LittleEndian, &stringTableIdx)
+		if err != nil {
+			return err
+		}
+		if stringTableIdx == 0 {
+			return nil
+		}
+		if int(stringTableIdx)-1 >= len(rawStruct.Strings) {
+			return fmt.Errorf("string index (%d) bigger than string table (%q)", stringTableIdx-1, rawStruct.Strings)
+		}
+		field.SetString(rawStruct.Strings[stringTableIdx-1])
+		return nil
+	}
+	return binary.Read(r, binary.LittleEndian, field.Addr().Interface())
+}
diff --git a/metropolis/pkg/smbios/structures.go b/metropolis/pkg/smbios/structures.go
new file mode 100644
index 0000000..64ef523
--- /dev/null
+++ b/metropolis/pkg/smbios/structures.go
@@ -0,0 +1,177 @@
+package smbios
+
+import (
+	"time"
+)
+
+const (
+	structTypeBIOSInformation      = 0
+	structTypeSystemInformation    = 1
+	structTypeBaseboardInformation = 2
+	structTypeSystemSlot           = 9
+	structTypeMemoryDevice         = 17
+)
+
+// BIOSInformationRaw contains decoded data from the BIOS Information structure
+// (SMBIOS Type 0). See Table 6 in the specification for detailed documentation
+// about the individual fields. Note that structure versions 2.1 and 2.2 are
+// "invented" here as both characteristics extensions bytes were optional
+// between 2.0 and 2.4.
+type BIOSInformationRaw struct {
+	Handle                                 uint16
+	StructureVersion                       Version
+	Vendor                                 string
+	BIOSVersion                            string
+	BIOSStartingAddressSegment             uint16
+	BIOSReleaseDate                        string
+	BIOSROMSize                            uint8
+	BIOSCharacteristics                    uint64
+	BIOSCharacteristicsExtensionByte1      uint8 `smbios_min_ver:"2.1"`
+	BIOSCharacteristicsExtensionByte2      uint8 `smbios_min_ver:"2.2"`
+	SystemBIOSMajorRelease                 uint8 `smbios_min_ver:"2.4"`
+	SystemBIOSMinorRelease                 uint8
+	EmbeddedControllerFirmwareMajorRelease uint8
+	EmbeddedControllerFirmwareMinorRelease uint8
+	ExtendedBIOSROMSize                    uint16 `smbios_min_ver:"3.1"`
+}
+
+// ROMSizeBytes returns the ROM size in bytes
+func (rb *BIOSInformationRaw) ROMSizeBytes() uint64 {
+	if rb.StructureVersion.AtLeast(3, 1) && rb.BIOSROMSize == 0xFF {
+		// Top 2 bits are SI prefix (starting at mega, i.e. 1024^2), lower 14
+		// are value. x*1024^n => x << log2(1024)*n => x << 10*n
+		return uint64(rb.ExtendedBIOSROMSize&0x3fff) << 10 * uint64(rb.ExtendedBIOSROMSize&0xc00+2)
+	} else {
+		// (n+1) * 64KiB
+		return (uint64(rb.BIOSROMSize) + 1) * (64 * 1024)
+	}
+}
+
+// ReleaseDate returns the release date of the BIOS as a time.Time value.
+func (rb *BIOSInformationRaw) ReleaseDate() (time.Time, error) {
+	return time.Parse("01/02/2006", rb.BIOSReleaseDate)
+}
+
+// SystemInformationRaw contains decoded data from the System Information
+// structure (SMBIOS Type 1). See Table 10 in the specification for detailed
+// documentation about the individual fields.
+type SystemInformationRaw struct {
+	Handle           uint16
+	StructureVersion Version
+	Manufacturer     string
+	ProductName      string
+	Version          string
+	SerialNumber     string
+	UUID             [16]byte `smbios_min_ver:"2.1"`
+	WakeupType       uint8
+	SKUNumber        string `smbios_min_ver:"2.4"`
+	Family           string
+}
+
+// BaseboardInformationRaw contains decoded data from the BIOS Information
+// structure (SMBIOS Type 3). See Table 13 in the specification for detailed
+// documentation about the individual fields.
+type BaseboardInformationRaw struct {
+	Handle                         uint16
+	StructureVersion               Version
+	Manufacturer                   string
+	Product                        string
+	Version                        string
+	SerialNumber                   string
+	AssetTag                       string `smbios_min_ver:"2.1"`
+	FeatureFlags                   uint8
+	LocationInChassis              string
+	ChassisHandle                  uint16
+	BoardType                      uint8
+	NumberOfContainedObjectHandles uint8
+	ContainedObjectHandles         []uint16 `smbios_repeat:"NumberOfContainedObjectHandles"`
+}
+
+// SystemSlotRaw contains decoded data from the System Slot structure
+// (SMBIOS Type 9). See Table 44 in the specification for detailed documentation
+// about the individual fields.
+type SystemSlotRaw struct {
+	Handle               uint16
+	StructureVersion     Version
+	SlotDesignation      string
+	SlotType             uint8
+	SlotDataBusWidth     uint8
+	CurrentUsage         uint8
+	SlotLength           uint8
+	SlotID               uint16
+	SlotCharacteristics1 uint8
+	SlotCharacteristics2 uint8  `smbios_min_ver:"2.1"`
+	SegmentGroupNumber   uint16 `smbios_min_ver:"2.6"`
+	BusNumber            uint8
+	DeviceFunctionNumber uint8
+	DataBusWidth         uint8 `smbios_min_ver:"3.2"`
+	PeerGroupingCount    uint8
+	PeerGroups           []SystemSlotPeerRaw `smbios_repeat:"PeerGroupingCount"`
+	SlotInformation      uint8               `smbios_min_ver:"3.4"`
+	SlotPhysicalWidth    uint8
+	SlotPitch            uint16
+	SlotHeight           uint8 `smbios_min_ver:"3.5"`
+}
+
+type SystemSlotPeerRaw struct {
+	SegmentGroupNumber   uint16
+	BusNumber            uint8
+	DeviceFunctionNumber uint8
+	DataBusWidth         uint8
+}
+
+// MemoryDeviceRaw contains decoded data from the BIOS Information structure
+// (SMBIOS Type 17). See Table 76 in the specification for detailed
+// documentation about the individual fields.
+type MemoryDeviceRaw struct {
+	Handle                                  uint16
+	StructureVersion                        Version
+	PhysicalMemoryArrayHandle               uint16 `smbios_min_ver:"2.1"`
+	MemoryErrorInformationHandle            uint16
+	TotalWidth                              uint16
+	DataWidth                               uint16
+	Size                                    uint16
+	FormFactor                              uint8
+	DeviceSet                               uint8
+	DeviceLocator                           string
+	BankLocator                             string
+	MemoryType                              uint8
+	TypeDetail                              uint16
+	Speed                                   uint16 `smbios_min_ver:"2.3"`
+	Manufacturer                            string
+	SerialNumber                            string
+	AssetTag                                string
+	PartNumber                              string
+	Attributes                              uint8  `smbios_min_ver:"2.6"`
+	ExtendedSize                            uint32 `smbios_min_ver:"2.7"`
+	ConfiguredMemorySpeed                   uint16
+	MinimumVoltage                          uint16 `smbios_min_ver:"2.8"`
+	MaximumVoltage                          uint16
+	ConfiguredVoltage                       uint16
+	MemoryTechnology                        uint8 `smbios_min_ver:"3.2"`
+	MemoryOperatingModeCapability           uint16
+	FirmwareVersion                         uint8
+	ModuleManufacturerID                    uint16
+	ModuleProductID                         uint16
+	MemorySubsystemControllerManufacturerID uint16
+	MemorySubsystemControllerProductID      uint16
+	NonVolatileSize                         uint64
+	VolatileSize                            uint64
+	CacheSize                               uint64
+	LogicalSize                             uint64
+	ExtendedSpeed                           uint32 `smbios_min_ver:"3.3"`
+	ExtendedConfiguredMemorySpeed           uint32
+}
+
+func (md *MemoryDeviceRaw) SizeBytes() (uint64, bool) {
+	if md.Size == 0 || md.Size == 0xFFFF {
+		// Device unpopulated / unknown memory, return ok false
+		return 0, false
+	}
+	if md.Size == 0x7FFF && md.StructureVersion.AtLeast(2, 7) {
+		// Bit 31 is reserved, rest is memory size in MiB
+		return uint64(md.ExtendedSize&0x7FFFFFFF) * (1024 * 1024), true
+	}
+	// Bit 15 flips between KiB and MiB, rest is size
+	return uint64(md.Size&0x7FFF) << 10 * uint64(md.Size&0x8000+1), true
+}