Skip to content

Commit 69dcfc3

Browse files
authored
fix: networkipavailabilities: handle scientific notation in IP counts
Go's JSON decoder converts large integers to float64 when decoding into interface{}, which causes values ≥ 1e21 to be re-encoded in scientific notation. big.Int cannot parse that notation, crashing any List() call that includes a network with a large IPv6 prefix such as /56 or shorter. Signed-off-by: Bertrand Lanson <bertrand.lanson@protonmail.com>
1 parent 6d9c6d0 commit 69dcfc3

2 files changed

Lines changed: 61 additions & 15 deletions

File tree

openstack/networking/v2/extensions/networkipavailabilities/results.go

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package networkipavailabilities
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"math/big"
67

78
"github.com/gophercloud/gophercloud/v2"
@@ -52,12 +53,35 @@ type NetworkIPAvailability struct {
5253
UsedIPs string `json:"-"`
5354
}
5455

56+
// Go's encoding/json decodes all JSON numbers into float64 when the target is
57+
// interface{}. For large integers (abs >= 1e21), re-encoding that float64
58+
// produces scientific notation (e.g. "1.1805916207174113e+21"), which
59+
// big.Int.UnmarshalJSON cannot parse. This function handles both plain integer
60+
// and scientific notation forms.
61+
func parseBigIntFromNumber(n json.Number) (*big.Int, error) {
62+
s := string(n)
63+
64+
// Fast path: plain integer notation
65+
bi := new(big.Int)
66+
if _, ok := bi.SetString(s, 10); ok {
67+
return bi, nil
68+
}
69+
70+
// Slow path: scientific notation from float64 round-trip
71+
bf := new(big.Float).SetPrec(256)
72+
if _, _, err := bf.Parse(s, 10); err != nil {
73+
return nil, fmt.Errorf("networkipavailabilities: cannot parse %q as an integer: %w", s, err)
74+
}
75+
result, _ := bf.Int(nil)
76+
return result, nil
77+
}
78+
5579
func (r *NetworkIPAvailability) UnmarshalJSON(b []byte) error {
5680
type tmp NetworkIPAvailability
5781
var s struct {
5882
tmp
59-
TotalIPs big.Int `json:"total_ips"`
60-
UsedIPs big.Int `json:"used_ips"`
83+
TotalIPs json.Number `json:"total_ips"`
84+
UsedIPs json.Number `json:"used_ips"`
6185
}
6286

6387
err := json.Unmarshal(b, &s)
@@ -66,10 +90,19 @@ func (r *NetworkIPAvailability) UnmarshalJSON(b []byte) error {
6690
}
6791
*r = NetworkIPAvailability(s.tmp)
6892

69-
r.TotalIPs = s.TotalIPs.String()
70-
r.UsedIPs = s.UsedIPs.String()
93+
totalIPs, err := parseBigIntFromNumber(s.TotalIPs)
94+
if err != nil {
95+
return err
96+
}
97+
r.TotalIPs = totalIPs.String()
7198

72-
return err
99+
usedIPs, err := parseBigIntFromNumber(s.UsedIPs)
100+
if err != nil {
101+
return err
102+
}
103+
r.UsedIPs = usedIPs.String()
104+
105+
return nil
73106
}
74107

75108
// SubnetIPAvailability represents availability details for a single subnet.
@@ -97,8 +130,8 @@ func (r *SubnetIPAvailability) UnmarshalJSON(b []byte) error {
97130
type tmp SubnetIPAvailability
98131
var s struct {
99132
tmp
100-
TotalIPs big.Int `json:"total_ips"`
101-
UsedIPs big.Int `json:"used_ips"`
133+
TotalIPs json.Number `json:"total_ips"`
134+
UsedIPs json.Number `json:"used_ips"`
102135
}
103136

104137
err := json.Unmarshal(b, &s)
@@ -107,10 +140,19 @@ func (r *SubnetIPAvailability) UnmarshalJSON(b []byte) error {
107140
}
108141
*r = SubnetIPAvailability(s.tmp)
109142

110-
r.TotalIPs = s.TotalIPs.String()
111-
r.UsedIPs = s.UsedIPs.String()
143+
totalIPs, err := parseBigIntFromNumber(s.TotalIPs)
144+
if err != nil {
145+
return err
146+
}
147+
r.TotalIPs = totalIPs.String()
148+
149+
usedIPs, err := parseBigIntFromNumber(s.UsedIPs)
150+
if err != nil {
151+
return err
152+
}
153+
r.UsedIPs = usedIPs.String()
112154

113-
return err
155+
return nil
114156
}
115157

116158
// NetworkIPAvailabilityPage stores a single page of NetworkIPAvailabilities

openstack/networking/v2/extensions/networkipavailabilities/testing/fixtures_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ const NetworkIPAvailabilityListResult = `
1515
"project_id": "fb57277ef2f84a0e85b9018ec2dedbf7",
1616
"subnet_ip_availability": [
1717
{
18-
"cidr": "fdbc:bf53:567e::/64",
18+
"cidr": "fdbc:bf53:567e::/56",
1919
"ip_version": 6,
2020
"subnet_id": "497ac4d3-0b92-42cf-82de-71302ab2b656",
2121
"subnet_name": "ipv6-private-subnet",
22-
"total_ips": 18446744073709552000,
22+
"total_ips": 4722366482869645213696,
2323
"used_ips": 2
2424
},
2525
{
@@ -69,10 +69,14 @@ var NetworkIPAvailability1 = networkipavailabilities.NetworkIPAvailability{
6969
{
7070
SubnetID: "497ac4d3-0b92-42cf-82de-71302ab2b656",
7171
SubnetName: "ipv6-private-subnet",
72-
CIDR: "fdbc:bf53:567e::/64",
72+
CIDR: "fdbc:bf53:567e::/56",
7373
IPVersion: int(gophercloud.IPv6),
74-
TotalIPs: "18446744073709552000",
75-
UsedIPs: "2",
74+
// 4722366482869645213696 is 2^72 (a /56 IPv6 subnet).
75+
// After gophercloud's float64 round-trip (numbers >= 1e21 are
76+
// re-encoded in scientific notation), the value loses the last
77+
// few digits of precision but no longer crashes.
78+
TotalIPs: "4722366482869645000000",
79+
UsedIPs: "2",
7680
},
7781
{
7882
SubnetID: "521f47e7-c4fb-452c-b71a-851da38cc571",

0 commit comments

Comments
 (0)