1<?php 2/* 3 * $Id: d468183f331a26b26ed5b0a1ebd6197c79d326fc $ 4 * 5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 * 17 * This software consists of voluntary contributions made by many individuals 18 * and is licensed under the LGPL. For more information please see 19 * <http://phing.info>. 20 */ 21 22include_once 'phing/system/io/FileSystem.php'; 23 24/** 25 * @package phing.system.io 26 */ 27class Win32FileSystem extends FileSystem { 28 29 protected $slash; 30 protected $altSlash; 31 protected $semicolon; 32 33 private static $driveDirCache = array(); 34 35 function __construct() { 36 $this->slash = self::getSeparator(); 37 $this->semicolon = self::getPathSeparator(); 38 $this->altSlash = ($this->slash === '\\') ? '/' : '\\'; 39 } 40 41 function isSlash($c) { 42 return ($c == '\\') || ($c == '/'); 43 } 44 45 function isLetter($c) { 46 return ((ord($c) >= ord('a')) && (ord($c) <= ord('z'))) 47 || ((ord($c) >= ord('A')) && (ord($c) <= ord('Z'))); 48 } 49 50 function slashify($p) { 51 if ((strlen($p) > 0) && ($p{0} != $this->slash)) { 52 return $this->slash.$p; 53 } 54 else { 55 return $p; 56 } 57 } 58 59 /* -- Normalization and construction -- */ 60 61 function getSeparator() { 62 // the ascii value of is the \ 63 return chr(92); 64 } 65 66 function getPathSeparator() { 67 return ';'; 68 } 69 70 /** 71 * A normal Win32 pathname contains no duplicate slashes, except possibly 72 * for a UNC prefix, and does not end with a slash. It may be the empty 73 * string. Normalized Win32 pathnames have the convenient property that 74 * the length of the prefix almost uniquely identifies the type of the path 75 * and whether it is absolute or relative: 76 * 77 * 0 relative to both drive and directory 78 * 1 drive-relative (begins with '\\') 79 * 2 absolute UNC (if first char is '\\'), else directory-relative (has form "z:foo") 80 * 3 absolute local pathname (begins with "z:\\") 81 */ 82 function normalizePrefix($strPath, $len, &$sb) { 83 $src = 0; 84 while (($src < $len) && $this->isSlash($strPath{$src})) { 85 $src++; 86 } 87 $c = ""; 88 if (($len - $src >= 2) 89 && $this->isLetter($c = $strPath{$src}) 90 && $strPath{$src + 1} === ':') { 91 /* Remove leading slashes if followed by drive specifier. 92 * This hack is necessary to support file URLs containing drive 93 * specifiers (e.g., "file://c:/path"). As a side effect, 94 * "/c:/path" can be used as an alternative to "c:/path". */ 95 $sb .= $c; 96 $sb .= ':'; 97 $src += 2; 98 } 99 else { 100 $src = 0; 101 if (($len >= 2) 102 && $this->isSlash($strPath{0}) 103 && $this->isSlash($strPath{1})) { 104 /* UNC pathname: Retain first slash; leave src pointed at 105 * second slash so that further slashes will be collapsed 106 * into the second slash. The result will be a pathname 107 * beginning with "\\\\" followed (most likely) by a host 108 * name. */ 109 $src = 1; 110 $sb.=$this->slash; 111 } 112 } 113 return $src; 114 } 115 116 /** Normalize the given pathname, whose length is len, starting at the given 117 offset; everything before this offset is already normal. */ 118 protected function normalizer($strPath, $len, $offset) { 119 if ($len == 0) { 120 return $strPath; 121 } 122 if ($offset < 3) { 123 $offset = 0; //Avoid fencepost cases with UNC pathnames 124 } 125 $src = 0; 126 $slash = $this->slash; 127 $sb = ""; 128 129 if ($offset == 0) { 130 // Complete normalization, including prefix 131 $src = $this->normalizePrefix($strPath, $len, $sb); 132 } else { 133 // Partial normalization 134 $src = $offset; 135 $sb .= substr($strPath, 0, $offset); 136 } 137 138 // Remove redundant slashes from the remainder of the path, forcing all 139 // slashes into the preferred slash 140 while ($src < $len) { 141 $c = $strPath{$src++}; 142 if ($this->isSlash($c)) { 143 while (($src < $len) && $this->isSlash($strPath{$src})) { 144 $src++; 145 } 146 if ($src === $len) { 147 /* Check for trailing separator */ 148 $sn = (int) strlen($sb); 149 if (($sn == 2) && ($sb{1} === ':')) { 150 // "z:\\" 151 $sb .= $slash; 152 break; 153 } 154 if ($sn === 0) { 155 // "\\" 156 $sb .= $slash; 157 break; 158 } 159 if (($sn === 1) && ($this->isSlash($sb{0}))) { 160 /* "\\\\" is not collapsed to "\\" because "\\\\" marks 161 the beginning of a UNC pathname. Even though it is 162 not, by itself, a valid UNC pathname, we leave it as 163 is in order to be consistent with the win32 APIs, 164 which treat this case as an invalid UNC pathname 165 rather than as an alias for the root directory of 166 the current drive. */ 167 $sb .= $slash; 168 break; 169 } 170 // Path does not denote a root directory, so do not append 171 // trailing slash 172 break; 173 } else { 174 $sb .= $slash; 175 } 176 } else { 177 $sb.=$c; 178 } 179 } 180 $rv = (string) $sb; 181 return $rv; 182 } 183 184 /** 185 * Check that the given pathname is normal. If not, invoke the real 186 * normalizer on the part of the pathname that requires normalization. 187 * This way we iterate through the whole pathname string only once. 188 * @param string $strPath 189 * @return string 190 */ 191 function normalize($strPath) { 192 if ($this->_isPharArchive($strPath)) { 193 return str_replace('\\', '/', $strPath); 194 } 195 196 $n = strlen($strPath); 197 $slash = $this->slash; 198 $altSlash = $this->altSlash; 199 $prev = 0; 200 for ($i = 0; $i < $n; $i++) { 201 $c = $strPath{$i}; 202 if ($c === $altSlash) { 203 return $this->normalizer($strPath, $n, ($prev === $slash) ? $i - 1 : $i); 204 } 205 if (($c === $slash) && ($prev === $slash) && ($i > 1)) { 206 return $this->normalizer($strPath, $n, $i - 1); 207 } 208 if (($c === ':') && ($i > 1)) { 209 return $this->normalizer($strPath, $n, 0); 210 } 211 $prev = $c; 212 } 213 if ($prev === $slash) { 214 return $this->normalizer($strPath, $n, $n - 1); 215 } 216 return $strPath; 217 } 218 219 function prefixLength($strPath) { 220 if ($this->_isPharArchive($strPath)) { 221 return 0; 222 } 223 224 $path = (string) $strPath; 225 $slash = (string) $this->slash; 226 $n = (int) strlen($path); 227 if ($n === 0) { 228 return 0; 229 } 230 $c0 = $path{0}; 231 $c1 = ($n > 1) ? $path{1} : 232 0; 233 if ($c0 === $slash) { 234 if ($c1 === $slash) { 235 return 2; // absolute UNC pathname "\\\\foo" 236 } 237 return 1; // drive-relative "\\foo" 238 } 239 240 if ($this->isLetter($c0) && ($c1 === ':')) { 241 if (($n > 2) && ($path{2}) === $slash) { 242 return 3; // Absolute local pathname "z:\\foo" */ 243 } 244 return 2; // Directory-relative "z:foo" 245 } 246 return 0; // Completely relative 247 } 248 249 function resolve($parent, $child) { 250 $parent = (string) $parent; 251 $child = (string) $child; 252 $slash = (string) $this->slash; 253 254 $pn = (int) strlen($parent); 255 if ($pn === 0) { 256 return $child; 257 } 258 $cn = (int) strlen($child); 259 if ($cn === 0) { 260 return $parent; 261 } 262 263 $c = $child; 264 if (($cn > 1) && ($c{0} === $slash)) { 265 if ($c{1} === $slash) { 266 // drop prefix when child is a UNC pathname 267 $c = substr($c, 2); 268 } 269 else { 270 //Drop prefix when child is drive-relative */ 271 $c = substr($c, 1); 272 } 273 } 274 275 $p = $parent; 276 if ($p{$pn - 1} === $slash) { 277 $p = substr($p, 0, $pn - 1); 278 } 279 return $p.$this->slashify($c); 280 } 281 282 function getDefaultParent() { 283 return (string) ("".$this->slash); 284 } 285 286 function fromURIPath($strPath) { 287 $p = (string) $strPath; 288 if ((strlen($p) > 2) && ($p{2} === ':')) { 289 290 // "/c:/foo" --> "c:/foo" 291 $p = substr($p,1); 292 293 // "c:/foo/" --> "c:/foo", but "c:/" --> "c:/" 294 if ((strlen($p) > 3) && StringHelper::endsWith('/', $p)) { 295 $p = substr($p, 0, strlen($p) - 1); 296 } 297 } elseif ((strlen($p) > 1) && StringHelper::endsWith('/', $p)) { 298 // "/foo/" --> "/foo" 299 $p = substr($p, 0, strlen($p) - 1); 300 } 301 return (string) $p; 302 } 303 304 305 /* -- Path operations -- */ 306 307 function isAbsolute(PhingFile $f) { 308 $pl = (int) $f->getPrefixLength(); 309 $p = (string) $f->getPath(); 310 return ((($pl === 2) && ($p{0} === $this->slash)) || ($pl === 3) || ($pl === 1 && $p{0} === $this->slash)); 311 } 312 313 /** private */ 314 function _driveIndex($d) { 315 $d = (string) $d{0}; 316 if ((ord($d) >= ord('a')) && (ord($d) <= ord('z'))) { 317 return ord($d) - ord('a'); 318 } 319 if ((ord($d) >= ord('A')) && (ord($d) <= ord('Z'))) { 320 return ord($d) - ord('A'); 321 } 322 return -1; 323 } 324 325 /** private */ 326 function _isPharArchive($strPath) { 327 return (strpos($strPath, 'phar://') === 0); 328 } 329 330 function _getDriveDirectory($drive) { 331 $drive = (string) $drive{0}; 332 $i = (int) $this->_driveIndex($drive); 333 if ($i < 0) { 334 return null; 335 } 336 337 $s = (isset(self::$driveDirCache[$i]) ? self::$driveDirCache[$i] : null); 338 339 if ($s !== null) { 340 return $s; 341 } 342 343 $s = $this->_getDriveDirectory($i + 1); 344 self::$driveDirCache[$i] = $s; 345 return $s; 346 } 347 348 function _getUserPath() { 349 //For both compatibility and security, we must look this up every time 350 return (string) $this->normalize(Phing::getProperty("user.dir")); 351 } 352 353 function _getDrive($path) { 354 $path = (string) $path; 355 $pl = $this->prefixLength($path); 356 return ($pl === 3) ? substr($path, 0, 2) : null; 357 } 358 359 function resolveFile(PhingFile $f) { 360 $path = $f->getPath(); 361 $pl = (int) $f->getPrefixLength(); 362 363 if (($pl === 2) && ($path{0} === $this->slash)) { 364 return $path; // UNC 365 } 366 367 if ($pl === 3) { 368 return $path; // Absolute local 369 } 370 371 if ($pl === 0) { 372 if ($this->_isPharArchive($path)) { 373 return $path; 374 } 375 return (string) ($this->_getUserPath().$this->slashify($path)); //Completely relative 376 } 377 378 if ($pl === 1) { // Drive-relative 379 $up = (string) $this->_getUserPath(); 380 $ud = (string) $this->_getDrive($up); 381 if ($ud !== null) { 382 return (string) $ud.$path; 383 } 384 return (string) $up.$path; //User dir is a UNC path 385 } 386 387 if ($pl === 2) { // Directory-relative 388 $up = (string) $this->_getUserPath(); 389 $ud = (string) $this->_getDrive($up); 390 if (($ud !== null) && StringHelper::startsWith($ud, $path)) { 391 return (string) ($up . $this->slashify(substr($path,2))); 392 } 393 $drive = (string) $path{0}; 394 $dir = (string) $this->_getDriveDirectory($drive); 395 396 $np = (string) ""; 397 if ($dir !== null) { 398 /* When resolving a directory-relative path that refers to a 399 drive other than the current drive, insist that the caller 400 have read permission on the result */ 401 $p = (string) $drive . (':'.$dir.$this->slashify(substr($path,2))); 402 403 if (!$this->checkAccess($p, false)) { 404 // FIXME 405 // throw security error 406 die("Can't resolve path $p"); 407 } 408 return $p; 409 } 410 return (string) $drive.':'.$this->slashify(substr($path,2)); //fake it 411 } 412 413 throw new InvalidArgumentException("Unresolvable path: " . $path); 414 } 415 416 /* -- most of the following is mapped to the functions mapped th php natives in FileSystem */ 417 418 /* -- Attribute accessors -- */ 419 420 function setReadOnly($f) { 421 // dunno how to do this on win 422 throw new Exception("WIN32FileSystem doesn't support read-only yet."); 423 } 424 425 /* -- Filesystem interface -- */ 426 427 protected function _access($path) { 428 if (!$this->checkAccess($path, false)) { 429 throw new Exception("Can't resolve path $p"); 430 } 431 return true; 432 } 433 434 function _nativeListRoots() { 435 // FIXME 436 } 437 438 function listRoots() { 439 $ds = _nativeListRoots(); 440 $n = 0; 441 for ($i = 0; $i < 26; $i++) { 442 if ((($ds >> $i) & 1) !== 0) { 443 if (!$this->access((string)( chr(ord('A') + $i) . ':' . $this->slash))) { 444 $ds &= ~(1 << $i); 445 } else { 446 $n++; 447 } 448 } 449 } 450 $fs = array(); 451 $j = (int) 0; 452 $slash = (string) $this->slash; 453 for ($i = 0; $i < 26; $i++) { 454 if ((($ds >> $i) & 1) !== 0) { 455 $fs[$j++] = new PhingFile(chr(ord('A') + $i) . ':' . $this->slash); 456 } 457 } 458 return $fs; 459 } 460 461 /* -- Basic infrastructure -- */ 462 463 /** compares file paths lexicographically */ 464 function compare(PhingFile $f1, PhingFile $f2) { 465 $f1Path = $f1->getPath(); 466 $f2Path = $f2->getPath(); 467 return (boolean) strcasecmp((string) $f1Path, (string) $f2Path); 468 } 469 470 471 /** 472 * returns the contents of a directory in an array 473 */ 474 function lister($f) { 475 $dir = @opendir($f->getAbsolutePath()); 476 if (!$dir) { 477 throw new Exception("Can't open directory " . $f->__toString()); 478 } 479 $vv = array(); 480 while (($file = @readdir($dir)) !== false) { 481 if ($file == "." || $file == "..") { 482 continue; 483 } 484 $vv[] = (string) $file; 485 } 486 @closedir($dir); 487 return $vv; 488 } 489 490} 491 492 493