Skip to content

Commit 54fd284

Browse files
peanballramonskie
authored andcommitted
feat: move IP and CIDRBlock to standard types
Uses net/netip.Addr and .Prefix (CIDR) to ensure the same computation. Error messages are now different, so tests were adapted there. All other tests remain.
1 parent 9bef4bd commit 54fd284

File tree

5 files changed

+150
-112
lines changed

5 files changed

+150
-112
lines changed

bosh/cidr_block.go

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,47 @@
11
package bosh
22

33
import (
4-
"fmt"
5-
"strconv"
6-
"strings"
4+
"encoding/binary"
5+
"math"
6+
"net/netip"
77
)
88

99
type CIDRBlock struct {
10-
CIDRSize int
11-
firstIP IP
10+
cidr netip.Prefix
1211
}
1312

1413
func ParseCIDRBlock(cidrBlock string) (CIDRBlock, error) {
15-
const HIGHEST_BITMASK = 32
16-
const CIDR_PARTS = 2
17-
18-
cidrParts := strings.Split(cidrBlock, "/")
19-
20-
if len(cidrParts) != CIDR_PARTS {
21-
return CIDRBlock{}, fmt.Errorf(`"%s" cannot parse CIDR block`, cidrBlock)
22-
}
23-
24-
ip, err := ParseIP(cidrParts[0])
25-
if err != nil {
26-
return CIDRBlock{}, err
27-
}
28-
29-
maskBits, err := strconv.Atoi(cidrParts[1])
14+
prefix, err := netip.ParsePrefix(cidrBlock)
3015
if err != nil {
3116
return CIDRBlock{}, err
3217
}
33-
34-
if maskBits < 0 || maskBits > HIGHEST_BITMASK {
35-
return CIDRBlock{}, fmt.Errorf("mask bits out of range")
36-
}
37-
38-
cidrSize := 1 << (HIGHEST_BITMASK - uint(maskBits))
39-
return CIDRBlock{
40-
CIDRSize: cidrSize,
41-
firstIP: ip,
42-
}, nil
18+
return CIDRBlock{cidr: prefix}, nil
4319
}
4420

4521
func (c CIDRBlock) GetFirstIP() IP {
4622
return c.GetNthIP(0)
4723
}
4824

4925
func (c CIDRBlock) GetNthIP(n int) IP {
50-
return c.firstIP.Add(n)
26+
ip := IP{c.cidr.Addr()}
27+
28+
if n > 0 {
29+
return ip.Add(n)
30+
}
31+
return ip.Subtract(n)
32+
5133
}
5234

5335
func (c CIDRBlock) GetLastIP() IP {
54-
return c.GetNthIP(c.CIDRSize - 1)
36+
a := c.cidr.Addr()
37+
if a.Is4() {
38+
four := a.As4()
39+
uint32Four := binary.BigEndian.Uint32(four[:])
40+
masklen := c.cidr.Addr().BitLen() - c.cidr.Bits()
41+
mask := uint32(math.Pow(2, float64(masklen))) - 1
42+
uint32Four += mask
43+
binary.BigEndian.PutUint32(four[:], uint32Four)
44+
return IP{netip.AddrFrom4(four)}
45+
}
46+
panic("not implemented")
5547
}

bosh/cidr_block_test.go

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,61 +11,62 @@ var _ = Describe("CIDRBlock", func() {
1111
var (
1212
cidrBlock bosh.CIDRBlock
1313
)
14+
Context("v4", func() {
15+
BeforeEach(func() {
16+
var err error
17+
cidrBlock, err = bosh.ParseCIDRBlock("10.0.16.0/20")
18+
Expect(err).NotTo(HaveOccurred())
19+
})
1420

15-
BeforeEach(func() {
16-
var err error
17-
cidrBlock, err = bosh.ParseCIDRBlock("10.0.16.0/20")
18-
Expect(err).NotTo(HaveOccurred())
19-
})
20-
21-
Describe("GetFirstIP", func() {
22-
It("returns the first ip of the cidr block", func() {
23-
ip := cidrBlock.GetFirstIP()
24-
Expect(ip.String()).To(Equal("10.0.16.0"))
21+
Describe("GetFirstIP", func() {
22+
It("returns the first ip of the cidr block", func() {
23+
ip := cidrBlock.GetFirstIP()
24+
Expect(ip.String()).To(Equal("10.0.16.0"))
25+
})
2526
})
26-
})
2727

28-
Describe("GetNthIP", func() {
29-
It("returns the nth ip of the cidr block", func() {
30-
ip := cidrBlock.GetNthIP(6)
31-
Expect(ip.String()).To(Equal("10.0.16.6"))
28+
Describe("GetNthIP", func() {
29+
It("returns the nth ip of the cidr block", func() {
30+
ip := cidrBlock.GetNthIP(6)
31+
Expect(ip.String()).To(Equal("10.0.16.6"))
32+
})
3233
})
33-
})
3434

35-
Describe("GetLastIP", func() {
36-
It("returns the first ip of the cidr block", func() {
37-
ip := cidrBlock.GetLastIP()
38-
Expect(ip.String()).To(Equal("10.0.31.255"))
35+
Describe("GetLastIP", func() {
36+
It("returns the first ip of the cidr block", func() {
37+
ip := cidrBlock.GetLastIP()
38+
Expect(ip.String()).To(Equal("10.0.31.255"))
39+
})
3940
})
40-
})
4141

42-
Describe("ParseCIDRBlock", func() {
43-
Context("failure cases", func() {
44-
Context("when input string is not a valid CIDR block", func() {
45-
It("returns an error", func() {
46-
_, err := bosh.ParseCIDRBlock("whatever")
47-
Expect(err).To(MatchError(ContainSubstring("cannot parse CIDR block")))
42+
Describe("ParseCIDRBlock", func() {
43+
Context("failure cases", func() {
44+
Context("when input string is not a valid CIDR block", func() {
45+
It("returns an error", func() {
46+
_, err := bosh.ParseCIDRBlock("whatever")
47+
Expect(err).To(MatchError(ContainSubstring("no '/'")))
48+
})
4849
})
49-
})
5050

51-
Context("when input string contains an invalid ip", func() {
52-
It("returns an error", func() {
53-
_, err := bosh.ParseCIDRBlock("not-an-ip/20")
54-
Expect(err).To(MatchError(ContainSubstring("not a valid ip address")))
51+
Context("when input string contains an invalid ip", func() {
52+
It("returns an error", func() {
53+
_, err := bosh.ParseCIDRBlock("not-an-ip/20")
54+
Expect(err).To(MatchError(ContainSubstring("unable to parse IP")))
55+
})
5556
})
56-
})
5757

58-
Context("when input string contains mask bits which are not an integer", func() {
59-
It("returns an error", func() {
60-
_, err := bosh.ParseCIDRBlock("0.0.0.0/not-mask-bits")
61-
Expect(err).To(MatchError(ContainSubstring("invalid syntax")))
58+
Context("when input string contains mask bits which are not an integer", func() {
59+
It("returns an error", func() {
60+
_, err := bosh.ParseCIDRBlock("0.0.0.0/not-mask-bits")
61+
Expect(err).To(MatchError(ContainSubstring(`bad bits after slash: "not-mask-bits"`)))
62+
})
6263
})
63-
})
6464

65-
Context("when input string contains mask bits which are out of range", func() {
66-
It("returns an error", func() {
67-
_, err := bosh.ParseCIDRBlock("0.0.0.0/243")
68-
Expect(err).To(MatchError(ContainSubstring("mask bits out of range")))
65+
Context("when input string contains mask bits which are out of range", func() {
66+
It("returns an error", func() {
67+
_, err := bosh.ParseCIDRBlock("0.0.0.0/243")
68+
Expect(err).To(MatchError(ContainSubstring("prefix length out of range")))
69+
})
6970
})
7071
})
7172
})

bosh/ip.go

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,46 @@
11
package bosh
22

33
import (
4-
"fmt"
5-
"strconv"
6-
"strings"
4+
"net/netip"
75
)
86

97
type IP struct {
10-
ip int64
8+
ip netip.Addr
119
}
1210

1311
func ParseIP(ip string) (IP, error) {
14-
const IP_PARTS = 4
15-
const MAX_IP_PART = int64(256)
16-
17-
ipParts := strings.Split(ip, ".")
18-
19-
if len(ipParts) != IP_PARTS {
20-
return IP{}, fmt.Errorf(`'%s' is not a valid ip address`, ip)
21-
}
22-
23-
ipValue := int64(0)
24-
for _, ipPart := range ipParts {
25-
ipPartInt, err := strconv.ParseInt(ipPart, 10, 0)
26-
if err != nil {
27-
return IP{}, err
28-
}
29-
30-
if ipPartInt < 0 || ipPartInt >= MAX_IP_PART {
31-
return IP{}, fmt.Errorf("invalid ip, %s has values out of range", ip)
32-
}
33-
34-
ipValue = ipValue*MAX_IP_PART + ipPartInt
12+
parsed, err := netip.ParseAddr(ip)
13+
if err != nil {
14+
return IP{}, err
3515
}
3616

3717
return IP{
38-
ip: ipValue,
18+
ip: parsed,
3919
}, nil
4020
}
4121

4222
func (i IP) Add(offset int) IP {
23+
next := i.ip
24+
for range offset {
25+
next = next.Next()
26+
}
27+
4328
return IP{
44-
ip: i.ip + int64(offset),
29+
ip: next,
4530
}
4631
}
4732

4833
func (i IP) Subtract(offset int) IP {
34+
prev := i.ip
35+
for range offset {
36+
prev = prev.Prev()
37+
}
38+
4939
return IP{
50-
ip: i.ip - int64(offset),
40+
ip: prev,
5141
}
5242
}
5343

5444
func (i IP) String() string {
55-
first := i.ip & 0xff000000 >> 24
56-
second := i.ip & 0xff0000 >> 16
57-
third := i.ip & 0xff00 >> 8
58-
fourth := i.ip & 0xff
59-
return fmt.Sprintf("%v.%v.%v.%v", first, second, third, fourth)
45+
return i.ip.String()
6046
}

bosh/ip_test.go

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
)
99

1010
var _ = Describe("IP", func() {
11+
Context("v4", func() {
1112
Describe("ParseIP", func() {
1213
It("returns an IP object that represents IP from string", func() {
1314
ip, err := bosh.ParseIP("10.0.16.255")
@@ -18,22 +19,22 @@ var _ = Describe("IP", func() {
1819
Context("failure cases", func() {
1920
It("returns an error if it cannot parse ip", func() {
2021
_, err := bosh.ParseIP("not valid")
21-
Expect(err).To(MatchError(ContainSubstring("not a valid ip address")))
22+
Expect(err).To(MatchError(ContainSubstring("unable to parse IP")))
2223
})
2324

2425
It("returns an error if ip parts are not digits", func() {
2526
_, err := bosh.ParseIP("x.x.x.x")
26-
Expect(err).To(MatchError(ContainSubstring("invalid syntax")))
27+
Expect(err).To(MatchError(ContainSubstring("unexpected character")))
2728
})
2829

2930
It("returns an error if ip parts are out of the allowed range", func() {
3031
_, err := bosh.ParseIP("999.999.999.999")
31-
Expect(err).To(MatchError(ContainSubstring("values out of range")))
32+
Expect(err).To(MatchError(ContainSubstring("IPv4 field has value >255")))
3233
})
3334

3435
It("returns an error if ip has too many parts", func() {
3536
_, err := bosh.ParseIP("1.1.1.1.1.1.1")
36-
Expect(err).To(MatchError(ContainSubstring("not a valid ip address")))
37+
Expect(err).To(MatchError(ContainSubstring("IPv4 address too long")))
3738
})
3839
})
3940
})
@@ -63,4 +64,62 @@ var _ = Describe("IP", func() {
6364
Expect(ip.String()).To(Equal("10.0.16.1"))
6465
})
6566
})
67+
})
68+
Context("v6", func() {
69+
Describe("ParseIP", func() {
70+
It("returns an IP object that represents IP from string", func() {
71+
ip, err := bosh.ParseIP("2001:db8:cf:0:0:ffff:1337")
72+
Expect(err).NotTo(HaveOccurred())
73+
Expect(ip.String()).To(Equal("2001:db8:cf::ffff:1337"))
74+
})
75+
76+
Context("failure cases", func() {
77+
It("returns an error if it cannot parse ip", func() {
78+
_, err := bosh.ParseIP("2001:db8:cf::not valid")
79+
Expect(err).To(MatchError(ContainSubstring("unable to parse IP")))
80+
})
81+
82+
It("returns an error if ip parts are not digits", func() {
83+
_, err := bosh.ParseIP("2001:db8:cf:x:x:x:x")
84+
Expect(err).To(MatchError(ContainSubstring("unexpected character")))
85+
})
86+
87+
It("returns an error if ip parts are out of the allowed range", func() {
88+
_, err := bosh.ParseIP("2001:db8:cf:G::")
89+
Expect(err).To(MatchError(ContainSubstring("IPv6 field has value >F")))
90+
})
91+
92+
It("returns an error if ip has too many parts", func() {
93+
_, err := bosh.ParseIP("2001:db8:cf:0:0:0:ffff:ffff:ffff")
94+
Expect(err).To(MatchError(ContainSubstring("IPv6 address too long")))
95+
})
96+
})
97+
})
98+
99+
Describe("Add", func() {
100+
It("returns an IP object that represents IP offsetted by 1", func() {
101+
ip, err := bosh.ParseIP("2001:db8:cf::")
102+
ip = ip.Add(1)
103+
Expect(err).NotTo(HaveOccurred())
104+
Expect(ip.String()).To(Equal("2001:db8:cf::1"))
105+
})
106+
})
107+
108+
Describe("Subtract", func() {
109+
It("returns an IP object that represents IP offsetted by -1", func() {
110+
ip, err := bosh.ParseIP("2001:db8:cf::2")
111+
ip = ip.Subtract(1)
112+
Expect(err).NotTo(HaveOccurred())
113+
Expect(ip.String()).To(Equal("2001:db8:cf::1"))
114+
})
115+
})
116+
117+
Describe("String", func() {
118+
It("returns a string representation of IP object", func() {
119+
ip, err := bosh.ParseIP("2001:db8:cf::ffff:ffff:ffff")
120+
Expect(err).NotTo(HaveOccurred())
121+
Expect(ip.String()).To(Equal("2001:db8:cf::ffff:ffff:ffff"))
122+
})
123+
})
124+
})
66125
})

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/cloudfoundry/bosh-bootloader
22

3-
go 1.20
3+
go 1.22
44

55
require (
66
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0

0 commit comments

Comments
 (0)