Passed
Push — master ( 50a58b...1d8c1d )
by Alexander
13:54 queued 12:24
created

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 12.7571

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