Explicitly check each interface type in core.IsAnyNilOrZero (#7168)

I introduced a bug in https://github.com/letsencrypt/boulder/pull/7162
which lead to incorrect type comparisons.

`IsAnyNilOrZero` takes a variadic amount of interfaces with each
interface containing a value with underlying type `string`, `int32`,
`float64`, etc and checks for that type's zero value in a switch
statement. When checking for multiple numeric types on the same case
statement line, the compiler will default to `int` rather than implying
the type of the untyped constant `0` leading to an incorrect result at
runtime. Contrast that to when each numeric type is on its own case
statement line where a correct comparison can be made.

Per [go.dev/blog/constants](https://go.dev/blog/constants):
> The default type of an untyped constant is determined by its syntax.
For string constants, the only possible implicit type is string. For
[numeric constants](https://go.dev/ref/spec#Numeric_types), the implicit
type has more variety. Integer constants default to int, floating-point
constants float64, rune constants to rune (an alias for int32), and
imaginary constants to complex128.

Per
[go.dev/doc/effective_go](https://go.dev/doc/effective_go#interfaces_and_types):
> Constants in Go are just that—constant. They are created at compile
time, even when defined as locals in functions, and can only be numbers,
characters (runes), strings or booleans. Because of the compile-time
restriction, the expressions that define them must be constant
expressions, evaluatable by the compiler.

See the following code for an explicit example.
```
package main
import "fmt"
func main() {
	for _, val := range []interface{}{1, int8(1), int16(1), int32(1), int64(1)} {
		switch v := val.(type) {
		case int, int8, int16, int32, int64:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		}
	}
        fmt.Println("-----------------------------")
	for _, val := range []interface{}{1, int8(1), int16(1), int32(1), int64(1)} {
		switch v := val.(type) {
		case int:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		case int8:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		case int16:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		case int32:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		case int64:
			fmt.Printf("Type %T:\t%v\n", v, v == 1)
		}
	}
}

-------
Type int:	true
Type int8:	false
Type int16:	false
Type int32:	false
Type int64:	false
-----------------------------
Type int:	true
Type int8:	true
Type int16:	true
Type int32:	true
Type int64:	true
```
This commit is contained in:
Phil Porada 2023-11-20 13:31:40 -05:00 committed by GitHub
parent 01f2336317
commit caf73f2762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 88 additions and 6 deletions

View File

@ -222,6 +222,7 @@ func IsAnyNilOrZero(vals ...interface{}) bool {
return true
}
case byte:
// Byte is an alias for uint8 and will cover that case.
if v == 0 {
return true
}
@ -229,11 +230,47 @@ func IsAnyNilOrZero(vals ...interface{}) bool {
if len(v) == 0 {
return true
}
case int, int8, int16, int32, int64, uint, uint16, uint32, uint64:
case int:
if v == 0 {
return true
}
case float32, float64:
case int8:
if v == 0 {
return true
}
case int16:
if v == 0 {
return true
}
case int32:
if v == 0 {
return true
}
case int64:
if v == 0 {
return true
}
case uint:
if v == 0 {
return true
}
case uint16:
if v == 0 {
return true
}
case uint32:
if v == 0 {
return true
}
case uint64:
if v == 0 {
return true
}
case float32:
if v == 0 {
return true
}
case float64:
if v == 0 {
return true
}

View File

@ -116,9 +116,36 @@ func TestIsAnyNilOrZero(t *testing.T) {
test.Assert(t, IsAnyNilOrZero(false), "False bool seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(true), "True bool seen as zero")
test.Assert(t, IsAnyNilOrZero(0), "Zero num seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint32(5)), "Non-zero num seen as zero")
test.Assert(t, !IsAnyNilOrZero(-12.345), "Non-zero num seen as zero")
test.Assert(t, IsAnyNilOrZero(0), "Untyped constant zero seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(1), "Untyped constant 1 seen as zero")
test.Assert(t, IsAnyNilOrZero(int(0)), "int(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(int(1)), "int(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(int8(0)), "int8(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(int8(1)), "int8(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(int16(0)), "int16(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(int16(1)), "int16(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(int32(0)), "int32(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(int32(1)), "int32(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(int64(0)), "int64(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(int64(1)), "int64(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(uint(0)), "uint(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint(1)), "uint(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(uint8(0)), "uint8(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint8(1)), "uint8(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(uint16(0)), "uint16(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint16(1)), "uint16(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(uint32(0)), "uint32(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint32(1)), "uint32(1) seen as zero")
test.Assert(t, IsAnyNilOrZero(uint64(0)), "uint64(0) seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(uint64(1)), "uint64(1) seen as zero")
test.Assert(t, !IsAnyNilOrZero(-12.345), "Untyped float32 seen as zero")
test.Assert(t, !IsAnyNilOrZero(float32(6.66)), "Non-empty float32 seen as zero")
test.Assert(t, IsAnyNilOrZero(float32(0)), "Empty float32 seen as non-zero")
test.Assert(t, !IsAnyNilOrZero(float64(7.77)), "Non-empty float64 seen as zero")
test.Assert(t, IsAnyNilOrZero(float64(0)), "Empty float64 seen as non-zero")
test.Assert(t, IsAnyNilOrZero(""), "Empty string seen as non-zero")
test.Assert(t, !IsAnyNilOrZero("string"), "Non-empty string seen as zero")
@ -158,10 +185,28 @@ func BenchmarkIsAnyNilOrZero(b *testing.B) {
}{
{input: int(0)},
{input: int(1)},
{input: int8(0)},
{input: int8(1)},
{input: int16(0)},
{input: int16(1)},
{input: int32(0)},
{input: int32(1)},
{input: int64(0)},
{input: int64(1)},
{input: uint(0)},
{input: uint(1)},
{input: uint8(0)},
{input: uint8(1)},
{input: uint16(0)},
{input: uint16(1)},
{input: uint32(0)},
{input: uint32(1)},
{input: uint64(0)},
{input: uint64(1)},
{input: float32(0)},
{input: float32(0.1)},
{input: float64(0)},
{input: float64(0.1)},
{input: ""},
{input: "ahoyhoy"},
{input: []string{}},
@ -185,7 +230,7 @@ func BenchmarkIsAnyNilOrZero(b *testing.B) {
}
for _, v := range table {
b.Run(fmt.Sprintf("input_%v", v.input), func(b *testing.B) {
b.Run(fmt.Sprintf("input_%T_%v", v.input, v.input), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = IsAnyNilOrZero(v.input)
}