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

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 15.663

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 12
cts 17
cp 0.7059
crap 15.663
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
        return static::windowsUnlink($path);
218
    }
219
220
    private static function windowsUnlink(string $path): bool
221
    {
222
        if (is_link($path) && is_dir($path)) {
223
            return rmdir($path);
224
        }
225
226
        try {
227
            return unlink($path);
228
        } catch (Exception $e) {
229
            // Last resort measure for Windows: remove via command DEL.
230
            // We can try remove only file, because when remove directory command DEL removed
231
            // only files, directory not removed.
232
            // @see https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/del
233
            if (is_dir($path) && !static::isEmptyDirectory($path)) {
234
                return false;
235
            }
236
237
            if (function_exists('exec') && file_exists($path)) {
238
                exec('DEL /F/Q ' . escapeshellarg(str_replace('/', '\\', $path)));
239
                return !file_exists($path);
240
            }
241
242
            return false;
243
        }
244
    }
245
246
    public static function isEmptyDirectory(string $path): bool
247
    {
248
        if (!is_dir($path)) {
249
            return false;
250
        }
251
252
        $handle = static::openDirectory($path);
253
        while (($file = readdir($handle)) !== false) {
254
            if ($file != '.' && $file != '..') {
255
                closedir(@$handle);
256
                return false;
257
            }
258
        }
259
        closedir($handle);
260
261
        return true;
262
    }
263
264
    /**
265
     * Copies a whole directory as another one.
266
     *
267
     * The files and sub-directories will also be copied over.
268
     *
269
     * @param string $source the source directory.
270
     * @param string $destination the destination directory.
271
     * @param array $options options for directory copy. Valid options are:
272
     *
273 12
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
274
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment
275 12
     *   setting.
276 12
     * - filter: callback, a PHP callback that is called for each directory or file.
277
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
278 12
     *   The callback can return one of the following values:
279
     *
280 10
     *   * true: the directory or file will be copied (the "only" and "except" options will be ignored).
281
     *   * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored).
282 10
     *   * null: the "only" and "except" options will determine whether the directory or file should be copied.
283
     *
284 9
     * - only: array, list of patterns that the file paths should match if they want to be copied. A path matches a
285
     *   pattern if it contains the pattern string at its end. For example, '.php' matches all file paths ending with
286 9
     *   '.php'.
287 9
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths. If a file path matches a pattern
288 9
     *   in both "only" and "except", it will NOT be copied.
289
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from
290
     *   being copied. A path matches a pattern if it contains the pattern string at its end. Patterns ending with '/'
291 7
     *   apply to directory paths only, and patterns not ending with '/' apply to file paths only. For example, '/a/b'
292 7
     *   matches all file paths ending with '/a/b'; and '.svn/' matches directory paths ending with '.svn'. Note, the
293
     *   '/' characters in a pattern matches both '/' and '\' in the paths.
294 7
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to
295 7
     *   true.
296 7
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
297 2
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. If the callback
298 2
     *   returns false, the copy operation for the sub-directory or file will be cancelled. The signature of the
299
     *   callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to be copied from,
300 7
     *   while `$to` is the copy target.
301 7
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
302 7
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file
303
     *   copied from, while `$to` is the copy target.
304 6
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
305 5
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
306
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
307
     *   `except`. Defaults to true.
308
     *
309
     * @throws \InvalidArgumentException if unable to open directory
310 9
     * @throws \Exception
311 9
     *
312
     * @return void
313
     */
314
    public static function copyDirectory(string $source, string $destination, array $options = []): void
315
    {
316
        $source = static::normalizePath($source);
317
        $destination = static::normalizePath($destination);
318
319
        static::assertNotSelfDirectory($source, $destination);
320
321 12
        $destinationExists = static::setDestination($destination, $options);
322
323 12
        $handle = static::openDirectory($source);
324 2
325
        $options = static::setBasePath($source, $options);
326 10
327
        while (($file = readdir($handle)) !== false) {
328
            if ($file === '.' || $file === '..') {
329
                continue;
330
            }
331
332
            $from = $source . '/' . $file;
333
            $to = $destination . '/' . $file;
334
335
            if (static::filterPath($from, $options)) {
336 33
                if (is_file($from)) {
337
                    if (!$destinationExists) {
338 33
                        static::createDirectory($destination, $options['dirMode'] ?? 0775);
339
                        $destinationExists = true;
340 33
                    }
341 3
                    copy($from, $to);
342
                    if (isset($options['fileMode'])) {
343
                        static::chmod($to, $options['fileMode']);
344 33
                    }
345
                } elseif (!isset($options['recursive']) || $options['recursive']) {
346
                    static::copyDirectory($from, $to, $options);
347
                }
348
            }
349
        }
350
351
        closedir($handle);
352
    }
353
354
    /**
355 9
     * Check copy it self directory.
356
     *
357 9
     * @param string $source
358
     * @param string $destination
359 9
     *
360 9
     * @throws \InvalidArgumentException
361
     */
362
    private static function assertNotSelfDirectory(string $source, string $destination): void
363 9
    {
364
        if ($source === $destination || strpos($destination, $source . '/') === 0) {
365
            throw new \InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
366
        }
367
    }
368
369
    /**
370
     * Open directory handle.
371
     *
372
     * @param string $directory
373
     *
374 10
     * @return resource
375
     * @throws \InvalidArgumentException
376 10
     */
377
    private static function openDirectory(string $directory)
378 10
    {
379 6
        $handle = @opendir($directory);
380 6
381
        if ($handle === false) {
382
            throw new \InvalidArgumentException("Unable to open directory: $directory");
383 10
        }
384
385
        return $handle;
386
    }
387
388
    /**
389
     * Set base path directory.
390
     *
391
     * @param string $source
392
     * @param array $options
393 9
     *
394
     * @return array
395 9
     */
396 9
    private static function setBasePath(string $source, array $options): array
397 9
    {
398
        if (!isset($options['basePath'])) {
399 9
            // this should be done only once
400
            $options['basePath'] = realpath($source);
401
            $options = static::normalizeOptions($options);
402
        }
403
404
        return $options;
405
    }
406
407
    /**
408
     * Set destination directory.
409 9
     *
410
     * @param string $destination
411 9
     * @param array $options
412 9
     *
413
     * @return bool
414
     */
415 9
    private static function setDestination(string $destination, array $options): bool
416
    {
417
        $destinationExists = is_dir($destination);
418
419
        if (!$destinationExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
420
            static::createDirectory($destination, $options['dirMode'] ?? 0775);
421
            $destinationExists = true;
422
        }
423
424
        return $destinationExists;
425 9
    }
426
427 9
    /**
428 1
     * Normalize options.
429 1
     *
430 1
     * @param array $options raw options.
431
     *
432
     * @return array normalized options.
433
     */
434
    protected static function normalizeOptions(array $options): array
435 9
    {
436
        $options = static::setCaseSensitive($options);
437
        $options = static::setExcept($options);
438
        $options = static::setOnly($options);
439
440
        return $options;
441
    }
442
443
    /**
444
     * Set options case sensitive.
445 9
     *
446
     * @param array $options
447 9
     *
448 2
     * @return array
449 2
     */
450 2
    private static function setCaseSensitive(array $options): array
451
    {
452
        if (!array_key_exists('caseSensitive', $options)) {
453
            $options['caseSensitive'] = true;
454
        }
455 9
456
        return $options;
457
    }
458
459
    /**
460
     * Set options except.
461
     *
462
     * @param array $options
463
     *
464
     * @return array
465
     */
466 18
    private static function setExcept(array $options): array
467
    {
468 18
        if (isset($options['except'])) {
469
            foreach ($options['except'] as $key => $value) {
470 18
                if (is_string($value)) {
471 5
                    $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
472 1
                }
473 1
            }
474
        }
475 4
476 4
        return $options;
477 2
    }
478
479
    /**
480
     * Set options only.
481 15
     *
482 3
     * @param array $options
483
     *
484 3
     * @return array
485 15
     */
486 2
    private static function setOnly(array $options): array
487
    {
488
        if (isset($options['only'])) {
489 14
            foreach ($options['only'] as $key => $value) {
490
                if (is_string($value)) {
491
                    $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
492 6
                }
493 6
            }
494
        }
495 6
496 5
        return $options;
497
    }
498
499 10
    /**
500
     * Checks if the given file path satisfies the filtering options.
501
     *
502
     * @param string $path the path of the file or directory to be checked.
503
     * @param array $options the filtering options.
504
     *
505
     * @return bool whether the file or directory satisfies the filtering options.
506
     */
507
    public static function filterPath(string $path, array $options): bool
508
    {
509 7
        $path = str_replace('\\', '/', $path);
510
511 7
        if (isset($options['filter'])) {
512 7
            if (!is_callable($options['filter'])) {
513 7
                $type = gettype($options['filter']);
514 7
                throw new \InvalidArgumentException("Option \"filter\" must be callable, $type given.");
515 7
            }
516
            $result = call_user_func($options['filter'], $path);
517 7
            if (is_bool($result)) {
518 7
                return $result;
519 7
            }
520
        }
521
522
        if (!empty($options['except']) && self::lastExcludeMatchingFromList(
523
            $options['basePath'] ?? '',
524
            $path,
525
            (array)$options['except']
526
        ) !== null) {
527
            return false;
528
        }
529
530
        if (!empty($options['only']) && !is_dir($path)) {
531
            // don't check PATTERN_NEGATIVE since those entries are not prefixed with !
532
            return
533
                self::lastExcludeMatchingFromList(
534
                    $options['basePath'] ?? '',
535
                    $path,
536
                    (array) $options['only']
537
                ) !== null;
538
        }
539
540 8
        return true;
541
    }
542 8
543 8
    /**
544 5
     * Searches for the first wildcard character in the pattern.
545
     *
546
     * @param string $pattern the pattern to search in.
547 8
     *
548 1
     * @return int|bool position of first wildcard character or false if not found.
549 1
     */
550
    private static function firstWildcardInPattern(string $pattern)
551
    {
552
        $wildcards = ['*', '?', '[', '\\'];
553 7
        $wildcardSearch = static function ($carry, $item) use ($pattern) {
554
            $position = strpos($pattern, $item);
555
            if ($position === false) {
556
                return $carry === false ? $position : $carry;
557 7
            }
558 5
            return $carry === false ? $position : min($carry, $position);
559 2
        };
560
        return array_reduce($wildcards, $wildcardSearch, false);
561 3
    }
562
563
564 2
    /**
565 2
     * Scan the given exclude list in reverse to see whether pathname should be ignored.
566
     *
567
     * The first match (i.e. the last on the list), if any, determines the fate.  Returns the element which matched,
568
     * or null for undecided.
569 5
     *
570
     * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
571
     *
572
     * @param string $basePath.
573
     * @param string $path.
574
     * @param array $excludes list of patterns to match $path against.
575
     *
576
     * @return null|array null or one of $excludes item as an array with keys: 'pattern', 'flags'.
577
     *
578
     * @throws \InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern,
579
     *                                   flags, firstWildcard.
580
     */
581
    private static function lastExcludeMatchingFromList(string $basePath, string $path, array $excludes): ?array
582
    {
583
        foreach (array_reverse($excludes) as $exclude) {
584 5
            if (is_string($exclude)) {
585
                $exclude = self::parseExcludePattern($exclude, false);
586 5
            }
587
588
            if (!isset($exclude['pattern'], $exclude['flags'], $exclude['firstWildcard'])) {
589
                throw new \InvalidArgumentException(
590 5
                    'If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'
591
                );
592 5
            }
593 5
594
            if (($exclude['flags'] & self::PATTERN_MUST_BE_DIR) && !is_dir($path)) {
595
                continue;
596
            }
597
598
            if ($exclude['flags'] & self::PATTERN_NO_DIR) {
599 5
                if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
600
                    return $exclude;
601 5
                }
602 5
                continue;
603
            }
604
605 5
            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
606
                return $exclude;
607
            }
608
        }
609
610
        return null;
611
    }
612
613
    /**
614
     * Performs a simple comparison of file or directory names.
615
     *
616
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
617
     *
618
     * @param string $baseName file or directory name to compare with the pattern.
619
     * @param string $pattern the pattern that $baseName will be compared against.
620
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern.
621 2
     * @param int $flags pattern flags
622
     *
623
     * @return bool whether the name matches against pattern
624 2
     */
625
    private static function matchBasename(string $baseName, string $pattern, $firstWildcard, int $flags): bool
626
    {
627
        if ($firstWildcard === false) {
628
            if ($pattern === $baseName) {
629
                return true;
630
            }
631 2
        } elseif ($flags & self::PATTERN_ENDS_WITH) {
632 2
            /* "*literal" matching against "fooliteral" */
633
            $n = StringHelper::byteLength($pattern);
634 2
            if (StringHelper::byteSubstring($pattern, 1, $n) === StringHelper::byteSubstring($baseName, -$n, $n)) {
635 2
                return true;
636 1
            }
637
        }
638
639
640 2
        $wildcardPattern = new WildcardPattern($pattern);
641 1
642
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
643
            $wildcardPattern = $wildcardPattern->ignoreCase();
644 2
        }
645 2
646
        return $wildcardPattern->match($baseName);
647
    }
648 2
649 2
    /**
650
     * Compares a path part against a pattern with optional wildcards.
651
     *
652 2
     * Based on match_pathname() from dir.c of git 1.8.5.3 sources.
653 1
     *
654
     * @param string $path full path to compare
655
     * @param string $basePath base of path that will not be compared
656
     * @param string $pattern the pattern that path part will be compared against
657 2
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
658 2
     * @param int $flags pattern flags
659
     *
660 2
     * @return bool whether the path part matches against pattern
661
     */
662
    private static function matchPathname(string $path, string $basePath, string $pattern, $firstWildcard, int $flags): bool
663
    {
664 2
        // match with FNM_PATHNAME; the pattern has base implicitly in front of it.
665
        if (strpos($pattern, '/') === 0) {
666
            $pattern = StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern));
667
            if ($firstWildcard !== false && $firstWildcard !== 0) {
668
                $firstWildcard--;
669
            }
670
        }
671
672
        $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
673
        $name = StringHelper::byteSubstring($path, -$namelen, $namelen);
674
675 7
        if ($firstWildcard !== 0) {
676
            if ($firstWildcard === false) {
677
                $firstWildcard = StringHelper::byteLength($pattern);
678 7
            }
679 7
680
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
681
            if ($firstWildcard > $namelen) {
682
                return false;
683 7
            }
684
685 7
            if (strncmp($pattern, $name, (int) $firstWildcard)) {
686
                return false;
687
            }
688
689 7
            $pattern = StringHelper::byteSubstring($pattern, (int) $firstWildcard, StringHelper::byteLength($pattern));
690
            $name = StringHelper::byteSubstring($name, (int) $firstWildcard, $namelen);
691
692
            // 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.
693
            if (empty($pattern) && empty($name)) {
694 7
                return true;
695
            }
696
        }
697
698
        $wildcardPattern = (new WildcardPattern($pattern))
699 7
            ->withExactSlashes();
700
701 7
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
702
            $wildcardPattern = $wildcardPattern->ignoreCase();
703 7
        }
704
705 7
        return $wildcardPattern->match($name);
706
    }
707 7
708
    /**
709
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
710
     *
711
     * @param string $pattern
712
     * @param bool $caseSensitive
713
     *
714
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
715
     */
716
    private static function parseExcludePattern(string $pattern, bool $caseSensitive): array
717
    {
718 7
        $result = [
719
            'pattern' => $pattern,
720 7
            'flags' => 0,
721 5
            'firstWildcard' => false,
722
        ];
723
724 7
        $result = static::isCaseInsensitive($caseSensitive, $result);
725
726
        if (!isset($pattern[0])) {
727
            return $result;
728
        }
729
730
        if (strpos($pattern, '!') === 0) {
731
            $result['flags'] |= self::PATTERN_NEGATIVE;
732
            $pattern = StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern));
733
        }
734
735 7
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstring($pattern, -1, 1) === '/') {
736
            $pattern = StringHelper::byteSubstring($pattern, 0, -1);
737 7
            $result['flags'] |= self::PATTERN_MUST_BE_DIR;
738 5
        }
739
740
        $result = static::isPatternNoDir($pattern, $result);
741 7
742
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
743
744
        $result = static::isPatternEndsWith($pattern, $result);
745
746
        $result['pattern'] = $pattern;
747
748
        return $result;
749
    }
750
751
    /**
752 7
     * Check isCaseInsensitive.
753
     *
754 7
     * @param boolean $caseSensitive
755 5
     * @param array $result
756
     *
757
     * @return array
758 7
     */
759
    private static function isCaseInsensitive(bool $caseSensitive, array $result): array
760
    {
761
        if (!$caseSensitive) {
762
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
763
        }
764
765
        return $result;
766
    }
767
768
    /**
769
     * Check pattern no directory.
770
     *
771
     * @param string $pattern
772
     * @param array $result
773
     *
774
     * @return array
775
     */
776
    private static function isPatternNoDir(string $pattern, array $result): array
777
    {
778
        if (strpos($pattern, '/') === false) {
779
            $result['flags'] |= self::PATTERN_NO_DIR;
780
        }
781
782
        return $result;
783
    }
784
785
    /**
786
     * Check pattern ends with
787
     *
788
     * @param string $pattern
789
     * @param array $result
790
     *
791
     * @return array
792
     */
793
    private static function isPatternEndsWith(string $pattern, array $result): array
794
    {
795
        if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern))) === false) {
796
            $result['flags'] |= self::PATTERN_ENDS_WITH;
797
        }
798
799
        return $result;
800
    }
801
}
802