1 module dfmt.globmatch_editorconfig;
2 
3 import std.path : CaseSensitive;
4 import std.range : isForwardRange, ElementEncodingType;
5 import std.string : isSomeChar, isSomeString, empty, save, front, popFront;
6 import std.typecons : Unqual;
7 import std.conv : to;
8 import std.path : filenameCharCmp, isDirSeparator;
9 
10 // From std.path with changes:
11 // * changes meaning to match all characters except '/'
12 // ** added to take over the old meaning of *
13 bool globMatchEditorConfig(CaseSensitive cs = CaseSensitive.osDefault, C, Range)(
14         Range path, const(C)[] pattern) @safe pure nothrow 
15         if (isForwardRange!Range && isSomeChar!(ElementEncodingType!Range)
16             && isSomeChar!C && is(Unqual!C == Unqual!(ElementEncodingType!Range)))
17 in
18 {
19     // Verify that pattern[] is valid
20     import std.algorithm : balancedParens;
21 
22     assert(balancedParens(pattern, '[', ']', 0));
23     assert(balancedParens(pattern, '{', '}', 0));
24 }
25 body
26 {
27     alias RC = Unqual!(ElementEncodingType!Range);
28 
29     static if (RC.sizeof == 1 && isSomeString!Range)
30     {
31         import std.utf : byChar;
32 
33         return globMatchEditorConfig!cs(path.byChar, pattern);
34     }
35     else static if (RC.sizeof == 2 && isSomeString!Range)
36     {
37         import std.utf : byWchar;
38 
39         return globMatchEditorConfig!cs(path.byWchar, pattern);
40     }
41     else
42     {
43         C[] pattmp;
44         foreach (ref pi; 0 .. pattern.length)
45         {
46             const pc = pattern[pi];
47             switch (pc)
48             {
49             case '*':
50                 if (pi < pattern.length - 1 && pattern[pi + 1] == '*')
51                 {
52                     if (pi + 2 == pattern.length)
53                         return true;
54                     for (; !path.empty; path.popFront())
55                     {
56                         auto p = path.save;
57                         if (globMatchEditorConfig!(cs, C)(p, pattern[pi + 2 .. pattern.length]))
58                             return true;
59                     }
60                     return false;
61                 }
62                 else
63                 {
64                     if (pi + 1 == pattern.length)
65                         return true;
66                     for (; !path.empty; path.popFront())
67                     {
68                         auto p = path.save;
69                         //if (p[0].to!dchar.isDirSeparator() && !pattern[pi+1].isDirSeparator())
70                         //    return false;
71                         if (globMatchEditorConfig!(cs, C)(p, pattern[pi + 1 .. pattern.length]))
72                             return true;
73                         if (p[0].to!dchar.isDirSeparator())
74                             return false;
75                     }
76                     return false;
77                 }
78             case '?':
79                 if (path.empty)
80                     return false;
81                 path.popFront();
82                 break;
83 
84             case '[':
85                 if (path.empty)
86                     return false;
87                 auto nc = path.front;
88                 path.popFront();
89                 auto not = false;
90                 ++pi;
91                 if (pattern[pi] == '!')
92                 {
93                     not = true;
94                     ++pi;
95                 }
96                 auto anymatch = false;
97                 while (1)
98                 {
99                     const pc2 = pattern[pi];
100                     if (pc2 == ']')
101                         break;
102                     if (!anymatch && (filenameCharCmp!cs(nc, pc2) == 0))
103                         anymatch = true;
104                     ++pi;
105                 }
106                 if (anymatch == not)
107                     return false;
108                 break;
109 
110             case '{':
111                 // find end of {} section
112                 auto piRemain = pi;
113                 for (; piRemain < pattern.length && pattern[piRemain] != '}'; ++piRemain)
114                 {
115                 }
116 
117                 if (piRemain < pattern.length)
118                     ++piRemain;
119                 ++pi;
120 
121                 while (pi < pattern.length)
122                 {
123                     const pi0 = pi;
124                     C pc3 = pattern[pi];
125                     // find end of current alternative
126                     for (; pi < pattern.length && pc3 != '}' && pc3 != ','; ++pi)
127                     {
128                         pc3 = pattern[pi];
129                     }
130 
131                     auto p = path.save;
132                     if (pi0 == pi)
133                     {
134                         if (globMatchEditorConfig!(cs, C)(p, pattern[piRemain .. $]))
135                         {
136                             return true;
137                         }
138                         ++pi;
139                     }
140                     else
141                     {
142                         /* Match for:
143                             *   pattern[pi0..pi-1] ~ pattern[piRemain..$]
144                             */
145                         if (pattmp.ptr == null) // Allocate this only once per function invocation.
146                             // Should do it with malloc/free, but that would make it impure.
147                             pattmp = new C[pattern.length];
148 
149                         const len1 = pi - 1 - pi0;
150                         pattmp[0 .. len1] = pattern[pi0 .. pi - 1];
151 
152                         const len2 = pattern.length - piRemain;
153                         pattmp[len1 .. len1 + len2] = pattern[piRemain .. $];
154 
155                         if (globMatchEditorConfig!(cs, C)(p, pattmp[0 .. len1 + len2]))
156                         {
157                             return true;
158                         }
159                     }
160                     if (pc3 == '}')
161                     {
162                         break;
163                     }
164                 }
165                 return false;
166 
167             default:
168                 if (path.empty)
169                     return false;
170                 if (filenameCharCmp!cs(pc, path.front) != 0)
171                     return false;
172                 path.popFront();
173                 break;
174             }
175         }
176         return path.empty;
177     }
178 }
179 
180 unittest
181 {
182     assert(globMatchEditorConfig!(CaseSensitive.no)("foo", "Foo"));
183     assert(!globMatchEditorConfig!(CaseSensitive.yes)("foo", "Foo"));
184 
185     assert(globMatchEditorConfig("foo", "*"));
186     assert(globMatchEditorConfig("foo.bar"w, "*"w));
187     assert(globMatchEditorConfig("foo.bar"d, "*.*"d));
188     assert(globMatchEditorConfig("foo.bar", "foo*"));
189     assert(globMatchEditorConfig("foo.bar"w, "f*bar"w));
190     assert(globMatchEditorConfig("foo.bar"d, "f*b*r"d));
191     assert(globMatchEditorConfig("foo.bar", "f???bar"));
192     assert(globMatchEditorConfig("foo.bar"w, "[fg]???bar"w));
193     assert(globMatchEditorConfig("foo.bar"d, "[!gh]*bar"d));
194 
195     assert(!globMatchEditorConfig("foo", "bar"));
196     assert(!globMatchEditorConfig("foo"w, "*.*"w));
197     assert(!globMatchEditorConfig("foo.bar"d, "f*baz"d));
198     assert(!globMatchEditorConfig("foo.bar", "f*b*x"));
199     assert(!globMatchEditorConfig("foo.bar", "[gh]???bar"));
200     assert(!globMatchEditorConfig("foo.bar"w, "[!fg]*bar"w));
201     assert(!globMatchEditorConfig("foo.bar"d, "[fg]???baz"d));
202     assert(!globMatchEditorConfig("foo.di", "*.d")); // test issue 6634: triggered bad assertion
203 
204     assert(globMatchEditorConfig("foo.bar", "{foo,bif}.bar"));
205     assert(globMatchEditorConfig("bif.bar"w, "{foo,bif}.bar"w));
206 
207     assert(globMatchEditorConfig("bar.foo"d, "bar.{foo,bif}"d));
208     assert(globMatchEditorConfig("bar.bif", "bar.{foo,bif}"));
209 
210     assert(globMatchEditorConfig("bar.fooz"w, "bar.{foo,bif}z"w));
211     assert(globMatchEditorConfig("bar.bifz"d, "bar.{foo,bif}z"d));
212 
213     assert(globMatchEditorConfig("bar.foo", "bar.{biz,,baz}foo"));
214     assert(globMatchEditorConfig("bar.foo"w, "bar.{biz,}foo"w));
215     assert(globMatchEditorConfig("bar.foo"d, "bar.{,biz}foo"d));
216     assert(globMatchEditorConfig("bar.foo", "bar.{}foo"));
217 
218     assert(globMatchEditorConfig("bar.foo"w, "bar.{ar,,fo}o"w));
219     assert(globMatchEditorConfig("bar.foo"d, "bar.{,ar,fo}o"d));
220     assert(globMatchEditorConfig("bar.o", "bar.{,ar,fo}o"));
221 
222     assert(!globMatchEditorConfig("foo", "foo?"));
223     assert(!globMatchEditorConfig("foo", "foo[]"));
224     assert(!globMatchEditorConfig("foo", "foob"));
225     assert(!globMatchEditorConfig("foo", "foo{b}"));
226 
227     assert(globMatchEditorConfig(`foo/foo\bar`, "f**b**r"));
228     assert(globMatchEditorConfig("foo", "**"));
229     assert(globMatchEditorConfig("foo/bar", "foo/bar"));
230     assert(globMatchEditorConfig("foo/bar", "foo/*"));
231     assert(globMatchEditorConfig("foo/bar", "*/bar"));
232     assert(globMatchEditorConfig("/foo/bar/gluu/sar.png", "**/sar.png"));
233     assert(globMatchEditorConfig("/foo/bar/gluu/sar.png", "**/*.png"));
234     assert(!globMatchEditorConfig("/foo/bar/gluu/sar.png", "*/sar.png"));
235     assert(!globMatchEditorConfig("/foo/bar/gluu/sar.png", "*/*.png"));
236 
237     static assert(globMatchEditorConfig("foo.bar", "[!gh]*bar"));
238 }