hugofs: Fix glob case-sensitivity bug

On Linux, `hugofs.Glob` does not hit any directories which includes
uppercase letters. (This does not happen on macOS.)

Since `resources.GetMatch/Match` uses `Glob`,

```
{{ resources.GetMatch "Foo/bar.css" }}
```

this does not match `assets/Foo/bar.css` .

On the other hand, you can get it with

```
{{ resources.Get "Foo/bar.css" }}
```
This commit is contained in:
satotake 2022-09-21 15:01:54 +00:00 committed by Bjørn Erik Pedersen
parent f3560aa0e1
commit 281554ee97
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
4 changed files with 54 additions and 23 deletions

View file

@ -26,14 +26,14 @@ import (
// Glob walks the fs and passes all matches to the handle func. // Glob walks the fs and passes all matches to the handle func.
// The handle func can return true to signal a stop. // The handle func can return true to signal a stop.
func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error)) error { func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error)) error {
pattern = glob.NormalizePath(pattern) pattern = glob.NormalizePathCaseSensitive(pattern)
if pattern == "" { if pattern == "" {
return nil return nil
} }
g, err := glob.GetGlob(pattern) g, err := glob.GetFilenamesGlob(pattern)
if err != nil { if err != nil {
return nil return err
} }
hasSuperAsterisk := strings.Contains(pattern, "**") hasSuperAsterisk := strings.Contains(pattern, "**")

View file

@ -30,15 +30,13 @@ const filepathSeparator = string(os.PathSeparator)
var ( var (
isWindows = runtime.GOOS == "windows" isWindows = runtime.GOOS == "windows"
defaultGlobCache = &globCache{ defaultGlobCache = &globCache{
isCaseSensitive: false, isWindows: isWindows,
isWindows: isWindows, cache: make(map[string]globErr),
cache: make(map[string]globErr),
} }
filenamesGlobCache = &globCache{ filenamesGlobCache = &globCache{
isCaseSensitive: false, // As long as the search strings are all lower case, this does not allocate. isWindows: isWindows,
isWindows: isWindows, cache: make(map[string]globErr),
cache: make(map[string]globErr),
} }
) )
@ -49,8 +47,7 @@ type globErr struct {
type globCache struct { type globCache struct {
// Config // Config
isCaseSensitive bool isWindows bool
isWindows bool
// Cache // Cache
sync.RWMutex sync.RWMutex
@ -72,19 +69,12 @@ func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) {
var err error var err error
pattern = filepath.ToSlash(pattern) pattern = filepath.ToSlash(pattern)
g, err = glob.Compile(strings.ToLower(pattern), '/')
if gc.isCaseSensitive {
g, err = glob.Compile(pattern, '/')
} else {
g, err = glob.Compile(strings.ToLower(pattern), '/')
}
eg = globErr{ eg = globErr{
globDecorator{ globDecorator{
g: g, g: g,
isCaseSensitive: gc.isCaseSensitive, isWindows: gc.isWindows},
isWindows: gc.isWindows},
err, err,
} }
@ -117,14 +107,50 @@ func (g globDecorator) Match(s string) bool {
return g.g.Match(s) return g.g.Match(s)
} }
type globDecoratorDouble struct {
lowerCase glob.Glob
originalCase glob.Glob
}
func (g globDecoratorDouble) Match(s string) bool {
return g.lowerCase.Match(s) || g.originalCase.Match(s)
}
func GetGlob(pattern string) (glob.Glob, error) { func GetGlob(pattern string) (glob.Glob, error) {
return defaultGlobCache.GetGlob(pattern) return defaultGlobCache.GetGlob(pattern)
} }
func GetFilenamesGlob(pattern string) (glob.Glob, error) {
lowered := strings.ToLower(pattern)
hasUpperCase := pattern != lowered
gLowered, err := filenamesGlobCache.GetGlob(lowered)
if err != nil {
return nil, err
}
if !hasUpperCase {
return gLowered, nil
}
gSensitive, err := filenamesGlobCache.GetGlob(pattern)
if err != nil {
return nil, err
}
return globDecoratorDouble{
lowerCase: gLowered,
originalCase: gSensitive,
}, nil
}
func NormalizePath(p string) string { func NormalizePath(p string) string {
return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.") return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.")
} }
func NormalizePathCaseSensitive(p string) string {
return strings.Trim(path.Clean(filepath.ToSlash(p)), "/.")
}
// ResolveRootDir takes a normalized path on the form "assets/**.json" and // ResolveRootDir takes a normalized path on the form "assets/**.json" and
// determines any root dir, i.e. any start path without any wildcards. // determines any root dir, i.e. any start path without any wildcards.
func ResolveRootDir(p string) string { func ResolveRootDir(p string) string {

View file

@ -49,12 +49,17 @@ func TestGlob(t *testing.T) {
create("jsonfiles/sub/d3.json") create("jsonfiles/sub/d3.json")
create("jsonfiles/d1.xml") create("jsonfiles/d1.xml")
create("a/b/c/e/f.json") create("a/b/c/e/f.json")
create("UPPER/sub/style.css")
create("root/UPPER/sub/style.css")
c.Assert(collect("**.json"), qt.HasLen, 5) c.Assert(collect("**.json"), qt.HasLen, 5)
c.Assert(collect("**"), qt.HasLen, 6) c.Assert(collect("**"), qt.HasLen, 8)
c.Assert(collect(""), qt.HasLen, 0) c.Assert(collect(""), qt.HasLen, 0)
c.Assert(collect("jsonfiles/*.json"), qt.HasLen, 2) c.Assert(collect("jsonfiles/*.json"), qt.HasLen, 2)
c.Assert(collect("*.json"), qt.HasLen, 1) c.Assert(collect("*.json"), qt.HasLen, 1)
c.Assert(collect("**.xml"), qt.HasLen, 1) c.Assert(collect("**.xml"), qt.HasLen, 1)
c.Assert(collect(filepath.FromSlash("/jsonfiles/*.json")), qt.HasLen, 2) c.Assert(collect(filepath.FromSlash("/jsonfiles/*.json")), qt.HasLen, 2)
c.Assert(collect("UPPER/sub/style.css"), qt.HasLen, 1)
c.Assert(collect("root/UPPER/sub/style.css"), qt.HasLen, 1)
} }

View file

@ -93,7 +93,7 @@ func (c *Client) GetMatch(pattern string) (resource.Resource, error) {
} }
func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) { func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) {
pattern = glob.NormalizePath(pattern) pattern = glob.NormalizePathCaseSensitive(pattern)
partitions := glob.FilterGlobParts(strings.Split(pattern, "/")) partitions := glob.FilterGlobParts(strings.Split(pattern, "/"))
if len(partitions) == 0 { if len(partitions) == 0 {
partitions = []string{resources.CACHE_OTHER} partitions = []string{resources.CACHE_OTHER}