Path::isRelative()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Filesystem;
13
14
use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
15
use Symfony\Component\Filesystem\Exception\RuntimeException;
16
17
/**
18
 * Contains utility methods for handling path strings.
19
 *
20
 * The methods in this class are able to deal with both UNIX and Windows paths
21
 * with both forward and backward slashes. All methods return normalized parts
22
 * containing only forward slashes and no excess "." and ".." segments.
23
 *
24
 * @author Bernhard Schussek <[email protected]>
25
 * @author Thomas Schulz <[email protected]>
26
 * @author Théo Fidry <[email protected]>
27
 */
28
final class Path
29
{
30
    /**
31
     * The number of buffer entries that triggers a cleanup operation.
32
     */
33
    private const CLEANUP_THRESHOLD = 1250;
34
35
    /**
36
     * The buffer size after the cleanup operation.
37
     */
38
    private const CLEANUP_SIZE = 1000;
39
40
    /**
41
     * Buffers input/output of {@link canonicalize()}.
42
     *
43
     * @var array<string, string>
44
     */
45
    private static array $buffer = [];
46
47
    private static int $bufferSize = 0;
48
49
    /**
50
     * Canonicalizes the given path.
51
     *
52
     * During normalization, all slashes are replaced by forward slashes ("/").
53
     * Furthermore, all "." and ".." segments are removed as far as possible.
54
     * ".." segments at the beginning of relative paths are not removed.
55
     *
56
     * ```php
57
     * echo Path::canonicalize("\symfony\puli\..\css\style.css");
58
     * // => /symfony/css/style.css
59
     *
60
     * echo Path::canonicalize("../css/./style.css");
61
     * // => ../css/style.css
62
     * ```
63
     *
64
     * This method is able to deal with both UNIX and Windows paths.
65
     */
66
    public static function canonicalize(string $path): string
67
    {
68
        if ('' === $path) {
69
            return '';
70
        }
71
72
        // This method is called by many other methods in this class. Buffer
73
        // the canonicalized paths to make up for the severe performance
74
        // decrease.
75
        if (isset(self::$buffer[$path])) {
76
            return self::$buffer[$path];
77
        }
78
79
        // Replace "~" with user's home directory.
80
        if ('~' === $path[0]) {
81
            $path = self::getHomeDirectory().substr($path, 1);
82
        }
83
84
        $path = self::normalize($path);
85
86
        [$root, $pathWithoutRoot] = self::split($path);
87
88
        $canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot);
89
90
        // Add the root directory again
91
        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
92
        ++self::$bufferSize;
93
94
        // Clean up regularly to prevent memory leaks
95
        if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
96
            self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
97
            self::$bufferSize = self::CLEANUP_SIZE;
98
        }
99
100
        return $canonicalPath;
101
    }
102
103
    /**
104
     * Normalizes the given path.
105
     *
106
     * During normalization, all slashes are replaced by forward slashes ("/").
107
     * Contrary to {@link canonicalize()}, this method does not remove invalid
108
     * or dot path segments. Consequently, it is much more efficient and should
109
     * be used whenever the given path is known to be a valid, absolute system
110
     * path.
111
     *
112
     * This method is able to deal with both UNIX and Windows paths.
113
     */
114
    public static function normalize(string $path): string
115
    {
116
        return str_replace('\\', '/', $path);
117
    }
118
119
    /**
120
     * Returns the directory part of the path.
121
     *
122
     * This method is similar to PHP's dirname(), but handles various cases
123
     * where dirname() returns a weird result:
124
     *
125
     *  - dirname() does not accept backslashes on UNIX
126
     *  - dirname("C:/symfony") returns "C:", not "C:/"
127
     *  - dirname("C:/") returns ".", not "C:/"
128
     *  - dirname("C:") returns ".", not "C:/"
129
     *  - dirname("symfony") returns ".", not ""
130
     *  - dirname() does not canonicalize the result
131
     *
132
     * This method fixes these shortcomings and behaves like dirname()
133
     * otherwise.
134
     *
135
     * The result is a canonical path.
136
     *
137
     * @return string The canonical directory part. Returns the root directory
138
     *                if the root directory is passed. Returns an empty string
139
     *                if a relative path is passed that contains no slashes.
140
     *                Returns an empty string if an empty string is passed.
141
     */
142
    public static function getDirectory(string $path): string
143
    {
144
        if ('' === $path) {
145
            return '';
146
        }
147
148
        $path = self::canonicalize($path);
149
150
        // Maintain scheme
151
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
152
            $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
153
            $path = substr($path, $schemeSeparatorPosition + 3);
154
        } else {
155
            $scheme = '';
156
        }
157
158
        if (false === $dirSeparatorPosition = strrpos($path, '/')) {
159
            return '';
160
        }
161
162
        // Directory equals root directory "/"
163
        if (0 === $dirSeparatorPosition) {
164
            return $scheme.'/';
165
        }
166
167
        // Directory equals Windows root "C:/"
168
        if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
169
            return $scheme.substr($path, 0, 3);
170
        }
171
172
        return $scheme.substr($path, 0, $dirSeparatorPosition);
173
    }
174
175
    /**
176
     * Returns canonical path of the user's home directory.
177
     *
178
     * Supported operating systems:
179
     *
180
     *  - UNIX
181
     *  - Windows8 and upper
182
     *
183
     * If your operating system or environment isn't supported, an exception is thrown.
184
     *
185
     * The result is a canonical path.
186
     *
187
     * @throws RuntimeException If your operating system or environment isn't supported
188
     */
189
    public static function getHomeDirectory(): string
190
    {
191
        // For UNIX support
192
        if (getenv('HOME')) {
193
            return self::canonicalize(getenv('HOME'));
194
        }
195
196
        // For >= Windows8 support
197
        if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
198
            return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
199
        }
200
201
        throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported.");
202
    }
203
204
    /**
205
     * Returns the root directory of a path.
206
     *
207
     * The result is a canonical path.
208
     *
209
     * @return string The canonical root directory. Returns an empty string if
210
     *                the given path is relative or empty.
211
     */
212
    public static function getRoot(string $path): string
213
    {
214
        if ('' === $path) {
215
            return '';
216
        }
217
218
        // Maintain scheme
219
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
220
            $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
221
            $path = substr($path, $schemeSeparatorPosition + 3);
222
        } else {
223
            $scheme = '';
224
        }
225
226
        $firstCharacter = $path[0];
227
228
        // UNIX root "/" or "\" (Windows style)
229
        if ('/' === $firstCharacter || '\\' === $firstCharacter) {
230
            return $scheme.'/';
231
        }
232
233
        $length = \strlen($path);
234
235
        // Windows root
236
        if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
237
            // Special case: "C:"
238
            if (2 === $length) {
239
                return $scheme.$path.'/';
240
            }
241
242
            // Normal case: "C:/ or "C:\"
243
            if ('/' === $path[2] || '\\' === $path[2]) {
244
                return $scheme.$firstCharacter.$path[1].'/';
245
            }
246
        }
247
248
        return '';
249
    }
250
251
    /**
252
     * Returns the file name without the extension from a file path.
253
     *
254
     * @param string|null $extension if specified, only that extension is cut
255
     *                               off (may contain leading dot)
256
     */
257
    public static function getFilenameWithoutExtension(string $path, ?string $extension = null): string
258
    {
259
        if ('' === $path) {
260
            return '';
261
        }
262
263
        if (null !== $extension) {
264
            // remove extension and trailing dot
265
            return rtrim(basename($path, $extension), '.');
266
        }
267
268
        return pathinfo($path, \PATHINFO_FILENAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($path, PATHINFO_FILENAME) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
269
    }
270
271
    /**
272
     * Returns the extension from a file path (without leading dot).
273
     *
274
     * @param bool $forceLowerCase forces the extension to be lower-case
275
     */
276
    public static function getExtension(string $path, bool $forceLowerCase = false): string
277
    {
278
        if ('' === $path) {
279
            return '';
280
        }
281
282
        $extension = pathinfo($path, \PATHINFO_EXTENSION);
283
284
        if ($forceLowerCase) {
285
            $extension = self::toLower($extension);
0 ignored issues
show
Bug introduced by
It seems like $extension can also be of type array; however, parameter $string of Symfony\Component\Filesystem\Path::toLower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

285
            $extension = self::toLower(/** @scrutinizer ignore-type */ $extension);
Loading history...
286
        }
287
288
        return $extension;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $extension could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
289
    }
290
291
    /**
292
     * Returns whether the path has an (or the specified) extension.
293
     *
294
     * @param string               $path       the path string
295
     * @param string|string[]|null $extensions if null or not provided, checks if
296
     *                                         an extension exists, otherwise
297
     *                                         checks for the specified extension
298
     *                                         or array of extensions (with or
299
     *                                         without leading dot)
300
     * @param bool                 $ignoreCase whether to ignore case-sensitivity
301
     */
302
    public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool
303
    {
304
        if ('' === $path) {
305
            return false;
306
        }
307
308
        $actualExtension = self::getExtension($path, $ignoreCase);
309
310
        // Only check if path has any extension
311
        if ([] === $extensions || null === $extensions) {
312
            return '' !== $actualExtension;
313
        }
314
315
        if (\is_string($extensions)) {
316
            $extensions = [$extensions];
317
        }
318
319
        foreach ($extensions as $key => $extension) {
320
            if ($ignoreCase) {
321
                $extension = self::toLower($extension);
322
            }
323
324
            // remove leading '.' in extensions array
325
            $extensions[$key] = ltrim($extension, '.');
326
        }
327
328
        return \in_array($actualExtension, $extensions, true);
329
    }
330
331
    /**
332
     * Changes the extension of a path string.
333
     *
334
     * @param string $path      The path string with filename.ext to change.
335
     * @param string $extension new extension (with or without leading dot)
336
     *
337
     * @return string the path string with new file extension
338
     */
339
    public static function changeExtension(string $path, string $extension): string
340
    {
341
        if ('' === $path) {
342
            return '';
343
        }
344
345
        $actualExtension = self::getExtension($path);
346
        $extension = ltrim($extension, '.');
347
348
        // No extension for paths
349
        if (str_ends_with($path, '/')) {
350
            return $path;
351
        }
352
353
        // No actual extension in path
354
        if (!$actualExtension) {
355
            return $path.(str_ends_with($path, '.') ? '' : '.').$extension;
356
        }
357
358
        return substr($path, 0, -\strlen($actualExtension)).$extension;
359
    }
360
361
    public static function isAbsolute(string $path): bool
362
    {
363
        if ('' === $path) {
364
            return false;
365
        }
366
367
        // Strip scheme
368
        if (false !== ($schemeSeparatorPosition = strpos($path, '://')) && 1 !== $schemeSeparatorPosition) {
369
            $path = substr($path, $schemeSeparatorPosition + 3);
370
        }
371
372
        $firstCharacter = $path[0];
373
374
        // UNIX root "/" or "\" (Windows style)
375
        if ('/' === $firstCharacter || '\\' === $firstCharacter) {
376
            return true;
377
        }
378
379
        // Windows root
380
        if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
381
            // Special case: "C:"
382
            if (2 === \strlen($path)) {
383
                return true;
384
            }
385
386
            // Normal case: "C:/ or "C:\"
387
            if ('/' === $path[2] || '\\' === $path[2]) {
388
                return true;
389
            }
390
        }
391
392
        return false;
393
    }
394
395
    public static function isRelative(string $path): bool
396
    {
397
        return !self::isAbsolute($path);
398
    }
399
400
    /**
401
     * Turns a relative path into an absolute path in canonical form.
402
     *
403
     * Usually, the relative path is appended to the given base path. Dot
404
     * segments ("." and "..") are removed/collapsed and all slashes turned
405
     * into forward slashes.
406
     *
407
     * ```php
408
     * echo Path::makeAbsolute("../style.css", "/symfony/puli/css");
409
     * // => /symfony/puli/style.css
410
     * ```
411
     *
412
     * If an absolute path is passed, that path is returned unless its root
413
     * directory is different than the one of the base path. In that case, an
414
     * exception is thrown.
415
     *
416
     * ```php
417
     * Path::makeAbsolute("/style.css", "/symfony/puli/css");
418
     * // => /style.css
419
     *
420
     * Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css");
421
     * // => C:/style.css
422
     *
423
     * Path::makeAbsolute("C:/style.css", "/symfony/puli/css");
424
     * // InvalidArgumentException
425
     * ```
426
     *
427
     * If the base path is not an absolute path, an exception is thrown.
428
     *
429
     * The result is a canonical path.
430
     *
431
     * @param string $basePath an absolute base path
432
     *
433
     * @throws InvalidArgumentException if the base path is not absolute or if
434
     *                                  the given path is an absolute path with
435
     *                                  a different root than the base path
436
     */
437
    public static function makeAbsolute(string $path, string $basePath): string
438
    {
439
        if ('' === $basePath) {
440
            throw new InvalidArgumentException(\sprintf('The base path must be a non-empty string. Got: "%s".', $basePath));
441
        }
442
443
        if (!self::isAbsolute($basePath)) {
444
            throw new InvalidArgumentException(\sprintf('The base path "%s" is not an absolute path.', $basePath));
445
        }
446
447
        if (self::isAbsolute($path)) {
448
            return self::canonicalize($path);
449
        }
450
451
        if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
452
            $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
453
            $basePath = substr($basePath, $schemeSeparatorPosition + 3);
454
        } else {
455
            $scheme = '';
456
        }
457
458
        return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
459
    }
460
461
    /**
462
     * Turns a path into a relative path.
463
     *
464
     * The relative path is created relative to the given base path:
465
     *
466
     * ```php
467
     * echo Path::makeRelative("/symfony/style.css", "/symfony/puli");
468
     * // => ../style.css
469
     * ```
470
     *
471
     * If a relative path is passed and the base path is absolute, the relative
472
     * path is returned unchanged:
473
     *
474
     * ```php
475
     * Path::makeRelative("style.css", "/symfony/puli/css");
476
     * // => style.css
477
     * ```
478
     *
479
     * If both paths are relative, the relative path is created with the
480
     * assumption that both paths are relative to the same directory:
481
     *
482
     * ```php
483
     * Path::makeRelative("style.css", "symfony/puli/css");
484
     * // => ../../../style.css
485
     * ```
486
     *
487
     * If both paths are absolute, their root directory must be the same,
488
     * otherwise an exception is thrown:
489
     *
490
     * ```php
491
     * Path::makeRelative("C:/symfony/style.css", "/symfony/puli");
492
     * // InvalidArgumentException
493
     * ```
494
     *
495
     * If the passed path is absolute, but the base path is not, an exception
496
     * is thrown as well:
497
     *
498
     * ```php
499
     * Path::makeRelative("/symfony/style.css", "symfony/puli");
500
     * // InvalidArgumentException
501
     * ```
502
     *
503
     * If the base path is not an absolute path, an exception is thrown.
504
     *
505
     * The result is a canonical path.
506
     *
507
     * @throws InvalidArgumentException if the base path is not absolute or if
508
     *                                  the given path has a different root
509
     *                                  than the base path
510
     */
511
    public static function makeRelative(string $path, string $basePath): string
512
    {
513
        $path = self::canonicalize($path);
514
        $basePath = self::canonicalize($basePath);
515
516
        [$root, $relativePath] = self::split($path);
517
        [$baseRoot, $relativeBasePath] = self::split($basePath);
518
519
        // If the base path is given as absolute path and the path is already
520
        // relative, consider it to be relative to the given absolute path
521
        // already
522
        if ('' === $root && '' !== $baseRoot) {
523
            // If base path is already in its root
524
            if ('' === $relativeBasePath) {
525
                $relativePath = ltrim($relativePath, './\\');
526
            }
527
528
            return $relativePath;
529
        }
530
531
        // If the passed path is absolute, but the base path is not, we
532
        // cannot generate a relative path
533
        if ('' !== $root && '' === $baseRoot) {
534
            throw new InvalidArgumentException(\sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
535
        }
536
537
        // Fail if the roots of the two paths are different
538
        if ($baseRoot && $root !== $baseRoot) {
539
            throw new InvalidArgumentException(\sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
540
        }
541
542
        if ('' === $relativeBasePath) {
543
            return $relativePath;
544
        }
545
546
        // Build a "../../" prefix with as many "../" parts as necessary
547
        $parts = explode('/', $relativePath);
548
        $baseParts = explode('/', $relativeBasePath);
549
        $dotDotPrefix = '';
550
551
        // Once we found a non-matching part in the prefix, we need to add
552
        // "../" parts for all remaining parts
553
        $match = true;
554
555
        foreach ($baseParts as $index => $basePart) {
556
            if ($match && isset($parts[$index]) && $basePart === $parts[$index]) {
557
                unset($parts[$index]);
558
559
                continue;
560
            }
561
562
            $match = false;
563
            $dotDotPrefix .= '../';
564
        }
565
566
        return rtrim($dotDotPrefix.implode('/', $parts), '/');
567
    }
568
569
    /**
570
     * Returns whether the given path is on the local filesystem.
571
     */
572
    public static function isLocal(string $path): bool
573
    {
574
        return '' !== $path && !str_contains($path, '://');
575
    }
576
577
    /**
578
     * Returns the longest common base path in canonical form of a set of paths or
579
     * `null` if the paths are on different Windows partitions.
580
     *
581
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
582
     * into forward slashes.
583
     *
584
     * ```php
585
     * $basePath = Path::getLongestCommonBasePath(
586
     *     '/symfony/css/style.css',
587
     *     '/symfony/css/..'
588
     * );
589
     * // => /symfony
590
     * ```
591
     *
592
     * The root is returned if no common base path can be found:
593
     *
594
     * ```php
595
     * $basePath = Path::getLongestCommonBasePath(
596
     *     '/symfony/css/style.css',
597
     *     '/puli/css/..'
598
     * );
599
     * // => /
600
     * ```
601
     *
602
     * If the paths are located on different Windows partitions, `null` is
603
     * returned.
604
     *
605
     * ```php
606
     * $basePath = Path::getLongestCommonBasePath(
607
     *     'C:/symfony/css/style.css',
608
     *     'D:/symfony/css/..'
609
     * );
610
     * // => null
611
     * ```
612
     */
613
    public static function getLongestCommonBasePath(string ...$paths): ?string
614
    {
615
        [$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths)));
616
617
        for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
618
            [$root, $path] = self::split(self::canonicalize(current($paths)));
619
620
            // If we deal with different roots (e.g. C:/ vs. D:/), it's time
621
            // to quit
622
            if ($root !== $bpRoot) {
623
                return null;
624
            }
625
626
            // Make the base path shorter until it fits into path
627
            while (true) {
628
                if ('.' === $basePath) {
629
                    // No more base paths
630
                    $basePath = '';
631
632
                    // next path
633
                    continue 2;
634
                }
635
636
                // Prevent false positives for common prefixes
637
                // see isBasePath()
638
                if (str_starts_with($path.'/', $basePath.'/')) {
639
                    // next path
640
                    continue 2;
641
                }
642
643
                $basePath = \dirname($basePath);
644
            }
645
        }
646
647
        return $bpRoot.$basePath;
648
    }
649
650
    /**
651
     * Joins two or more path strings into a canonical path.
652
     */
653
    public static function join(string ...$paths): string
654
    {
655
        $finalPath = null;
656
        $wasScheme = false;
657
658
        foreach ($paths as $path) {
659
            if ('' === $path) {
660
                continue;
661
            }
662
663
            if (null === $finalPath) {
664
                // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
665
                $finalPath = $path;
666
                $wasScheme = str_contains($path, '://');
667
                continue;
668
            }
669
670
            // Only add slash if previous part didn't end with '/' or '\'
671
            if (!\in_array(substr($finalPath, -1), ['/', '\\'], true)) {
0 ignored issues
show
Bug introduced by
$finalPath of type void is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

671
            if (!\in_array(substr(/** @scrutinizer ignore-type */ $finalPath, -1), ['/', '\\'], true)) {
Loading history...
672
                $finalPath .= '/';
673
            }
674
675
            // If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim
676
            $finalPath .= $wasScheme ? $path : ltrim($path, '/');
677
            $wasScheme = false;
678
        }
679
680
        if (null === $finalPath) {
681
            return '';
682
        }
683
684
        return self::canonicalize($finalPath);
685
    }
686
687
    /**
688
     * Returns whether a path is a base path of another path.
689
     *
690
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
691
     * into forward slashes.
692
     *
693
     * ```php
694
     * Path::isBasePath('/symfony', '/symfony/css');
695
     * // => true
696
     *
697
     * Path::isBasePath('/symfony', '/symfony');
698
     * // => true
699
     *
700
     * Path::isBasePath('/symfony', '/symfony/..');
701
     * // => false
702
     *
703
     * Path::isBasePath('/symfony', '/puli');
704
     * // => false
705
     * ```
706
     */
707
    public static function isBasePath(string $basePath, string $ofPath): bool
708
    {
709
        $basePath = self::canonicalize($basePath);
710
        $ofPath = self::canonicalize($ofPath);
711
712
        // Append slashes to prevent false positives when two paths have
713
        // a common prefix, for example /base/foo and /base/foobar.
714
        // Don't append a slash for the root "/", because then that root
715
        // won't be discovered as common prefix ("//" is not a prefix of
716
        // "/foobar/").
717
        return str_starts_with($ofPath.'/', rtrim($basePath, '/').'/');
718
    }
719
720
    /**
721
     * @return string[]
722
     */
723
    private static function findCanonicalParts(string $root, string $pathWithoutRoot): array
724
    {
725
        $parts = explode('/', $pathWithoutRoot);
726
727
        $canonicalParts = [];
728
729
        // Collapse "." and "..", if possible
730
        foreach ($parts as $part) {
731
            if ('.' === $part || '' === $part) {
732
                continue;
733
            }
734
735
            // Collapse ".." with the previous part, if one exists
736
            // Don't collapse ".." if the previous part is also ".."
737
            if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) {
738
                array_pop($canonicalParts);
739
740
                continue;
741
            }
742
743
            // Only add ".." prefixes for relative paths
744
            if ('..' !== $part || '' === $root) {
745
                $canonicalParts[] = $part;
746
            }
747
        }
748
749
        return $canonicalParts;
750
    }
751
752
    /**
753
     * Splits a canonical path into its root directory and the remainder.
754
     *
755
     * If the path has no root directory, an empty root directory will be
756
     * returned.
757
     *
758
     * If the root directory is a Windows style partition, the resulting root
759
     * will always contain a trailing slash.
760
     *
761
     * list ($root, $path) = Path::split("C:/symfony")
762
     * // => ["C:/", "symfony"]
763
     *
764
     * list ($root, $path) = Path::split("C:")
765
     * // => ["C:/", ""]
766
     *
767
     * @return array{string, string} an array with the root directory and the remaining relative path
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string, string} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
768
     */
769
    private static function split(string $path): array
770
    {
771
        if ('' === $path) {
772
            return ['', ''];
773
        }
774
775
        // Remember scheme as part of the root, if any
776
        if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
777
            $root = substr($path, 0, $schemeSeparatorPosition + 3);
778
            $path = substr($path, $schemeSeparatorPosition + 3);
779
        } else {
780
            $root = '';
781
        }
782
783
        $length = \strlen($path);
784
785
        // Remove and remember root directory
786
        if (str_starts_with($path, '/')) {
787
            $root .= '/';
788
            $path = $length > 1 ? substr($path, 1) : '';
789
        } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
790
            if (2 === $length) {
791
                // Windows special case: "C:"
792
                $root .= $path.'/';
793
                $path = '';
794
            } elseif ('/' === $path[2]) {
795
                // Windows normal case: "C:/"..
796
                $root .= substr($path, 0, 3);
797
                $path = $length > 3 ? substr($path, 3) : '';
798
            }
799
        }
800
801
        return [$root, $path];
802
    }
803
804
    private static function toLower(string $string): string
805
    {
806
        if (false !== $encoding = mb_detect_encoding($string, null, true)) {
807
            return mb_strtolower($string, $encoding);
808
        }
809
810
        return strtolower($string);
811
    }
812
813
    private function __construct()
814
    {
815
    }
816
}
817