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

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 12.0292

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