These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | /** |
||
6 | * This file is part of phpDocumentor. |
||
7 | * |
||
8 | * For the full copyright and license information, please view the LICENSE |
||
9 | * file that was distributed with this source code. |
||
10 | * |
||
11 | * Many thanks to webmozart by providing the original code in webmozart/glob |
||
12 | * |
||
13 | * @link https://github.com/webmozart/glob/blob/master/src/Glob.php |
||
14 | * @link http://phpdoc.org |
||
15 | */ |
||
16 | |||
17 | namespace Flyfinder\Specification; |
||
18 | |||
19 | use InvalidArgumentException; |
||
20 | use function array_slice; |
||
21 | use function count; |
||
22 | use function explode; |
||
23 | use function implode; |
||
24 | use function max; |
||
25 | use function min; |
||
26 | use function preg_match; |
||
27 | use function rtrim; |
||
28 | use function sprintf; |
||
29 | use function strlen; |
||
30 | use function strpos; |
||
31 | use function substr; |
||
32 | |||
33 | /** |
||
34 | * Glob specification class |
||
35 | * |
||
36 | * @psalm-immutable |
||
37 | */ |
||
38 | final class Glob extends CompositeSpecification |
||
39 | { |
||
40 | /** @var string */ |
||
41 | private $regex; |
||
42 | |||
43 | /** |
||
44 | * The "static prefix" is the part of the glob up to the first wildcard "*". |
||
45 | * If the glob does not contain wildcards, the full glob is returned. |
||
46 | * |
||
47 | * @var string |
||
48 | */ |
||
49 | private $staticPrefix; |
||
50 | |||
51 | /** |
||
52 | * The "bounded prefix" is the part of the glob up to the first recursive wildcard "**". |
||
53 | * It is the longest prefix for which the number of directory segments in the partial match |
||
54 | * is known. If the glob does not contain the recursive wildcard "**", the full glob is returned. |
||
55 | * |
||
56 | * @var string |
||
57 | */ |
||
58 | private $boundedPrefix; |
||
59 | |||
60 | /** |
||
61 | * The "total prefix" is the part of the glob before the trailing catch-all wildcard sequence if the glob |
||
62 | * ends with one, otherwise null. It is needed for implementing the A-quantifier pruning hint. |
||
63 | * |
||
64 | * @var string|null |
||
65 | */ |
||
66 | private $totalPrefix; |
||
67 | |||
68 | public function __construct(string $glob) |
||
69 | { |
||
70 | $this->regex = self::toRegEx($glob); |
||
71 | $this->staticPrefix = self::getStaticPrefix($glob); |
||
72 | $this->boundedPrefix = self::getBoundedPrefix($glob); |
||
73 | $this->totalPrefix = self::getTotalPrefix($glob); |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * @inheritDoc |
||
78 | */ |
||
79 | public function isSatisfiedBy(array $value) : bool |
||
80 | { |
||
81 | //Flysystem paths are not absolute, so make it that way. |
||
82 | $path = '/' . $value['path']; |
||
83 | if (strpos($path, $this->staticPrefix) !== 0) { |
||
84 | return false; |
||
85 | } |
||
86 | |||
87 | if (preg_match($this->regex, $path)) { |
||
88 | return true; |
||
89 | } |
||
90 | |||
91 | return false; |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * Returns the static prefix of a glob. |
||
96 | * |
||
97 | * The "static prefix" is the part of the glob up to the first wildcard "*". |
||
98 | * If the glob does not contain wildcards, the full glob is returned. |
||
99 | * |
||
100 | * @param string $glob The canonical glob. The glob should contain forward |
||
101 | * slashes as directory separators only. It must not |
||
102 | * contain any "." or ".." segments. |
||
103 | * |
||
104 | * @return string The static prefix of the glob. |
||
105 | * |
||
106 | * @psalm-pure |
||
107 | */ |
||
108 | private static function getStaticPrefix(string $glob) : string |
||
109 | { |
||
110 | self::assertValidGlob($glob); |
||
111 | $prefix = ''; |
||
112 | $length = strlen($glob); |
||
113 | for ($i = 0; $i < $length; ++$i) { |
||
114 | $c = $glob[$i]; |
||
115 | switch ($c) { |
||
116 | case '/': |
||
117 | $prefix .= '/'; |
||
118 | if (self::isRecursiveWildcard($glob, $i)) { |
||
119 | break 2; |
||
120 | } |
||
121 | break; |
||
122 | case '*': |
||
123 | case '?': |
||
124 | case '{': |
||
125 | case '[': |
||
126 | break 2; |
||
127 | case '\\': |
||
128 | [$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i); |
||
0 ignored issues
–
show
The variable
$consumedChars does not exist. Did you forget to declare it?
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.
Loading history...
|
|||
129 | $prefix .= $unescaped; |
||
130 | $i += $consumedChars; |
||
131 | break; |
||
132 | default: |
||
133 | $prefix .= $c; |
||
134 | break; |
||
135 | } |
||
136 | } |
||
137 | return $prefix; |
||
138 | } |
||
139 | |||
140 | private static function getBoundedPrefix(string $glob) : string |
||
141 | { |
||
142 | self::assertValidGlob($glob); |
||
143 | $prefix = ''; |
||
144 | $length = strlen($glob); |
||
145 | |||
146 | for ($i = 0; $i < $length; ++$i) { |
||
147 | $c = $glob[$i]; |
||
148 | switch ($c) { |
||
149 | case '/': |
||
150 | $prefix .= '/'; |
||
151 | if (self::isRecursiveWildcard($glob, $i)) { |
||
152 | break 2; |
||
153 | } |
||
154 | break; |
||
155 | case '\\': |
||
156 | [$unescaped, $consumedChars] = self::scanBackslashSequence($glob, $i); |
||
0 ignored issues
–
show
The variable
$consumedChars does not exist. Did you forget to declare it?
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.
Loading history...
|
|||
157 | $prefix .= $unescaped; |
||
158 | $i += $consumedChars; |
||
159 | break; |
||
160 | default: |
||
161 | $prefix .= $c; |
||
162 | break; |
||
163 | } |
||
164 | } |
||
165 | return $prefix; |
||
166 | } |
||
167 | |||
168 | private static function getTotalPrefix(string $glob) : ?string |
||
169 | { |
||
170 | self::assertValidGlob($glob); |
||
171 | $matches = []; |
||
172 | return preg_match('~(?<!\\\\)/\\*\\*(?:/\\*\\*?)+$~', $glob, $matches) |
||
173 | ? substr($glob, 0, strlen($glob)-strlen($matches[0])) |
||
174 | : null; |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * @return mixed[] |
||
179 | * |
||
180 | * @psalm-return array{0: string, 1:int} |
||
181 | * @psalm-pure |
||
182 | */ |
||
183 | private static function scanBackslashSequence(string $glob, int $offset) : array |
||
184 | { |
||
185 | $startOffset = $offset; |
||
186 | $result = ''; |
||
187 | switch ($c = $glob[$offset + 1] ?? '') { |
||
188 | case '*': |
||
189 | case '?': |
||
190 | case '{': |
||
191 | case '}': |
||
192 | case '[': |
||
193 | case ']': |
||
194 | case '-': |
||
195 | case '^': |
||
196 | case '$': |
||
197 | case '~': |
||
198 | case '\\': |
||
199 | $result .= $c; |
||
200 | ++$offset; |
||
201 | break; |
||
202 | default: |
||
203 | $result .= '\\'; |
||
204 | } |
||
205 | return [$result, $offset - $startOffset]; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Asserts that glob is well formed |
||
210 | * |
||
211 | * @psalm-pure |
||
212 | */ |
||
213 | private static function assertValidGlob(string $glob) : void |
||
214 | { |
||
215 | if (strpos($glob, '/') !== 0 && strpos($glob, '://') === false) { |
||
216 | throw new InvalidArgumentException(sprintf( |
||
217 | 'The glob "%s" is not absolute and not a URI.', |
||
218 | $glob |
||
219 | )); |
||
220 | } |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Checks if the current position the glob is start of a Recursive directory wildcard |
||
225 | * |
||
226 | * @psalm-pure |
||
227 | */ |
||
228 | private static function isRecursiveWildcard(string $glob, int $i) : bool |
||
229 | { |
||
230 | return isset($glob[$i + 3]) && $glob[$i + 1] . $glob[$i + 2] . $glob[$i + 3] === '**/'; |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Converts a glob to a regular expression. |
||
235 | * |
||
236 | * @param string $glob The canonical glob. The glob should contain forward |
||
237 | * slashes as directory separators only. It must not |
||
238 | * contain any "." or ".." segments. |
||
239 | * |
||
240 | * @return string The regular expression for matching the glob. |
||
241 | * |
||
242 | * @psalm-pure |
||
243 | */ |
||
244 | private static function toRegEx(string $glob) : string |
||
245 | { |
||
246 | $delimiter = '~'; |
||
247 | $inSquare = false; |
||
248 | $curlyLevels = 0; |
||
249 | $regex = ''; |
||
250 | $length = strlen($glob); |
||
251 | for ($i = 0; $i < $length; ++$i) { |
||
252 | $c = $glob[$i]; |
||
253 | switch ($c) { |
||
254 | case '.': |
||
255 | case '(': |
||
256 | case ')': |
||
257 | case '|': |
||
258 | case '+': |
||
259 | case '^': |
||
260 | case '$': |
||
261 | case $delimiter: |
||
262 | $regex .= '\\' . $c; |
||
263 | break; |
||
264 | case '/': |
||
265 | if (self::isRecursiveWildcard($glob, $i)) { |
||
266 | $regex .= '/([^/]+/)*'; |
||
267 | $i += 3; |
||
268 | } else { |
||
269 | $regex .= '/'; |
||
270 | } |
||
271 | break; |
||
272 | case '*': |
||
273 | $regex .= '[^/]*'; |
||
274 | break; |
||
275 | case '?': |
||
276 | $regex .= '.'; |
||
277 | break; |
||
278 | case '{': |
||
279 | $regex .= '('; |
||
280 | ++$curlyLevels; |
||
281 | break; |
||
282 | case '}': |
||
283 | if ($curlyLevels > 0) { |
||
284 | $regex .= ')'; |
||
285 | --$curlyLevels; |
||
286 | } else { |
||
287 | $regex .= '}'; |
||
288 | } |
||
289 | break; |
||
290 | case ',': |
||
291 | $regex .= $curlyLevels > 0 ? '|' : ','; |
||
292 | break; |
||
293 | case '[': |
||
294 | $regex .= '['; |
||
295 | $inSquare = true; |
||
296 | if (isset($glob[$i + 1]) && $glob[$i + 1] === '^') { |
||
297 | $regex .= '^'; |
||
298 | ++$i; |
||
299 | } |
||
300 | break; |
||
301 | case ']': |
||
302 | $regex .= $inSquare ? ']' : '\\]'; |
||
303 | $inSquare = false; |
||
304 | break; |
||
305 | case '-': |
||
306 | $regex .= $inSquare ? '-' : '\\-'; |
||
307 | break; |
||
308 | case '\\': |
||
309 | if (isset($glob[$i + 1])) { |
||
310 | switch ($glob[$i + 1]) { |
||
311 | case '*': |
||
312 | case '?': |
||
313 | case '{': |
||
314 | case '}': |
||
315 | case '[': |
||
316 | case ']': |
||
317 | case '-': |
||
318 | case '^': |
||
319 | case '$': |
||
320 | case '~': |
||
321 | case '\\': |
||
322 | $regex .= '\\' . $glob[$i + 1]; |
||
323 | ++$i; |
||
324 | break; |
||
325 | default: |
||
326 | $regex .= '\\\\'; |
||
327 | } |
||
328 | } |
||
329 | break; |
||
330 | default: |
||
331 | $regex .= $c; |
||
332 | break; |
||
333 | } |
||
334 | } |
||
335 | if ($inSquare) { |
||
336 | throw new InvalidArgumentException(sprintf( |
||
337 | 'Invalid glob: missing ] in %s', |
||
338 | $glob |
||
339 | )); |
||
340 | } |
||
341 | if ($curlyLevels > 0) { |
||
342 | throw new InvalidArgumentException(sprintf( |
||
343 | 'Invalid glob: missing } in %s', |
||
344 | $glob |
||
345 | )); |
||
346 | } |
||
347 | return $delimiter . '^' . $regex . '$' . $delimiter; |
||
348 | } |
||
349 | |||
350 | /** @inheritDoc */ |
||
351 | public function canBeSatisfiedBySomethingBelow(array $value) : bool |
||
352 | { |
||
353 | $valueSegments = explode('/', '/' . $value['path']); |
||
354 | $boundedPrefixSegments = explode('/', rtrim($this->boundedPrefix, '/')); |
||
355 | $howManySegmentsToConsider = min(count($valueSegments), count($boundedPrefixSegments)); |
||
356 | $boundedPrefixGlob = implode('/', array_slice($boundedPrefixSegments, 0, $howManySegmentsToConsider)); |
||
357 | $valuePathPrefix = implode('/', array_slice($valueSegments, 1, max($howManySegmentsToConsider-1, 0))); |
||
358 | $prefixValue = $value; |
||
359 | $prefixValue['path'] = $valuePathPrefix; |
||
360 | $spec = new Glob($boundedPrefixGlob); |
||
361 | return $spec->isSatisfiedBy($prefixValue); |
||
362 | } |
||
363 | |||
364 | /** @inheritDoc */ |
||
365 | public function willBeSatisfiedByEverythingBelow(array $value) : bool |
||
366 | { |
||
367 | if ($this->totalPrefix === null) { |
||
368 | return false; |
||
369 | } |
||
370 | $spec = new Glob(rtrim($this->totalPrefix, '/') . '/**/*'); |
||
371 | $terminatedValue = $value; |
||
372 | $terminatedValue['path'] = rtrim($terminatedValue['path'], '/') . '/x/x'; |
||
373 | return $spec->isSatisfiedBy($terminatedValue); |
||
374 | } |
||
375 | } |
||
376 |
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.