Version 1
[yaffs-website] / vendor / webmozart / path-util / src / Path.php
1 <?php
2
3 /*
4  * This file is part of the webmozart/path-util package.
5  *
6  * (c) Bernhard Schussek <bschussek@gmail.com>
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 <bschussek@gmail.com>
28  * @author Thomas Schulz <mail@king2500.net>
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     public static function canonicalize($path)
82     {
83         if ('' === $path) {
84             return '';
85         }
86
87         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         if (isset(self::$buffer[$path])) {
93             return self::$buffer[$path];
94         }
95
96         // Replace "~" with user's home directory.
97         if ('~' === $path[0]) {
98             $path = static::getHomeDirectory().substr($path, 1);
99         }
100
101         $path = str_replace('\\', '/', $path);
102
103         list($root, $pathWithoutRoot) = self::split($path);
104
105         $parts = explode('/', $pathWithoutRoot);
106         $canonicalParts = array();
107
108         // Collapse "." and "..", if possible
109         foreach ($parts as $part) {
110             if ('.' === $part || '' === $part) {
111                 continue;
112             }
113
114             // Collapse ".." with the previous part, if one exists
115             // Don't collapse ".." if the previous part is also ".."
116             if ('..' === $part && count($canonicalParts) > 0
117                     && '..' !== $canonicalParts[count($canonicalParts) - 1]) {
118                 array_pop($canonicalParts);
119
120                 continue;
121             }
122
123             // Only add ".." prefixes for relative paths
124             if ('..' !== $part || '' === $root) {
125                 $canonicalParts[] = $part;
126             }
127         }
128
129         // Add the root directory again
130         self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
131         ++self::$bufferSize;
132
133         // Clean up regularly to prevent memory leaks
134         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         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     public static function normalize($path)
160     {
161         Assert::string($path, 'The path must be a string. Got: %s');
162
163         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     public static function getDirectory($path)
195     {
196         if ('' === $path) {
197             return '';
198         }
199
200         $path = static::canonicalize($path);
201
202         // Maintain scheme
203         if (false !== ($pos = strpos($path, '://'))) {
204             $scheme = substr($path, 0, $pos + 3);
205             $path = substr($path, $pos + 3);
206         } else {
207             $scheme = '';
208         }
209
210         if (false !== ($pos = strrpos($path, '/'))) {
211             // Directory equals root directory "/"
212             if (0 === $pos) {
213                 return $scheme.'/';
214             }
215
216             // Directory equals Windows root "C:/"
217             if (2 === $pos && ctype_alpha($path[0]) && ':' === $path[1]) {
218                 return $scheme.substr($path, 0, 3);
219             }
220
221             return $scheme.substr($path, 0, $pos);
222         }
223
224         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     public static function getHomeDirectory()
246     {
247         // For UNIX support
248         if (getenv('HOME')) {
249             return static::canonicalize(getenv('HOME'));
250         }
251
252         // For >= Windows8 support
253         if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
254             return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
255         }
256
257         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     public static function getRoot($path)
274     {
275         if ('' === $path) {
276             return '';
277         }
278
279         Assert::string($path, 'The path must be a string. Got: %s');
280
281         // Maintain scheme
282         if (false !== ($pos = strpos($path, '://'))) {
283             $scheme = substr($path, 0, $pos + 3);
284             $path = substr($path, $pos + 3);
285         } else {
286             $scheme = '';
287         }
288
289         // UNIX root "/" or "\" (Windows style)
290         if ('/' === $path[0] || '\\' === $path[0]) {
291             return $scheme.'/';
292         }
293
294         $length = strlen($path);
295
296         // Windows root
297         if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
298             // Special case: "C:"
299             if (2 === $length) {
300                 return $scheme.$path.'/';
301             }
302
303             // Normal case: "C:/ or "C:\"
304             if ('/' === $path[2] || '\\' === $path[2]) {
305                 return $scheme.$path[0].$path[1].'/';
306             }
307         }
308
309         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     public static function getFilename($path)
323     {
324         if ('' === $path) {
325             return '';
326         }
327
328         Assert::string($path, 'The path must be a string. Got: %s');
329
330         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     public static function getFilenameWithoutExtension($path, $extension = null)
346     {
347         if ('' === $path) {
348             return '';
349         }
350
351         Assert::string($path, 'The path must be a string. Got: %s');
352         Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s');
353
354         if (null !== $extension) {
355             // remove extension and trailing dot
356             return rtrim(basename($path, $extension), '.');
357         }
358
359         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     public static function getExtension($path, $forceLowerCase = false)
376     {
377         if ('' === $path) {
378             return '';
379         }
380
381         Assert::string($path, 'The path must be a string. Got: %s');
382
383         $extension = pathinfo($path, PATHINFO_EXTENSION);
384
385         if ($forceLowerCase) {
386             $extension = self::toLower($extension);
387         }
388
389         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     public static function hasExtension($path, $extensions = null, $ignoreCase = false)
413     {
414         if ('' === $path) {
415             return false;
416         }
417
418         $extensions = is_object($extensions) ? array($extensions) : (array) $extensions;
419
420         Assert::allString($extensions, 'The extensions must be strings. Got: %s');
421
422         $actualExtension = self::getExtension($path, $ignoreCase);
423
424         // Only check if path has any extension
425         if (empty($extensions)) {
426             return '' !== $actualExtension;
427         }
428
429         foreach ($extensions as $key => $extension) {
430             if ($ignoreCase) {
431                 $extension = self::toLower($extension);
432             }
433
434             // remove leading '.' in extensions array
435             $extensions[$key] = ltrim($extension, '.');
436         }
437
438         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     public static function changeExtension($path, $extension)
453     {
454         if ('' === $path) {
455             return '';
456         }
457
458         Assert::string($extension, 'The extension must be a string. Got: %s');
459
460         $actualExtension = self::getExtension($path);
461         $extension = ltrim($extension, '.');
462
463         // No extension for paths
464         if ('/' === substr($path, -1)) {
465             return $path;
466         }
467
468         // No actual extension in path
469         if (empty($actualExtension)) {
470             return $path.('.' === substr($path, -1) ? '' : '.').$extension;
471         }
472
473         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     public static function isAbsolute($path)
488     {
489         if ('' === $path) {
490             return false;
491         }
492
493         Assert::string($path, 'The path must be a string. Got: %s');
494
495         // Strip scheme
496         if (false !== ($pos = strpos($path, '://'))) {
497             $path = substr($path, $pos + 3);
498         }
499
500         // UNIX root "/" or "\" (Windows style)
501         if ('/' === $path[0] || '\\' === $path[0]) {
502             return true;
503         }
504
505         // Windows root
506         if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
507             // Special case: "C:"
508             if (2 === strlen($path)) {
509                 return true;
510             }
511
512             // Normal case: "C:/ or "C:\"
513             if ('/' === $path[2] || '\\' === $path[2]) {
514                 return true;
515             }
516         }
517
518         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     public static function isRelative($path)
533     {
534         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     public static function makeAbsolute($path, $basePath)
583     {
584         Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s');
585
586         if (!static::isAbsolute($basePath)) {
587             throw new InvalidArgumentException(sprintf(
588                 'The base path "%s" is not an absolute path.',
589                 $basePath
590             ));
591         }
592
593         if (static::isAbsolute($path)) {
594             return static::canonicalize($path);
595         }
596
597         if (false !== ($pos = strpos($basePath, '://'))) {
598             $scheme = substr($basePath, 0, $pos + 3);
599             $basePath = substr($basePath, $pos + 3);
600         } else {
601             $scheme = '';
602         }
603
604         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     public static function makeRelative($path, $basePath)
666     {
667         Assert::string($basePath, 'The base path must be a string. Got: %s');
668
669         $path = static::canonicalize($path);
670         $basePath = static::canonicalize($basePath);
671
672         list($root, $relativePath) = self::split($path);
673         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         if ('' === $root && '' !== $baseRoot) {
679             // If base path is already in its root
680             if ('' === $relativeBasePath) {
681                 $relativePath = ltrim($relativePath, './\\');
682             }
683
684             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         if ('' !== $root && '' === $baseRoot) {
690             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                 'path instead.',
694                 $path,
695                 $basePath
696             ));
697         }
698
699         // Fail if the roots of the two paths are different
700         if ($baseRoot && $root !== $baseRoot) {
701             throw new InvalidArgumentException(sprintf(
702                 'The path "%s" cannot be made relative to "%s", because they '.
703                 'have different roots ("%s" and "%s").',
704                 $path,
705                 $basePath,
706                 $root,
707                 $baseRoot
708             ));
709         }
710
711         if ('' === $relativeBasePath) {
712             return $relativePath;
713         }
714
715         // Build a "../../" prefix with as many "../" parts as necessary
716         $parts = explode('/', $relativePath);
717         $baseParts = explode('/', $relativeBasePath);
718         $dotDotPrefix = '';
719
720         // Once we found a non-matching part in the prefix, we need to add
721         // "../" parts for all remaining parts
722         $match = true;
723
724         foreach ($baseParts as $i => $basePart) {
725             if ($match && isset($parts[$i]) && $basePart === $parts[$i]) {
726                 unset($parts[$i]);
727
728                 continue;
729             }
730
731             $match = false;
732             $dotDotPrefix .= '../';
733         }
734
735         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     public static function isLocal($path)
749     {
750         Assert::string($path, 'The path must be a string. Got: %s');
751
752         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     public static function getLongestCommonBasePath(array $paths)
800     {
801         Assert::allString($paths, 'The paths must be strings. Got: %s');
802
803         list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths)));
804
805         for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
806             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             if ($root !== $bpRoot) {
811                 return null;
812             }
813
814             // Make the base path shorter until it fits into path
815             while (true) {
816                 if ('.' === $basePath) {
817                     // No more base paths
818                     $basePath = '';
819
820                     // Next path
821                     continue 2;
822                 }
823
824                 // Prevent false positives for common prefixes
825                 // see isBasePath()
826                 if (0 === strpos($path.'/', $basePath.'/')) {
827                     // Next path
828                     continue 2;
829                 }
830
831                 $basePath = dirname($basePath);
832             }
833         }
834
835         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     public static function join($paths)
850     {
851         if (!is_array($paths)) {
852             $paths = func_get_args();
853         }
854
855         Assert::allString($paths, 'The paths must be strings. Got: %s');
856
857         $finalPath = null;
858         $wasScheme = false;
859
860         foreach ($paths as $path) {
861             $path = (string) $path;
862
863             if ('' === $path) {
864                 continue;
865             }
866
867             if (null === $finalPath) {
868                 // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
869                 $finalPath = $path;
870                 $wasScheme = (strpos($path, '://') !== false);
871                 continue;
872             }
873
874             // Only add slash if previous part didn't end with '/' or '\'
875             if (!in_array(substr($finalPath, -1), array('/', '\\'))) {
876                 $finalPath .= '/';
877             }
878
879             // If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim
880             $finalPath .= $wasScheme ? $path : ltrim($path, '/');
881             $wasScheme = false;
882         }
883
884         if (null === $finalPath) {
885             return '';
886         }
887
888         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     public static function isBasePath($basePath, $ofPath)
920     {
921         Assert::string($basePath, 'The base path must be a string. Got: %s');
922
923         $basePath = self::canonicalize($basePath);
924         $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         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     private static function split($path)
955     {
956         if ('' === $path) {
957             return array('', '');
958         }
959
960         // Remember scheme as part of the root, if any
961         if (false !== ($pos = strpos($path, '://'))) {
962             $root = substr($path, 0, $pos + 3);
963             $path = substr($path, $pos + 3);
964         } else {
965             $root = '';
966         }
967
968         $length = strlen($path);
969
970         // Remove and remember root directory
971         if ('/' === $path[0]) {
972             $root .= '/';
973             $path = $length > 1 ? substr($path, 1) : '';
974         } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
975             if (2 === $length) {
976                 // Windows special case: "C:"
977                 $root .= $path.'/';
978                 $path = '';
979             } elseif ('/' === $path[2]) {
980                 // Windows normal case: "C:/"..
981                 $root .= substr($path, 0, 3);
982                 $path = $length > 3 ? substr($path, 3) : '';
983             }
984         }
985
986         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     private static function toLower($str)
997     {
998         if (function_exists('mb_strtolower')) {
999             return mb_strtolower($str, mb_detect_encoding($str));
1000         }
1001
1002         return strtolower($str);
1003     }
1004
1005     private function __construct()
1006     {
1007     }
1008 }