summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dhcpv4/option_userclass.go85
-rw-r--r--dhcpv4/option_userclass_test.go122
2 files changed, 207 insertions, 0 deletions
diff --git a/dhcpv4/option_userclass.go b/dhcpv4/option_userclass.go
new file mode 100644
index 0000000..1505dbb
--- /dev/null
+++ b/dhcpv4/option_userclass.go
@@ -0,0 +1,85 @@
+package dhcpv4
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+// This option implements the User Class option
+// https://tools.ietf.org/html/rfc3004
+
+// OptUserClass represents an option encapsulating User Classes.
+type OptUserClass struct {
+ UserClasses [][]byte
+}
+
+// Code returns the option code
+func (op *OptUserClass) Code() OptionCode {
+ return OptionUserClassInformation
+}
+
+// ToBytes serializes the option and returns it as a sequence of bytes
+func (op *OptUserClass) ToBytes() []byte {
+ buf := []byte{byte(op.Code()), byte(op.Length())}
+ for _, uc := range op.UserClasses {
+ buf = append(buf, byte(len(uc)))
+ buf = append(buf, uc...)
+ }
+ return buf
+}
+
+// Length returns the option length
+func (op *OptUserClass) Length() int {
+ ret := 0
+ for _, uc := range op.UserClasses {
+ ret += 1 + len(uc)
+ }
+ return ret
+}
+
+func (op *OptUserClass) String() string {
+ ucStrings := make([]string, len(op.UserClasses))
+ for _, uc := range op.UserClasses {
+ ucStrings = append(ucStrings, string(uc))
+ }
+ return fmt.Sprintf("OptUserClass{userclass=[%s]}", strings.Join(ucStrings, ", "))
+}
+
+// ParseOptUserClass returns a new OptUserClass from a byte stream or
+// error if any
+func ParseOptUserClass(data []byte) (*OptUserClass, error) {
+ opt := OptUserClass{}
+
+ if len(data) < 4 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionUserClassInformation {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionUserClassInformation, code)
+ }
+
+ totalLength := int(data[1])
+ data = data[2:]
+ if len(data) < totalLength {
+ return nil, fmt.Errorf("ParseOptUserClass: short data: length is %d but got %d bytes",
+ totalLength, len(data))
+ }
+
+ for i := 0; i < totalLength; {
+ ucLen := int(data[i])
+ if ucLen == 0 {
+ return nil, errors.New("User Class value has invalid length of 0")
+ }
+ base := i + 1
+ if len(data) < base+ucLen {
+ return nil, fmt.Errorf("ParseOptUserClass: short data: %d bytes; want: %d", len(data), base+ucLen)
+ }
+ opt.UserClasses = append(opt.UserClasses, data[base:base+ucLen])
+ i += base + ucLen
+ }
+ if len(opt.UserClasses) < 1 {
+ return nil, errors.New("ParseOptUserClass: at least one user class is required")
+ }
+ return &opt, nil
+}
diff --git a/dhcpv4/option_userclass_test.go b/dhcpv4/option_userclass_test.go
new file mode 100644
index 0000000..5b71ea5
--- /dev/null
+++ b/dhcpv4/option_userclass_test.go
@@ -0,0 +1,122 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptUserClassToBytes(t *testing.T) {
+ opt := OptUserClass{
+ UserClasses: [][]byte{[]byte("linuxboot")},
+ }
+ data := opt.ToBytes()
+ expected := []byte{
+ 77, // OPTION_USER_CLASS
+ 10, // length
+ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ require.Equal(t, expected, data)
+}
+
+func TestParseOptUserClassMultiple(t *testing.T) {
+ expected := []byte{
+ 77, 15,
+ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ 4, 't', 'e', 's', 't',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, len(opt.UserClasses), 2)
+ require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
+ require.Equal(t, []byte("test"), opt.UserClasses[1])
+}
+
+func TestParseOptUserClassNone(t *testing.T) {
+ expected := []byte{}
+ _, err := ParseOptUserClass(expected)
+ require.Error(t, err)
+}
+
+func TestParseOptUserClass(t *testing.T) {
+ expected := []byte{
+ 77, 10, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(opt.UserClasses))
+ require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
+}
+
+func TestOptUserClassToBytesMultiple(t *testing.T) {
+ opt := OptUserClass{
+ UserClasses: [][]byte{
+ []byte("linuxboot"),
+ []byte("test"),
+ },
+ }
+ data := opt.ToBytes()
+ expected := []byte{
+ 77, // OPTION_USER_CLASS
+ 15, // length
+ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ 4, 't', 'e', 's', 't',
+ }
+ require.Equal(t, expected, data)
+}
+
+func TestParseOptUserClassLongerThanLength(t *testing.T) {
+ expected := []byte{
+ 77, 10, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 'X',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(opt.UserClasses))
+ require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
+}
+
+func TestParseOptUserClassShorterThanLength(t *testing.T) {
+ expected := []byte{
+ 77, 10, 10, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ _, err := ParseOptUserClass(expected)
+ require.Error(t, err)
+}
+
+func TestParseOptUserClassShorterTotalLength(t *testing.T) {
+ expected := []byte{
+ 77, 11, 10, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ _, err := ParseOptUserClass(expected)
+ require.Error(t, err)
+}
+
+func TestOptUserClassLength(t *testing.T) {
+ expected := []byte{
+ 77, 10, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 'X',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 10, opt.Length())
+}
+
+func TestParseOptUserClassZeroLength(t *testing.T) {
+ expected := []byte{
+ 77, 1, 0, 0,
+ }
+ _, err := ParseOptUserClass(expected)
+ require.Error(t, err)
+}
+
+func TestParseOptUserClassMultipleWithZeroLength(t *testing.T) {
+ expected := []byte{
+ 77, 12, 10, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0,
+ }
+ _, err := ParseOptUserClass(expected)
+ require.Error(t, err)
+}
+
+func TestOptUserClassCode(t *testing.T) {
+ opt := OptUserClass{}
+ require.Equal(t, OptionUserClassInformation, opt.Code())
+}