diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c033004 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: test + +on: + - push + - pull_request + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Go Setup + uses: actions/setup-go@v4 + with: + go-version: "1.20" + + - name: Run Tests + run: go test -v ./... -coverprofile cover.out + + - name: Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb5f160 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env* +cover.* +addr diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29d1118 --- /dev/null +++ b/LICENSE @@ -0,0 +1,32 @@ +The Clear BSD License + +Copyright (c) 2023 Matthew D Love +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..be59c30 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +
+

+ go-asn +

+
+Autonomous System Number Utility for Go +
+
+ + GitHub Workflow Status + + + Codecov + + + GitHub release (latest SemVer) + + +
+ +## Installation + +```console +go get github.com/thatmattlove/go-asn +``` + +## Usage + +### Parsing + +```go +a, err := asn.Parse("65000") +a, err := asn.FromDecimal("65000") +a, err := asn.FromASDot("64086.59904") +a, err := asn.FromUint64(65001) +a, err := asn.FromBytes(255, 255, 253, 232) +a := asn.From4Bytes(255, 255, 253, 232) +a := asn.From2Bytes(253, 232) +a := asn.FromUint32(65001) +a := asn.MustParse("65000") +a := asn.MustDecimal("65000") +a := asn.MustASDot("0.65000") +``` + +### Formatting + +```go +a := asn.MustParse("65000") +a.Size() +// 2 +a.ASPlain() +// 65000 +a.ASDot() +// 65000 +a.ASDotPlus() +// 0.65000 +a.String() +// 65000 +a.ByteString() +// {0,0,253,232} + +a = asn.MustParse("4200000000") +a.Size() +// 4 +a.ASPlain() +// 4200000000 +a.ASDot() +// 64086.59904 +a.ASDotPlus() +// 64086.59904 +a.String() +// 4200000000 +a.ByteString() +// {250,86,234,0} +``` + +### Comparison + +```go +a := asn.MustParse("65000") +b := asn.MustParse("65001") +c := asn.MustParse("65002") +d := asn.MustParse("65000") +e := asn.MustParse("64512") +a.Equal(b) +// false +a.Equal(d) +// true +a.LessThan(b) +// true +a.LEqual(c) +// true +a.GreaterThan(e) +// true +a.GEqual(e) +// true +``` + +### Iteration + +```go +start := asn.MustParse("65000") +end := asn.MustParse("65005") + +for iter := start.Range(end); iter.Continue(); { + next := iter.Next() + fmt.Println(next.ASPlain()) +} +// 65001 +// 65002 +// 65003 +// 65004 +// 65505 + +a := asn.MustParse("65000") +for iter := a.Iter(); iter.Continue(); { + next := iter.Next() + fmt.Println(next.ASPlain()) +} +// 65001 +// 65002 +// ... +// 4294967294 +``` + +![GitHub](https://img.shields.io/github/license/thatmattlove/go-asn?style=for-the-badge&color=black) \ No newline at end of file diff --git a/asn.go b/asn.go new file mode 100644 index 0000000..a4b0fd6 --- /dev/null +++ b/asn.go @@ -0,0 +1,60 @@ +package asn + +// ASN represents a single autonomous system number, a slice of bytes. Both 2-byte (16-bit) and +// 4-byte (32-bit) ASNs are supported. +type ASN []byte + +// Size returns either 2 or 4, depending on if the ASN is 2-bytes or 4-bytes. +func (asn ASN) Size() int { + if asn[0] == 0 && asn[1] == 0 { + return 2 + } + return 4 +} + +// Next returns the next ASN after the current ASN. For example, if the current ASN is 65000, +// Next() would return 65001. +func (asn ASN) Next() ASN { + next := make(ASN, BYTE_SIZE) + copy(next, asn) + for i := len(next) - 1; i >= 0; i-- { + if next[i] < 255 { + next[i]++ + break + } else { + next[i]-- + } + } + return next +} + +// Previous returns the previous ASN before the current ASN. For example, if the current ASN is 65001, +// Previous() would return 65000. +func (asn ASN) Previous() ASN { + prev := asn + for i := len(prev) - 1; i >= 0; i-- { + if prev[i] > 0 { + prev[i]-- + break + } else { + prev[i] = 255 + } + } + return prev +} + +// IsGlobal returns true if the ASN is global, i.e. not private. +// See RFC6996. +func (asn ASN) IsGlobal() bool { + return !asn.IsPrivate() +} + +// IsGlobal returns true if the ASN is private, i.e. not global. +// See RFC6996. +func (asn ASN) IsPrivate() bool { + n := asn.Uint32() + if asn.Size() == 2 { + return n >= 64512 && n <= 65534 + } + return n >= 4200000000 && n <= 4294967294 +} diff --git a/asn_test.go b/asn_test.go new file mode 100644 index 0000000..6464103 --- /dev/null +++ b/asn_test.go @@ -0,0 +1,112 @@ +package asn_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn" +) + +func TestASN_Uint32(t *testing.T) { + a := asn.ASN{0, 0, 253, 232} + n := a.Uint32() + assert.Equal(t, uint32(65000), n) +} + +func TestASN_Size(t *testing.T) { + type caseT struct { + size int + asn asn.ASN + } + cases := []caseT{ + {2, asn.ASN{0, 0, 255, 255}}, + {4, asn.ASN{255, 255, 255, 255}}, + } + for _, c := range cases { + c := c + t.Run(fmt.Sprint(c.size), func(t *testing.T) { + t.Parallel() + assert.Equal(t, c.size, c.asn.Size()) + }) + } +} + +func TestASN_Next(t *testing.T) { + t.Run("basic", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + expected := asn.ASN{0, 0, 0x38, 0xbe} + next := a.Next() + assert.True(t, next.Equal(expected), "next=%s, expected=%s", next.ByteString(), expected.ByteString()) + }) + t.Run("basic2", func(t *testing.T) { + t.Parallel() + a := asn.ASN{255, 255, 255, 254} + e := asn.ASN{255, 255, 255, 255} + next := a.Next() + assert.True(t, next.Equal(e), "next=%s, expected=%s", next.ByteString(), e.ByteString()) + }) +} + +func TestASN_Previous(t *testing.T) { + t.Run("basic", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + expected := asn.ASN{0, 0, 0x38, 0xbc} + prev := a.Previous() + assert.True(t, prev.Equal(expected), "prev=%s, expected=%s", prev.ByteString(), expected.ByteString()) + }) + t.Run("basic2", func(t *testing.T) { + t.Parallel() + a := asn.ASN{255, 255, 255, 0} + e := asn.ASN{255, 255, 254, 255} + next := a.Previous() + assert.True(t, next.Equal(e), "next=%s, expected=%s", next.ByteString(), e.ByteString()) + }) +} + +type privateCasesT struct { + asn string + want bool +} + +var privateCases = []privateCasesT{ + {"65000", true}, + {"65534", true}, + {"64512", true}, + {"64600", true}, + {"14525", false}, + {"13335", false}, + {"4200000000", true}, + {"4200000005", true}, + {"4200090000", true}, + {"4294967294", true}, + {"4294967293", true}, + {"4204967293", true}, + {"4194967294", false}, + {"395077", false}, + {"4199999999", false}, +} + +func TestASN_IsPrivate(t *testing.T) { + for _, c := range privateCases { + c := c + t.Run(c.asn, func(t *testing.T) { + t.Parallel() + a := asn.MustParse(c.asn) + assert.Equal(t, c.want, a.IsPrivate()) + }) + } +} + +func TestASN_IsGlobal(t *testing.T) { + for _, c := range privateCases { + c := c + t.Run(c.asn, func(t *testing.T) { + t.Parallel() + a := asn.MustParse(c.asn) + assert.Equal(t, !c.want, a.IsGlobal()) + }) + } +} diff --git a/comparison.go b/comparison.go new file mode 100644 index 0000000..4556178 --- /dev/null +++ b/comparison.go @@ -0,0 +1,34 @@ +package asn + +// Equal determines if this ASN is equal to the input ASN. +func (asn ASN) Equal(other ASN) bool { + if len(asn) != len(other) { + return false + } + for i := range asn { + if asn[i] != other[i] { + return false + } + } + return true +} + +// GreaterThan determines if this ASN is greater than (higher number) the input ASN. +func (asn ASN) GreaterThan(other ASN) bool { + return asn.Uint32() > other.Uint32() +} + +// LessThan determines if this ASN is less than (lower number) the input ASN. +func (asn ASN) LessThan(other ASN) bool { + return asn.Uint32() < other.Uint32() +} + +// GEqual determines if this ASN is greater than (higher number) or equal to the input ASN. +func (asn ASN) GEqual(other ASN) bool { + return asn.GreaterThan(other) || asn.Equal(other) +} + +// LEqual determines if this ASN is less than (lower number) or equal to the input ASN. +func (asn ASN) LEqual(other ASN) bool { + return asn.LessThan(other) || asn.Equal(other) +} diff --git a/comparison_test.go b/comparison_test.go new file mode 100644 index 0000000..19e3c1d --- /dev/null +++ b/comparison_test.go @@ -0,0 +1,119 @@ +package asn_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn" +) + +func TestASN_Equal(t *testing.T) { + t.Run("basic", func(t *testing.T) { + t.Parallel() + one := asn.ASN{0, 0, 0x38, 0xbd} + two := asn.ASN{0, 0, 0x38, 0xbd} + assert.True(t, one.Equal(two)) + }) + t.Run("not equal", func(t *testing.T) { + t.Parallel() + one := asn.ASN{0, 0, 0x38, 0xbd} + two := asn.ASN{1, 2, 3, 4} + assert.False(t, one.Equal(two)) + }) + t.Run("mismatching lengths", func(t *testing.T) { + one := asn.ASN{1, 2, 3, 4} + two := asn.ASN{1, 2} + assert.False(t, one.Equal(two)) + }) + t.Run("not equal with next", func(t *testing.T) { + t.Parallel() + one := asn.ASN{255, 255, 255, 255} + next := one.Next() + expected := asn.ASN{254, 255, 255, 255} + assert.False(t, next.Equal(expected)) + }) + t.Run("not equal with previous", func(t *testing.T) { + t.Parallel() + one := asn.ASN{255, 255, 255, 255} + prev := one.Previous() + expected := asn.ASN{254, 255, 255, 255} + assert.False(t, prev.Equal(expected)) + }) +} +func TestASN_GreaterThan(t *testing.T) { + t.Run("greater", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbc} + assert.True(t, a.GreaterThan(o)) + }) + + t.Run("less", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbe} + assert.False(t, a.GreaterThan(o)) + }) +} + +func TestASN_LessThan(t *testing.T) { + t.Run("less", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbe} + assert.True(t, a.LessThan(o)) + }) + + t.Run("greater", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbc} + assert.False(t, a.LessThan(o)) + }) +} + +func TestASN_GEqual(t *testing.T) { + t.Run("less", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbc} + assert.True(t, a.GEqual(o)) + }) + + t.Run("greater", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbe} + assert.False(t, a.GEqual(o)) + }) + + t.Run("equal", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbd} + assert.True(t, a.GEqual(o)) + }) +} + +func TestASN_LEqual(t *testing.T) { + t.Run("less", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbe} + assert.True(t, a.LEqual(o)) + }) + + t.Run("greater", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbc} + assert.False(t, a.LEqual(o)) + }) + + t.Run("equal", func(t *testing.T) { + t.Parallel() + a := asn.ASN{0, 0, 0x38, 0xbd} + o := asn.ASN{0, 0, 0x38, 0xbd} + assert.True(t, a.LEqual(o)) + }) +} diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..c05e818 --- /dev/null +++ b/constants.go @@ -0,0 +1,15 @@ +package asn + +import "regexp" + +const ( + // BYTE_SIZE is the size of all ASN objects in bytes. + BYTE_SIZE int = 4 + // MAX_32 is the maximum value allowed for a 32-bit (4-byte) ASN. + MAX_32 uint32 = 4294967295 +) + +var ( + asdotPattern = regexp.MustCompile(`^\d+\.\d+$`) + asDecimalPattern = regexp.MustCompile(`^\d+$`) +) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..1b55480 --- /dev/null +++ b/errors.go @@ -0,0 +1,14 @@ +package asn + +import "errors" + +// ErrInvalidInput is returned when parsing string input fails. +var ErrInvalidInput = errors.New("invalid input") + +// ErrOutOf2ByteRange is returned when parsing string input fails because a required 16 bit value +// exceeds the allowable range. +var ErrOutOf2ByteRange = errors.New("value out of range (0-65535)") + +// ErrOutOf2ByteRange is returned when parsing string input fails because a required 32 bit value +// exceeds the allowable range. +var ErrOutOf4ByteRange = errors.New("value out of range (0-4294967295)") diff --git a/format.go b/format.go new file mode 100644 index 0000000..f72390d --- /dev/null +++ b/format.go @@ -0,0 +1,54 @@ +package asn + +import ( + "encoding/binary" + "fmt" + "strings" + + "github.com/thatmattlove/go-asn/internal/util" +) + +// Decimal formats the ASN in asplain format. See RFC5396. +func (asn ASN) Decimal() string { + d := binary.BigEndian.Uint32(asn) + return fmt.Sprint(d) +} + +// ASPlain formats the ASN in asplain format. See RFC5396. +func (asn ASN) ASPlain() string { + return asn.Decimal() +} + +// ASDotPlus formats the ASN in asdot+ format. See RFC5396. +func (asn ASN) ASDotPlus() string { + high := binary.BigEndian.Uint32(util.ZeroPad(asn[0:2], BYTE_SIZE)) + low := binary.BigEndian.Uint32(util.ZeroPad(asn[2:4], BYTE_SIZE)) + return fmt.Sprintf("%d.%d", high, low) +} + +// ASDot formats the ASN in asdot format. See RFC5396. +func (asn ASN) ASDot() string { + if asn.Size() == 2 { + return asn.Decimal() + } + return asn.ASDotPlus() +} + +// String formats the ASN in decimal/asplain format. See RFC5396. +func (asn ASN) String() string { + return asn.Decimal() +} + +// Uint32 returns the ASN as a 32-bit unsigned integer. +func (asn ASN) Uint32() uint32 { + return binary.BigEndian.Uint32(asn) +} + +// ByteString returns a string representation of each ASN byte. +func (asn ASN) ByteString() string { + bs := []string{} + for _, b := range asn { + bs = append(bs, fmt.Sprint(b)) + } + return fmt.Sprintf("{%s}", strings.Join(bs, ",")) +} diff --git a/format_test.go b/format_test.go new file mode 100644 index 0000000..e98d488 --- /dev/null +++ b/format_test.go @@ -0,0 +1,80 @@ +package asn_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn" +) + +type decimalFmtT struct { + from asn.ASN + want string +} + +var decimalCases = []decimalFmtT{ + {asn.ASN{0, 0, 0, 1}, "1"}, + {asn.ASN{0, 0, 56, 189}, "14525"}, +} + +func TestASN_Decimal(t *testing.T) { + for _, c := range decimalCases { + c := c + t.Run(c.want, func(t *testing.T) { + t.Parallel() + assert.Equal(t, c.want, c.from.Decimal()) + }) + } +} + +func TestASN_ASPlain(t *testing.T) { + for _, c := range decimalCases { + c := c + t.Run(c.want, func(t *testing.T) { + t.Parallel() + assert.Equal(t, c.want, c.from.ASPlain()) + }) + } +} + +func TestASN_String(t *testing.T) { + for _, c := range decimalCases { + c := c + t.Run(c.want, func(t *testing.T) { + t.Parallel() + assert.Equal(t, c.want, c.from.String()) + }) + } +} + +func TestASN_ASDot(t *testing.T) { + t.Run("4B", func(t *testing.T) { + t.Parallel() + a := asn.MustDecimal("4200000000") + assert.Equal(t, "64086.59904", a.ASDot()) + }) + t.Run("2B", func(t *testing.T) { + t.Parallel() + a := asn.MustDecimal("65000") + assert.Equal(t, "65000", a.ASDot()) + }) +} + +func TestASN_ASDotPlus(t *testing.T) { + t.Run("2B", func(t *testing.T) { + t.Parallel() + a := asn.MustParse("65000") + assert.Equal(t, "0.65000", a.ASDotPlus()) + }) + t.Run("4B", func(t *testing.T) { + t.Parallel() + a := asn.MustParse("4200000000") + assert.Equal(t, "64086.59904", a.ASDotPlus()) + }) +} + +func TestASN_ByteString(t *testing.T) { + asn := asn.MustParse("65000") + expected := "{0,0,253,232}" + assert.Equal(t, expected, asn.ByteString()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6ca5f7a --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/thatmattlove/go-asn + +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/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..7e64d57 --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,15 @@ +package util + +// ZeroPad takes a byte slice and ensures it is the specified size by zero-padding leading slots. +func ZeroPad(b []byte, size int) []byte { + l := len(b) + if l == size { + return b + } + if l > size { + return b[l-size:] + } + tmp := make([]byte, size) + copy(tmp[size-l:], b) + return tmp +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000..45f0195 --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,33 @@ +package util_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn/internal/util" +) + +func Test_ZeroPad(t *testing.T) { + t.Run("2-4", func(t *testing.T) { + t.Parallel() + in := []byte{1, 2} + expected := []byte{0, 0, 1, 2} + result := util.ZeroPad(in, len(expected)) + assert.Equal(t, expected, result) + }) + t.Run("4-4", func(t *testing.T) { + t.Parallel() + in := []byte{1, 2, 3, 4} + expected := in + result := util.ZeroPad(in, len(expected)) + assert.Equal(t, expected, result) + }) + + t.Run("4-2", func(t *testing.T) { + t.Parallel() + in := []byte{1, 2, 3, 4} + expected := []byte{3, 4} + result := util.ZeroPad(in, 2) + assert.Equal(t, expected, result) + }) +} diff --git a/iteration.go b/iteration.go new file mode 100644 index 0000000..a7e0992 --- /dev/null +++ b/iteration.go @@ -0,0 +1,54 @@ +package asn + +type iterRange struct { + start ASN + stop ASN + value ASN +} + +type iter struct { + start ASN + value ASN +} + +// Continue returns true if the current iteration value is less than the high end of the range. +func (i *iterRange) Continue() bool { + return i.value.LessThan(i.stop) +} + +// Next returns the next ASN in the range. +func (i *iterRange) Next() ASN { + i.value = i.value.Next() + return i.value +} + +// Range returns an iterator object that can be used to iterate through a range of ASNs starting +// with the current object and ending with the input ASN. +func (asn ASN) Range(high ASN) iterRange { + return iterRange{ + start: asn, + stop: high, + value: asn, + } +} + +// Continue returns true if the current iteration value is less than the highest possible ASN. +func (i *iter) Continue() bool { + return i.value.LessThan(ASN{255, 255, 255, 255}) +} + +// Next returns the next ASN. +func (i *iter) Next() ASN { + next := i.value.Next() + i.value = next + return next +} + +// Iter returns an iterator object that can be used to iterate through ASNs, starting from the +// current ASN and ending with the highest possible ASN. +func (asn ASN) Iter() iter { + return iter{ + start: asn, + value: asn, + } +} diff --git a/iteration_test.go b/iteration_test.go new file mode 100644 index 0000000..1add020 --- /dev/null +++ b/iteration_test.go @@ -0,0 +1,43 @@ +package asn_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn" +) + +func TestASN_Range(t *testing.T) { + start := 1 + first := asn.ASN{0, 0, 1, byte(start)} + last := asn.ASN{0, 0, 1, 5} + var final asn.ASN + for iter := first.Range(last); iter.Continue(); { + next := iter.Next() + start++ + expected := asn.ASN{0, 0, 1, byte(start)} + final = expected + if !next.Equal(expected) { + break + } + } + assert.True(t, final.Equal(last), final.ByteString()) +} + +func TestASN_Iter(t *testing.T) { + start := 253 + first := asn.ASN{255, 255, 255, byte(start)} + last := asn.ASN{255, 255, 255, 255} + var final asn.ASN + for iter := first.Iter(); iter.Continue(); { + next := iter.Next() + start++ + expected := asn.ASN{255, 255, 255, byte(start)} + final = expected + if !next.Equal(expected) { + t.Logf("first=%s, next=%s, exp=%s", first.ByteString(), next.ByteString(), expected.ByteString()) + break + } + } + assert.True(t, final.Equal(last), "final=%s, last=%s", final.ByteString(), last.ByteString()) +} diff --git a/parsers.go b/parsers.go new file mode 100644 index 0000000..77091b0 --- /dev/null +++ b/parsers.go @@ -0,0 +1,134 @@ +package asn + +import ( + "encoding/binary" + "errors" + "strconv" + "strings" +) + +// MustDecimal parses an ASN in decimal/asplain format and panics if the input is invalid. +func MustDecimal(d string) ASN { + a, err := FromDecimal(d) + if err != nil { + panic(err) + } + return a +} + +// MustASDot parses an ASN in asdot format and panics if the input is invalid. +func MustASDot(d string) ASN { + a, err := FromASDot(d) + if err != nil { + panic(err) + } + return a +} + +// MustParse parses an ASN in any valid format and panics if the input is invalid. +func MustParse(d string) ASN { + a, err := Parse(d) + if err != nil { + panic(err) + } + return a +} + +// FromDecimal parses an ASN in decimal/asplain format and returns an error if the input is invalid. +func FromDecimal(d string) (ASN, error) { + n64, err := strconv.ParseUint(d, 10, 32) + if err != nil { + if errors.Is(err, strconv.ErrRange) { + return nil, ErrOutOf4ByteRange + } + return nil, ErrInvalidInput + } + n := uint32(n64) + a := make(ASN, BYTE_SIZE) + binary.BigEndian.PutUint32(a, n) + return a, nil +} + +// FromASDot parses an ASN in asdot format and returns an error if the input is invalid. +func FromASDot(i string) (ASN, error) { + parts := strings.Split(strings.TrimSpace(i), ".") + + if len(parts) != 2 { + return nil, ErrInvalidInput + } + + asn := make(ASN, 4) + + high, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, ErrInvalidInput + } + + low, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, ErrInvalidInput + } + + if high < 0 || high > 65535 || low < 0 || low > 65535 { + return nil, ErrOutOf2ByteRange + } + + asn[0] = byte(high >> 8) + asn[1] = byte(high) + asn[2] = byte(low >> 8) + asn[3] = byte(low) + + return asn, nil +} + +// FromUint32 parses an ASN from an unsigned 32-bit integer. +func FromUint32(n uint32) ASN { + a := make(ASN, BYTE_SIZE) + binary.BigEndian.PutUint32(a, n) + return a +} + +// FromUint64 parses an ASN from an unsigned 64-bit integer and returns an error if the value is +// greater than 32 bits. +func FromUint64(n uint64) (ASN, error) { + if n > uint64(MAX_32) { + return nil, ErrOutOf4ByteRange + } + a := make(ASN, BYTE_SIZE) + binary.BigEndian.PutUint32(a, uint32(n)) + return a, nil +} + +// From4Bytes creates an ASN object from 4 bytes. +func From4Bytes(one, two, three, four byte) ASN { + return ASN{one, two, three, four} +} + +// From2Bytes creates an ASN object from 2 bytes. +func From2Bytes(one, two byte) ASN { + return ASN{0, 0, one, two} +} + +// FromBytes creates an ASN object from either 2 or 4 bytes. An error is returned if the number of +// bytes provided is not 2 or 4. +func FromBytes(bytes ...byte) (ASN, error) { + if len(bytes) != 2 && len(bytes) != 4 { + return nil, ErrOutOf4ByteRange + } + if len(bytes) == 2 { + return From2Bytes(bytes[0], bytes[1]), nil + } + return From4Bytes(bytes[0], bytes[1], bytes[2], bytes[3]), nil +} + +// Parse parses and validates an ASN from an input string. An error is returned if the input is +// invalid. +func Parse(in string) (ASN, error) { + if asdotPattern.MatchString(in) { + return FromASDot(in) + } + if asDecimalPattern.MatchString(in) { + return FromDecimal(in) + } + return nil, ErrInvalidInput +} diff --git a/parsers_test.go b/parsers_test.go new file mode 100644 index 0000000..e5de0ca --- /dev/null +++ b/parsers_test.go @@ -0,0 +1,201 @@ +package asn_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thatmattlove/go-asn" +) + +func Test_MustDecimal(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + asn.MustDecimal("65000") + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + asn.MustDecimal("this will panic") + }) + }) +} + +func Test_MustASDot(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + asn.MustASDot("0.65000") + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + asn.MustASDot("this will panic") + }) + }) +} + +func Test_MustParse(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Parallel() + assert.NotPanics(t, func() { + asn.MustParse("4200000000") + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Parallel() + assert.Panics(t, func() { + asn.MustParse("this will panic") + }) + }) +} + +func Test_FromDecimal(t *testing.T) { + type caseT struct { + from string + want string + } + cases := []caseT{ + {"65000", "65000"}, + {"65546", "65546"}, + } + for _, c := range cases { + c := c + t.Run(c.want, func(t *testing.T) { + t.Parallel() + a, err := asn.FromDecimal(c.from) + assert.NoError(t, err) + assert.Equal(t, c.want, a.Decimal()) + }) + } + t.Run("error with asdot", func(t *testing.T) { + t.Parallel() + _, err := asn.FromDecimal("6.1861") + assert.ErrorIs(t, err, asn.ErrInvalidInput) + }) + t.Run("error with number out of range", func(t *testing.T) { + t.Parallel() + _, err := asn.FromDecimal("5294967295") + assert.ErrorIs(t, err, asn.ErrOutOf4ByteRange) + }) +} + +func Test_FromASDot(t *testing.T) { + t.Run("basic", func(t *testing.T) { + asd := "6.1861" + a, err := asn.FromASDot(asd) + assert.NoError(t, err) + assert.Equal(t, asd, a.ASDot()) + }) + + type errorCasesT struct { + from string + want error + } + + errorCases := []errorCasesT{ + {"65000", asn.ErrInvalidInput}, + {"65536.0", asn.ErrOutOf2ByteRange}, + {"0.65536", asn.ErrOutOf2ByteRange}, + {"thiswillfail", asn.ErrInvalidInput}, + {"0.thiswillfail", asn.ErrInvalidInput}, + {"thiswillfail.0", asn.ErrInvalidInput}, + } + + for _, c := range errorCases { + c := c + t.Run(c.from, func(t *testing.T) { + t.Parallel() + _, err := asn.FromASDot(c.from) + assert.ErrorIs(t, err, c.want) + }) + } +} + +func Test_Parse(t *testing.T) { + type caseT struct { + from string + want string + } + casesDecimal := []caseT{ + {"6.1861", "395077"}, + {"395077", "395077"}, + {"65000", "65000"}, + } + for _, c := range casesDecimal { + c := c + t.Run(c.want, func(t *testing.T) { + t.Parallel() + a, err := asn.Parse(c.from) + assert.NoError(t, err) + assert.Equal(t, c.want, a.Decimal()) + }) + } + t.Run("errors", func(t *testing.T) { + t.Parallel() + _, err := asn.Parse("default error") + assert.ErrorIs(t, err, asn.ErrInvalidInput) + }) +} + +func Test_FromUint32(t *testing.T) { + e := asn.ASN{0, 0, 253, 232} + a := asn.FromUint32(65000) + assert.True(t, a.Equal(e), "parsed=%s", a.ByteString()) +} + +func Test_FromUint64(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Parallel() + e := asn.ASN{0, 0, 253, 232} + a, err := asn.FromUint64(65000) + assert.NoError(t, err) + assert.True(t, a.Equal(e), "parsed=%s", a.ByteString()) + }) + t.Run("invalid", func(t *testing.T) { + t.Parallel() + _, err := asn.FromUint64(5294967295) + assert.ErrorIs(t, err, asn.ErrOutOf4ByteRange) + }) +} + +func Test_From2Bytes(t *testing.T) { + expected := asn.ASN{0, 0, 253, 232} + result := asn.From2Bytes(253, 232) + assert.True(t, result.Equal(expected)) +} + +func Test_From4Bytes(t *testing.T) { + expected := asn.ASN{255, 255, 253, 232} + result := asn.From4Bytes(255, 255, 253, 232) + assert.True(t, result.Equal(expected)) +} + +func Test_FromBytes(t *testing.T) { + t.Run("2B", func(t *testing.T) { + t.Parallel() + expected := asn.ASN{0, 0, 253, 232} + result, err := asn.FromBytes(253, 232) + assert.NoError(t, err) + assert.True(t, result.Equal(expected)) + }) + t.Run("4B", func(t *testing.T) { + t.Parallel() + expected := asn.ASN{255, 255, 253, 232} + result, err := asn.FromBytes(255, 255, 253, 232) + assert.NoError(t, err) + assert.True(t, result.Equal(expected)) + }) + t.Run("too many bytes", func(t *testing.T) { + t.Parallel() + _, err := asn.FromBytes(255, 255, 253, 232, 255, 252) + assert.ErrorIs(t, err, asn.ErrOutOf4ByteRange) + }) + t.Run("too few bytes", func(t *testing.T) { + t.Parallel() + _, err := asn.FromBytes(255) + assert.ErrorIs(t, err, asn.ErrOutOf4ByteRange) + }) +}