Failed Conditions
Pull Request — master (#25)
by
unknown
02:26
created

Path::canonicalize()   C

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 0
Metric Value
dl 0
loc 60
ccs 25
cts 27
cp 0.9259
rs 6.3453
c 0
b 0
f 0
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 512
    public static function canonicalize($path)
82
    {
83 512
        if ('' === $path) {
84 4
            return '';
85
        }
86
87 511
        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 506
        if (isset(self::$buffer[$path])) {
93 243
            return self::$buffer[$path];
94
        }
95
96
        // Replace "~" with user's home directory.
97 342
        if ('~' === $path[0]) {
98 10
            $path = static::getHomeDirectory().substr($path, 1);
99
        }
100
101 342
        $path = str_replace('\\', '/', $path);
102
103 342
        list($root, $pathWithoutRoot) = self::split($path);
104
105 342
        $parts = explode('/', $pathWithoutRoot);
106 342
        $canonicalParts = array();
107
108
        // Collapse "." and "..", if possible
109 342
        foreach ($parts as $part) {
110 342
            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 333
            if ('..' === $part && count($canonicalParts) > 0
117 333
                    && '..' !== $canonicalParts[count($canonicalParts) - 1]) {
118 72
                array_pop($canonicalParts);
119
120 72
                continue;
121
            }
122
123
            // Only add ".." prefixes for relative paths
124 333
            if ('..' !== $part || '' === $root) {
125 333
                $canonicalParts[] = $part;
126
            }
127
        }
128
129
        // Add the root directory again
130 342
        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
131 342
        ++self::$bufferSize;
132
133
        // Clean up regularly to prevent memory leaks
134 342
        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 342
        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 2
        if (posix_getpwuid(posix_getuid())) {
253 2
            return static::canonicalize(posix_getpwuid(posix_getuid()));
0 ignored issues
show
Documentation introduced by
posix_getpwuid(posix_getuid()) is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
254
        }
255
256
        // For >= Windows8 support
257
        if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
258
            return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
259
        }
260
261
        throw new RuntimeException("Your environment or operation system isn't supported");
262
    }
263
264
    /**
265
     * Returns the root directory of a path.
266
     *
267
     * The result is a canonical path.
268
     *
269
     * @param string $path A path string.
270
     *
271
     * @return string The canonical root directory. Returns an empty string if
272
     *                the given path is relative or empty.
273
     *
274
     * @since 1.0 Added method.
275
     * @since 2.0 Method now fails if $path is not a string.
276
     */
277 18
    public static function getRoot($path)
278
    {
279 18
        if ('' === $path) {
280 1
            return '';
281
        }
282
283 17
        Assert::string($path, 'The path must be a string. Got: %s');
284
285
        // Maintain scheme
286 16 View Code Duplication
        if (false !== ($pos = strpos($path, '://'))) {
287 5
            $scheme = substr($path, 0, $pos + 3);
288 5
            $path = substr($path, $pos + 3);
289
        } else {
290 11
            $scheme = '';
291
        }
292
293
        // UNIX root "/" or "\" (Windows style)
294 16
        if ('/' === $path[0] || '\\' === $path[0]) {
295 6
            return $scheme.'/';
296
        }
297
298 10
        $length = strlen($path);
299
300
        // Windows root
301 10
        if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
302
            // Special case: "C:"
303 8
            if (2 === $length) {
304 2
                return $scheme.$path.'/';
305
            }
306
307
            // Normal case: "C:/ or "C:\"
308 6
            if ('/' === $path[2] || '\\' === $path[2]) {
309 6
                return $scheme.$path[0].$path[1].'/';
310
            }
311
        }
312
313 2
        return '';
314
    }
315
316
    /**
317
     * Returns the file name from a file path.
318
     *
319
     * @param string $path The path string.
320
     *
321
     * @return string The file name.
322
     *
323
     * @since 1.1 Added method.
324
     * @since 2.0 Method now fails if $path is not a string.
325
     */
326 8
    public static function getFilename($path)
327
    {
328 8
        if ('' === $path) {
329 1
            return '';
330
        }
331
332 7
        Assert::string($path, 'The path must be a string. Got: %s');
333
334 6
        return basename($path);
335
    }
336
337
    /**
338
     * Returns the file name without the extension from a file path.
339
     *
340
     * @param string      $path      The path string.
341
     * @param string|null $extension If specified, only that extension is cut
342
     *                               off (may contain leading dot).
343
     *
344
     * @return string The file name without extension.
345
     *
346
     * @since 1.1 Added method.
347
     * @since 2.0 Method now fails if $path or $extension have invalid types.
348
     */
349 20
    public static function getFilenameWithoutExtension($path, $extension = null)
350
    {
351 20
        if ('' === $path) {
352 1
            return '';
353
        }
354
355 19
        Assert::string($path, 'The path must be a string. Got: %s');
356 18
        Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s');
357
358 17
        if (null !== $extension) {
359
            // remove extension and trailing dot
360 10
            return rtrim(basename($path, $extension), '.');
361
        }
362
363 7
        return pathinfo($path, PATHINFO_FILENAME);
364
    }
365
366
    /**
367
     * Returns the extension from a file path.
368
     *
369
     * @param string $path           The path string.
370
     * @param bool   $forceLowerCase Forces the extension to be lower-case
371
     *                               (requires mbstring extension for correct
372
     *                               multi-byte character handling in extension).
373
     *
374
     * @return string The extension of the file path (without leading dot).
375
     *
376
     * @since 1.1 Added method.
377
     * @since 2.0 Method now fails if $path is not a string.
378
     */
379 51
    public static function getExtension($path, $forceLowerCase = false)
380
    {
381 51
        if ('' === $path) {
382 1
            return '';
383
        }
384
385 50
        Assert::string($path, 'The path must be a string. Got: %s');
386
387 47
        $extension = pathinfo($path, PATHINFO_EXTENSION);
388
389 47
        if ($forceLowerCase) {
390 6
            $extension = self::toLower($extension);
391
        }
392
393 47
        return $extension;
394
    }
395
396
    /**
397
     * Returns whether the path has an extension.
398
     *
399
     * @param string            $path       The path string.
400
     * @param string|array|null $extensions If null or not provided, checks if
401
     *                                      an extension exists, otherwise
402
     *                                      checks for the specified extension
403
     *                                      or array of extensions (with or
404
     *                                      without leading dot).
405
     * @param bool              $ignoreCase Whether to ignore case-sensitivity
406
     *                                      (requires mbstring extension for
407
     *                                      correct multi-byte character
408
     *                                      handling in the extension).
409
     *
410
     * @return bool Returns `true` if the path has an (or the specified)
411
     *              extension and `false` otherwise.
412
     *
413
     * @since 1.1 Added method.
414
     * @since 2.0 Method now fails if $path or $extensions have invalid types.
415
     */
416 30
    public static function hasExtension($path, $extensions = null, $ignoreCase = false)
417
    {
418 30
        if ('' === $path) {
419 2
            return false;
420
        }
421
422 28
        $extensions = is_object($extensions) ? array($extensions) : (array) $extensions;
423
424 28
        Assert::allString($extensions, 'The extensions must be strings. Got: %s');
425
426 27
        $actualExtension = self::getExtension($path, $ignoreCase);
427
428
        // Only check if path has any extension
429 26
        if (empty($extensions)) {
430 6
            return '' !== $actualExtension;
431
        }
432
433 20
        foreach ($extensions as $key => $extension) {
434 20
            if ($ignoreCase) {
435 4
                $extension = self::toLower($extension);
436
            }
437
438
            // remove leading '.' in extensions array
439 20
            $extensions[$key] = ltrim($extension, '.');
440
        }
441
442 20
        return in_array($actualExtension, $extensions);
443
    }
444
445
    /**
446
     * Changes the extension of a path string.
447
     *
448
     * @param string $path      The path string with filename.ext to change.
449
     * @param string $extension New extension (with or without leading dot).
450
     *
451
     * @return string The path string with new file extension.
452
     *
453
     * @since 1.1 Added method.
454
     * @since 2.0 Method now fails if $path or $extension is not a string.
455
     */
456 14
    public static function changeExtension($path, $extension)
457
    {
458 14
        if ('' === $path) {
459 1
            return '';
460
        }
461
462 13
        Assert::string($extension, 'The extension must be a string. Got: %s');
463
464 12
        $actualExtension = self::getExtension($path);
465 11
        $extension = ltrim($extension, '.');
466
467
        // No extension for paths
468 11
        if ('/' === substr($path, -1)) {
469 2
            return $path;
470
        }
471
472
        // No actual extension in path
473 9
        if (empty($actualExtension)) {
474 3
            return $path.('.' === substr($path, -1) ? '' : '.').$extension;
475
        }
476
477 6
        return substr($path, 0, -strlen($actualExtension)).$extension;
478
    }
479
480
    /**
481
     * Returns whether a path is absolute.
482
     *
483
     * @param string $path A path string.
484
     *
485
     * @return bool Returns true if the path is absolute, false if it is
486
     *              relative or empty.
487
     *
488
     * @since 1.0 Added method.
489
     * @since 2.0 Method now fails if $path is not a string.
490
     */
491 111
    public static function isAbsolute($path)
492
    {
493 111
        if ('' === $path) {
494 3
            return false;
495
        }
496
497 109
        Assert::string($path, 'The path must be a string. Got: %s');
498
499
        // Strip scheme
500 107
        if (false !== ($pos = strpos($path, '://'))) {
501 22
            $path = substr($path, $pos + 3);
502
        }
503
504
        // UNIX root "/" or "\" (Windows style)
505 107
        if ('/' === $path[0] || '\\' === $path[0]) {
506 60
            return true;
507
        }
508
509
        // Windows root
510 89
        if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
511
            // Special case: "C:"
512 50
            if (2 === strlen($path)) {
513 3
                return true;
514
            }
515
516
            // Normal case: "C:/ or "C:\"
517 47
            if ('/' === $path[2] || '\\' === $path[2]) {
518 45
                return true;
519
            }
520
        }
521
522 61
        return false;
523
    }
524
525
    /**
526
     * Returns whether a path is relative.
527
     *
528
     * @param string $path A path string.
529
     *
530
     * @return bool Returns true if the path is relative or empty, false if
531
     *              it is absolute.
532
     *
533
     * @since 1.0 Added method.
534
     * @since 2.0 Method now fails if $path is not a string.
535
     */
536 16
    public static function isRelative($path)
537
    {
538 16
        return !static::isAbsolute($path);
539
    }
540
541
    /**
542
     * Turns a relative path into an absolute path.
543
     *
544
     * Usually, the relative path is appended to the given base path. Dot
545
     * segments ("." and "..") are removed/collapsed and all slashes turned
546
     * into forward slashes.
547
     *
548
     * ```php
549
     * echo Path::makeAbsolute("../style.css", "/webmozart/puli/css");
550
     * // => /webmozart/puli/style.css
551
     * ```
552
     *
553
     * If an absolute path is passed, that path is returned unless its root
554
     * directory is different than the one of the base path. In that case, an
555
     * exception is thrown.
556
     *
557
     * ```php
558
     * Path::makeAbsolute("/style.css", "/webmozart/puli/css");
559
     * // => /style.css
560
     *
561
     * Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css");
562
     * // => C:/style.css
563
     *
564
     * Path::makeAbsolute("C:/style.css", "/webmozart/puli/css");
565
     * // InvalidArgumentException
566
     * ```
567
     *
568
     * If the base path is not an absolute path, an exception is thrown.
569
     *
570
     * The result is a canonical path.
571
     *
572
     * @param string $path     A path to make absolute.
573
     * @param string $basePath An absolute base path.
574
     *
575
     * @return string An absolute path in canonical form.
576
     *
577
     * @throws InvalidArgumentException If the base path is not absolute or if
578
     *                                  the given path is an absolute path with
579
     *                                  a different root than the base path.
580
     *
581
     * @since 1.0   Added method.
582
     * @since 2.0   Method now fails if $path or $basePath is not a string.
583
     * @since 2.2.2 Method does not fail anymore of $path and $basePath are
584
     *              absolute, but on different partitions.
585
     */
586 82
    public static function makeAbsolute($path, $basePath)
587
    {
588 82
        Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s');
589
590 79
        if (!static::isAbsolute($basePath)) {
591 1
            throw new InvalidArgumentException(sprintf(
592 1
                'The base path "%s" is not an absolute path.',
593 1
                $basePath
594
            ));
595
        }
596
597 78
        if (static::isAbsolute($path)) {
598 22
            return static::canonicalize($path);
599
        }
600
601 55 View Code Duplication
        if (false !== ($pos = strpos($basePath, '://'))) {
602 12
            $scheme = substr($basePath, 0, $pos + 3);
603 12
            $basePath = substr($basePath, $pos + 3);
604
        } else {
605 43
            $scheme = '';
606
        }
607
608 55
        return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
609
    }
610
611
    /**
612
     * Turns a path into a relative path.
613
     *
614
     * The relative path is created relative to the given base path:
615
     *
616
     * ```php
617
     * echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli");
618
     * // => ../style.css
619
     * ```
620
     *
621
     * If a relative path is passed and the base path is absolute, the relative
622
     * path is returned unchanged:
623
     *
624
     * ```php
625
     * Path::makeRelative("style.css", "/webmozart/puli/css");
626
     * // => style.css
627
     * ```
628
     *
629
     * If both paths are relative, the relative path is created with the
630
     * assumption that both paths are relative to the same directory:
631
     *
632
     * ```php
633
     * Path::makeRelative("style.css", "webmozart/puli/css");
634
     * // => ../../../style.css
635
     * ```
636
     *
637
     * If both paths are absolute, their root directory must be the same,
638
     * otherwise an exception is thrown:
639
     *
640
     * ```php
641
     * Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli");
642
     * // InvalidArgumentException
643
     * ```
644
     *
645
     * If the passed path is absolute, but the base path is not, an exception
646
     * is thrown as well:
647
     *
648
     * ```php
649
     * Path::makeRelative("/webmozart/style.css", "webmozart/puli");
650
     * // InvalidArgumentException
651
     * ```
652
     *
653
     * If the base path is not an absolute path, an exception is thrown.
654
     *
655
     * The result is a canonical path.
656
     *
657
     * @param string $path     A path to make relative.
658
     * @param string $basePath A base path.
659
     *
660
     * @return string A relative path in canonical form.
661
     *
662
     * @throws InvalidArgumentException If the base path is not absolute or if
663
     *                                  the given path has a different root
664
     *                                  than the base path.
665
     *
666
     * @since 1.0 Added method.
667
     * @since 2.0 Method now fails if $path or $basePath is not a string.
668
     */
669 97
    public static function makeRelative($path, $basePath)
670
    {
671 97
        Assert::string($basePath, 'The base path must be a string. Got: %s');
672
673 95
        $path = static::canonicalize($path);
674 94
        $basePath = static::canonicalize($basePath);
675
676 94
        list($root, $relativePath) = self::split($path);
677 94
        list($baseRoot, $relativeBasePath) = self::split($basePath);
678
679
        // If the base path is given as absolute path and the path is already
680
        // relative, consider it to be relative to the given absolute path
681
        // already
682 94
        if ('' === $root && '' !== $baseRoot) {
683
            // If base path is already in its root
684 32
            if ('' === $relativeBasePath) {
685 30
                $relativePath = ltrim($relativePath, './\\');
686
            }
687
688 32
            return $relativePath;
689
        }
690
691
        // If the passed path is absolute, but the base path is not, we
692
        // cannot generate a relative path
693 62
        if ('' !== $root && '' === $baseRoot) {
694 2
            throw new InvalidArgumentException(sprintf(
695
                'The absolute path "%s" cannot be made relative to the '.
696
                'relative path "%s". You should provide an absolute base '.
697 2
                'path instead.',
698 2
                $path,
699 2
                $basePath
700
            ));
701
        }
702
703
        // Fail if the roots of the two paths are different
704 60
        if ($baseRoot && $root !== $baseRoot) {
705 18
            throw new InvalidArgumentException(sprintf(
706
                'The path "%s" cannot be made relative to "%s", because they '.
707 18
                'have different roots ("%s" and "%s").',
708 18
                $path,
709 18
                $basePath,
710 18
                $root,
711 18
                $baseRoot
712
            ));
713
        }
714
715 42
        if ('' === $relativeBasePath) {
716 5
            return $relativePath;
717
        }
718
719
        // Build a "../../" prefix with as many "../" parts as necessary
720 37
        $parts = explode('/', $relativePath);
721 37
        $baseParts = explode('/', $relativeBasePath);
722 37
        $dotDotPrefix = '';
723
724
        // Once we found a non-matching part in the prefix, we need to add
725
        // "../" parts for all remaining parts
726 37
        $match = true;
727
728 37
        foreach ($baseParts as $i => $basePart) {
729 37
            if ($match && isset($parts[$i]) && $basePart === $parts[$i]) {
730 21
                unset($parts[$i]);
731
732 21
                continue;
733
            }
734
735 26
            $match = false;
736 26
            $dotDotPrefix .= '../';
737
        }
738
739 37
        return rtrim($dotDotPrefix.implode('/', $parts), '/');
740
    }
741
742
    /**
743
     * Returns whether the given path is on the local filesystem.
744
     *
745
     * @param string $path A path string.
746
     *
747
     * @return bool Returns true if the path is local, false for a URL.
748
     *
749
     * @since 1.0 Added method.
750
     * @since 2.0 Method now fails if $path is not a string.
751
     */
752 6
    public static function isLocal($path)
753
    {
754 6
        Assert::string($path, 'The path must be a string. Got: %s');
755
756 5
        return '' !== $path && false === strpos($path, '://');
757
    }
758
759
    /**
760
     * Returns the longest common base path of a set of paths.
761
     *
762
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
763
     * into forward slashes.
764
     *
765
     * ```php
766
     * $basePath = Path::getLongestCommonBasePath(array(
767
     *     '/webmozart/css/style.css',
768
     *     '/webmozart/css/..'
769
     * ));
770
     * // => /webmozart
771
     * ```
772
     *
773
     * The root is returned if no common base path can be found:
774
     *
775
     * ```php
776
     * $basePath = Path::getLongestCommonBasePath(array(
777
     *     '/webmozart/css/style.css',
778
     *     '/puli/css/..'
779
     * ));
780
     * // => /
781
     * ```
782
     *
783
     * If the paths are located on different Windows partitions, `null` is
784
     * returned.
785
     *
786
     * ```php
787
     * $basePath = Path::getLongestCommonBasePath(array(
788
     *     'C:/webmozart/css/style.css',
789
     *     'D:/webmozart/css/..'
790
     * ));
791
     * // => null
792
     * ```
793
     *
794
     * @param array $paths A list of paths.
795
     *
796
     * @return string|null The longest common base path in canonical form or
797
     *                     `null` if the paths are on different Windows
798
     *                     partitions.
799
     *
800
     * @since 1.0 Added method.
801
     * @since 2.0 Method now fails if $paths are not strings.
802
     */
803 81
    public static function getLongestCommonBasePath(array $paths)
804
    {
805 81
        Assert::allString($paths, 'The paths must be strings. Got: %s');
806
807 80
        list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths)));
808
809 80
        for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
810 75
            list($root, $path) = self::split(self::canonicalize(current($paths)));
811
812
            // If we deal with different roots (e.g. C:/ vs. D:/), it's time
813
            // to quit
814 75
            if ($root !== $bpRoot) {
815 15
                return null;
816
            }
817
818
            // Make the base path shorter until it fits into path
819 60
            while (true) {
820 60
                if ('.' === $basePath) {
821
                    // No more base paths
822 12
                    $basePath = '';
823
824
                    // Next path
825 12
                    continue 2;
826
                }
827
828
                // Prevent false positives for common prefixes
829
                // see isBasePath()
830 60
                if (0 === strpos($path.'/', $basePath.'/')) {
831
                    // Next path
832 48
                    continue 2;
833
                }
834
835 36
                $basePath = dirname($basePath);
836
            }
837
        }
838
839 65
        return $bpRoot.$basePath;
840
    }
841
842
    /**
843
     * Joins two or more path strings.
844
     *
845
     * The result is a canonical path.
846
     *
847
     * @param string[]|string $paths Path parts as parameters or array.
848
     *
849
     * @return string The joint path.
850
     *
851
     * @since 2.0 Added method.
852
     */
853 61
    public static function join($paths)
854
    {
855 61
        if (!is_array($paths)) {
856 52
            $paths = func_get_args();
857
        }
858
859 61
        Assert::allString($paths, 'The paths must be strings. Got: %s');
860
861 60
        $finalPath = null;
862 60
        $wasScheme = false;
863
864 60
        foreach ($paths as $path) {
865 59
            $path = (string) $path;
866
867 59
            if ('' === $path) {
868 6
                continue;
869
            }
870
871 58
            if (null === $finalPath) {
872
                // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
873 58
                $finalPath = $path;
874 58
                $wasScheme = (strpos($path, '://') !== false);
875 58
                continue;
876
            }
877
878
            // Only add slash if previous part didn't end with '/' or '\'
879 53
            if (!in_array(substr($finalPath, -1), array('/', '\\'))) {
880 28
                $finalPath .= '/';
881
            }
882
883
            // If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim
884 53
            $finalPath .= $wasScheme ? $path : ltrim($path, '/');
885 53
            $wasScheme = false;
886
        }
887
888 60
        if (null === $finalPath) {
889 2
            return '';
890
        }
891
892 58
        return self::canonicalize($finalPath);
893
    }
894
895
    /**
896
     * Returns whether a path is a base path of another path.
897
     *
898
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
899
     * into forward slashes.
900
     *
901
     * ```php
902
     * Path::isBasePath('/webmozart', '/webmozart/css');
903
     * // => true
904
     *
905
     * Path::isBasePath('/webmozart', '/webmozart');
906
     * // => true
907
     *
908
     * Path::isBasePath('/webmozart', '/webmozart/..');
909
     * // => false
910
     *
911
     * Path::isBasePath('/webmozart', '/puli');
912
     * // => false
913
     * ```
914
     *
915
     * @param string $basePath The base path to test.
916
     * @param string $ofPath   The other path.
917
     *
918
     * @return bool Whether the base path is a base path of the other path.
919
     *
920
     * @since 1.0 Added method.
921
     * @since 2.0 Method now fails if $basePath or $ofPath is not a string.
922
     */
923 61
    public static function isBasePath($basePath, $ofPath)
924
    {
925 61
        Assert::string($basePath, 'The base path must be a string. Got: %s');
926
927 60
        $basePath = self::canonicalize($basePath);
928 60
        $ofPath = self::canonicalize($ofPath);
929
930
        // Append slashes to prevent false positives when two paths have
931
        // a common prefix, for example /base/foo and /base/foobar.
932
        // Don't append a slash for the root "/", because then that root
933
        // won't be discovered as common prefix ("//" is not a prefix of
934
        // "/foobar/").
935 59
        return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/');
936
    }
937
938
    /**
939
     * Splits a part into its root directory and the remainder.
940
     *
941
     * If the path has no root directory, an empty root directory will be
942
     * returned.
943
     *
944
     * If the root directory is a Windows style partition, the resulting root
945
     * will always contain a trailing slash.
946
     *
947
     * list ($root, $path) = Path::split("C:/webmozart")
948
     * // => array("C:/", "webmozart")
949
     *
950
     * list ($root, $path) = Path::split("C:")
951
     * // => array("C:/", "")
952
     *
953
     * @param string $path The canonical path to split.
954
     *
955
     * @return string[] An array with the root directory and the remaining
956
     *                  relative path.
957
     */
958 407
    private static function split($path)
959
    {
960 407
        if ('' === $path) {
961 3
            return array('', '');
962
        }
963
964
        // Remember scheme as part of the root, if any
965 407 View Code Duplication
        if (false !== ($pos = strpos($path, '://'))) {
966 87
            $root = substr($path, 0, $pos + 3);
967 87
            $path = substr($path, $pos + 3);
968
        } else {
969 336
            $root = '';
970
        }
971
972 407
        $length = strlen($path);
973
974
        // Remove and remember root directory
975 407
        if ('/' === $path[0]) {
976 188
            $root .= '/';
977 188
            $path = $length > 1 ? substr($path, 1) : '';
978 252
        } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
979 194
            if (2 === $length) {
980
                // Windows special case: "C:"
981 1
                $root .= $path.'/';
982 1
                $path = '';
983 193
            } elseif ('/' === $path[2]) {
984
                // Windows normal case: "C:/"..
985 191
                $root .= substr($path, 0, 3);
986 191
                $path = $length > 3 ? substr($path, 3) : '';
987
            }
988
        }
989
990 407
        return array($root, $path);
991
    }
992
993
    /**
994
     * Converts string to lower-case (multi-byte safe if mbstring is installed).
995
     *
996
     * @param string $str The string
997
     *
998
     * @return string Lower case string
999
     */
1000 6
    private static function toLower($str)
1001
    {
1002 6
        if (function_exists('mb_strtolower')) {
1003 6
            return mb_strtolower($str, mb_detect_encoding($str));
1004
        }
1005
1006
        return strtolower($str);
1007
    }
1008
1009
    private function __construct()
1010
    {
1011
    }
1012
}
1013