Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test
on:
pull_request:
branches:
- master
- main
push:
branches:
- master
- main
jobs:
test:
name: Walk order on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
go-version: [ 1.27 ]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Run tests
run: go test -v ./...
shell: bash
6 changes: 3 additions & 3 deletions .github/workflows/vulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ 1.19 ]
go-version: [ 1.27 ]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- uses: actions/setup-go@v3
uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go-version }}
check-latest: true
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/minio/filepath

go 1.18
go 1.27
15 changes: 13 additions & 2 deletions walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ func Walk(root string, walkFn WalkFunc) error {
return walk(root, info, walkFn)
}

// flatKeySeparator is what a directory name is taken to end in while entries
// are sorted.
//
// An object store sorts `a/b` under `a` because `/` is 0x2F, below every
// character a sibling name can begin with. os.PathSeparator is that same byte
// on Unix and 0x5C on Windows, which sits above the digits and the upper case
// letters: sorting by it there puts a directory after siblings it should come
// before, which is neither flat key order nor the order an object store lists
// the same names in.
const flatKeySeparator = "/"

// byName implements sort.Interface for sorting os.FileInfo list.
type byName []os.FileInfo

Expand All @@ -38,12 +49,12 @@ func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f byName) Less(i, j int) bool {
n1 := f[i].Name()
if f[i].IsDir() {
n1 = n1 + string(os.PathSeparator)
n1 = n1 + flatKeySeparator
}

n2 := f[j].Name()
if f[j].IsDir() {
n2 = n2 + string(os.PathSeparator)
n2 = n2 + flatKeySeparator
}

return n1 < n2
Expand Down
94 changes: 94 additions & 0 deletions walk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the https://golang.org/LICENSE file.

package filepath

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)

// TestWalkIsFlatKeySorted checks the order this package exists to produce:
//
// lexically ordered output treats directories with `/` at the end before
// sorting, this is sorting is flat key sorting instead of regular
// filesystem sorting of entries.
//
// Flat key order is an object store's order. A key `a/b` sorts under `a`
// because `/` is 0x2F, below every character a sibling name can start with.
// byName appends os.PathSeparator rather than `/`: the same byte on Unix, and
// 0x5C on Windows, which sits above the digits and the upper case letters.
//
// So one directory and its siblings are ordered differently on Windows, and
// the walk there is neither flat key sorted nor in the order an object store
// lists the same names in. The tree below puts a directory among siblings
// whose first differing byte falls on each side of the two separators:
//
// 0x2D a-1.bin below `/` and below `\`
// 0x2F a/ the directory, where flat key order puts it
// 0x30 a0.bin above `/`, below `\` -- moves on Windows
// 0x5A aZ.bin above `/`, below `\` -- moves on Windows
// 0x5C a\ the directory, where this walk puts it on Windows
// 0x5F a_.bin above both
// 0x61 aa.bin above both
//
// Every name is reported in slash form, so what is compared here is the order
// alone and not how a platform spells a path.
func TestWalkIsFlatKeySorted(t *testing.T) {
root := t.TempDir()
if err := os.Mkdir(filepath.Join(root, "a"), 0o755); err != nil {
t.Fatal(err)
}
for _, name := range []string{
"a/inner.bin",
"a-1.bin",
"a0.bin",
"aZ.bin",
"a_.bin",
"aa.bin",
} {
if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(name)), []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
}

var got []string
err := Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
if rel == "." {
return nil
}
got = append(got, filepath.ToSlash(rel))
return nil
})
if err != nil {
t.Fatal(err)
}

want := []string{"a-1.bin", "a", "a/inner.bin", "a0.bin", "aZ.bin", "a_.bin", "aa.bin"}

t.Logf("GOOS=%s, os.PathSeparator=%q (0x%02X)",
runtime.GOOS, os.PathSeparator, os.PathSeparator)
t.Logf("flat key order: %s", strings.Join(want, " "))
t.Logf("this walk : %s", strings.Join(got, " "))

if strings.Join(got, " ") != strings.Join(want, " ") {
t.Errorf("the walk is not flat key sorted on %s\n"+
" want: %s\n"+
" got: %s\n"+
"byName appends os.PathSeparator (%q) where the package comment says `/`, "+
"so the directory sorts above every sibling whose next byte is below 0x%02X.",
runtime.GOOS, strings.Join(want, " "), strings.Join(got, " "),
os.PathSeparator, os.PathSeparator)
}
}
Loading