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