Path::canonicalize()   C
last analyzed

Complexity

Conditions 13
Paths 22

Size

Total Lines 60
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 13.0687

Importance

Changes 7
Bugs 2 Features 3
Metric Value
c 7
b 2
f 3
dl 0
loc 60
ccs 25
cts 27
cp 0.9259
rs 6.3453
cc 13
eloc 27
nc 22
nop 1
crap 13.0687

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the webmozart/path-util package.
5
 *
6
 * (c) Bernhard Schussek <[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 Webmozart\PathUtil;
13
14
use InvalidArgumentException;
15
use RuntimeException;
16
use Webmozart\Assert\Assert;
17
18
/**
19
 * Contains utility methods for handling path strings.
20
 *
21
 * The methods in this class are able to deal with both UNIX and Windows paths
22
 * with both forward and backward slashes. All methods return normalized parts
23
 * containing only forward slashes and no excess "." and ".." segments.
24
 *
25
 * @since  1.0
26
 *
27
 * @author Bernhard Schussek <[email protected]>
28
 * @author Thomas Schulz <[email protected]>
29
 */
30
final class Path
31
{
32
    /**
33
     * The number of buffer entries that triggers a cleanup operation.
34
     */
35
    const CLEANUP_THRESHOLD = 1250;
36
37
    /**
38
     * The buffer size after the cleanup operation.
39
     */
40
    const CLEANUP_SIZE = 1000;
41
42
    /**
43
     * Buffers input/output of {@link canonicalize()}.
44
     *
45
     * @var array
46
     */
47
    private static $buffer = array();
48
49
    /**
50
     * The size of the buffer.
51
     *
52
     * @var int
53
     */
54
    private static $bufferSize = 0;
55
56
    /**
57
     * Canonicalizes the given path.
58
     *
59
     * During normalization, all slashes are replaced by forward slashes ("/").
60
     * Furthermore, all "." and ".." segments are removed as far as possible.
61
     * ".." segments at the beginning of relative paths are not removed.
62
     *
63
     * ```php
64
     * echo Path::canonicalize("\webmozart\puli\..\css\style.css");
65
     * // => /webmozart/css/style.css
66
     *
67
     * echo Path::canonicalize("../css/./style.css");
68
     * // => ../css/style.css
69
     * ```
70
     *
71
     * This method is able to deal with both UNIX and Windows paths.
72
     *
73
     * @param string $path A path string.
74
     *
75
     * @return string The canonical path.
76
     *
77
     * @since 1.0 Added method.
78
     * @since 2.0 Method now fails if $path is not a string.
79
     * @since 2.1 Added support for `~`.
80
     */
81 511
    public static function canonicalize($path)
82
    {
83 511
        if ('' === $path) {
84 4
            return '';
85
        }
86
87 510
        Assert::string($path, 'The path must be a string. Got: %s');
88
89
        // This method is called by many other methods in this class. Buffer
90
        // the canonicalized paths to make up for the severe performance
91
        // decrease.
92 507
        if (isset(self::$buffer[$path])) {
93 243
            return self::$buffer[$path];
94
        }
95
96
        // Replace "~" with user's home directory.
97 343
        if ('~' === $path[0]) {
98 10
            $path = static::getHomeDirectory().substr($path, 1);
99
        }
100
101 343
        $path = str_replace('\\', '/', $path);
102
103 343
        list($root, $pathWithoutRoot) = self::split($path);
104
105 343
        $parts = explode('/', $pathWithoutRoot);
106 343
        $canonicalParts = array();
107
108
        // Collapse "." and "..", if possible
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
109 343
        foreach ($parts as $part) {
110 343
            if ('.' === $part || '' === $part) {
111 134
                continue;
112
            }
113
114
            // Collapse ".." with the previous part, if one exists
115
            // Don't collapse ".." if the previous part is also ".."
116 334
            if ('..' === $part && count($canonicalParts) > 0
117 334
                    && '..' !== $canonicalParts[count($canonicalParts) - 1]) {
118 72
                array_pop($canonicalParts);
119
120 72
                continue;
121
            }
122
123
            // Only add ".." prefixes for relative paths
124 334
            if ('..' !== $part || '' === $root) {
125 334
                $canonicalParts[] = $part;
126
            }
127
        }
128
129
        // Add the root directory again
130 343
        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
131 343
        ++self::$bufferSize;
132
133
        // Clean up regularly to prevent memory leaks
134 343
        if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
135
            self::$buffer = array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
136
            self::$bufferSize = self::CLEANUP_SIZE;
137
        }
138
139 343
        return $canonicalPath;
140
    }
141
142
    /**
143
     * Normalizes the given path.
144
     *
145
     * During normalization, all slashes are replaced by forward slashes ("/").
146
     * Contrary to {@link canonicalize()}, this method does not remove invalid
147
     * or dot path segments. Consequently, it is much more efficient and should
148
     * be used whenever the given path is known to be a valid, absolute system
149
     * path.
150
     *
151
     * This method is able to deal with both UNIX and Windows paths.
152
     *
153
     * @param string $path A path string.
154
     *
155
     * @return string The normalized path.
156
     *
157
     * @since 2.2 Added method.
158
     */
159 2
    public static function normalize($path)
160
    {
161 2
        Assert::string($path, 'The path must be a string. Got: %s');
162
163 1
        return str_replace('\\', '/', $path);
164
    }
165
166
    /**
167
     * Returns the directory part of the path.
168
     *
169
     * This method is similar to PHP's dirname(), but handles various cases
170
     * where dirname() returns a weird result:
171
     *
172
     *  - dirname() does not accept backslashes on UNIX
173
     *  - dirname("C:/webmozart") returns "C:", not "C:/"
174
     *  - dirname("C:/") returns ".", not "C:/"
175
     *  - dirname("C:") returns ".", not "C:/"
176
     *  - dirname("webmozart") returns ".", not ""
177
     *  - dirname() does not canonicalize the result
178
     *
179
     * This method fixes these shortcomings and behaves like dirname()
180
     * otherwise.
181
     *
182
     * The result is a canonical path.
183
     *
184
     * @param string $path A path string.
185
     *
186
     * @return string The canonical directory part. Returns the root directory
187
     *                if the root directory is passed. Returns an empty string
188
     *                if a relative path is passed that contains no slashes.
189
     *                Returns an empty string if an empty string is passed.
190
     *
191
     * @since 1.0 Added method.
192
     * @since 2.0 Method now fails if $path is not a string.
193
     */
194 41
    public static function getDirectory($path)
195
    {
196 41
        if ('' === $path) {
197 1
            return '';
198
        }
199
200 40
        $path = static::canonicalize($path);
201
202
        // Maintain scheme
203 39 View Code Duplication
        if (false !== ($pos = strpos($path, '://'))) {
204 8
            $scheme = substr($path, 0, $pos + 3);
205 8
            $path = substr($path, $pos + 3);
206
        } else {
207 31
            $scheme = '';
208
        }
209
210 39
        if (false !== ($pos = strrpos($path, '/'))) {
211
            // Directory equals root directory "/"
212 36
            if (0 === $pos) {
213 8
                return $scheme.'/';
214
            }
215
216
            // Directory equals Windows root "C:/"
217 28
            if (2 === $pos && ctype_alpha($path[0]) && ':' === $path[1]) {
218 7
                return $scheme.substr($path, 0, 3);
219
            }
220
221 21
            return $scheme.substr($path, 0, $pos);
222
        }
223
224 3
        return '';
225
    }
226
227
    /**
228
     * Returns canonical path of the user's home directory.
229
     *
230
     * Supported operating systems:
231
     *
232
     *  - UNIX
233
     *  - Windows8 and upper
234
     *
235
     * If your operation system or environment isn't supported, an exception is thrown.
236
     *
237
     * The result is a canonical path.
238
     *
239
     * @return string The canonical home directory
240
     *
241
     * @throws RuntimeException If your operation system or environment isn't supported
242
     *
243
     * @since 2.1 Added method.
244
     */
245 13
    public static function getHomeDirectory()
246
    {
247
        // For UNIX support
248 13
        if (getenv('HOME')) {
249 11
            return static::canonicalize(getenv('HOME'));
250
        }
251
252
        // For >= Windows8 support
253 2
        if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
254 1
            return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
255
        }
256
257 1
        throw new RuntimeException("Your environment or operation system isn't supported");
258
    }
259
260
    /**
261
     * Returns the root directory of a path.
262
     *
263
     * The result is a canonical path.
264
     *
265
     * @param string $path A path string.
266
     *
267
     * @return string The canonical root directory. Returns an empty string if
268
     *                the given path is relative or empty.
269
     *
270
     * @since 1.0 Added method.
271
     * @since 2.0 Method now fails if $path is not a string.
272
     */
273 18
    public static function getRoot($path)
274
    {
275 18
        if ('' === $path) {
276 1
            return '';
277
        }
278
279 17
        Assert::string($path, 'The path must be a string. Got: %s');
280
281
        // Maintain scheme
282 16 View Code Duplication
        if (false !== ($pos = strpos($path, '://'))) {
283 5
            $scheme = substr($path, 0, $pos + 3);
284 5
            $path = substr($path, $pos + 3);
285
        } else {
286 11
            $scheme = '';
287
        }
288
289
        // UNIX root "/" or "\" (Windows style)
290 16
        if ('/' === $path[0] || '\\' === $path[0]) {
291 6
            return $scheme.'/';
292
        }
293
294 10
        $length = strlen($path);
295
296
        // Windows root
297 10
        if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
298
            // Special case: "C:"
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
299 8
            if (2 === $length) {
300 2
                return $scheme.$path.'/';
301
            }
302
303
            // Normal case: "C:/ or "C:\"
304 6
            if ('/' === $path[2] || '\\' === $path[2]) {
305 6
                return $scheme.$path[0].$path[1].'/';
306
            }
307
        }
308
309 2
        return '';
310
    }
311
312
    /**
313
     * Returns the file name from a file path.
314
     *
315
     * @param string $path The path string.
316
     *
317
     * @return string The file name.
318
     *
319
     * @since 1.1 Added method.
320
     * @since 2.0 Method now fails if $path is not a string.
321
     */
322 8
    public static function getFilename($path)
323
    {
324 8
        if ('' === $path) {
325 1
            return '';
326
        }
327
328 7
        Assert::string($path, 'The path must be a string. Got: %s');
329
330 6
        return basename($path);
331
    }
332
333
    /**
334
     * Returns the file name without the extension from a file path.
335
     *
336
     * @param string      $path      The path string.
337
     * @param string|null $extension If specified, only that extension is cut
338
     *                               off (may contain leading dot).
339
     *
340
     * @return string The file name without extension.
341
     *
342
     * @since 1.1 Added method.
343
     * @since 2.0 Method now fails if $path or $extension have invalid types.
344
     */
345 20
    public static function getFilenameWithoutExtension($path, $extension = null)
346
    {
347 20
        if ('' === $path) {
348 1
            return '';
349
        }
350
351 19
        Assert::string($path, 'The path must be a string. Got: %s');
352 18
        Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s');
353
354 17
        if (null !== $extension) {
355
            // remove extension and trailing dot
356 10
            return rtrim(basename($path, $extension), '.');
357
        }
358
359 7
        return pathinfo($path, PATHINFO_FILENAME);
360
    }
361
362
    /**
363
     * Returns the extension from a file path.
364
     *
365
     * @param string $path           The path string.
366
     * @param bool   $forceLowerCase Forces the extension to be lower-case
367
     *                               (requires mbstring extension for correct
368
     *                               multi-byte character handling in extension).
369
     *
370
     * @return string The extension of the file path (without leading dot).
371
     *
372
     * @since 1.1 Added method.
373
     * @since 2.0 Method now fails if $path is not a string.
374
     */
375 51
    public static function getExtension($path, $forceLowerCase = false)
376
    {
377 51
        if ('' === $path) {
378 1
            return '';
379
        }
380
381 50
        Assert::string($path, 'The path must be a string. Got: %s');
382
383 47
        $extension = pathinfo($path, PATHINFO_EXTENSION);
384
385 47
        if ($forceLowerCase) {
386 6
            $extension = self::toLower($extension);
387
        }
388
389 47
        return $extension;
390
    }
391
392
    /**
393
     * Returns whether the path has an extension.
394
     *
395
     * @param string            $path       The path string.
396
     * @param string|array|null $extensions If null or not provided, checks if
397
     *                                      an extension exists, otherwise
398
     *                                      checks for the specified extension
399
     *                                      or array of extensions (with or
400
     *                                      without leading dot).
401
     * @param bool              $ignoreCase Whether to ignore case-sensitivity
402
     *                                      (requires mbstring extension for
403
     *                                      correct multi-byte character
404
     *                                      handling in the extension).
405
     *
406
     * @return bool Returns `true` if the path has an (or the specified)
407
     *              extension and `false` otherwise.
408
     *
409
     * @since 1.1 Added method.
410
     * @since 2.0 Method now fails if $path or $extensions have invalid types.
411
     */
412 30
    public static function hasExtension($path, $extensions = null, $ignoreCase = false)
413
    {
414 30
        if ('' === $path) {
415 2
            return false;
416
        }
417
418 28
        $extensions = is_object($extensions) ? array($extensions) : (array) $extensions;
419
420 28
        Assert::allString($extensions, 'The extensions must be strings. Got: %s');
421
422 27
        $actualExtension = self::getExtension($path, $ignoreCase);
423
424
        // Only check if path has any extension
425 26
        if (empty($extensions)) {
426 6
            return '' !== $actualExtension;
427
        }
428
429 20
        foreach ($extensions as $key => $extension) {
430 20
            if ($ignoreCase) {
431 4
                $extension = self::toLower($extension);
432
            }
433
434
            // remove leading '.' in extensions array
435 20
            $extensions[$key] = ltrim($extension, '.');
436
        }
437
438 20
        return in_array($actualExtension, $extensions);
439
    }
440
441
    /**
442
     * Changes the extension of a path string.
443
     *
444
     * @param string $path      The path string with filename.ext to change.
445
     * @param string $extension New extension (with or without leading dot).
446
     *
447
     * @return string The path string with new file extension.
448
     *
449
     * @since 1.1 Added method.
450
     * @since 2.0 Method now fails if $path or $extension is not a string.
451
     */
452 14
    public static function changeExtension($path, $extension)
453
    {
454 14
        if ('' === $path) {
455 1
            return '';
456
        }
457
458 13
        Assert::string($extension, 'The extension must be a string. Got: %s');
459
460 12
        $actualExtension = self::getExtension($path);
461 11
        $extension = ltrim($extension, '.');
462
463
        // No extension for paths
464 11
        if ('/' === substr($path, -1)) {
465 2
            return $path;
466
        }
467
468
        // No actual extension in path
469 9
        if (empty($actualExtension)) {
470 3
            return $path.('.' === substr($path, -1) ? '' : '.').$extension;
471
        }
472
473 6
        return substr($path, 0, -strlen($actualExtension)).$extension;
474
    }
475
476
    /**
477
     * Returns whether a path is absolute.
478
     *
479
     * @param string $path A path string.
480
     *
481
     * @return bool Returns true if the path is absolute, false if it is
482
     *              relative or empty.
483
     *
484
     * @since 1.0 Added method.
485
     * @since 2.0 Method now fails if $path is not a string.
486
     */
487 111
    public static function isAbsolute($path)
488
    {
489 111
        if ('' === $path) {
490 3
            return false;
491
        }
492
493 109
        Assert::string($path, 'The path must be a string. Got: %s');
494
495
        // Strip scheme
496 107
        if (false !== ($pos = strpos($path, '://'))) {
497 22
            $path = substr($path, $pos + 3);
498
        }
499
500
        // UNIX root "/" or "\" (Windows style)
501 107
        if ('/' === $path[0] || '\\' === $path[0]) {
502 60
            return true;
503
        }
504
505
        // Windows root
506 89
        if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
507
            // Special case: "C:"
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
508 50
            if (2 === strlen($path)) {
509 3
                return true;
510
            }
511
512
            // Normal case: "C:/ or "C:\"
513 47
            if ('/' === $path[2] || '\\' === $path[2]) {
514 45
                return true;
515
            }
516
        }
517
518 61
        return false;
519
    }
520
521
    /**
522
     * Returns whether a path is relative.
523
     *
524
     * @param string $path A path string.
525
     *
526
     * @return bool Returns true if the path is relative or empty, false if
527
     *              it is absolute.
528
     *
529
     * @since 1.0 Added method.
530
     * @since 2.0 Method now fails if $path is not a string.
531
     */
532 16
    public static function isRelative($path)
533
    {
534 16
        return !static::isAbsolute($path);
535
    }
536
537
    /**
538
     * Turns a relative path into an absolute path.
539
     *
540
     * Usually, the relative path is appended to the given base path. Dot
541
     * segments ("." and "..") are removed/collapsed and all slashes turned
542
     * into forward slashes.
543
     *
544
     * ```php
545
     * echo Path::makeAbsolute("../style.css", "/webmozart/puli/css");
546
     * // => /webmozart/puli/style.css
547
     * ```
548
     *
549
     * If an absolute path is passed, that path is returned unless its root
550
     * directory is different than the one of the base path. In that case, an
551
     * exception is thrown.
552
     *
553
     * ```php
554
     * Path::makeAbsolute("/style.css", "/webmozart/puli/css");
555
     * // => /style.css
556
     *
557
     * Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css");
558
     * // => C:/style.css
559
     *
560
     * Path::makeAbsolute("C:/style.css", "/webmozart/puli/css");
561
     * // InvalidArgumentException
562
     * ```
563
     *
564
     * If the base path is not an absolute path, an exception is thrown.
565
     *
566
     * The result is a canonical path.
567
     *
568
     * @param string $path     A path to make absolute.
569
     * @param string $basePath An absolute base path.
570
     *
571
     * @return string An absolute path in canonical form.
572
     *
573
     * @throws InvalidArgumentException If the base path is not absolute or if
574
     *                                  the given path is an absolute path with
575
     *                                  a different root than the base path.
576
     *
577
     * @since 1.0   Added method.
578
     * @since 2.0   Method now fails if $path or $basePath is not a string.
579
     * @since 2.2.2 Method does not fail anymore of $path and $basePath are
580
     *              absolute, but on different partitions.
581
     */
582 82
    public static function makeAbsolute($path, $basePath)
583
    {
584 82
        Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s');
585
586 79
        if (!static::isAbsolute($basePath)) {
587 1
            throw new InvalidArgumentException(sprintf(
588 1
                'The base path "%s" is not an absolute path.',
589
                $basePath
590
            ));
591
        }
592
593 78
        if (static::isAbsolute($path)) {
594 22
            return static::canonicalize($path);
595
        }
596
597 55 View Code Duplication
        if (false !== ($pos = strpos($basePath, '://'))) {
598 12
            $scheme = substr($basePath, 0, $pos + 3);
599 12
            $basePath = substr($basePath, $pos + 3);
600
        } else {
601 43
            $scheme = '';
602
        }
603
604 55
        return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
605
    }
606
607
    /**
608
     * Turns a path into a relative path.
609
     *
610
     * The relative path is created relative to the given base path:
611
     *
612
     * ```php
613
     * echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli");
614
     * // => ../style.css
615
     * ```
616
     *
617
     * If a relative path is passed and the base path is absolute, the relative
618
     * path is returned unchanged:
619
     *
620
     * ```php
621
     * Path::makeRelative("style.css", "/webmozart/puli/css");
622
     * // => style.css
623
     * ```
624
     *
625
     * If both paths are relative, the relative path is created with the
626
     * assumption that both paths are relative to the same directory:
627
     *
628
     * ```php
629
     * Path::makeRelative("style.css", "webmozart/puli/css");
630
     * // => ../../../style.css
631
     * ```
632
     *
633
     * If both paths are absolute, their root directory must be the same,
634
     * otherwise an exception is thrown:
635
     *
636
     * ```php
637
     * Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli");
638
     * // InvalidArgumentException
639
     * ```
640
     *
641
     * If the passed path is absolute, but the base path is not, an exception
642
     * is thrown as well:
643
     *
644
     * ```php
645
     * Path::makeRelative("/webmozart/style.css", "webmozart/puli");
646
     * // InvalidArgumentException
647
     * ```
648
     *
649
     * If the base path is not an absolute path, an exception is thrown.
650
     *
651
     * The result is a canonical path.
652
     *
653
     * @param string $path     A path to make relative.
654
     * @param string $basePath A base path.
655
     *
656
     * @return string A relative path in canonical form.
657
     *
658
     * @throws InvalidArgumentException If the base path is not absolute or if
659
     *                                  the given path has a different root
660
     *                                  than the base path.
661
     *
662
     * @since 1.0 Added method.
663
     * @since 2.0 Method now fails if $path or $basePath is not a string.
664
     */
665 97
    public static function makeRelative($path, $basePath)
666
    {
667 97
        Assert::string($basePath, 'The base path must be a string. Got: %s');
668
669 95
        $path = static::canonicalize($path);
670 94
        $basePath = static::canonicalize($basePath);
671
672 94
        list($root, $relativePath) = self::split($path);
673 94
        list($baseRoot, $relativeBasePath) = self::split($basePath);
674
675
        // If the base path is given as absolute path and the path is already
676
        // relative, consider it to be relative to the given absolute path
677
        // already
678 94
        if ('' === $root && '' !== $baseRoot) {
679
            // If base path is already in its root
680 32
            if ('' === $relativeBasePath) {
681 30
                $relativePath = ltrim($relativePath, './\\');
682
            }
683
684 32
            return $relativePath;
685
        }
686
687
        // If the passed path is absolute, but the base path is not, we
688
        // cannot generate a relative path
689 62
        if ('' !== $root && '' === $baseRoot) {
690 2
            throw new InvalidArgumentException(sprintf(
691
                'The absolute path "%s" cannot be made relative to the '.
692
                'relative path "%s". You should provide an absolute base '.
693 2
                'path instead.',
694
                $path,
695
                $basePath
696
            ));
697
        }
698
699
        // Fail if the roots of the two paths are different
700 60
        if ($baseRoot && $root !== $baseRoot) {
701 18
            throw new InvalidArgumentException(sprintf(
702
                'The path "%s" cannot be made relative to "%s", because they '.
703 18
                'have different roots ("%s" and "%s").',
704
                $path,
705
                $basePath,
706
                $root,
707
                $baseRoot
708
            ));
709
        }
710
711 42
        if ('' === $relativeBasePath) {
712 5
            return $relativePath;
713
        }
714
715
        // Build a "../../" prefix with as many "../" parts as necessary
716 37
        $parts = explode('/', $relativePath);
717 37
        $baseParts = explode('/', $relativeBasePath);
718 37
        $dotDotPrefix = '';
719
720
        // Once we found a non-matching part in the prefix, we need to add
721
        // "../" parts for all remaining parts
722 37
        $match = true;
723
724 37
        foreach ($baseParts as $i => $basePart) {
725 37
            if ($match && isset($parts[$i]) && $basePart === $parts[$i]) {
726 21
                unset($parts[$i]);
727
728 21
                continue;
729
            }
730
731 26
            $match = false;
732 26
            $dotDotPrefix .= '../';
733
        }
734
735 37
        return rtrim($dotDotPrefix.implode('/', $parts), '/');
736
    }
737
738
    /**
739
     * Returns whether the given path is on the local filesystem.
740
     *
741
     * @param string $path A path string.
742
     *
743
     * @return bool Returns true if the path is local, false for a URL.
744
     *
745
     * @since 1.0 Added method.
746
     * @since 2.0 Method now fails if $path is not a string.
747
     */
748 6
    public static function isLocal($path)
749
    {
750 6
        Assert::string($path, 'The path must be a string. Got: %s');
751
752 5
        return '' !== $path && false === strpos($path, '://');
753
    }
754
755
    /**
756
     * Returns the longest common base path of a set of paths.
757
     *
758
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
759
     * into forward slashes.
760
     *
761
     * ```php
762
     * $basePath = Path::getLongestCommonBasePath(array(
763
     *     '/webmozart/css/style.css',
764
     *     '/webmozart/css/..'
765
     * ));
766
     * // => /webmozart
767
     * ```
768
     *
769
     * The root is returned if no common base path can be found:
770
     *
771
     * ```php
772
     * $basePath = Path::getLongestCommonBasePath(array(
773
     *     '/webmozart/css/style.css',
774
     *     '/puli/css/..'
775
     * ));
776
     * // => /
777
     * ```
778
     *
779
     * If the paths are located on different Windows partitions, `null` is
780
     * returned.
781
     *
782
     * ```php
783
     * $basePath = Path::getLongestCommonBasePath(array(
784
     *     'C:/webmozart/css/style.css',
785
     *     'D:/webmozart/css/..'
786
     * ));
787
     * // => null
788
     * ```
789
     *
790
     * @param array $paths A list of paths.
791
     *
792
     * @return string|null The longest common base path in canonical form or
793
     *                     `null` if the paths are on different Windows
794
     *                     partitions.
795
     *
796
     * @since 1.0 Added method.
797
     * @since 2.0 Method now fails if $paths are not strings.
798
     */
799 81
    public static function getLongestCommonBasePath(array $paths)
800
    {
801 81
        Assert::allString($paths, 'The paths must be strings. Got: %s');
802
803 80
        list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths)));
804
805 80
        for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
806 75
            list($root, $path) = self::split(self::canonicalize(current($paths)));
807
808
            // If we deal with different roots (e.g. C:/ vs. D:/), it's time
809
            // to quit
810 75
            if ($root !== $bpRoot) {
811 15
                return null;
812
            }
813
814
            // Make the base path shorter until it fits into path
815 60
            while (true) {
816 60
                if ('.' === $basePath) {
817
                    // No more base paths
818 12
                    $basePath = '';
819
820
                    // Next path
821 12
                    continue 2;
822
                }
823
824
                // Prevent false positives for common prefixes
825
                // see isBasePath()
826 60
                if (0 === strpos($path.'/', $basePath.'/')) {
827
                    // Next path
828 48
                    continue 2;
829
                }
830
831 36
                $basePath = dirname($basePath);
832
            }
833
        }
834
835 65
        return $bpRoot.$basePath;
836
    }
837
838
    /**
839
     * Joins two or more path strings.
840
     *
841
     * The result is a canonical path.
842
     *
843
     * @param string[]|string $paths Path parts as parameters or array.
844
     *
845
     * @return string The joint path.
846
     *
847
     * @since 2.0 Added method.
848
     */
849 61
    public static function join($paths)
850
    {
851 61
        if (!is_array($paths)) {
852 52
            $paths = func_get_args();
853
        }
854
855 61
        Assert::allString($paths, 'The paths must be strings. Got: %s');
856
857 60
        $finalPath = null;
858 60
        $wasScheme = false;
859
860 60
        foreach ($paths as $path) {
861 59
            $path = (string) $path;
862
863 59
            if ('' === $path) {
864 6
                continue;
865
            }
866
867 58
            if (null === $finalPath) {
868
                // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
869 58
                $finalPath = $path;
870 58
                $wasScheme = (strpos($path, '://') !== false);
871 58
                continue;
872
            }
873
874
            // Only add slash if previous part didn't end with '/' or '\'
875 53
            if (!in_array(substr($finalPath, -1), array('/', '\\'))) {
876 28
                $finalPath .= '/';
877
            }
878
879
            // If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim
880 53
            $finalPath .= $wasScheme ? $path : ltrim($path, '/');
881 53
            $wasScheme = false;
882
        }
883
884 60
        if (null === $finalPath) {
885 2
            return '';
886
        }
887
888 58
        return self::canonicalize($finalPath);
889
    }
890
891
    /**
892
     * Returns whether a path is a base path of another path.
893
     *
894
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
895
     * into forward slashes.
896
     *
897
     * ```php
898
     * Path::isBasePath('/webmozart', '/webmozart/css');
899
     * // => true
900
     *
901
     * Path::isBasePath('/webmozart', '/webmozart');
902
     * // => true
903
     *
904
     * Path::isBasePath('/webmozart', '/webmozart/..');
905
     * // => false
906
     *
907
     * Path::isBasePath('/webmozart', '/puli');
908
     * // => false
909
     * ```
910
     *
911
     * @param string $basePath The base path to test.
912
     * @param string $ofPath   The other path.
913
     *
914
     * @return bool Whether the base path is a base path of the other path.
915
     *
916
     * @since 1.0 Added method.
917
     * @since 2.0 Method now fails if $basePath or $ofPath is not a string.
918
     */
919 61
    public static function isBasePath($basePath, $ofPath)
920
    {
921 61
        Assert::string($basePath, 'The base path must be a string. Got: %s');
922
923 60
        $basePath = self::canonicalize($basePath);
924 60
        $ofPath = self::canonicalize($ofPath);
925
926
        // Append slashes to prevent false positives when two paths have
927
        // a common prefix, for example /base/foo and /base/foobar.
928
        // Don't append a slash for the root "/", because then that root
929
        // won't be discovered as common prefix ("//" is not a prefix of
930
        // "/foobar/").
931 59
        return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/');
932
    }
933
934
    /**
935
     * Splits a part into its root directory and the remainder.
936
     *
937
     * If the path has no root directory, an empty root directory will be
938
     * returned.
939
     *
940
     * If the root directory is a Windows style partition, the resulting root
941
     * will always contain a trailing slash.
942
     *
943
     * list ($root, $path) = Path::split("C:/webmozart")
944
     * // => array("C:/", "webmozart")
945
     *
946
     * list ($root, $path) = Path::split("C:")
947
     * // => array("C:/", "")
948
     *
949
     * @param string $path The canonical path to split.
950
     *
951
     * @return string[] An array with the root directory and the remaining
952
     *                  relative path.
953
     */
954 408
    private static function split($path)
955
    {
956 408
        if ('' === $path) {
957 3
            return array('', '');
958
        }
959
960
        // Remember scheme as part of the root, if any
961 408 View Code Duplication
        if (false !== ($pos = strpos($path, '://'))) {
962 87
            $root = substr($path, 0, $pos + 3);
963 87
            $path = substr($path, $pos + 3);
964
        } else {
965 337
            $root = '';
966
        }
967
968 408
        $length = strlen($path);
969
970
        // Remove and remember root directory
971 408
        if ('/' === $path[0]) {
972 188
            $root .= '/';
973 188
            $path = $length > 1 ? substr($path, 1) : '';
974 253
        } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
975 195
            if (2 === $length) {
976
                // Windows special case: "C:"
977 1
                $root .= $path.'/';
978 1
                $path = '';
979 194
            } elseif ('/' === $path[2]) {
980
                // Windows normal case: "C:/"..
981 192
                $root .= substr($path, 0, 3);
982 192
                $path = $length > 3 ? substr($path, 3) : '';
983
            }
984
        }
985
986 408
        return array($root, $path);
987
    }
988
989
    /**
990
     * Converts string to lower-case (multi-byte safe if mbstring is installed).
991
     *
992
     * @param string $str The string
993
     *
994
     * @return string Lower case string
995
     */
996 6
    private static function toLower($str)
997
    {
998 6
        if (function_exists('mb_strtolower')) {
999 6
            return mb_strtolower($str, mb_detect_encoding($str));
1000
        }
1001
1002
        return strtolower($str);
1003
    }
1004
1005
    private function __construct()
1006
    {
1007
    }
1008
}
1009