Skip to content
Open
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
4 changes: 2 additions & 2 deletions modules/caddyhttp/fileserver/browsetplcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func (l byName) Len() int { return len(l.Items) }
func (l byName) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.Items[i] }

func (l byName) Less(i, j int) bool {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name))
}

func (l byNameDirFirst) Len() int { return len(l.Items) }
Expand All @@ -338,7 +338,7 @@ func (l byNameDirFirst) Swap(i, j int) { l.Items[i], l.Items[j] = l.Items[j], l.
func (l byNameDirFirst) Less(i, j int) bool {
// sort by name if both are dir or file
if l.Items[i].IsDir == l.Items[j].IsDir {
return strings.ToLower(l.Items[i].Name) < strings.ToLower(l.Items[j].Name)
return naturalLess(strings.ToLower(l.Items[i].Name), strings.ToLower(l.Items[j].Name))
}
// sort dir ahead of file
return l.Items[i].IsDir
Expand Down
65 changes: 65 additions & 0 deletions modules/caddyhttp/fileserver/natsort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package fileserver

func isDigit(b byte) bool { return '0' <= b && b <= '9' }

// naturalLess compares two strings using natural ordering. This means that e.g.
// "abc2" < "abc12".
//
// Non-digit sequences and numbers are compared separately. The former are
// compared bytewise, while digits are compared numerically (except that
// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
//
// Limitation: only ASCII digits (0-9) are considered.
//
// This implementation is copied from https://github.com/fvbommel/sortorder,
// which is MIT licensed.
func naturalLess(str1, str2 string) bool {
idx1, idx2 := 0, 0
for idx1 < len(str1) && idx2 < len(str2) {
c1, c2 := str1[idx1], str2[idx2]
dig1, dig2 := isDigit(c1), isDigit(c2)
switch {
case dig1 != dig2: // Digits before other characters.
return dig1 // True if LHS is a digit, false if the RHS is one.
case !dig1: // && !dig2, because dig1 == dig2
// UTF-8 compares bytewise-lexicographically, no need to decode
// codepoints.
if c1 != c2 {
return c1 < c2
}
idx1++
idx2++
default: // Digits
// Eat zeros.
for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ {
}
for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ {
}
// Eat all digits.
nonZero1, nonZero2 := idx1, idx2
for ; idx1 < len(str1) && isDigit(str1[idx1]); idx1++ {
}
for ; idx2 < len(str2) && isDigit(str2[idx2]); idx2++ {
}
// If lengths of numbers with non-zero prefix differ, the shorter
// one is less.
if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
return len1 < len2
}
// If they're equally long, string comparison is correct.
if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
return nr1 < nr2
}
// Otherwise, the one with less zeros is less.
// Because everything up to the number is equal, comparing the index
// after the zeros is sufficient.
if nonZero1 != nonZero2 {
return nonZero1 < nonZero2
}
}
// They're identical so far, so continue comparing.
}
// So far they are identical. At least one is ended. If the other continues,
// it sorts last.
return len(str1) < len(str2)
}
63 changes: 63 additions & 0 deletions modules/caddyhttp/fileserver/natsort_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fileserver

import (
"context"
"io/fs"
"net/http/httptest"
"os"
"testing"

"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

// TestNatSort confirms that, although an ASCIIbetical sort would order foo2.txt
// after foo10.txt, Caddy will return them in a "natural" human-intuitive order.
func TestNatSort(t *testing.T) {
fsrv := &FileServer{Browse: &Browse{}}

base := "./testdata"
dirName := "natsort"

fsys := os.DirFS(base)
f, err := fsys.Open(dirName)
if err != nil {
t.Fatalf("opening testdata dir: %v", err)
}
defer f.Close()

repl := caddyhttp.NewTestReplacer(httptest.NewRequest("GET", "/", nil))

listing, err := fsrv.loadDirectoryContents(context.Background(), fsys, f.(fs.ReadDirFile), base, "/natsort/", repl)
if err != nil {
t.Fatalf("loadDirectoryContents returned error: %v", err)
}

if len(listing.Items) != 3 {
t.Fatalf("expected 3 items in listing, got %d", len(listing.Items))
}

listing.applySortAndLimit(sortByNameDirFirst, sortOrderAsc, "", "")

got := []string{listing.Items[0].Name, listing.Items[1].Name, listing.Items[2].Name}
want := []string{"foo1.txt", "foo2.txt", "foo10.txt"}

for i := range want {
if got[i] != want[i] {
t.Fatalf("unexpected item at index %v: got %v, want %v", i, got, want)
}
}
}
1 change: 1 addition & 0 deletions modules/caddyhttp/fileserver/testdata/natsort/foo1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo1.txt
1 change: 1 addition & 0 deletions modules/caddyhttp/fileserver/testdata/natsort/foo10.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo10.txt
1 change: 1 addition & 0 deletions modules/caddyhttp/fileserver/testdata/natsort/foo2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo2.txt
Loading