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