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