Skip to content

Commit

Permalink
feat(netmask): implement new encoding interfaces
Browse files Browse the repository at this point in the history
Go 1.24 introduces two new interfaces to the encoding package, which
have been implemented in net/netip: BinaryAppender and TextAppender.
These interfaces are similar to the BinaryMarshaler and TextMarshaler
interfaces, respectfully, but instead of creating a new byte-slice they
append to a provided slice.

As the two interfaces are closely related, this changeset implements
the *Marshaler interfaces in terms of the *Appender interfaces.
  • Loading branch information
terinjokes committed Dec 24, 2024
1 parent b9331f1 commit a608f45
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 18 deletions.
68 changes: 50 additions & 18 deletions netmask/mask.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,26 +194,42 @@ func (mask Mask) Bits() int {
}
}

// MarshalBinary implements the encoding.BinaryMarshaler interface.
// AppendBinary implements the [encoding.BinaryAppender] interface.
func (mask Mask) AppendBinary(b []byte) ([]byte, error) {
switch mask.z {
case z0:
return b, nil
case z4:
return append(b,
byte(mask.mask>>24),
byte(mask.mask>>16&0xFF),
byte(mask.mask>>8&0xFF),
byte(mask.mask&0xFF),
), nil
default:
return append(b, byte(mask.mask)), nil
}
}

// MarshalBinary implements the [encoding.BinaryMarshaler] interface.
// It returns a zero-length slice for the zero Mask, the 4-byte mask
// for IPv4, and a 1-byte prefix for IPv6.
func (mask Mask) MarshalBinary() ([]byte, error) {
return mask.AppendBinary(make([]byte, 0, mask.marshalBinarySize()))
}

func (mask Mask) marshalBinarySize() int {
switch mask.z {
case z0:
return []byte{}, nil
return 0
case z4:
b := make([]byte, 4)
b[0] = uint8(mask.mask >> 24)
b[1] = uint8(mask.mask >> 16 & 0xFF)
b[2] = uint8(mask.mask >> 8 & 0xFF)
b[3] = uint8(mask.mask & 0xFF)
return b, nil
return 4
default:
return []byte{byte(mask.mask)}, nil
return 1
}
}

// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. It
// UnmarshalBinary implements the [encoding.BinaryUnmarshaler] interface. It
// expects data in the form generated by MarshalBinary.
func (mask *Mask) UnmarshalBinary(b []byte) error {
n := len(b)
Expand All @@ -232,21 +248,37 @@ func (mask *Mask) UnmarshalBinary(b []byte) error {
return errors.New("unexpected slice size")
}

// MarshalText implements the encoding.TextMarshaler interface. The encoding is
// AppendText implements the [encoding.TextAppender] interface.
func (mask Mask) AppendText(b []byte) ([]byte, error) {
switch mask.z {
case z0:
return b, nil
case z4:
return appendTextIPv4(mask, b), nil
default:
return strconv.AppendUint(b, uint64(mask.mask), 10), nil
}
}

// MarshalText implements the [encoding.TextMarshaler] interface. The encoding is
// the same as returned by String, with one exception: If mask is the zero Mask,
// the encoding is the empty string.
func (mask Mask) MarshalText() ([]byte, error) {
return mask.AppendText(make([]byte, 0, mask.marshalTextSize()))
}

func (mask Mask) marshalTextSize() int {
switch mask.z {
case z0:
return []byte(""), nil
return 0
case z4:
return textIPv4(mask), nil
return len("255.255.255.255")
default:
return []byte(strconv.FormatUint(uint64(mask.mask), 10)), nil
return 1
}
}

// UnmarshalText implements the encoding.TextUnmarshaler interface. The mask
// UnmarshalText implements the [encoding.TextUnmarshaler] interface. The mask
// is expected in a form generated by MarshalText.
func (mask *Mask) UnmarshalText(text []byte) error {
n := len(text)
Expand Down Expand Up @@ -292,7 +324,8 @@ func (mask Mask) String() string {
case z0:
return "invalid Mask"
case z4:
return string(textIPv4(mask))
b := make([]byte, 0, len("255.255.255.255"))
return string(appendTextIPv4(mask, b))
default:
return strconv.FormatUint(uint64(mask.mask), 10)
}
Expand All @@ -302,8 +335,7 @@ func (x Mask) Equal(y Mask) bool {
return x == y
}

func textIPv4(mask Mask) []byte {
b := make([]byte, 0, len("255.255.255.255"))
func appendTextIPv4(mask Mask, b []byte) []byte {
b = strconv.AppendUint(b, uint64(uint8(mask.mask>>24)), 10)
b = append(b, '.')
b = strconv.AppendUint(b, uint64(uint8(mask.mask>>16)), 10)
Expand Down
10 changes: 10 additions & 0 deletions netmask/mask_go124.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build go1.24

package netmask

import (
"encoding"
)

var _ encoding.BinaryAppender = (*Mask)(nil)
var _ encoding.TextAppender = (*Mask)(nil)
81 changes: 81 additions & 0 deletions netmask/mask_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,35 @@ func TestNetmask_Bits(t *testing.T) {
})
}

func TestNetmask_AppendBinary(t *testing.T) {
type testCase struct {
name string
mask Mask
expected []byte
}

run := func(t *testing.T, tc testCase) {
b := make([]byte, 4, 32)
out, err := tc.mask.AppendBinary(b)
assert.NilError(t, err)
assert.DeepEqual(t, out[4:], tc.expected)
}

testCases := []testCase{
{name: "zero mask", mask: Mask{}, expected: []byte{}},
{name: "ipv4 mask", mask: MaskFrom(31, 32), expected: []byte{0xFF, 0xFF, 0xFF, 0xFE}},
{name: "weird ipv4 mask", mask: MaskFrom4([...]byte{0xFF, 0x00, 0xFF, 0x00}), expected: []byte{0xFF, 0x00, 0xFF, 0x00}},
{name: "ipv6 mask", mask: MaskFrom(128, 128), expected: []byte{128}},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}

func TestNetmask_MarshalBinary(t *testing.T) {
type testCase struct {
name string
Expand Down Expand Up @@ -592,6 +621,32 @@ func TestNetmask_BinaryMarshaller(t *testing.T) {
})
}

func TestNetmask_BinaryEncodingEquivalence(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
mask := rapid.Custom(func(t *rapid.T) Mask {
z := rapid.SampledFrom([]int8{z0, z4, z6}).Draw(t, "z")

switch z {
case z4:
return MaskFrom(rapid.IntRange(0, 32).Draw(t, "ones"), 32)
case z6:
return MaskFrom(rapid.IntRange(0, 128).Draw(t, "ones"), 128)
default:
return Mask{}
}
}).Draw(t, "mask")

p, err := mask.MarshalBinary()
assert.NilError(t, err)

b := make([]byte, 4, 32)
out, err := mask.AppendBinary(b)
assert.NilError(t, err)

assert.DeepEqual(t, p, out[4:])
})
}

func TestNetmask_MarshalText(t *testing.T) {
type testCase struct {
name string
Expand Down Expand Up @@ -674,6 +729,32 @@ func TestNetmask_TextMarshaller(t *testing.T) {
})
}

func TestNetmask_TextEncodingEquivalence(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
mask := rapid.Custom(func(t *rapid.T) Mask {
z := rapid.SampledFrom([]int8{z0, z4, z6}).Draw(t, "z")

switch z {
case z4:
return MaskFrom(rapid.IntRange(0, 32).Draw(t, "ones"), 32)
case z6:
return MaskFrom(rapid.IntRange(0, 128).Draw(t, "ones"), 128)
default:
return Mask{}
}
}).Draw(t, "mask")

p, err := mask.MarshalText()
assert.NilError(t, err)

b := make([]byte, 4, 32)
out, err := mask.AppendText(b)
assert.NilError(t, err)

assert.DeepEqual(t, p, out[4:])
})
}

func TestNetmask_String(t *testing.T) {
type testCase struct {
name string
Expand Down

0 comments on commit a608f45

Please sign in to comment.