|
1 | 1 | package github |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "errors" |
5 | 6 | "fmt" |
6 | 7 | "math" |
@@ -60,64 +61,138 @@ func isAcceptedError(err error) bool { |
60 | 61 | return errors.As(err, &acceptedError) |
61 | 62 | } |
62 | 63 |
|
63 | | -// toInt converts a value to int, handling both float64 and string representations. |
64 | | -// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, |
65 | | -// fractional values, and values outside the int range. |
66 | | -func toInt(val any) (int, error) { |
67 | | - var f float64 |
68 | | - switch v := val.(type) { |
69 | | - case float64: |
70 | | - f = v |
71 | | - case string: |
72 | | - var err error |
73 | | - f, err = strconv.ParseFloat(v, 64) |
74 | | - if err != nil { |
75 | | - return 0, fmt.Errorf("invalid numeric value: %s", v) |
76 | | - } |
77 | | - default: |
78 | | - return 0, fmt.Errorf("expected number, got %T", val) |
| 64 | +// float64ToInt64 converts a float64 to int64, rejecting non-finite, fractional, |
| 65 | +// and out-of-range values, including those that would lose precision. |
| 66 | +func float64ToInt64(f float64) (int64, error) { |
| 67 | + if math.IsNaN(f) || math.IsInf(f, 0) { |
| 68 | + return 0, fmt.Errorf("non-finite numeric value") |
| 69 | + } |
| 70 | + if f != math.Trunc(f) { |
| 71 | + return 0, fmt.Errorf("non-integer numeric value: %v", f) |
| 72 | + } |
| 73 | + if f > float64(math.MaxInt64) || f < float64(math.MinInt64) { |
| 74 | + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) |
79 | 75 | } |
| 76 | + result := int64(f) |
| 77 | + if float64(result) != f { |
| 78 | + return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) |
| 79 | + } |
| 80 | + return result, nil |
| 81 | +} |
| 82 | + |
| 83 | +// float64ToInt converts a float64 to int, rejecting non-finite, fractional, |
| 84 | +// and out-of-range values. |
| 85 | +func float64ToInt(f float64) (int, error) { |
80 | 86 | if math.IsNaN(f) || math.IsInf(f, 0) { |
81 | 87 | return 0, fmt.Errorf("non-finite numeric value") |
82 | 88 | } |
83 | 89 | if f != math.Trunc(f) { |
84 | 90 | return 0, fmt.Errorf("non-integer numeric value: %v", f) |
85 | 91 | } |
86 | | - if f > math.MaxInt || f < math.MinInt { |
| 92 | + if f > float64(math.MaxInt) || f < float64(math.MinInt) { |
87 | 93 | return 0, fmt.Errorf("numeric value out of int range: %v", f) |
88 | 94 | } |
89 | 95 | return int(f), nil |
90 | 96 | } |
91 | 97 |
|
92 | | -// toInt64 converts a value to int64, handling both float64 and string representations. |
93 | | -// Some MCP clients send numeric values as strings. It rejects NaN, ±Inf, |
94 | | -// fractional values, and values that lose precision in the float64→int64 conversion. |
95 | | -func toInt64(val any) (int64, error) { |
96 | | - var f float64 |
| 98 | +// toInt converts a value to int, handling float64, integer, and string representations. |
| 99 | +// Native integer types are preserved without a float64 round-trip so large values |
| 100 | +// are not corrupted. It rejects NaN, ±Inf, fractional values, and out-of-range values. |
| 101 | +func toInt(val any) (int, error) { |
97 | 102 | switch v := val.(type) { |
| 103 | + case int: |
| 104 | + return v, nil |
| 105 | + case int32: |
| 106 | + return int(v), nil |
| 107 | + case int64: |
| 108 | + if v > int64(math.MaxInt) || v < int64(math.MinInt) { |
| 109 | + return 0, fmt.Errorf("numeric value out of int range: %v", v) |
| 110 | + } |
| 111 | + return int(v), nil |
| 112 | + case uint: |
| 113 | + if v > uint(math.MaxInt) { |
| 114 | + return 0, fmt.Errorf("numeric value out of int range: %v", v) |
| 115 | + } |
| 116 | + return int(v), nil |
| 117 | + case uint32: |
| 118 | + return int(v), nil |
| 119 | + case uint64: |
| 120 | + if v > uint64(math.MaxInt) { |
| 121 | + return 0, fmt.Errorf("numeric value out of int range: %v", v) |
| 122 | + } |
| 123 | + return int(v), nil |
98 | 124 | case float64: |
99 | | - f = v |
| 125 | + return float64ToInt(v) |
| 126 | + case float32: |
| 127 | + return float64ToInt(float64(v)) |
100 | 128 | case string: |
101 | | - var err error |
102 | | - f, err = strconv.ParseFloat(v, 64) |
| 129 | + i, err := strconv.ParseInt(v, 10, 0) |
103 | 130 | if err != nil { |
104 | 131 | return 0, fmt.Errorf("invalid numeric value: %s", v) |
105 | 132 | } |
| 133 | + return int(i), nil |
| 134 | + case json.Number: |
| 135 | + if i, err := v.Int64(); err == nil { |
| 136 | + if i > int64(math.MaxInt) || i < int64(math.MinInt) { |
| 137 | + return 0, fmt.Errorf("numeric value out of int range: %v", i) |
| 138 | + } |
| 139 | + return int(i), nil |
| 140 | + } |
| 141 | + f, err := v.Float64() |
| 142 | + if err != nil { |
| 143 | + return 0, fmt.Errorf("invalid numeric value: %s", v) |
| 144 | + } |
| 145 | + return float64ToInt(f) |
106 | 146 | default: |
107 | 147 | return 0, fmt.Errorf("expected number, got %T", val) |
108 | 148 | } |
109 | | - if math.IsNaN(f) || math.IsInf(f, 0) { |
110 | | - return 0, fmt.Errorf("non-finite numeric value") |
111 | | - } |
112 | | - if f != math.Trunc(f) { |
113 | | - return 0, fmt.Errorf("non-integer numeric value: %v", f) |
114 | | - } |
115 | | - result := int64(f) |
116 | | - // Check round-trip to detect precision loss for large int64 values |
117 | | - if float64(result) != f { |
118 | | - return 0, fmt.Errorf("numeric value %v is too large to fit in int64", f) |
| 149 | +} |
| 150 | + |
| 151 | +// toInt64 converts a value to int64, handling float64, integer, and string representations. |
| 152 | +// Native integer types are preserved without a float64 round-trip so large values |
| 153 | +// are not corrupted. It rejects NaN, ±Inf, fractional values, and out-of-range values. |
| 154 | +func toInt64(val any) (int64, error) { |
| 155 | + switch v := val.(type) { |
| 156 | + case int: |
| 157 | + return int64(v), nil |
| 158 | + case int32: |
| 159 | + return int64(v), nil |
| 160 | + case int64: |
| 161 | + return v, nil |
| 162 | + case uint: |
| 163 | + if uint64(v) > uint64(math.MaxInt64) { |
| 164 | + return 0, fmt.Errorf("numeric value out of int64 range: %v", v) |
| 165 | + } |
| 166 | + return int64(v), nil |
| 167 | + case uint32: |
| 168 | + return int64(v), nil |
| 169 | + case uint64: |
| 170 | + if v > uint64(math.MaxInt64) { |
| 171 | + return 0, fmt.Errorf("numeric value out of int64 range: %v", v) |
| 172 | + } |
| 173 | + return int64(v), nil |
| 174 | + case float64: |
| 175 | + return float64ToInt64(v) |
| 176 | + case float32: |
| 177 | + return float64ToInt64(float64(v)) |
| 178 | + case string: |
| 179 | + i, err := strconv.ParseInt(v, 10, 64) |
| 180 | + if err != nil { |
| 181 | + return 0, fmt.Errorf("invalid numeric value: %s", v) |
| 182 | + } |
| 183 | + return i, nil |
| 184 | + case json.Number: |
| 185 | + if i, err := v.Int64(); err == nil { |
| 186 | + return i, nil |
| 187 | + } |
| 188 | + f, err := v.Float64() |
| 189 | + if err != nil { |
| 190 | + return 0, fmt.Errorf("invalid numeric value: %s", v) |
| 191 | + } |
| 192 | + return float64ToInt64(f) |
| 193 | + default: |
| 194 | + return 0, fmt.Errorf("expected number, got %T", val) |
119 | 195 | } |
120 | | - return result, nil |
121 | 196 | } |
122 | 197 |
|
123 | 198 | // RequiredParam is a helper function that can be used to fetch a requested parameter from the request. |
@@ -149,7 +224,7 @@ func RequiredParam[T comparable](args map[string]any, p string) (T, error) { |
149 | 224 | // RequiredInt is a helper function that can be used to fetch a requested parameter from the request. |
150 | 225 | // It does the following checks: |
151 | 226 | // 1. Checks if the parameter is present in the request. |
152 | | -// 2. Checks if the parameter is of the expected type (float64 or numeric string). |
| 227 | +// 2. Checks if the parameter is a valid integer (float64, native integer, or numeric string). |
153 | 228 | // 3. Checks if the parameter is not empty, i.e: non-zero value |
154 | 229 | func RequiredInt(args map[string]any, p string) (int, error) { |
155 | 230 | v, ok := args[p] |
|
0 commit comments