250902.1 · c9ad02f5df20cc7b77a037a835015334a0a5eefc
internal/dirent: use binary.NativeEndian for parsing ints (#63)
* internal/dirent: use binary.NativeEndian for parsing ints
This change removes the platform specific integer parsing logic from internal/dirent and replaces it with binary.NativeEndian which is well tested and removes the need for us to maintain platform specific code.
Fixes: #61
* mod: update minimum supported Go version to 1.21
* internal/dirent: improve readInt test coverage
reading declarations
@@ -10,7 +10,7 @@ jobs: strategy: # Test all supported versions on Linux (since it's fast) matrix:- go: ['1.20', '1.21', '1.22', '1.23', '1.24']+ go: ['1.21', '1.22', '1.23', '1.24', '1.25'] steps: - uses: actions/checkout@v4 with:
@@ -1,5 +1,5 @@ module github.com/charlievieth/fastwalk -go 1.20+go 1.21 retract v1.0.7 // Build broken on Go 1.20
@@ -3,6 +3,7 @@ package dirent import (+ "encoding/binary" "os" "runtime" "syscall"@@ -11,6 +12,30 @@ import ( const InvalidMode = os.FileMode(1<<32 - 1) +// readInt returns the size-bytes unsigned integer in native byte order at offset off.+func readInt(b []byte, off, size uintptr) (uint64, bool) {+ if len(b) < int(off+size) {+ return 0, false+ }+ b = b[off:]+ switch size {+ case 1:+ return uint64(b[0]), true+ case 2:+ return uint64(binary.NativeEndian.Uint16(b)), true+ case 4:+ return uint64(binary.NativeEndian.Uint32(b)), true+ case 8:+ return uint64(binary.NativeEndian.Uint64(b)), true+ default:+ // This case is impossible if the tests pass. Previously, we panicked+ // here but for performance reasons (escape analysis) it's better to+ // trigger a panic via an out-of-bounds reference.+ _ = b[len(b)]+ return 0, false+ }+}+ func Parse(buf []byte) (consumed int, name string, typ os.FileMode) { reclen, ok := direntReclen(buf)
internal/dirent/dirent_test.go
@@ -0,0 +1,64 @@+//go:build aix || dragonfly || freebsd || (js && wasm) || linux || netbsd || openbsd || solaris++package dirent++import (+ "encoding/binary"+ "fmt"+ "math"+ "math/rand"+ "testing"+)++func TestReadInt(t *testing.T) {+ for _, sz := range []int{1, 2, 4, 8} {+ rr := rand.New(rand.NewSource(1))+ t.Run(fmt.Sprintf("%d", sz), func(t *testing.T) {+ var fn func() uint64+ switch sz {+ case 1:+ fn = func() uint64 { return uint64(rr.Int63n(256)) }+ case 2:+ fn = func() uint64 { return uint64(rr.Int63n(math.MaxUint16)) }+ case 4:+ fn = func() uint64 { return uint64(rr.Int63n(math.MaxUint32)) }+ case 8:+ fn = func() uint64 { return uint64(rr.Uint64()) }+ default:+ t.Fatal("invalid size:", sz)+ }+ buf := make([]byte, 8)+ fails := 0+ for i := 0; i < 100; i++ {+ want := fn()+ binary.NativeEndian.PutUint64(buf[:], want)+ got, ok := readInt(buf, 0, uintptr(sz))+ if got != want || !ok {+ fails+++ t.Errorf("readInt(%q, 0, %d) = %d, %t; want: %d, %t", buf, sz, got, ok, want, true)+ }+ if fails >= 10 {+ t.Fatal("too many errors:", fails)+ }+ }+ })+ }+ if i, ok := readInt(nil, 1, 1); i != 0 || ok {+ t.Errorf("readInt(nil, 1, 1) = %d, %t; want: %d, %t", i, ok, 0, false)+ }+}++func TestReadIntSize(t *testing.T) {+ if i, ok := readInt(nil, 1, 1); i != 0 || ok {+ t.Errorf("readInt(nil, 1, 1) = %d, %t; want: %d, %t", i, ok, 0, false)+ }+}++func TestReadIntInvalidSize(t *testing.T) {+ defer func() {+ if e := recover(); e == nil {+ t.Errorf("Expected panic for invalid size")+ }+ }()+ readInt(make([]byte, 32), 0, 9)+}
internal/dirent/endian_big.go
@@ -1,35 +0,0 @@-// Copyright 2016 The Go Authors. All rights reserved.-// Use of this source code is governed by a BSD-style-// license that can be found in the LICENSE file.--//go:build armbe || arm64be || m68k || mips || mips64 || mips64p32 || ppc || ppc64 || s390 || s390x || shbe || sparc || sparc64--package dirent--// readInt returns the size-bytes unsigned integer in native byte order at offset off.-func readInt(b []byte, off, size uintptr) (uint64, bool) {- if len(b) < int(off+size) {- return 0, false- }- b = b[off:]- switch size {- case 1:- return uint64(b[0]), true- case 2:- _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[1]) | uint64(b[0])<<8, true- case 4:- _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[3]) | uint64(b[2])<<8 | uint64(b[1])<<16 | uint64(b[0])<<24, true- case 8:- _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |- uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56, true- default:- // This case is impossible if the tests pass. Previously, we panicked- // here but for performance reasons (escape analysis) it's better to- // trigger a panic via an out-of-bounds reference.- _ = b[len(b)]- return 0, false- }-}
internal/dirent/endian_big_test.go
@@ -1,7 +0,0 @@-//go:build armbe || arm64be || m68k || mips || mips64 || mips64p32 || ppc || ppc64 || s390 || s390x || shbe || sparc || sparc64--package dirent--import "encoding/binary"--var encoder = binary.BigEndian
internal/dirent/endian_little.go
@@ -1,35 +0,0 @@-// Copyright 2016 The Go Authors. All rights reserved.-// Use of this source code is governed by a BSD-style-// license that can be found in the LICENSE file.--//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh || wasm--package dirent--// readInt returns the size-bytes unsigned integer in native byte order at offset off.-func readInt(b []byte, off, size uintptr) (uint64, bool) {- if len(b) < int(off+size) {- return 0, false- }- b = b[off:]- switch size {- case 1:- return uint64(b[0]), true- case 2:- _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[0]) | uint64(b[1])<<8, true- case 4:- _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24, true- case 8:- _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808- return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |- uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56, true- default:- // This case is impossible if the tests pass. Previously, we panicked- // here but for performance reasons (escape analysis) it's better to- // trigger a panic via an out-of-bounds reference.- _ = b[len(b)]- return 0, false- }-}
internal/dirent/endian_little_test.go
@@ -1,7 +0,0 @@-//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh || wasm--package dirent--import "encoding/binary"--var encoder = binary.LittleEndian
internal/dirent/endian_test.go
@@ -1,38 +0,0 @@-package dirent--import (- "strconv"- "testing"-)--func TestReadInt(t *testing.T) {- t.Run("Size", func(t *testing.T) {- if i, ok := readInt(nil, 1, 1); i != 0 || ok {- t.Errorf("readInt(nil, 1, 1) = %d, %t; want: %d, %t", i, ok, 0, false)- }- })-- base := make([]byte, 8)- encoder.PutUint64(base, 1<<63-1)- for sz := 1; sz <= 8; sz <<= 1 {- t.Run(strconv.Itoa(sz), func(t *testing.T) {- b := make([]byte, 8)- copy(b[:sz], base)- want := encoder.Uint64(b)- got, ok := readInt(b, 0, uintptr(sz))- if !ok || got != want {- t.Errorf("readInt(%q, %d, %d) = %d, %t want: %d, %t",- b, 0, sz, got, ok, want, true)- }- })- }-- t.Run("InvalidSize", func(t *testing.T) {- defer func() {- if e := recover(); e == nil {- t.Errorf("Expected panic for invalid size")- }- }()- readInt(make([]byte, 32), 0, 9)- })-}