Passed
Pull Request — master (#17)
by Sergei
01:34
created

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 55
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12.215

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 34
c 1
b 0
f 0
nc 72
nop 5
dl 0
loc 55
ccs 31
cts 35
cp 0.8857
crap 12.215
rs 6.9666

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

650
            echo '$firstWildcard='./** @scrutinizer ignore-type */ $firstWildcard."\n";
Loading history...
651
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
652 1
            if ($firstWildcard > $namelen) {
653 1
                echo 'a';
654 1
                return false;
655
            }
656
657 1
            if (strncmp($pattern, $name, (int) $firstWildcard)) {
658 1
                echo 'b';
659 1
                return false;
660
            }
661
662 1
            $pattern = StringHelper::byteSubstring($pattern, (int) $firstWildcard, StringHelper::byteLength($pattern));
663 1
            $name = StringHelper::byteSubstring($name, (int) $firstWildcard, $namelen);
664
665
            // 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.
666 1
            if (empty($pattern) && empty($name)) {
667 1
                echo 'c';
668 1
                return true;
669
            }
670
        }
671
672 1
        $wildcardPattern = (new WildcardPattern($pattern))
673 1
            ->withExactSlashes();
674
675 1
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
676
            $wildcardPattern = $wildcardPattern->ignoreCase();
677
        }
678 1
        echo 'd';
679 1
        return $wildcardPattern->match($name);
680
    }
681
682
    /**
683
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
684
     *
685
     * @param string $pattern
686
     * @param bool $caseSensitive
687
     *
688
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
689
     */
690 1
    private static function parseExcludePattern(string $pattern, bool $caseSensitive): array
691
    {
692
        $result = [
693 1
            'pattern' => $pattern,
694 1
            'flags' => 0,
695
            'firstWildcard' => false,
696
        ];
697
698 1
        $result = static::isCaseInsensitive($caseSensitive, $result);
699
700 1
        if (!isset($pattern[0])) {
701
            return $result;
702
        }
703
704 1
        if (strpos($pattern, '!') === 0) {
705
            $result['flags'] |= self::PATTERN_NEGATIVE;
706
            $pattern = StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern));
707
        }
708
709 1
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstring($pattern, -1, 1) === '/') {
710
            $pattern = StringHelper::byteSubstring($pattern, 0, -1);
711
            $result['flags'] |= self::PATTERN_MUST_BE_DIR;
712
        }
713
714 1
        $result = static::isPatternNoDir($pattern, $result);
715
716 1
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
717
718 1
        $result = static::isPatternEndsWith($pattern, $result);
719
720 1
        $result['pattern'] = $pattern;
721
722 1
        return $result;
723
    }
724
725
    /**
726
     * Check isCaseInsensitive.
727
     *
728
     * @param boolean $caseSensitive
729
     * @param array $result
730
     *
731
     * @return array
732
     */
733 1
    private static function isCaseInsensitive(bool $caseSensitive, array $result): array
734
    {
735 1
        if (!$caseSensitive) {
736
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
737
        }
738
739 1
        return $result;
740
    }
741
742
    /**
743
     * Check pattern no directory.
744
     *
745
     * @param string $pattern
746
     * @param array $result
747
     *
748
     * @return array
749
     */
750 1
    private static function isPatternNoDir(string $pattern, array $result): array
751
    {
752 1
        if (strpos($pattern, '/') === false) {
753
            $result['flags'] |= self::PATTERN_NO_DIR;
754
        }
755
756 1
        return $result;
757
    }
758
759
    /**
760
     * Check pattern ends with
761
     *
762
     * @param string $pattern
763
     * @param array $result
764
     *
765
     * @return array
766
     */
767 1
    private static function isPatternEndsWith(string $pattern, array $result): array
768
    {
769 1
        if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstring($pattern, 1, StringHelper::byteLength($pattern))) === false) {
770
            $result['flags'] |= self::PATTERN_ENDS_WITH;
771
        }
772
773 1
        return $result;
774
    }
775
}
776