1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705: 706: 707: 708: 709: 710: 711: 712: 713: 714: 715: 716: 717: 718: 719: 720: 721: 722: 723: 724: 725: 726: 727: 728: 729: 730: 731: 732: 733: 734: 735: 736: 737: 738: 739: 740: 741: 742: 743: 744: 745: 746: 747: 748: 749: 750: 751: 752: 753: 754: 755: 756: 757: 758: 759: 760: 761: 762: 763: 764: 765: 766: 767: 768: 769: 770: 771: 772: 773: 774: 775: 776: 777: 778: 779: 780: 781: 782: 783: 784: 785: 786: 787: 788: 789: 790: 791: 792: 793: 794: 795: 796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806: 807: 808: 809: 810: 811: 812: 813: 814: 815: 816: 817: 818: 819: 820: 821: 822: 823: 824: 825: 826: 827: 828: 829: 830: 831: 832: 833: 834: 835: 836: 837: 838: 839: 840: 841: 842: 843: 844: 845: 846: 847: 848: 849: 850: 851: 852: 853: 854: 855: 856: 857: 858: 859: 860: 861: 862: 863: 864: 865: 866: 867: 868: 869: 870: 871: 872: 873: 874: 875: 876: 877: 878: 879: 880: 881: 882: 883: 884: 885: 886: 887: 888: 889: 890: 891: 892: 893: 894: 895: 896: 897: 898: 899: 900: 901: 902: 903: 904: 905: 906: 907: 908: 909: 910: 911: 912: 913: 914: 915: 916: 917: 918: 919: 920: 921: 922: 923: 924: 925: 926: 927: 928: 929: 930: 931: 932: 933: 934: 935: 936: 937: 938: 939: 940: 941: 942: 943: 944: 945: 946: 947: 948: 949: 950: 951: 952: 953: 954: 955: 956: 957: 958: 959: 960: 961: 962: 963: 964: 965: 966: 967: 968: 969: 970: 971: 972: 973: 974: 975: 976: 977: 978: 979: 980: 981: 982: 983: 984: 985: 986: 987: 988: 989: 990: 991: 992: 993: 994: 995: 996: 997: 998: 999: 1000: 1001: 1002: 1003: 1004:
<?php
/*
* This file is part of the webmozart/path-util package.
*
* (c) Bernhard Schussek <bschussek@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Webmozart\PathUtil;
use InvalidArgumentException;
use RuntimeException;
use Webmozart\Assert\Assert;
/**
* Contains utility methods for handling path strings.
*
* The methods in this class are able to deal with both UNIX and Windows paths
* with both forward and backward slashes. All methods return normalized parts
* containing only forward slashes and no excess "." and ".." segments.
*
* @since 1.0
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Thomas Schulz <mail@king2500.net>
*/
final class Path
{
/**
* The number of buffer entries that triggers a cleanup operation.
*/
const CLEANUP_THRESHOLD = 1250;
/**
* The buffer size after the cleanup operation.
*/
const CLEANUP_SIZE = 1000;
/**
* Buffers input/output of {@link canonicalize()}.
*
* @var array
*/
private static $buffer = array();
/**
* The size of the buffer.
*
* @var int
*/
private static $bufferSize = 0;
/**
* Canonicalizes the given path.
*
* During normalization, all slashes are replaced by forward slashes ("/").
* Furthermore, all "." and ".." segments are removed as far as possible.
* ".." segments at the beginning of relative paths are not removed.
*
* ```php
* echo Path::canonicalize("\webmozart\puli\..\css\style.css");
* // => /webmozart/css/style.css
*
* echo Path::canonicalize("../css/./style.css");
* // => ../css/style.css
* ```
*
* This method is able to deal with both UNIX and Windows paths.
*
* @param string $path A path string.
*
* @return string The canonical path.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
* @since 2.1 Added support for `~`.
*/
public static function canonicalize($path)
{
if ('' === $path) {
return '';
}
Assert::string($path, 'The path must be a string. Got: %s');
// This method is called by many other methods in this class. Buffer
// the canonicalized paths to make up for the severe performance
// decrease.
if (isset(self::$buffer[$path])) {
return self::$buffer[$path];
}
// Replace "~" with user's home directory.
if ('~' === $path[0]) {
$path = static::getHomeDirectory().substr($path, 1);
}
$path = str_replace('\\', '/', $path);
list($root, $pathWithoutRoot) = self::split($path);
$parts = explode('/', $pathWithoutRoot);
$canonicalParts = array();
// Collapse "." and "..", if possible
foreach ($parts as $part) {
if ('.' === $part || '' === $part) {
continue;
}
// Collapse ".." with the previous part, if one exists
// Don't collapse ".." if the previous part is also ".."
if ('..' === $part && count($canonicalParts) > 0
&& '..' !== $canonicalParts[count($canonicalParts) - 1]) {
array_pop($canonicalParts);
continue;
}
// Only add ".." prefixes for relative paths
if ('..' !== $part || '' === $root) {
$canonicalParts[] = $part;
}
}
// Add the root directory again
self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
++self::$bufferSize;
// Clean up regularly to prevent memory leaks
if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
self::$buffer = array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
self::$bufferSize = self::CLEANUP_SIZE;
}
return $canonicalPath;
}
/**
* Normalizes the given path.
*
* During normalization, all slashes are replaced by forward slashes ("/").
* Contrary to {@link canonicalize()}, this method does not remove invalid
* or dot path segments. Consequently, it is much more efficient and should
* be used whenever the given path is known to be a valid, absolute system
* path.
*
* This method is able to deal with both UNIX and Windows paths.
*
* @param string $path A path string.
*
* @return string The normalized path.
*
* @since 2.2 Added method.
*/
public static function normalize($path)
{
Assert::string($path, 'The path must be a string. Got: %s');
return str_replace('\\', '/', $path);
}
/**
* Returns the directory part of the path.
*
* This method is similar to PHP's dirname(), but handles various cases
* where dirname() returns a weird result:
*
* - dirname() does not accept backslashes on UNIX
* - dirname("C:/webmozart") returns "C:", not "C:/"
* - dirname("C:/") returns ".", not "C:/"
* - dirname("C:") returns ".", not "C:/"
* - dirname("webmozart") returns ".", not ""
* - dirname() does not canonicalize the result
*
* This method fixes these shortcomings and behaves like dirname()
* otherwise.
*
* The result is a canonical path.
*
* @param string $path A path string.
*
* @return string The canonical directory part. Returns the root directory
* if the root directory is passed. Returns an empty string
* if a relative path is passed that contains no slashes.
* Returns an empty string if an empty string is passed.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function getDirectory($path)
{
if ('' === $path) {
return '';
}
$path = static::canonicalize($path);
// Maintain scheme
if (false !== ($pos = strpos($path, '://'))) {
$scheme = substr($path, 0, $pos + 3);
$path = substr($path, $pos + 3);
} else {
$scheme = '';
}
if (false !== ($pos = strrpos($path, '/'))) {
// Directory equals root directory "/"
if (0 === $pos) {
return $scheme.'/';
}
// Directory equals Windows root "C:/"
if (2 === $pos && ctype_alpha($path[0]) && ':' === $path[1]) {
return $scheme.substr($path, 0, 3);
}
return $scheme.substr($path, 0, $pos);
}
return '';
}
/**
* Returns canonical path of the user's home directory.
*
* Supported operating systems:
*
* - UNIX
* - Windows8 and upper
*
* If your operation system or environment isn't supported, an exception is thrown.
*
* The result is a canonical path.
*
* @return string The canonical home directory
*
* @throws RuntimeException If your operation system or environment isn't supported
*
* @since 2.1 Added method.
*/
public static function getHomeDirectory()
{
// For UNIX support
if (getenv('HOME')) {
return static::canonicalize(getenv('HOME'));
}
// For >= Windows8 support
if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
}
throw new RuntimeException("Your environment or operation system isn't supported");
}
/**
* Returns the root directory of a path.
*
* The result is a canonical path.
*
* @param string $path A path string.
*
* @return string The canonical root directory. Returns an empty string if
* the given path is relative or empty.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function getRoot($path)
{
if ('' === $path) {
return '';
}
Assert::string($path, 'The path must be a string. Got: %s');
// Maintain scheme
if (false !== ($pos = strpos($path, '://'))) {
$scheme = substr($path, 0, $pos + 3);
$path = substr($path, $pos + 3);
} else {
$scheme = '';
}
// UNIX root "/" or "\" (Windows style)
if ('/' === $path[0] || '\\' === $path[0]) {
return $scheme.'/';
}
$length = strlen($path);
// Windows root
if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
// Special case: "C:"
if (2 === $length) {
return $scheme.$path.'/';
}
// Normal case: "C:/ or "C:\"
if ('/' === $path[2] || '\\' === $path[2]) {
return $scheme.$path[0].$path[1].'/';
}
}
return '';
}
/**
* Returns the file name from a file path.
*
* @param string $path The path string.
*
* @return string The file name.
*
* @since 1.1 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function getFilename($path)
{
if ('' === $path) {
return '';
}
Assert::string($path, 'The path must be a string. Got: %s');
return basename($path);
}
/**
* Returns the file name without the extension from a file path.
*
* @param string $path The path string.
* @param string|null $extension If specified, only that extension is cut
* off (may contain leading dot).
*
* @return string The file name without extension.
*
* @since 1.1 Added method.
* @since 2.0 Method now fails if $path or $extension have invalid types.
*/
public static function getFilenameWithoutExtension($path, $extension = null)
{
if ('' === $path) {
return '';
}
Assert::string($path, 'The path must be a string. Got: %s');
Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s');
if (null !== $extension) {
// remove extension and trailing dot
return rtrim(basename($path, $extension), '.');
}
return pathinfo($path, PATHINFO_FILENAME);
}
/**
* Returns the extension from a file path.
*
* @param string $path The path string.
* @param bool $forceLowerCase Forces the extension to be lower-case
* (requires mbstring extension for correct
* multi-byte character handling in extension).
*
* @return string The extension of the file path (without leading dot).
*
* @since 1.1 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function getExtension($path, $forceLowerCase = false)
{
if ('' === $path) {
return '';
}
Assert::string($path, 'The path must be a string. Got: %s');
$extension = pathinfo($path, PATHINFO_EXTENSION);
if ($forceLowerCase) {
$extension = self::toLower($extension);
}
return $extension;
}
/**
* Returns whether the path has an extension.
*
* @param string $path The path string.
* @param string|array|null $extensions If null or not provided, checks if
* an extension exists, otherwise
* checks for the specified extension
* or array of extensions (with or
* without leading dot).
* @param bool $ignoreCase Whether to ignore case-sensitivity
* (requires mbstring extension for
* correct multi-byte character
* handling in the extension).
*
* @return bool Returns `true` if the path has an (or the specified)
* extension and `false` otherwise.
*
* @since 1.1 Added method.
* @since 2.0 Method now fails if $path or $extensions have invalid types.
*/
public static function hasExtension($path, $extensions = null, $ignoreCase = false)
{
if ('' === $path) {
return false;
}
$extensions = is_object($extensions) ? array($extensions) : (array) $extensions;
Assert::allString($extensions, 'The extensions must be strings. Got: %s');
$actualExtension = self::getExtension($path, $ignoreCase);
// Only check if path has any extension
if (empty($extensions)) {
return '' !== $actualExtension;
}
foreach ($extensions as $key => $extension) {
if ($ignoreCase) {
$extension = self::toLower($extension);
}
// remove leading '.' in extensions array
$extensions[$key] = ltrim($extension, '.');
}
return in_array($actualExtension, $extensions);
}
/**
* Changes the extension of a path string.
*
* @param string $path The path string with filename.ext to change.
* @param string $extension New extension (with or without leading dot).
*
* @return string The path string with new file extension.
*
* @since 1.1 Added method.
* @since 2.0 Method now fails if $path or $extension is not a string.
*/
public static function changeExtension($path, $extension)
{
if ('' === $path) {
return '';
}
Assert::string($extension, 'The extension must be a string. Got: %s');
$actualExtension = self::getExtension($path);
$extension = ltrim($extension, '.');
// No extension for paths
if ('/' === substr($path, -1)) {
return $path;
}
// No actual extension in path
if (empty($actualExtension)) {
return $path.('.' === substr($path, -1) ? '' : '.').$extension;
}
return substr($path, 0, -strlen($actualExtension)).$extension;
}
/**
* Returns whether a path is absolute.
*
* @param string $path A path string.
*
* @return bool Returns true if the path is absolute, false if it is
* relative or empty.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function isAbsolute($path)
{
if ('' === $path) {
return false;
}
Assert::string($path, 'The path must be a string. Got: %s');
// Strip scheme
if (false !== ($pos = strpos($path, '://'))) {
$path = substr($path, $pos + 3);
}
// UNIX root "/" or "\" (Windows style)
if ('/' === $path[0] || '\\' === $path[0]) {
return true;
}
// Windows root
if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
// Special case: "C:"
if (2 === strlen($path)) {
return true;
}
// Normal case: "C:/ or "C:\"
if ('/' === $path[2] || '\\' === $path[2]) {
return true;
}
}
return false;
}
/**
* Returns whether a path is relative.
*
* @param string $path A path string.
*
* @return bool Returns true if the path is relative or empty, false if
* it is absolute.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function isRelative($path)
{
return !static::isAbsolute($path);
}
/**
* Turns a relative path into an absolute path.
*
* Usually, the relative path is appended to the given base path. Dot
* segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* echo Path::makeAbsolute("../style.css", "/webmozart/puli/css");
* // => /webmozart/puli/style.css
* ```
*
* If an absolute path is passed, that path is returned unless its root
* directory is different than the one of the base path. In that case, an
* exception is thrown.
*
* ```php
* Path::makeAbsolute("/style.css", "/webmozart/puli/css");
* // => /style.css
*
* Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css");
* // => C:/style.css
*
* Path::makeAbsolute("C:/style.css", "/webmozart/puli/css");
* // InvalidArgumentException
* ```
*
* If the base path is not an absolute path, an exception is thrown.
*
* The result is a canonical path.
*
* @param string $path A path to make absolute.
* @param string $basePath An absolute base path.
*
* @return string An absolute path in canonical form.
*
* @throws InvalidArgumentException If the base path is not absolute or if
* the given path is an absolute path with
* a different root than the base path.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path or $basePath is not a string.
* @since 2.2.2 Method does not fail anymore of $path and $basePath are
* absolute, but on different partitions.
*/
public static function makeAbsolute($path, $basePath)
{
Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s');
if (!static::isAbsolute($basePath)) {
throw new InvalidArgumentException(sprintf(
'The base path "%s" is not an absolute path.',
$basePath
));
}
if (static::isAbsolute($path)) {
return static::canonicalize($path);
}
if (false !== ($pos = strpos($basePath, '://'))) {
$scheme = substr($basePath, 0, $pos + 3);
$basePath = substr($basePath, $pos + 3);
} else {
$scheme = '';
}
return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
}
/**
* Turns a path into a relative path.
*
* The relative path is created relative to the given base path:
*
* ```php
* echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli");
* // => ../style.css
* ```
*
* If a relative path is passed and the base path is absolute, the relative
* path is returned unchanged:
*
* ```php
* Path::makeRelative("style.css", "/webmozart/puli/css");
* // => style.css
* ```
*
* If both paths are relative, the relative path is created with the
* assumption that both paths are relative to the same directory:
*
* ```php
* Path::makeRelative("style.css", "webmozart/puli/css");
* // => ../../../style.css
* ```
*
* If both paths are absolute, their root directory must be the same,
* otherwise an exception is thrown:
*
* ```php
* Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli");
* // InvalidArgumentException
* ```
*
* If the passed path is absolute, but the base path is not, an exception
* is thrown as well:
*
* ```php
* Path::makeRelative("/webmozart/style.css", "webmozart/puli");
* // InvalidArgumentException
* ```
*
* If the base path is not an absolute path, an exception is thrown.
*
* The result is a canonical path.
*
* @param string $path A path to make relative.
* @param string $basePath A base path.
*
* @return string A relative path in canonical form.
*
* @throws InvalidArgumentException If the base path is not absolute or if
* the given path has a different root
* than the base path.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path or $basePath is not a string.
*/
public static function makeRelative($path, $basePath)
{
Assert::string($basePath, 'The base path must be a string. Got: %s');
$path = static::canonicalize($path);
$basePath = static::canonicalize($basePath);
list($root, $relativePath) = self::split($path);
list($baseRoot, $relativeBasePath) = self::split($basePath);
// If the base path is given as absolute path and the path is already
// relative, consider it to be relative to the given absolute path
// already
if ('' === $root && '' !== $baseRoot) {
return $relativePath;
}
// If the passed path is absolute, but the base path is not, we
// cannot generate a relative path
if ('' !== $root && '' === $baseRoot) {
throw new InvalidArgumentException(sprintf(
'The absolute path "%s" cannot be made relative to the '.
'relative path "%s". You should provide an absolute base '.
'path instead.',
$path,
$basePath
));
}
// Fail if the roots of the two paths are different
if ($baseRoot && $root !== $baseRoot) {
throw new InvalidArgumentException(sprintf(
'The path "%s" cannot be made relative to "%s", because they '.
'have different roots ("%s" and "%s").',
$path,
$basePath,
$root,
$baseRoot
));
}
if ('' === $relativeBasePath) {
return $relativePath;
}
// Build a "../../" prefix with as many "../" parts as necessary
$parts = explode('/', $relativePath);
$baseParts = explode('/', $relativeBasePath);
$dotDotPrefix = '';
// Once we found a non-matching part in the prefix, we need to add
// "../" parts for all remaining parts
$match = true;
foreach ($baseParts as $i => $basePart) {
if ($match && isset($parts[$i]) && $basePart === $parts[$i]) {
unset($parts[$i]);
continue;
}
$match = false;
$dotDotPrefix .= '../';
}
return $dotDotPrefix.implode('/', $parts);
}
/**
* Returns whether the given path is on the local filesystem.
*
* @param string $path A path string.
*
* @return bool Returns true if the path is local, false for a URL.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $path is not a string.
*/
public static function isLocal($path)
{
Assert::string($path, 'The path must be a string. Got: %s');
return '' !== $path && false === strpos($path, '://');
}
/**
* Returns the longest common base path of a set of paths.
*
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* $basePath = Path::getLongestCommonBasePath(array(
* '/webmozart/css/style.css',
* '/webmozart/css/..'
* ));
* // => /webmozart
* ```
*
* The root is returned if no common base path can be found:
*
* ```php
* $basePath = Path::getLongestCommonBasePath(array(
* '/webmozart/css/style.css',
* '/puli/css/..'
* ));
* // => /
* ```
*
* If the paths are located on different Windows partitions, `null` is
* returned.
*
* ```php
* $basePath = Path::getLongestCommonBasePath(array(
* 'C:/webmozart/css/style.css',
* 'D:/webmozart/css/..'
* ));
* // => null
* ```
*
* @param array $paths A list of paths.
*
* @return string|null The longest common base path in canonical form or
* `null` if the paths are on different Windows
* partitions.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $paths are not strings.
*/
public static function getLongestCommonBasePath(array $paths)
{
Assert::allString($paths, 'The paths must be strings. Got: %s');
list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths)));
for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
list($root, $path) = self::split(self::canonicalize(current($paths)));
// If we deal with different roots (e.g. C:/ vs. D:/), it's time
// to quit
if ($root !== $bpRoot) {
return null;
}
// Make the base path shorter until it fits into path
while (true) {
if ('.' === $basePath) {
// No more base paths
$basePath = '';
// Next path
continue 2;
}
// Prevent false positives for common prefixes
// see isBasePath()
if (0 === strpos($path.'/', $basePath.'/')) {
// Next path
continue 2;
}
$basePath = dirname($basePath);
}
}
return $bpRoot.$basePath;
}
/**
* Joins two or more path strings.
*
* The result is a canonical path.
*
* @param string[]|string $paths Path parts as parameters or array.
*
* @return string The joint path.
*
* @since 2.0 Added method.
*/
public static function join($paths)
{
if (!is_array($paths)) {
$paths = func_get_args();
}
Assert::allString($paths, 'The paths must be strings. Got: %s');
$finalPath = null;
$wasScheme = false;
foreach ($paths as $path) {
$path = (string) $path;
if ('' === $path) {
continue;
}
if (null === $finalPath) {
// For first part we keep slashes, like '/top', 'C:\' or 'phar://'
$finalPath = $path;
$wasScheme = (strpos($path, '://') !== false);
continue;
}
// Only add slash if previous part didn't end with '/' or '\'
if (!in_array(substr($finalPath, -1), array('/', '\\'))) {
$finalPath .= '/';
}
// If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim
$finalPath .= $wasScheme ? $path : ltrim($path, '/');
$wasScheme = false;
}
if (null === $finalPath) {
return '';
}
return self::canonicalize($finalPath);
}
/**
* Returns whether a path is a base path of another path.
*
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* Path::isBasePath('/webmozart', '/webmozart/css');
* // => true
*
* Path::isBasePath('/webmozart', '/webmozart');
* // => true
*
* Path::isBasePath('/webmozart', '/webmozart/..');
* // => false
*
* Path::isBasePath('/webmozart', '/puli');
* // => false
* ```
*
* @param string $basePath The base path to test.
* @param string $ofPath The other path.
*
* @return bool Whether the base path is a base path of the other path.
*
* @since 1.0 Added method.
* @since 2.0 Method now fails if $basePath or $ofPath is not a string.
*/
public static function isBasePath($basePath, $ofPath)
{
Assert::string($basePath, 'The base path must be a string. Got: %s');
$basePath = self::canonicalize($basePath);
$ofPath = self::canonicalize($ofPath);
// Append slashes to prevent false positives when two paths have
// a common prefix, for example /base/foo and /base/foobar.
// Don't append a slash for the root "/", because then that root
// won't be discovered as common prefix ("//" is not a prefix of
// "/foobar/").
return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/');
}
/**
* Splits a part into its root directory and the remainder.
*
* If the path has no root directory, an empty root directory will be
* returned.
*
* If the root directory is a Windows style partition, the resulting root
* will always contain a trailing slash.
*
* list ($root, $path) = Path::split("C:/webmozart")
* // => array("C:/", "webmozart")
*
* list ($root, $path) = Path::split("C:")
* // => array("C:/", "")
*
* @param string $path The canonical path to split.
*
* @return string[] An array with the root directory and the remaining
* relative path.
*/
private static function split($path)
{
if ('' === $path) {
return array('', '');
}
// Remember scheme as part of the root, if any
if (false !== ($pos = strpos($path, '://'))) {
$root = substr($path, 0, $pos + 3);
$path = substr($path, $pos + 3);
} else {
$root = '';
}
$length = strlen($path);
// Remove and remember root directory
if ('/' === $path[0]) {
$root .= '/';
$path = $length > 1 ? substr($path, 1) : '';
} elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
if (2 === $length) {
// Windows special case: "C:"
$root .= $path.'/';
$path = '';
} elseif ('/' === $path[2]) {
// Windows normal case: "C:/"..
$root .= substr($path, 0, 3);
$path = $length > 3 ? substr($path, 3) : '';
}
}
return array($root, $path);
}
/**
* Converts string to lower-case (multi-byte safe if mbstring is installed).
*
* @param string $str The string
*
* @return string Lower case string
*/
private static function toLower($str)
{
if (function_exists('mb_strtolower')) {
return mb_strtolower($str, mb_detect_encoding($str));
}
return strtolower($str);
}
private function __construct()
{
}
}