diff --git a/base.go b/base.go index c4982d2..239da4c 100644 --- a/base.go +++ b/base.go @@ -1,7 +1,67 @@ package devicepath -type DevicePathNode interface{} +import ( + "errors" + "strings" +) -type DevicePath interface { +type DeviceNodeType uint8 + +const ( + HardwareDevicePath DeviceNodeType = 0x01 + ACPIDevicePath DeviceNodeType = 0x02 + MessagingDevicePath DeviceNodeType = 0x03 + MediaDevicePath DeviceNodeType = 0x04 + BIOSBootSpecificationsDevicePath DeviceNodeType = 0x05 + EndOfHardwareDevicePath DeviceNodeType = 0x7F +) + +type DeviceNodeSubType uint8 + +const ( + PCIHardware DeviceNodeSubType = 0x01 +) + +const ( + SATAMessaging DeviceNodeSubType = 18 +) + +const EndOfEntireDevicePath DeviceNodeSubType = 0xFF + +const ACPISubType DeviceNodeSubType = 1 + +type DevicePathNode interface { + Type() DeviceNodeType + SubType() DeviceNodeSubType + ParseString(raw string) error String() string + Bytes() []byte +} + +var ErrPathComplete = errors.New("device path already complete") + +type DevicePath struct { + nodes []DevicePathNode +} + +func (dp *DevicePath) complete() bool { + return len(dp.nodes) > 0 && dp.nodes[len(dp.nodes)-1].Type() == EndOfHardwareDevicePath +} + +func (dp *DevicePath) PushNode(node DevicePathNode) error { + if dp.complete() { + return ErrPathComplete + } + dp.nodes = append(dp.nodes, node) + return nil +} + +func (dp *DevicePath) String() string { + var res []string + for _, node := range dp.nodes { + if node.String() != "" { + res = append(res, node.String()) + } + } + return strings.Join(res, "/") } diff --git a/end.go b/end.go new file mode 100644 index 0000000..1de6b94 --- /dev/null +++ b/end.go @@ -0,0 +1,26 @@ +package devicepath + +import "errors" + +type FinalDevicePath struct { +} + +func (fp *FinalDevicePath) Type() DeviceNodeType { + return EndOfHardwareDevicePath +} + +func (fp *FinalDevicePath) SubType() DeviceNodeSubType { + return EndOfEntireDevicePath +} + +func (fp *FinalDevicePath) Bytes() []byte { + return nil +} + +func (fp *FinalDevicePath) String() string { + return "" +} + +func (fp *FinalDevicePath) ParseString(raw string) error { + return errors.New("unparsable") +} diff --git a/go.mod b/go.mod index b28cea0..7c67964 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module git.faercol.me/faercol/device-path go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parser.go b/parser.go index 2265c91..1a92ab5 100644 --- a/parser.go +++ b/parser.go @@ -1,9 +1,65 @@ package devicepath -func ParseNodeString(string) (DevicePathNode, error) { - return nil, nil +import ( + "errors" + "fmt" + "strings" +) + +var ( + ErrUnknownSubType = errors.New("unknown device sub-type") + ErrMalformedString = errors.New("malformed device string") +) + +type ErrInvalidArguments struct { + subErr error } -func ParsePathString(string) (DevicePath, error) { - return nil, nil +func (e *ErrInvalidArguments) Error() string { + return fmt.Sprintf("invalid argument: %s", e.subErr.Error()) +} + +type ErrInvalidPath struct { + subErr error + node string +} + +func (e *ErrInvalidPath) Error() string { + return fmt.Sprintf("invalid device path on section %q: %s", e.node, e.subErr.Error()) +} + +type nodeGenerator func() DevicePathNode + +var prefixTypeAssociation = map[string]nodeGenerator{ + sataPrefix: func() DevicePathNode { return &SataDevicePath{} }, + pciPrefix: func() DevicePathNode { return &PCIDevicePath{} }, + pciRootPrefix: func() DevicePathNode { return &PCIRootDevicePath{} }, +} + +func Parsenode(raw string) (DevicePathNode, error) { + prefix := getPrefix(raw) + devGen, ok := prefixTypeAssociation[prefix] + if !ok { + return nil, ErrUnknownSubType + } + dev := devGen() + if err := dev.ParseString(raw); err != nil { + return nil, &ErrInvalidArguments{subErr: err} + } + return dev, nil +} + +func ParseDevicePath(raw string) (*DevicePath, error) { + dp := DevicePath{} + for _, nodeStr := range strings.Split(raw, "/") { + node, err := Parsenode(nodeStr) + if err != nil { + return nil, &ErrInvalidPath{subErr: err, node: nodeStr} + } + if err := dp.PushNode(node); err != nil { + return nil, err + } + } + dp.PushNode(&FinalDevicePath{}) + return &dp, nil } diff --git a/parser_test.go b/parser_test.go index 7a8aa38..9856db5 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,7 +1,25 @@ package devicepath_test -import "testing" +import ( + "testing" -func TestTest(t *testing.T) { + devicepath "git.faercol.me/faercol/device-path" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) +func TestOK(t *testing.T) { + input := "Sata(1,65535,0)" + res, err := devicepath.Parsenode(input) + require.NoError(t, err) + assert.Equal(t, devicepath.MessagingDevicePath, res.Type()) + assert.Equal(t, devicepath.SATAMessaging, res.SubType()) + assert.Equal(t, "Sata(1,65535,0)", res.String()) +} + +func TestParseDevicePathOK(t *testing.T) { + input := "PciRoot(0x0)/Pci(0x1,0x2)/Pci(0x0,0x1)/Sata(0,65535,0)" + res, err := devicepath.ParseDevicePath(input) + require.NoError(t, err) + assert.Equal(t, "PciRoot(0x0)/Pci(0x1,0x2)/Pci(0x0,0x1)/Sata(0,65535,0)", res.String()) } diff --git a/pci.go b/pci.go new file mode 100644 index 0000000..28930f6 --- /dev/null +++ b/pci.go @@ -0,0 +1,72 @@ +package devicepath + +import ( + "encoding/binary" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + pciLength uint16 = 6 + pciPrefix string = "Pci" + pciStringLength int = 12 +) + +type PCIDevicePath struct { + function uint8 + device uint8 +} + +func (pcp *PCIDevicePath) Type() DeviceNodeType { + return HardwareDevicePath +} + +func (pcp *PCIDevicePath) SubType() DeviceNodeSubType { + return PCIHardware +} + +func (pcp *PCIDevicePath) Bytes() []byte { + res := make([]byte, pciLength) + res[0] = byte(pcp.Type()) + res[1] = byte(pcp.SubType()) + binary.LittleEndian.PutUint16(res[2:3], pciLength) + res[4] = pcp.function + res[5] = pcp.device + return res +} + +func (pcp *PCIDevicePath) String() string { + return fmt.Sprintf("%s(0x%x,0x%x)", pciPrefix, pcp.function, pcp.device) +} + +func (pcp *PCIDevicePath) ParseString(raw string) error { + if !checkStringFormat(raw, pciPrefix, pciStringLength) { + return ErrMalformedString + } + args := strings.Split(raw[len(pciPrefix)+1:pciStringLength-1], ",") + if len(args) != 2 { + return errors.New("unexpected number of arguments") + } + if function, err := strconv.ParseUint(args[0], 0, 8); err != nil { + return err + } else { + fmt.Println("function", function) + pcp.function = uint8(function) + } + if device, err := strconv.ParseUint(args[1], 0, 8); err != nil { + return err + } else { + fmt.Println("device", device) + pcp.device = uint8(device) + } + return nil +} + +func NewPCIDevicePath(function, device uint8) *PCIDevicePath { + return &PCIDevicePath{ + function: function, + device: device, + } +} diff --git a/pci_test.go b/pci_test.go new file mode 100644 index 0000000..2e182cf --- /dev/null +++ b/pci_test.go @@ -0,0 +1,22 @@ +package devicepath_test + +import ( + "testing" + + devicepath "git.faercol.me/faercol/device-path" + "github.com/stretchr/testify/assert" +) + +func TestToString(t *testing.T) { + t.Run("Only digits", func(t *testing.T) { + expected := "Pci(0x0,0x1)" + dev := devicepath.NewPCIDevicePath(0, 1) + assert.Equal(t, expected, dev.String()) + }) + + t.Run("Letters", func(t *testing.T) { + expected := "Pci(0x2,0xa)" + dev := devicepath.NewPCIDevicePath(2, 10) + assert.Equal(t, expected, dev.String()) + }) +} diff --git a/pciroot.go b/pciroot.go new file mode 100644 index 0000000..4c28396 --- /dev/null +++ b/pciroot.go @@ -0,0 +1,46 @@ +package devicepath + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const pciRootPrefix string = "PciRoot" + +type PCIRootDevicePath struct { + uid int32 +} + +func (prp *PCIRootDevicePath) Type() DeviceNodeType { + return ACPIDevicePath +} + +func (prp *PCIRootDevicePath) SubType() DeviceNodeSubType { + return ACPISubType +} + +func (prp *PCIRootDevicePath) Bytes() []byte { + return nil +} + +func (prp *PCIRootDevicePath) String() string { + return fmt.Sprintf("%s(0x%x)", pciRootPrefix, prp.uid) +} + +func (prp *PCIRootDevicePath) ParseString(raw string) error { + if !checkStringFormat(raw, pciRootPrefix, -1) { + return ErrMalformedString + } + args := strings.Split(raw[len(pciRootPrefix)+1:len(raw)-1], ",") + if len(args) != 1 { + return errors.New("unexpected number of arguments") + } + if uid, err := strconv.ParseInt(args[0], 0, 32); err != nil { + return err + } else { + prp.uid = int32(uid) + } + return nil +} diff --git a/sata.go b/sata.go new file mode 100644 index 0000000..1290668 --- /dev/null +++ b/sata.go @@ -0,0 +1,71 @@ +package devicepath + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + defaultPortMultiplier = 0xffff + defaultLogicalUnitNumber = 0 + sataLength uint16 = 10 + sataPrefix string = "Sata" +) + +type SataDevicePath struct { + hbaPortNumber uint16 + portMultiplierPortNumber uint16 + logicalUnitNumber uint16 +} + +func (sp *SataDevicePath) Type() DeviceNodeType { + return MessagingDevicePath +} + +func (sp *SataDevicePath) SubType() DeviceNodeSubType { + return SATAMessaging +} + +func (sp *SataDevicePath) Bytes() []byte { + return nil +} + +func (sp *SataDevicePath) String() string { + return fmt.Sprintf("%s(%d,%d,%d)", sataPrefix, sp.hbaPortNumber, sp.portMultiplierPortNumber, sp.logicalUnitNumber) +} + +func (sp *SataDevicePath) ParseString(raw string) error { + if !checkStringFormat(raw, sataPrefix, -1) { + return ErrMalformedString + } + args := strings.Split(raw[len(sataPrefix)+1:len(raw)-1], ",") + if len(args) != 3 { + return errors.New("unexpected number of arguments") + } + if hbaPortNumber, err := strconv.Atoi(args[0]); err != nil { + return err + } else { + sp.hbaPortNumber = uint16(hbaPortNumber) + } + if portMultiplierPortNumber, err := strconv.Atoi(args[1]); err != nil { + return err + } else { + sp.portMultiplierPortNumber = uint16(portMultiplierPortNumber) + } + if logicalUnitNumber, err := strconv.Atoi(args[2]); err != nil { + return err + } else { + sp.logicalUnitNumber = uint16(logicalUnitNumber) + } + return nil +} + +func NewSataFromPortNumber(portNumber uint16) *SataDevicePath { + return &SataDevicePath{ + hbaPortNumber: portNumber, + portMultiplierPortNumber: defaultPortMultiplier, + logicalUnitNumber: defaultLogicalUnitNumber, + } +} diff --git a/sata_test.go b/sata_test.go new file mode 100644 index 0000000..39db850 --- /dev/null +++ b/sata_test.go @@ -0,0 +1,42 @@ +package devicepath_test + +import ( + "testing" + + devicepath "git.faercol.me/faercol/device-path" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseString(t *testing.T) { + t.Run("OK", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + require.NoError(t, sataDev.ParseString("Sata(1,65535,0)")) + assert.Equal(t, "Sata(1,65535,0)", sataDev.String()) + }) + + t.Run("Err invalid prefix", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + assert.ErrorIs(t, sataDev.ParseString("PciRoot(0x0)"), devicepath.ErrMalformedString) + }) + + t.Run("Err invalid number arguments", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + assert.ErrorContains(t, sataDev.ParseString("Sata(1,2,3,4)"), "unexpected number of arguments") + }) + + t.Run("Err invalid format arg0", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + assert.ErrorContains(t, sataDev.ParseString("Sata(toto,65535,0)"), `strconv.Atoi: parsing "toto": invalid syntax`) + }) + + t.Run("Err invalid format arg1", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + assert.ErrorContains(t, sataDev.ParseString("Sata(0,toto,0)"), `strconv.Atoi: parsing "toto": invalid syntax`) + }) + + t.Run("Err invalid format arg2", func(t *testing.T) { + var sataDev devicepath.SataDevicePath + assert.ErrorContains(t, sataDev.ParseString("Sata(0,65535,toto)"), `strconv.Atoi: parsing "toto": invalid syntax`) + }) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..799c6b8 --- /dev/null +++ b/utils.go @@ -0,0 +1,18 @@ +package devicepath + +import "strings" + +func getPrefix(raw string) string { + firstCharAfterId := strings.Index(raw, "(") + if firstCharAfterId == -1 { + return "" + } + return raw[0:firstCharAfterId] +} + +func checkStringFormat(raw, prefix string, length int) bool { + if length > 0 && len(raw) != length { + return false + } + return strings.HasPrefix(raw, prefix+"(") && strings.HasSuffix(raw, ")") +}