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