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/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())
+}