Completed
Push — master ( dbd389...46c589 )
by Bernhard
06:23
created

Path::makeAbsolute()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 14

Duplication

Lines 6
Ratio 25 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 5
Bugs 1 Features 3
Metric Value
c 5
b 1
f 3
dl 6
loc 24
ccs 12
cts 12
cp 1
rs 8.6846
cc 4
eloc 14
nc 4
nop 2
crap 4
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
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:"
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:"
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