Skip to content

Commit e5c15a5

Browse files
committed
resources: Refix resources.GetMatch/Match case-sensitivity bug
- add integration tests - use canonical path for glog - fix cache key for resources Fix #7686 Fix #10112 - ref - #10305 - 281554e
1 parent d8aba18 commit e5c15a5

File tree

4 files changed

+248
-3
lines changed

4 files changed

+248
-3
lines changed

hugofs/glob.go

+85
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ package hugofs
1515

1616
import (
1717
"errors"
18+
"fmt"
19+
"os"
1820
"path/filepath"
1921
"strings"
2022

@@ -69,6 +71,15 @@ func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error
6971
return nil
7072
}
7173

74+
root, err = CanonicalizeFilepath(fs, root)
75+
76+
if os.IsNotExist(err) {
77+
// TODO logger
78+
return nil
79+
} else if err != nil {
80+
return err
81+
}
82+
7283
w := NewWalkway(WalkwayConfig{
7384
Root: root,
7485
Fs: fs,
@@ -83,3 +94,77 @@ func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error
8394

8495
return nil
8596
}
97+
98+
// return case-matched path from case-insensitive input
99+
// before using this, you have to apply `NormalizePath`
100+
func CanonicalizeFilepath(fs afero.Fs, path string) (string, error) {
101+
if path == "." || path == "" {
102+
return path, nil
103+
}
104+
paths := strings.Split(path, "/")
105+
106+
var ret []string
107+
108+
for _, p := range paths {
109+
dir := filepath.Join(ret...)
110+
joined := filepath.Join(dir, p)
111+
fi, ok, err := lstatIfPossible(fs, joined)
112+
113+
if !os.IsNotExist(err) {
114+
return "", err
115+
}
116+
117+
if ok {
118+
ret = append(ret, fi.Name())
119+
continue
120+
}
121+
122+
fi, err = statWithCaseInsensitiveName(fs, dir, p)
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
ret = append(ret, fi.Name())
128+
// ret = append(ret, p)
129+
}
130+
131+
return filepath.Join(ret...), nil
132+
}
133+
134+
// case-insenstively search in parent dir, and return os.FileInfo
135+
func statWithCaseInsensitiveName(fs afero.Fs, parent, name string) (os.FileInfo, error) {
136+
fi, err := fs.Stat(parent)
137+
138+
if err != nil {
139+
return nil, err
140+
}
141+
142+
if !fi.IsDir() {
143+
return nil, fmt.Errorf("%s is not directory", parent)
144+
}
145+
146+
f, err := fs.Open(parent)
147+
if err != nil {
148+
return nil, err
149+
}
150+
defer f.Close()
151+
152+
names, err := f.Readdirnames(-1)
153+
154+
if err != nil {
155+
return nil, err
156+
}
157+
158+
baseLowered := strings.ToLower(name)
159+
for _, name := range names {
160+
if baseLowered == strings.ToLower(name) {
161+
p := filepath.Join(parent, name)
162+
fi, _, err := lstatIfPossible(fs, p)
163+
if err != nil {
164+
return nil, err
165+
}
166+
return fi, err
167+
}
168+
}
169+
return nil, os.ErrNotExist
170+
}

resources/resource_cache.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ func (c *ResourceCache) Contains(key string) bool {
148148
}
149149

150150
func (c *ResourceCache) cleanKey(key string) string {
151-
return strings.TrimPrefix(path.Clean(strings.ToLower(key)), "/")
151+
return strings.ToLower(c.cleanKeyNoLower(key))
152+
}
153+
154+
func (c *ResourceCache) cleanKeyNoLower(key string) string {
155+
return strings.TrimPrefix(path.Clean(key), "/")
152156
}
153157

154158
func (c *ResourceCache) get(key string) (any, bool) {
@@ -175,7 +179,7 @@ func (c *ResourceCache) GetOrCreateResources(key string, f func() (resource.Reso
175179
}
176180

177181
func (c *ResourceCache) getOrCreate(key string, f func() (any, error)) (any, error) {
178-
key = c.cleanKey(key)
182+
key = c.cleanKeyNoLower(key)
179183
// First check in-memory cache.
180184
r, found := c.get(key)
181185
if found {

resources/resource_factories/create/create.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (c *Client) GetMatch(pattern string) (resource.Resource, error) {
9393
}
9494

9595
func (c *Client) match(name, pattern string, matchFunc func(r resource.Resource) bool, firstOnly bool) (resource.Resources, error) {
96-
pattern = glob.NormalizePath(pattern)
96+
pattern = glob.NormalizePathNoLower(pattern)
9797
partitions := glob.FilterGlobParts(strings.Split(pattern, "/"))
9898
if len(partitions) == 0 {
9999
partitions = []string{resources.CACHE_OTHER}

tpl/resources/integration_test.go

+156
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,159 @@ func TestCopyPageShouldFail(t *testing.T) {
9898
b.Assert(err, qt.IsNotNil)
9999

100100
}
101+
102+
func TestMatchPatternCaseSensitivity(t *testing.T) {
103+
t.Parallel()
104+
105+
files := `
106+
-- config.toml --
107+
baseURL = "http://example.com/blog"
108+
-- assets/DIR1/sub/x.txt --
109+
content in x.txt
110+
-- assets/dir2/SUB/y.txt --
111+
content in y.txt
112+
-- layouts/index.html --
113+
{{ with resources.GetMatch "does/not/exist.txt" }}
114+
GetMatch "does/not/exist.txt" matched
115+
{{ else }}
116+
GetMatch "does/not/exist.txt" unmatched
117+
{{ end }}
118+
119+
{{ with resources.GetMatch "DIR1/sub/x.txt" }}
120+
GetMatch "DIR1/sub/x.txt" | .Content => {{ .Content }}
121+
GetMatch "DIR1/sub/x.txt" | .Name => {{ .Name }}
122+
{{ end }}
123+
124+
{{ with resources.GetMatch "dir2/SUB/y.txt" }}
125+
GetMatch "dir2/SUB/y.txt" | .Content => {{ .Content }}
126+
GetMatch "dir2/SUB/y.txt" | .Name => {{ .Name }}
127+
{{ end }}
128+
129+
{{ with resources.GetMatch "dir1/sub/x.txt" }}
130+
GetMatch "dir1/sub/x.txt" | .Content => {{ .Content }}
131+
GetMatch "dir1/sub/x.txt" | .Name => {{ .Name }}
132+
{{ end }}
133+
134+
{{ with resources.GetMatch "dir2/sub/y.txt" }}
135+
GetMatch "dir2/sub/y.txt" | .Content => {{ .Content }}
136+
GetMatch "dir2/sub/y.txt" | .Name => {{ .Name }}
137+
{{ end }}
138+
139+
{{ with resources.GetMatch "DIR1/SUB/X.TXT" }}
140+
GetMatch "DIR1/SUB/X.TXT" | .Content => {{ .Content }}
141+
GetMatch "DIR1/SUB/X.TXT" | .Name => {{ .Name }}
142+
{{ end }}
143+
144+
{{ with resources.GetMatch "DIR2/SUB/Y.TXT" }}
145+
GetMatch "DIR2/SUB/Y.TXT" | .Content => {{ .Content }}
146+
GetMatch "DIR2/SUB/Y.TXT" | .Name => {{ .Name }}
147+
{{ end }}
148+
149+
{{ with resources.GetMatch "DIR1/*/x.txt" }}
150+
GetMatch "DIR1/*/x.txt" | .Content => {{ .Content }}
151+
GetMatch "DIR1/*/x.txt" | .Name => {{ .Name }}
152+
{{ end }}
153+
154+
{{ with resources.GetMatch "dir2/*/y.txt" }}
155+
GetMatch "dir2/*/y.txt" | .Content => {{ .Content }}
156+
GetMatch "dir2/*/y.txt" | .Name => {{ .Name }}
157+
{{ end }}
158+
159+
{{ with resources.GetMatch "dir1/*/x.txt" }}
160+
GetMatch "dir1/*/x.txt" | .Content => {{ .Content }}
161+
GetMatch "dir1/*/x.txt" | .Name => {{ .Name }}
162+
{{ end }}
163+
164+
{{ with resources.GetMatch "DIR2/*/Y.TXT" }}
165+
GetMatch "DIR2/*/Y.TXT" | .Content => {{ .Content }}
166+
GetMatch "DIR2/*/Y.TXT" | .Name => {{ .Name }}
167+
{{ end }}
168+
169+
{{ with resources.GetMatch "DIR1/**/x.txt" }}
170+
GetMatch "DIR1/**/x.txt" | .Content => {{ .Content }}
171+
GetMatch "DIR1/**/x.txt" | .Name => {{ .Name }}
172+
{{ end }}
173+
174+
{{ with resources.GetMatch "dir2/**/y.txt" }}
175+
GetMatch "dir2/**/y.txt" | .Content => {{ .Content }}
176+
GetMatch "dir2/**/y.txt" | .Name => {{ .Name }}
177+
{{ end }}
178+
179+
{{ with resources.GetMatch "dir1/**/x.txt" }}
180+
GetMatch "dir1/**/x.txt" | .Content => {{ .Content }}
181+
GetMatch "dir1/**/x.txt" | .Name => {{ .Name }}
182+
{{ end }}
183+
184+
{{ with resources.GetMatch "DIR2/**/Y.TXT" }}
185+
GetMatch "DIR2/**/Y.TXT" | .Content => {{ .Content }}
186+
GetMatch "DIR2/**/Y.TXT" | .Name => {{ .Name }}
187+
{{ end }}
188+
189+
{{ with resources.Match "**/*.txt" }}
190+
Match "**/*.txt" | len => {{ len . }}
191+
{{ end }}
192+
193+
{{ with resources.Match "*/*/*.txt" }}
194+
Match "*/*/*.txt" | len => {{ len . }}
195+
{{ end }}
196+
197+
`
198+
199+
b := hugolib.NewIntegrationTestBuilder(
200+
hugolib.IntegrationTestConfig{
201+
T: t,
202+
TxtarString: files,
203+
}).Build()
204+
205+
want := `
206+
GetMatch "does/not/exist.txt" unmatched
207+
208+
GetMatch "DIR1/sub/x.txt" | .Content => content in x.txt
209+
GetMatch "DIR1/sub/x.txt" | .Name => DIR1/sub/x.txt
210+
211+
GetMatch "dir2/SUB/y.txt" | .Content => content in y.txt
212+
GetMatch "dir2/SUB/y.txt" | .Name => dir2/SUB/y.txt
213+
214+
GetMatch "dir1/sub/x.txt" | .Content => content in x.txt
215+
GetMatch "dir1/sub/x.txt" | .Name => DIR1/sub/x.txt
216+
217+
GetMatch "dir2/sub/y.txt" | .Content => content in y.txt
218+
GetMatch "dir2/sub/y.txt" | .Name => dir2/SUB/y.txt
219+
220+
GetMatch "DIR1/SUB/X.TXT" | .Content => content in x.txt
221+
GetMatch "DIR1/SUB/X.TXT" | .Name => DIR1/sub/x.txt
222+
223+
GetMatch "DIR2/SUB/Y.TXT" | .Content => content in y.txt
224+
GetMatch "DIR2/SUB/Y.TXT" | .Name => dir2/SUB/y.txt
225+
226+
GetMatch "DIR1/*/x.txt" | .Content => content in x.txt
227+
GetMatch "DIR1/*/x.txt" | .Name => DIR1/sub/x.txt
228+
229+
GetMatch "dir2/*/y.txt" | .Content => content in y.txt
230+
GetMatch "dir2/*/y.txt" | .Name => dir2/SUB/y.txt
231+
232+
GetMatch "dir1/*/x.txt" | .Content => content in x.txt
233+
GetMatch "dir1/*/x.txt" | .Name => DIR1/sub/x.txt
234+
235+
GetMatch "DIR2/*/Y.TXT" | .Content => content in y.txt
236+
GetMatch "DIR2/*/Y.TXT" | .Name => dir2/SUB/y.txt
237+
238+
GetMatch "DIR1/**/x.txt" | .Content => content in x.txt
239+
GetMatch "DIR1/**/x.txt" | .Name => DIR1/sub/x.txt
240+
241+
GetMatch "dir2/**/y.txt" | .Content => content in y.txt
242+
GetMatch "dir2/**/y.txt" | .Name => dir2/SUB/y.txt
243+
244+
GetMatch "dir1/**/x.txt" | .Content => content in x.txt
245+
GetMatch "dir1/**/x.txt" | .Name => DIR1/sub/x.txt
246+
247+
GetMatch "DIR2/**/Y.TXT" | .Content => content in y.txt
248+
GetMatch "DIR2/**/Y.TXT" | .Name => dir2/SUB/y.txt
249+
250+
Match "*/*/*.txt" | len => 2
251+
Match "**/*.txt" | len => 2
252+
`
253+
254+
b.AssertFileContent("public/index.html", want)
255+
256+
}

0 commit comments

Comments
 (0)