treewide: introduce osbase package and move things around

All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.

Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/smbios/BUILD.bazel b/osbase/smbios/BUILD.bazel
new file mode 100644
index 0000000..b974095
--- /dev/null
+++ b/osbase/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/osbase/smbios",
+    visibility = ["//visibility:public"],
+)
diff --git a/osbase/smbios/smbios.go b/osbase/smbios/smbios.go
new file mode 100644
index 0000000..e9bcfc3
--- /dev/null
+++ b/osbase/smbios/smbios.go
@@ -0,0 +1,269 @@
+// 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:
+			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 {
+	if v.Major > major {
+		return true
+	}
+	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 minVer := fieldType.Tag.Get("smbios_min_ver"); minVer != "" {
+			var ver Version
+			if _, err := fmt.Sscanf(minVer, "%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/osbase/smbios/structures.go b/osbase/smbios/structures.go
new file mode 100644
index 0000000..4c75709
--- /dev/null
+++ b/osbase/smbios/structures.go
@@ -0,0 +1,191 @@
+package smbios
+
+import (
+	"time"
+)
+
+const (
+	structTypeBIOSInformation      = 0
+	structTypeSystemInformation    = 1
+	structTypeBaseboardInformation = 2
+	structTypeSystemSlot           = 9
+	structTypeMemoryDevice         = 17
+)
+
+// UEFISpecificationSupported is a bitmask for accessing the third bit,
+// which displays the support of UEFI in the smbios structure. For more
+// information check the SMBIOS documentation at Table 7.1.2.2 Bit 3.
+const UEFISpecificationSupported = 1 << 3
+
+// 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
+}
+
+const (
+	kibLeftShift = 10 // 2^10 = 1KiB
+	mibLeftShift = 20 // 2^20 = 1MiB
+)
+
+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) << mibLeftShift, true
+	}
+	// Bit 15 flips between MiB and KiB, rest is size
+	var shift uint64 = mibLeftShift
+	if (md.Size & 0x8000) != 0 { // Bit set means KiB
+		shift = kibLeftShift
+	}
+	return uint64(md.Size&0x7FFF) << shift, true
+}