Repository: mdlayher/netlink (go)

Issue:
add support for signed integers encoding/decoding
Netlink has support for [signed integer attributes](https://elixir.bootlin.com/linux/v5.10.15/source/include/net/netlink.h#L104), which the library currently is missing. These are for example required when talking to rtnetlink to [set and modify the file descriptors of XDP programs](https://elixir.bootlin.com/linux/v5.10.15/source/net/core/rtnetlink.c#L2911) loaded to the links.

Providing these through the library would offer tested and validated methods of encoding these to the users, blocking workarounds through `Bytes` and `Uint32` implementations that could lead to bugs.

I've done some minor work on this on a [fork](https://github.com/fbegyn/netlink/tree/f-signed-ints), which is sufficient for my application, but can expand and modify it if to be upstreamed if this deemed as a desired feature.

Pull request description:
implement signed integer encoding/decoding
Resolves #184 by implementing the signed integer encoders and decoders.

The `int(uint(` is not necessarily pretty, but the other options to my knowledge are:
* unsafe pointer casting (like in `nlenc`)
* manually parsing the bytes (but then endianess is a thing)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd567ff..3267a44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # CHANGELOG
 
+## v1.3.2
+
+- [Improvement]
+  [commit](https://github.com/mdlayher/netlink/commit/ebc6e2e28bcf1a0671411288423d8116ff924d6d):
+  `github.com/google/go-cmp` is no longer a (non-test) dependency of this module.
+
 ## v1.3.1
 
 - [Improvement]: many internal cleanups and simplifications. The library is now
diff --git a/attribute.go b/attribute.go
index 4c2ff25..67fd5e6 100644
--- a/attribute.go
+++ b/attribute.go
@@ -359,6 +359,66 @@ func (ad *AttributeDecoder) Uint64() uint64 {
 	return ad.ByteOrder.Uint64(b)
 }
 
+// Int8 returns the Int8 representation of the current Attribute's data.
+func (ad *AttributeDecoder) Int8() int8 {
+	if ad.err != nil {
+		return 0
+	}
+
+	b := ad.data()
+	if len(b) != 1 {
+		ad.err = fmt.Errorf("netlink: attribute %d is not a int8; length: %d", ad.Type(), len(b))
+		return 0
+	}
+
+	return int8(b[0])
+}
+
+// Int16 returns the Int16 representation of the current Attribute's data.
+func (ad *AttributeDecoder) Int16() int16 {
+	if ad.err != nil {
+		return 0
+	}
+
+	b := ad.data()
+	if len(b) != 2 {
+		ad.err = fmt.Errorf("netlink: attribute %d is not a int16; length: %d", ad.Type(), len(b))
+		return 0
+	}
+
+	return int16(ad.ByteOrder.Uint16(b))
+}
+
+// Int32 returns the Int32 representation of the current Attribute's data.
+func (ad *AttributeDecoder) Int32() int32 {
+	if ad.err != nil {
+		return 0
+	}
+
+	b := ad.data()
+	if len(b) != 4 {
+		ad.err = fmt.Errorf("netlink: attribute %d is not a int32; length: %d", ad.Type(), len(b))
+		return 0
+	}
+
+	return int32(ad.ByteOrder.Uint32(b))
+}
+
+// Int64 returns the Int64 representation of the current Attribute's data.
+func (ad *AttributeDecoder) Int64() int64 {
+	if ad.err != nil {
+		return 0
+	}
+
+	b := ad.data()
+	if len(b) != 8 {
+		ad.err = fmt.Errorf("netlink: attribute %d is not a int64; length: %d", ad.Type(), len(b))
+		return 0
+	}
+
+	return int64(ad.ByteOrder.Uint64(b))
+}
+
 // Flag returns a boolean representing the Attribute.
 func (ad *AttributeDecoder) Flag() bool {
 	if ad.err != nil {
@@ -501,6 +561,63 @@ func (ae *AttributeEncoder) Uint64(typ uint16, v uint64) {
 	})
 }
 
+// Int8 encodes int8 data into an Attribute specified by typ.
+func (ae *AttributeEncoder) Int8(typ uint16, v int8) {
+	if ae.err != nil {
+		return
+	}
+
+	ae.attrs = append(ae.attrs, Attribute{
+		Type: typ,
+		Data: []byte{uint8(v)},
+	})
+}
+
+// Int16 encodes int16 data into an Attribute specified by typ.
+func (ae *AttributeEncoder) Int16(typ uint16, v int16) {
+	if ae.err != nil {
+		return
+	}
+
+	b := make([]byte, 2)
+	ae.ByteOrder.PutUint16(b, uint16(v))
+
+	ae.attrs = append(ae.attrs, Attribute{
+		Type: typ,
+		Data: b,
+	})
+}
+
+// Int32 encodes int32 data into an Attribute specified by typ.
+func (ae *AttributeEncoder) Int32(typ uint16, v int32) {
+	if ae.err != nil {
+		return
+	}
+
+	b := make([]byte, 4)
+	ae.ByteOrder.PutUint32(b, uint32(v))
+
+	ae.attrs = append(ae.attrs, Attribute{
+		Type: typ,
+		Data: b,
+	})
+}
+
+// Int64 encodes int64 data into an Attribute specified by typ.
+func (ae *AttributeEncoder) Int64(typ uint16, v int64) {
+	if ae.err != nil {
+		return
+	}
+
+	b := make([]byte, 8)
+	ae.ByteOrder.PutUint64(b, uint64(v))
+
+	ae.attrs = append(ae.attrs, Attribute{
+		Type: typ,
+		Data: b,
+	})
+}
+
 // Flag encodes a flag into an Attribute specidied by typ.
 func (ae *AttributeEncoder) Flag(typ uint16, v bool) {
 	// Only set flag on no previous error or v == true.
diff --git a/fuzz.go b/fuzz.go
index e554e0c..566eef2 100644
--- a/fuzz.go
+++ b/fuzz.go
@@ -1,3 +1,5 @@
+//+build gofuzz
+
 package netlink
 
 import "github.com/google/go-cmp/cmp"
diff --git a/gofuzz.go b/gofuzz.go
deleted file mode 100644
index c7d920f..0000000
--- a/gofuzz.go
+++ /dev/null
@@ -1,5 +0,0 @@
-//+build gofuzz
-
-package netlink
-
-func Fuzz(b []byte) int { return fuzz(b) }


Test patch:
diff --git a/.github/workflows/linux-test.yml b/.github/workflows/linux-test.yml
index 98099c7..a64d898 100644
--- a/.github/workflows/linux-test.yml
+++ b/.github/workflows/linux-test.yml
@@ -29,4 +29,4 @@ jobs:
       run: sudo ip netns add unpriv0
 
     - name: Run tests
-      run: go test -v -race ./...
+      run: go test -v -race -tags gofuzz ./...
diff --git a/attribute_test.go b/attribute_test.go
index 9e0e539..7435bdd 100644
--- a/attribute_test.go
+++ b/attribute_test.go
@@ -461,6 +461,42 @@ func TestAttributeDecoderError(t *testing.T) {
 				ad.Uint64()
 			},
 		},
+		{
+			name:  "int8",
+			attrs: bad,
+			fn: func(ad *AttributeDecoder) {
+				ad.Int8()
+				ad.Next()
+				ad.Int8()
+			},
+		},
+		{
+			name:  "int16",
+			attrs: bad,
+			fn: func(ad *AttributeDecoder) {
+				ad.Int16()
+				ad.Next()
+				ad.Int16()
+			},
+		},
+		{
+			name:  "int32",
+			attrs: bad,
+			fn: func(ad *AttributeDecoder) {
+				ad.Int32()
+				ad.Next()
+				ad.Int32()
+			},
+		},
+		{
+			name:  "int64",
+			attrs: bad,
+			fn: func(ad *AttributeDecoder) {
+				ad.Int64()
+				ad.Next()
+				ad.Int64()
+			},
+		},
 		{
 			name:  "do",
 			attrs: bad,
@@ -527,17 +563,17 @@ func TestAttributeDecoderOK(t *testing.T) {
 			},
 		},
 		{
-			name:  "uint native endian",
+			name:  "uint-int native endian",
 			attrs: adEndianAttrs(native.Endian),
 			fn:    adEndianTest(native.Endian),
 		},
 		{
-			name:  "uint little endian",
+			name:  "uint-int little endian",
 			attrs: adEndianAttrs(binary.LittleEndian),
 			fn:    adEndianTest(binary.LittleEndian),
 		},
 		{
-			name:  "uint big endian",
+			name:  "uint-int big endian",
 			attrs: adEndianAttrs(binary.BigEndian),
 			fn:    adEndianTest(binary.BigEndian),
 		},
@@ -832,6 +868,36 @@ func adEndianAttrs(order binary.ByteOrder) []Attribute {
 				return b
 			}(),
 		},
+		{
+			Type: 5,
+			Data: func() []byte {
+				return []byte{uint8(int8(5))}
+			}(),
+		},
+		{
+			Type: 6,
+			Data: func() []byte {
+				b := make([]byte, 2)
+				order.PutUint16(b, uint16(int16(6)))
+				return b
+			}(),
+		},
+		{
+			Type: 7,
+			Data: func() []byte {
+				b := make([]byte, 4)
+				order.PutUint32(b, uint32(int32(7)))
+				return b
+			}(),
+		},
+		{
+			Type: 8,
+			Data: func() []byte {
+				b := make([]byte, 8)
+				order.PutUint64(b, uint64(int64(8)))
+				return b
+			}(),
+		},
 	}
 }
 
@@ -853,6 +919,14 @@ func adEndianTest(order binary.ByteOrder) func(ad *AttributeDecoder) {
 			v = int(ad.Uint32())
 		case 4:
 			v = int(ad.Uint64())
+		case 5:
+			v = int(ad.Int8())
+		case 6:
+			v = int(ad.Int16())
+		case 7:
+			v = int(ad.Int32())
+		case 8:
+			v = int(ad.Int64())
 		default:
 			panicf("unhandled attribute type: %d", t)
 		}
@@ -909,18 +983,18 @@ func TestAttributeEncoderOK(t *testing.T) {
 			},
 		},
 		{
-			name:  "uint native endian",
+			name:  "uint-int native endian",
 			attrs: adEndianAttrs(native.Endian),
 			fn:    aeEndianTest(native.Endian),
 		},
 		{
-			name:   "uint little endian",
+			name:   "uint-int little endian",
 			attrs:  adEndianAttrs(binary.LittleEndian),
 			endian: binary.LittleEndian,
 			fn:     aeEndianTest(binary.LittleEndian),
 		},
 		{
-			name:   "uint big endian",
+			name:   "uint-int big endian",
 			attrs:  adEndianAttrs(binary.BigEndian),
 			endian: binary.BigEndian,
 			fn:     aeEndianTest(binary.BigEndian),
@@ -1071,5 +1145,9 @@ func aeEndianTest(order binary.ByteOrder) func(ae *AttributeEncoder) {
 		ae.Uint16(2, uint16(2))
 		ae.Uint32(3, uint32(3))
 		ae.Uint64(4, uint64(4))
+		ae.Int8(5, int8(5))
+		ae.Int16(6, int16(6))
+		ae.Int32(7, int32(7))
+		ae.Int64(8, int64(8))
 	}
 }
diff --git a/fuzz_test.go b/fuzz_test.go
index c6c28fa..daa3e20 100644
--- a/fuzz_test.go
+++ b/fuzz_test.go
@@ -1,3 +1,5 @@
+//+build gofuzz
+
 package netlink
 
 import "testing"
diff --git a/nlenc/int_test.go b/nlenc/int_test.go
index 2e7f11f..09b9c08 100644
--- a/nlenc/int_test.go
+++ b/nlenc/int_test.go
@@ -125,6 +125,13 @@ func TestUintPanic(t *testing.T) {
 				Uint64(b)
 			},
 		},
+		{
+			name: "short put signed 32",
+			b:    make([]byte, 3),
+			fn: func(b []byte) {
+				PutInt32(b, 0)
+			},
+		},
 		{
 			name: "short get signed 32",
 			b:    make([]byte, 3),
@@ -132,6 +139,13 @@ func TestUintPanic(t *testing.T) {
 				Int32(b)
 			},
 		},
+		{
+			name: "long put signed 32",
+			b:    make([]byte, 5),
+			fn: func(b []byte) {
+				PutInt32(b, 0)
+			},
+		},
 		{
 			name: "long get signed 32",
 			b:    make([]byte, 5),