@ 260812.1

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

.github/workflows/linux.yml

@@ -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:

go.mod

@@ -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

internal/dirent/dirent.go

@@ -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

new file mode 100644@@ -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

deleted file mode 100644@@ -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

deleted file mode 100644@@ -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

deleted file mode 100644@@ -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

deleted file mode 100644@@ -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

deleted file mode 100644@@ -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)-	})-}