glob.go (3496B)
1 // Copyright 2021 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package glob
15
16 import (
17 "os"
18 "path"
19 "path/filepath"
20 "runtime"
21 "strings"
22 "sync"
23
24 "github.com/gobwas/glob"
25 "github.com/gobwas/glob/syntax"
26 )
27
28 const filepathSeparator = string(os.PathSeparator)
29
30 var (
31 isWindows = runtime.GOOS == "windows"
32 defaultGlobCache = &globCache{
33 isCaseSensitive: false,
34 isWindows: isWindows,
35 cache: make(map[string]globErr),
36 }
37
38 filenamesGlobCache = &globCache{
39 isCaseSensitive: false, // As long as the search strings are all lower case, this does not allocate.
40 isWindows: isWindows,
41 cache: make(map[string]globErr),
42 }
43 )
44
45 type globErr struct {
46 glob glob.Glob
47 err error
48 }
49
50 type globCache struct {
51 // Config
52 isCaseSensitive bool
53 isWindows bool
54
55 // Cache
56 sync.RWMutex
57 cache map[string]globErr
58 }
59
60 func (gc *globCache) GetGlob(pattern string) (glob.Glob, error) {
61 var eg globErr
62
63 gc.RLock()
64 var found bool
65 eg, found = gc.cache[pattern]
66 gc.RUnlock()
67 if found {
68 return eg.glob, eg.err
69 }
70
71 var g glob.Glob
72 var err error
73
74 pattern = filepath.ToSlash(pattern)
75
76 if gc.isCaseSensitive {
77 g, err = glob.Compile(pattern, '/')
78 } else {
79 g, err = glob.Compile(strings.ToLower(pattern), '/')
80
81 }
82
83 eg = globErr{
84 globDecorator{
85 g: g,
86 isCaseSensitive: gc.isCaseSensitive,
87 isWindows: gc.isWindows},
88 err,
89 }
90
91 gc.Lock()
92 gc.cache[pattern] = eg
93 gc.Unlock()
94
95 return eg.glob, eg.err
96 }
97
98 type globDecorator struct {
99 // Whether both pattern and the strings to match will be matched
100 // by their original case.
101 isCaseSensitive bool
102
103 // On Windows we may get filenames with Windows slashes to match,
104 // which wee need to normalize.
105 isWindows bool
106
107 g glob.Glob
108 }
109
110 func (g globDecorator) Match(s string) bool {
111 if g.isWindows {
112 s = filepath.ToSlash(s)
113 }
114 if !g.isCaseSensitive {
115 s = strings.ToLower(s)
116 }
117 return g.g.Match(s)
118 }
119
120 func GetGlob(pattern string) (glob.Glob, error) {
121 return defaultGlobCache.GetGlob(pattern)
122 }
123
124 func NormalizePath(p string) string {
125 return strings.Trim(path.Clean(filepath.ToSlash(strings.ToLower(p))), "/.")
126 }
127
128 // ResolveRootDir takes a normalized path on the form "assets/**.json" and
129 // determines any root dir, i.e. any start path without any wildcards.
130 func ResolveRootDir(p string) string {
131 parts := strings.Split(path.Dir(p), "/")
132 var roots []string
133 for _, part := range parts {
134 if HasGlobChar(part) {
135 break
136 }
137 roots = append(roots, part)
138 }
139
140 if len(roots) == 0 {
141 return ""
142 }
143
144 return strings.Join(roots, "/")
145 }
146
147 // FilterGlobParts removes any string with glob wildcard.
148 func FilterGlobParts(a []string) []string {
149 b := a[:0]
150 for _, x := range a {
151 if !HasGlobChar(x) {
152 b = append(b, x)
153 }
154 }
155 return b
156 }
157
158 // HasGlobChar returns whether s contains any glob wildcards.
159 func HasGlobChar(s string) bool {
160 for i := 0; i < len(s); i++ {
161 if syntax.Special(s[i]) {
162 return true
163 }
164 }
165 return false
166 }