Passed
Pull Request — master (#24)
by Sergei
12:12
created

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 18.3287

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 22
c 1
b 0
f 0
nc 72
nop 5
dl 0
loc 44
ccs 11
cts 17
cp 0.6471
crap 18.3287
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Files;
6
7
use Exception;
8
use Yiisoft\Strings\StringHelper;
9
use Yiisoft\Strings\WildcardPattern;
10
11
use function is_array;
12
use function is_string;
13
14
/**
15
 * FileHelper provides useful methods to manage files and directories
16
 */
17
class FileHelper
18
{
19
    /**
20
     * @var int PATTERN_NO_DIR
21
     */
22
    private const PATTERN_NO_DIR = 1;
23
24
    /**
25
     * @var int PATTERN_ENDS_WITH
26
     */
27
    private const PATTERN_ENDS_WITH = 4;
28
29
    /**
30
     * @var int PATTERN_MUST_BE_DIR
31
     */
32
    private const PATTERN_MUST_BE_DIR = 8;
33
34
    /**
35
     * @var int PATTERN_NEGATIVE
36
     */
37
    private const PATTERN_NEGATIVE = 16;
38
39
    /**
40
     * @var int PATTERN_CASE_INSENSITIVE
41
     */
42
    private const PATTERN_CASE_INSENSITIVE = 32;
43
44
    /**
45
     * Creates a new directory.
46
     *
47
     * This method is similar to the PHP `mkdir()` function except that it uses `chmod()` to set the permission of the
48
     * created directory in order to avoid the impact of the `umask` setting.
49
     *
50
     * @param string $path path of the directory to be created.
51
     * @param int $mode the permission to be set for the created directory.
52
     *
53
     * @return bool whether the directory is created successfully.
54 33
     */
55
    public static function createDirectory(string $path, int $mode = 0775): bool
56 33
    {
57
        $path = static::normalizePath($path);
58
59 33
        try {
60 33
            if (!mkdir($path, $mode, true) && !is_dir($path)) {
61
                return false;
62 2
            }
63 2
        } catch (\Exception $e) {
64 1
            if (!is_dir($path)) {
65 1
                throw new \RuntimeException(
66 1
                    "Failed to create directory \"$path\": " . $e->getMessage(),
67
                    $e->getCode(),
68
                    $e
69
                );
70
            }
71
        }
72 33
73
        return static::chmod($path, $mode);
74
    }
75
76
    /**
77
     * Set permissions directory.
78
     *
79
     * @param string $path
80
     * @param integer $mode
81
     *
82
     * @throws \RuntimeException
83
     *
84
     * @return boolean|null
85 33
     */
86
    private static function chmod(string $path, int $mode): ?bool
87
    {
88 33
        try {
89
            return chmod($path, $mode);
90
        } catch (\Exception $e) {
91
            throw new \RuntimeException(
92
                "Failed to change permissions for directory \"$path\": " . $e->getMessage(),
93
                $e->getCode(),
94
                $e
95
            );
96
        }
97
    }
98
99
    /**
100
     * Normalizes a file/directory path.
101
     *
102
     * The normalization does the following work:
103
     *
104
     * - Convert all directory separators into `/` (e.g. "\a/b\c" becomes "/a/b/c")
105
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
106
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
107
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
108
     *
109
     * @param string $path the file/directory path to be normalized
110
     *
111
     * @return string the normalized file/directory path
112 33
     */
113
    public static function normalizePath(string $path): string
114 33
    {
115
        $isWindowsShare = strpos($path, '\\\\') === 0;
116 33
117 1
        if ($isWindowsShare) {
118
            $path = substr($path, 2);
119
        }
120 33
121
        $path = rtrim(strtr($path, '/\\', '//'), '/');
122 33
123 33
        if (strpos('/' . $path, '/.') === false && strpos($path, '//') === false) {
124
            return $isWindowsShare ? "\\\\$path" : $path;
125
        }
126 1
127
        $parts = [];
128 1
129 1
        foreach (explode('/', $path) as $part) {
130 1
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
131 1
                array_pop($parts);
132 1
            } elseif ($part !== '.' && ($part !== '' || empty($parts))) {
133
                $parts[] = $part;
134
            }
135
        }
136 1
137
        $path = implode('/', $parts);
138 1
139 1
        if ($isWindowsShare) {
140
            $path = '\\\\' . $path;
141
        }
142 1
143
        return $path === '' ? '.' : $path;
144
    }
145
146
    /**
147
     * Removes a directory (and all its content) recursively.
148
     *
149
     * @param string $directory the directory to be deleted recursively.
150
     * @param array $options options for directory remove ({@see clearDirectory()}).
151
     *
152
     * @return void
153 33
     */
154
    public static function removeDirectory(string $directory, array $options = []): void
155
    {
156 33
        try {
157 1
            static::clearDirectory($directory, $options);
158 1
        } catch (\InvalidArgumentException $e) {
159
            return;
160
        }
161 33
162 2
        if (is_link($directory)) {
163
            self::unlink($directory);
164 33
        } else {
165
            rmdir($directory);
166 33
        }
167
    }
168
169
    /**
170
     * Clear all directory content.
171
     *
172
     * @param string $directory the directory to be cleared.
173
     * @param array $options options for directory clear . Valid options are:
174
     *
175
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
176
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
177
     *   Only symlink would be removed in that default case.
178
     *
179
     * @throws \InvalidArgumentException if unable to open directory
180
     *
181
     * @return void
182 33
     */
183
    public static function clearDirectory(string $directory, array $options = []): void
184 33
    {
185 33
        $handle = static::openDirectory($directory);
186 33
        if (!empty($options['traverseSymlinks']) || !is_link($directory)) {
187 33
            while (($file = readdir($handle)) !== false) {
188 33
                if ($file === '.' || $file === '..') {
189
                    continue;
190 21
                }
191 21
                $path = $directory . '/' . $file;
192 21
                if (is_dir($path)) {
193
                    self::removeDirectory($path, $options);
194 14
                } else {
195
                    self::unlink($path);
196
                }
197 33
            }
198
            closedir($handle);
199 33
        }
200
    }
201
202
    /**
203
     * Removes a file or symlink in a cross-platform way.
204
     *
205
     * @param string $path
206
     *
207
     * @return bool
208 14
     */
209
    public static function unlink(string $path): bool
210 14
    {
211
        $isWindows = DIRECTORY_SEPARATOR === '\\';
212 14
213 14
        if (!$isWindows) {
214
            return unlink($path);
215
        }
216
217
        if (is_link($path) && is_dir($path)) {
218
            return rmdir($path);
219
        }
220
221
        try {
222
            return unlink($path);
223
        } catch (Exception $e) {
224
            // last resort measure for Windows
225
            if (!static::isEmptyDirectory($path)) {
226
                return false;
227
            }
228
229
            // @see https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/del
230
            if (function_exists('exec') && file_exists($path)) {
231
                exec('DEL /F/Q ' . escapeshellarg(str_replace('/', '\\', $path)));
232
                return !file_exists($path);
233
            }
234
235
            return false;
236
        }
237
    }
238
239
    public static function isEmptyDirectory(string $path): bool
240
    {
241
        if (!is_dir($path)) {
242
            return false;
243
        }
244
245
        $handle = static::openDirectory($path);
246
        while (($file = readdir($handle)) !== false) {
247
            if ($file != '.' && $file != '..') {
248
                closedir(@$handle);
249
                return false;
250
            }
251
        }
252
        closedir($handle);
253
254
        return true;
255
    }
256
257
    /**
258
     * Copies a whole directory as another one.
259
     *
260
     * The files and sub-directories will also be copied over.
261
     *
262
     * @param string $source the source directory.
263
     * @param string $destination the destination directory.
264
     * @param array $options options for directory copy. Valid options are:
265
     *
266
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
267
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment
268
     *   setting.
269
     * - filter: callback, a PHP callback that is called for each directory or file.
270
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
271
     *   The callback can return one of the following values:
272
     *
273 12
     *   * true: the directory or file will be copied (the "only" and "except" options will be ignored).
274
     *   * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored).
275 12
     *   * null: the "only" and "except" options will determine whether the directory or file should be copied.
276 12
     *
277
     * - only: array, list of patterns that the file paths should match if they want to be copied. A path matches a
278 12
     *   pattern if it contains the pattern string at its end. For example, '.php' matches all file paths ending with
279
     *   '.php'.
280 10
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths. If a file path matches a pattern
281
     *   in both "only" and "except", it will NOT be copied.
282 10
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from
283
     *   being copied. A path matches a pattern if it contains the pattern string at its end. Patterns ending with '/'
284 9
     *   apply to directory paths only, and patterns not ending with '/' apply to file paths only. For example, '/a/b'
285
     *   matches all file paths ending with '/a/b'; and '.svn/' matches directory paths ending with '.svn'. Note, the
286 9
     *   '/' characters in a pattern matches both '/' and '\' in the paths.
287 9
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to
288 9
     *   true.
289
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
290
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. If the callback
291 7
     *   returns false, the copy operation for the sub-directory or file will be cancelled. The signature of the
292 7
     *   callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to be copied from,
293
     *   while `$to` is the copy target.
294 7
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
295 7
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file
296 7
     *   copied from, while `$to` is the copy target.
297 2
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
298 2
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
299
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
300 7
     *   `except`. Defaults to true.
301 7
     *
302 7
     * @throws \InvalidArgumentException if unable to open directory
303
     * @throws \Exception
304 6
     *
305 5
     * @return void
306
     */
307
    public static function copyDirectory(string $source, string $destination, array $options = []): void
308
    {
309
        $source = static::normalizePath($source);
310 9
        $destination = static::normalizePath($destination);
311 9
312
        static::assertNotSelfDirectory($source, $destination);
313
314
        $destinationExists = static::setDestination($destination, $options);
315
316
        $handle = static::openDirectory($source);
317
318
        $options = static::setBasePath($source, $options);
319
320
        while (($file = readdir($handle)) !== false) {
321 12
            if ($file === '.' || $file === '..') {
322
                continue;
323 12
            }
324 2
325
            $from = $source . '/' . $file;
326 10
            $to = $destination . '/' . $file;
327
328
            if (static::filterPath($from, $options)) {
329
                if (is_file($from)) {
330
                    if (!$destinationExists) {
331
                        static::createDirectory($destination, $options['dirMode'] ?? 0775);
332
                        $destinationExists = true;
333
                    }
334
                    copy($from, $to);
335
                    if (isset($options['fileMode'])) {
336 33
                        static::chmod($to, $options['fileMode']);
337
                    }
338 33
                } elseif (!isset($options['recursive']) || $options['recursive']) {
339
                    static::copyDirectory($from, $to, $options);
340 33
                }
341 3
            }
342
        }
343
344 33
        closedir($handle);
345
    }
346
347
    /**
348
     * Check copy it self directory.
349
     *
350
     * @param string $source
351
     * @param string $destination
352
     *
353
     * @throws \InvalidArgumentException
354
     */
355 9
    private static function assertNotSelfDirectory(string $source, string $destination): void
356
    {
357 9
        if ($source === $destination || strpos($destination, $source . '/') === 0) {
358
            throw new \InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
359 9
        }
360 9
    }
361
362
    /**
363 9
     * Open directory handle.
364
     *
365
     * @param string $directory
366
     *
367
     * @return resource
368
     * @throws \InvalidArgumentException
369
     */
370
    private static function openDirectory(string $directory)
371
    {
372
        $handle = @opendir($directory);
373
374 10
        if ($handle === false) {
375
            throw new \InvalidArgumentException("Unable to open directory: $directory");
376 10
        }
377
378 10
        return $handle;
379 6
    }
380 6
381
    /**
382
     * Set base path directory.
383 10
     *
384
     * @param string $source
385
     * @param array $options
386
     *
387
     * @return array
388
     */
389
    private static function setBasePath(string $source, array $options): array
390
    {
391
        if (!isset($options['basePath'])) {
392
            // this should be done only once
393 9
            $options['basePath'] = realpath($source);
394
            $options = static::normalizeOptions($options);
395 9
        }
396 9
397 9
        return $options;
398
    }
399 9
400
    /**
401
     * Set destination directory.
402
     *
403
     * @param string $destination
404
     * @param array $options
405
     *
406
     * @return bool
407
     */
408
    private static function setDestination(string $destination, array $options): bool
409 9
    {
410
        $destinationExists = is_dir($destination);
411 9
412 9
        if (!$destinationExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
413
            static::createDirectory($destination, $options['dirMode'] ?? 0775);
414
            $destinationExists = true;
415 9
        }
416
417
        return $destinationExists;
418
    }
419
420
    /**
421
     * Normalize options.
422
     *
423
     * @param array $options raw options.
424
     *
425 9
     * @return array normalized options.
426
     */
427 9
    protected static function normalizeOptions(array $options): array
428 1
    {
429 1
        $options = static::setCaseSensitive($options);
430 1
        $options = static::setExcept($options);
431
        $options = static::setOnly($options);
432
433
        return $options;
434
    }
435 9
436
    /**
437
     * Set options case sensitive.
438
     *
439
     * @param array $options
440
     *
441
     * @return array
442
     */
443
    private static function setCaseSensitive(array $options): array
444
    {
445 9
        if (!array_key_exists('caseSensitive', $options)) {
446
            $options['caseSensitive'] = true;
447 9
        }
448 2
449 2
        return $options;
450 2
    }
451
452
    /**
453
     * Set options except.
454
     *
455 9
     * @param array $options
456
     *
457
     * @return array
458
     */
459
    private static function setExcept(array $options): array
460
    {
461
        if (isset($options['except'])) {
462
            foreach ($options['except'] as $key => $value) {
463
                if (is_string($value)) {
464
                    $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
465
                }
466 18
            }
467
        }
468 18
469
        return $options;
470 18
    }
471 5
472 1
    /**
473 1
     * Set options only.
474
     *
475 4
     * @param array $options
476 4
     *
477 2
     * @return array
478
     */
479
    private static function setOnly(array $options): array
480
    {
481 15
        if (isset($options['only'])) {
482 3
            foreach ($options['only'] as $key => $value) {
483
                if (is_string($value)) {
484 3
                    $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
485 15
                }
486 2
            }
487
        }
488
489 14
        return $options;
490
    }
491
492 6
    /**
493 6
     * Checks if the given file path satisfies the filtering options.
494
     *
495 6
     * @param string $path the path of the file or directory to be checked.
496 5
     * @param array $options the filtering options.
497
     *
498
     * @return bool whether the file or directory satisfies the filtering options.
499 10
     */
500
    public static function filterPath(string $path, array $options): bool
501
    {
502
        $path = str_replace('\\', '/', $path);
503
504
        if (isset($options['filter'])) {
505
            if (!is_callable($options['filter'])) {
506
                $type = gettype($options['filter']);
507
                throw new \InvalidArgumentException("Option \"filter\" must be callable, $type given.");
508
            }
509 7
            $result = call_user_func($options['filter'], $path);
510
            if (is_bool($result)) {
511 7
                return $result;
512 7
            }
513 7
        }
514 7
515 7
        if (!empty($options['except']) && self::lastExcludeMatchingFromList(
516
            $options['basePath'] ?? '',
517 7
            $path,
518 7
            (array)$options['except']
519 7
        ) !== null) {
520
            return false;
521
        }
522
523
        if (!empty($options['only']) && !is_dir($path)) {
524
            // don't check PATTERN_NEGATIVE since those entries are not prefixed with !
525
            return
526
                self::lastExcludeMatchingFromList(
527
                    $options['basePath'] ?? '',
528
                    $path,
529
                    (array) $options['only']
530
                ) !== null;
531
        }
532
533
        return true;
534
    }
535
536
    /**
537
     * Searches for the first wildcard character in the pattern.
538
     *
539
     * @param string $pattern the pattern to search in.
540 8
     *
541
     * @return int|bool position of first wildcard character or false if not found.
542 8
     */
543 8
    private static function firstWildcardInPattern(string $pattern)
544 5
    {
545
        $wildcards = ['*', '?', '[', '\\'];
546
        $wildcardSearch = static function ($carry, $item) use ($pattern) {
547 8
            $position = strpos($pattern, $item);
548 1
            if ($position === false) {
549 1
                return $carry === false ? $position : $carry;
550
            }
551
            return $carry === false ? $position : min($carry, $position);
552
        };
553 7
        return array_reduce($wildcards, $wildcardSearch, false);
554
    }
555
556
557 7
    /**
558 5
     * Scan the given exclude list in reverse to see whether pathname should be ignored.
559 2
     *
560
     * The first match (i.e. the last on the list), if any, determines the fate.  Returns the element which matched,
561 3
     * or null for undecided.
562
     *
563
     * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
564 2
     *
565 2
     * @param string $basePath.
566
     * @param string $path.
567
     * @param array $excludes list of patterns to match $path against.
568
     *
569 5
     * @return null|array null or one of $excludes item as an array with keys: 'pattern', 'flags'.
570
     *
571
     * @throws \InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern,
572
     *                                   flags, firstWildcard.
573
     */
574
    private static function lastExcludeMatchingFromList(string $basePath, string $path, array $excludes): ?array
575
    {
576
        foreach (array_reverse($excludes) as $exclude) {
577
            if (is_string($exclude)) {
578
                $exclude = self::parseExcludePattern($exclude, false);
579
            }
580
581
            if (!isset($exclude['pattern'], $exclude['flags'], $exclude['firstWildcard'])) {
582
                throw new \InvalidArgumentException(
583
                    'If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'
584 5
                );
585
            }
586 5
587
            if (($exclude['flags'] & self::PATTERN_MUST_BE_DIR) && !is_dir($path)) {
588
                continue;
589
            }
590 5
591
            if ($exclude['flags'] & self::PATTERN_NO_DIR) {
592 5
                if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
593 5
                    return $exclude;
594
                }
595
                continue;
596
            }
597
598
            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
599 5
                return $exclude;
600
            }
601 5
        }
602 5
603
        return null;
604
    }
605 5
606
    /**
607
     * Performs a simple comparison of file or directory names.
608
     *
609
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
610
     *
611
     * @param string $baseName file or directory name to compare with the pattern.
612
     * @param string $pattern the pattern that $baseName will be compared against.
613
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern.
614
     * @param int $flags pattern flags
615
     *
616
     * @return bool whether the name matches against pattern
617
     */
618
    private static function matchBasename(string $baseName, string $pattern, $firstWildcard, int $flags): bool
619
    {
620
        if ($firstWildcard === false) {
621 2
            if ($pattern === $baseName) {
622
                return true;
623
            }
624 2
        } elseif ($flags & self::PATTERN_ENDS_WITH) {
625
            /* "*literal" matching against "fooliteral" */
626
            $n = StringHelper::byteLength($pattern);
627
            if (StringHelper::byteSubstring($pattern, 1, $n) === StringHelper::byteSubstring($baseName, -$n, $n)) {
628
                return true;
629
            }
630
        }
631 2
632 2
633
        $wildcardPattern = new WildcardPattern($pattern);
634 2
635 2
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
636 1
            $wildcardPattern = $wildcardPattern->ignoreCase();
637
        }
638
639
        return $wildcardPattern->match($baseName);
640 2
    }
641 1
642
    /**
643
     * Compares a path part against a pattern with optional wildcards.
644 2
     *
645 2
     * Based on match_pathname() from dir.c of git 1.8.5.3 sources.
646
     *
647
     * @param string $path full path to compare
648 2
     * @param string $basePath base of path that will not be compared
649 2
     * @param string $pattern the pattern that path part will be compared against
650
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
651
     * @param int $flags pattern flags
652 2
     *
653 1
     * @return bool whether the path part matches against pattern
654
     */
655
    private static function matchPathname(string $path, string $basePath, string $pattern, $firstWildcard, int $flags): bool
656
    {
657 2
        // match with FNM_PATHNAME; the pattern has base implicitly in front of it.
658 2
        if (strpos($pattern, '/') === 0) {
659
            $pattern = StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern));
660 2
            if ($firstWildcard !== false && $firstWildcard !== 0) {
661
                $firstWildcard--;
662
            }
663
        }
664 2
665
        $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
666
        $name = StringHelper::byteSubstring($path, -$namelen, $namelen);
667
668
        if ($firstWildcard !== 0) {
669
            if ($firstWildcard === false) {
670
                $firstWildcard = StringHelper::byteLength($pattern);
671
            }
672
673
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
674
            if ($firstWildcard > $namelen) {
675 7
                return false;
676
            }
677
678 7
            if (strncmp($pattern, $name, (int) $firstWildcard)) {
679 7
                return false;
680
            }
681
682
            $pattern = StringHelper::byteSubstring($pattern, (int) $firstWildcard, StringHelper::byteLength($pattern));
683 7
            $name = StringHelper::byteSubstring($name, (int) $firstWildcard, $namelen);
684
685 7
            // If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all.
686
            if (empty($pattern) && empty($name)) {
687
                return true;
688
            }
689 7
        }
690
691
        $wildcardPattern = (new WildcardPattern($pattern))
692
            ->withExactSlashes();
693
694 7
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
695
            $wildcardPattern = $wildcardPattern->ignoreCase();
696
        }
697
698
        return $wildcardPattern->match($name);
699 7
    }
700
701 7
    /**
702
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
703 7
     *
704
     * @param string $pattern
705 7
     * @param bool $caseSensitive
706
     *
707 7
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
708
     */
709
    private static function parseExcludePattern(string $pattern, bool $caseSensitive): array
710
    {
711
        $result = [
712
            'pattern' => $pattern,
713
            'flags' => 0,
714
            'firstWildcard' => false,
715
        ];
716
717
        $result = static::isCaseInsensitive($caseSensitive, $result);
718 7
719
        if (!isset($pattern[0])) {
720 7
            return $result;
721 5
        }
722
723
        if (strpos($pattern, '!') === 0) {
724 7
            $result['flags'] |= self::PATTERN_NEGATIVE;
725
            $pattern = StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern));
726
        }
727
728
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstring($pattern, -1, 1) === '/') {
729
            $pattern = StringHelper::byteSubstring($pattern, 0, -1);
730
            $result['flags'] |= self::PATTERN_MUST_BE_DIR;
731
        }
732
733
        $result = static::isPatternNoDir($pattern, $result);
734
735 7
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
736
737 7
        $result = static::isPatternEndsWith($pattern, $result);
738 5
739
        $result['pattern'] = $pattern;
740
741 7
        return $result;
742
    }
743
744
    /**
745
     * Check isCaseInsensitive.
746
     *
747
     * @param boolean $caseSensitive
748
     * @param array $result
749
     *
750
     * @return array
751
     */
752 7
    private static function isCaseInsensitive(bool $caseSensitive, array $result): array
753
    {
754 7
        if (!$caseSensitive) {
755 5
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
756
        }
757
758 7
        return $result;
759
    }
760
761
    /**
762
     * Check pattern no directory.
763
     *
764
     * @param string $pattern
765
     * @param array $result
766
     *
767
     * @return array
768
     */
769
    private static function isPatternNoDir(string $pattern, array $result): array
770
    {
771
        if (strpos($pattern, '/') === false) {
772
            $result['flags'] |= self::PATTERN_NO_DIR;
773
        }
774
775
        return $result;
776
    }
777
778
    /**
779
     * Check pattern ends with
780
     *
781
     * @param string $pattern
782
     * @param array $result
783
     *
784
     * @return array
785
     */
786
    private static function isPatternEndsWith(string $pattern, array $result): array
787
    {
788
        if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern))) === false) {
789
            $result['flags'] |= self::PATTERN_ENDS_WITH;
790
        }
791
792
        return $result;
793
    }
794
}
795