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