Passed
Push — main ( 4889b2...808ce0 )
by Michiel
07:43
created

WindowsFileSystem::getDrive()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 6
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
namespace Phing\Io;
21
22
use InvalidArgumentException;
23
use Phing\Phing;
24
use Phing\Util\StringHelper;
25
26
/**
27
 */
28
class WindowsFileSystem extends FileSystem
29
{
30
    protected $slash;
31
    protected $altSlash;
32
    protected $semicolon;
33
34
    private static $driveDirCache = [];
35
36
    /**
37
     *
38
     */
39 1
    public function __construct()
40
    {
41 1
        $this->slash = self::getSeparator();
0 ignored issues
show
Bug Best Practice introduced by
The method Phing\Io\WindowsFileSystem::getSeparator() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

41
        /** @scrutinizer ignore-call */ 
42
        $this->slash = self::getSeparator();
Loading history...
42 1
        $this->semicolon = self::getPathSeparator();
0 ignored issues
show
Bug Best Practice introduced by
The method Phing\Io\WindowsFileSystem::getPathSeparator() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

42
        /** @scrutinizer ignore-call */ 
43
        $this->semicolon = self::getPathSeparator();
Loading history...
43 1
        $this->altSlash = ($this->slash === '\\') ? '/' : '\\';
44 1
    }
45
46
    /**
47
     * @param $c
48
     * @return bool
49
     */
50
    public function isSlash($c)
51
    {
52
        return ($c == '\\') || ($c == '/');
53
    }
54
55
    /**
56
     * @param $c
57
     * @return bool
58
     */
59
    public function isLetter($c)
60
    {
61
        return ((ord($c) >= ord('a')) && (ord($c) <= ord('z')))
62
            || ((ord($c) >= ord('A')) && (ord($c) <= ord('Z')));
63
    }
64
65
    /**
66
     * @param $p
67
     * @return string
68
     */
69
    public function slashify($p)
70
    {
71
        if ((strlen($p) > 0) && ($p[0] != $this->slash)) {
72
            return $this->slash . $p;
73
        }
74
75
        return $p;
76
    }
77
78
    /* -- Normalization and construction -- */
79
80
    /**
81
     * @return string
82
     */
83 1
    public function getSeparator()
84
    {
85
        // the ascii value of is the \
86 1
        return chr(92);
87
    }
88
89
    /**
90
     * @return string
91
     */
92 1
    public function getPathSeparator()
93
    {
94 1
        return ';';
95
    }
96
97
    /**
98
     * A normal Win32 pathname contains no duplicate slashes, except possibly
99
     * for a UNC prefix, and does not end with a slash.  It may be the empty
100
     * string.  Normalized Win32 pathnames have the convenient property that
101
     * the length of the prefix almost uniquely identifies the type of the path
102
     * and whether it is absolute or relative:
103
     *
104
     *    0  relative to both drive and directory
105
     *    1  drive-relative (begins with '\\')
106
     *    2  absolute UNC (if first char is '\\'), else directory-relative (has form "z:foo")
107
     *    3  absolute local pathname (begins with "z:\\")
108
     *
109
     * @param  $strPath
110
     * @param  $len
111
     * @param  $sb
112
     * @return int
113
     */
114
    public function normalizePrefix($strPath, $len, &$sb)
115
    {
116
        $src = 0;
117
        while (($src < $len) && $this->isSlash($strPath[$src])) {
118
            $src++;
119
        }
120
        $c = "";
0 ignored issues
show
Unused Code introduced by
The assignment to $c is dead and can be removed.
Loading history...
121
        if (
122
            ($len - $src >= 2)
123
            && $this->isLetter($c = $strPath[$src])
124
            && $strPath[$src + 1] === ':'
125
        ) {
126
            /* Remove leading slashes if followed by drive specifier.
127
             * This hack is necessary to support file URLs containing drive
128
             * specifiers (e.g., "file://c:/path").  As a side effect,
129
             * "/c:/path" can be used as an alternative to "c:/path". */
130
            $sb .= $c;
131
            $sb .= ':';
132
            $src += 2;
133
        } else {
134
            $src = 0;
135
            if (
136
                ($len >= 2)
137
                && $this->isSlash($strPath[0])
138
                && $this->isSlash($strPath[1])
139
            ) {
140
                /* UNC pathname: Retain first slash; leave src pointed at
141
                 * second slash so that further slashes will be collapsed
142
                 * into the second slash.  The result will be a pathname
143
                 * beginning with "\\\\" followed (most likely) by a host
144
                 * name. */
145
                $src = 1;
146
                $sb .= $this->slash;
147
            }
148
        }
149
150
        return $src;
151
    }
152
153
    /**
154
     * Normalize the given pathname, whose length is len, starting at the given
155
     * offset; everything before this offset is already normal.
156
     *
157
     * @param  $strPath
158
     * @param  $len
159
     * @param  $offset
160
     * @return string
161
     */
162
    protected function normalizer($strPath, $len, $offset)
163
    {
164
        if ($len == 0) {
165
            return $strPath;
166
        }
167
        if ($offset < 3) {
168
            $offset = 0; //Avoid fencepost cases with UNC pathnames
169
        }
170
        $src = 0;
171
        $slash = $this->slash;
172
        $sb = "";
173
174
        if ($offset == 0) {
175
            // Complete normalization, including prefix
176
            $src = $this->normalizePrefix($strPath, $len, $sb);
177
        } else {
178
            // Partial normalization
179
            $src = $offset;
180
            $sb .= substr($strPath, 0, $offset);
181
        }
182
183
        // Remove redundant slashes from the remainder of the path, forcing all
184
        // slashes into the preferred slash
185
        while ($src < $len) {
186
            $c = $strPath[$src++];
187
            if ($this->isSlash($c)) {
188
                while (($src < $len) && $this->isSlash($strPath[$src])) {
189
                    $src++;
190
                }
191
                if ($src === $len) {
192
                    /* Check for trailing separator */
193
                    $sn = (int) strlen($sb);
194
                    if (($sn == 2) && ($sb[1] === ':')) {
195
                        // "z:\\"
196
                        $sb .= $slash;
197
                        break;
198
                    }
199
                    if ($sn === 0) {
200
                        // "\\"
201
                        $sb .= $slash;
202
                        break;
203
                    }
204
                    if (($sn === 1) && ($this->isSlash($sb[0]))) {
205
                        /* "\\\\" is not collapsed to "\\" because "\\\\" marks
206
                        the beginning of a UNC pathname.  Even though it is
207
                        not, by itself, a valid UNC pathname, we leave it as
208
                        is in order to be consistent with the win32 APIs,
209
                        which treat this case as an invalid UNC pathname
210
                        rather than as an alias for the root directory of
211
                        the current drive. */
212
                        $sb .= $slash;
213
                        break;
214
                    }
215
                    // Path does not denote a root directory, so do not append
216
                    // trailing slash
217
                    break;
218
                }
219
220
                $sb .= $slash;
221
            } else {
222
                $sb .= $c;
223
            }
224
        }
225
        return (string) $sb;
226
    }
227
228
    /**
229
     * Check that the given pathname is normal.  If not, invoke the real
230
     * normalizer on the part of the pathname that requires normalization.
231
     * This way we iterate through the whole pathname string only once.
232
     *
233
     * @param string $strPath
234
     * @return string
235
     */
236
    public function normalize($strPath)
237
    {
238
        $strPath = $this->fixEncoding($strPath);
239
240
        if ($this->isPharArchive($strPath)) {
241
            return str_replace('\\', '/', $strPath);
242
        }
243
244
        $n = strlen($strPath);
0 ignored issues
show
Bug introduced by
It seems like $strPath can also be of type array; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

244
        $n = strlen(/** @scrutinizer ignore-type */ $strPath);
Loading history...
245
        $slash = $this->slash;
246
        $altSlash = $this->altSlash;
247
        $prev = 0;
248
        for ($i = 0; $i < $n; $i++) {
249
            $c = $strPath[$i];
250
            if ($c === $altSlash) {
251
                return $this->normalizer($strPath, $n, ($prev === $slash) ? $i - 1 : $i);
252
            }
253
            if (($c === $slash) && ($prev === $slash) && ($i > 1)) {
254
                return $this->normalizer($strPath, $n, $i - 1);
255
            }
256
            if (($c === ':') && ($i > 1)) {
257
                return $this->normalizer($strPath, $n, 0);
258
            }
259
            $prev = $c;
260
        }
261
        if ($prev === $slash) {
262
            return $this->normalizer($strPath, $n, $n - 1);
263
        }
264
265
        return $strPath;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $strPath also could return the type array which is incompatible with the documented return type string.
Loading history...
266
    }
267
268
    /**
269
     * @param string $strPath
270
     * @return int
271
     */
272
    public function prefixLength($strPath)
273
    {
274
        if ($this->isPharArchive($strPath)) {
275
            return 0;
276
        }
277
278
        $path = (string) $strPath;
279
        $slash = (string) $this->slash;
280
        $n = (int) strlen($path);
281
        if ($n === 0) {
282
            return 0;
283
        }
284
        $c0 = $path[0];
285
        $c1 = ($n > 1) ? $path[1] :
286
            0;
287
        if ($c0 === $slash) {
288
            if ($c1 === $slash) {
289
                return 2; // absolute UNC pathname "\\\\foo"
290
            }
291
292
            return 1; // drive-relative "\\foo"
293
        }
294
295
        if ($this->isLetter($c0) && ($c1 === ':')) {
296
            if (($n > 2) && ($path[2]) === $slash) {
297
                return 3; // Absolute local pathname "z:\\foo" */
298
            }
299
300
            return 2; // Directory-relative "z:foo"
301
        }
302
303
        return 0; // Completely relative
304
    }
305
306
    /**
307
     * @param string $parent
308
     * @param string $child
309
     * @return string
310
     */
311
    public function resolve($parent, $child)
312
    {
313
        $parent = (string) $parent;
314
        $child = (string) $child;
315
        $slash = (string) $this->slash;
316
317
        $pn = (int) strlen($parent);
318
        if ($pn === 0) {
319
            return $child;
320
        }
321
        $cn = (int) strlen($child);
322
        if ($cn === 0) {
323
            return $parent;
324
        }
325
326
        $c = $child;
327
        if (($cn > 1) && ($c[0] === $slash)) {
328
            if ($c[1] === $slash) {
329
                // drop prefix when child is a UNC pathname
330
                $c = substr($c, 2);
331
            } else {
332
                //Drop prefix when child is drive-relative */
333
                $c = substr($c, 1);
334
            }
335
        }
336
337
        $p = $parent;
338
        if ($p[$pn - 1] === $slash) {
339
            $p = substr($p, 0, $pn - 1);
340
        }
341
342
        return $p . $this->slashify($c);
343
    }
344
345
    /**
346
     * @return string
347
     */
348
    public function getDefaultParent()
349
    {
350
        return (string) ("" . $this->slash);
351
    }
352
353
    /**
354
     * @param string $strPath
355
     * @return string
356
     */
357
    public function fromURIPath($strPath)
358
    {
359
        $p = (string) $strPath;
360
        if ((strlen($p) > 2) && ($p[2] === ':')) {
361
            // "/c:/foo" --> "c:/foo"
362
            $p = substr($p, 1);
363
364
            // "c:/foo/" --> "c:/foo", but "c:/" --> "c:/"
365
            if ((strlen($p) > 3) && StringHelper::endsWith('/', $p)) {
366
                $p = substr($p, 0, strlen($p) - 1);
367
            }
368
        } elseif ((strlen($p) > 1) && StringHelper::endsWith('/', $p)) {
369
            // "/foo/" --> "/foo"
370
            $p = substr($p, 0, strlen($p) - 1);
371
        }
372
373
        return (string) $p;
374
    }
375
376
    /* -- Path operations -- */
377
378
    /**
379
     * @param File $f
380
     * @return bool
381
     */
382
    public function isAbsolute(File $f)
383
    {
384
        $pl = (int) $f->getPrefixLength();
385
        $p = (string) $f->getPath();
386
387
        return ((($pl === 2) && ($p[0] === $this->slash)) || ($pl === 3) || ($pl === 1 && $p[0] === $this->slash));
388
    }
389
390
    /**
391
     * @param  $d
392
     * @return int
393
     */
394
    private function getDriveIndex($d)
395
    {
396
        $d = (string) $d[0];
397
        if ((ord($d) >= ord('a')) && (ord($d) <= ord('z'))) {
398
            return ord($d) - ord('a');
399
        }
400
        if ((ord($d) >= ord('A')) && (ord($d) <= ord('Z'))) {
401
            return ord($d) - ord('A');
402
        }
403
404
        return -1;
405
    }
406
407
    /**
408
     * @param  $strPath
409
     * @return bool
410
     */
411
    private function isPharArchive($strPath)
412
    {
413
        return (strpos($strPath, 'phar://') === 0);
414
    }
415
416
    /**
417
     * @param $drive
418
     * @return mixed|null
419
     */
420
    private function getDriveDirectory($drive)
421
    {
422
        $drive = (string) $drive[0];
423
        $i = (int) $this->getDriveIndex($drive);
424
        if ($i < 0) {
425
            return null;
426
        }
427
428
        $s = (self::$driveDirCache[$i] ?? null);
429
430
        if ($s !== null) {
431
            return $s;
432
        }
433
434
        $s = $this->getDriveDirectory($i + 1);
435
        self::$driveDirCache[$i] = $s;
436
437
        return $s;
438
    }
439
440
    /**
441
     * @return string
442
     */
443
    private function getUserPath()
444
    {
445
        //For both compatibility and security, we must look this up every time
446
        return (string) $this->normalize(Phing::getProperty("user.dir"));
447
    }
448
449
    /**
450
     * @param $path
451
     * @return null|string
452
     */
453
    private function getDrive($path)
454
    {
455
        $path = (string) $path;
456
        $pl = $this->prefixLength($path);
457
458
        return ($pl === 3) ? substr($path, 0, 2) : null;
459
    }
460
461
    /**
462
     * @param File $f
463
     */
464
    public function resolveFile(File $f)
465
    {
466
        $path = $f->getPath();
467
        $pl = (int) $f->getPrefixLength();
468
469
        if (($pl === 2) && ($path[0] === $this->slash)) {
470
            return $path; // UNC
471
        }
472
473
        if ($pl === 3) {
474
            return $path; // Absolute local
475
        }
476
477
        if ($pl === 0) {
478
            if ($this->isPharArchive($path)) {
479
                return $path;
480
            }
481
482
            return (string) ($this->getUserPath() . $this->slashify($path)); //Completely relative
483
        }
484
485
        if ($pl === 1) { // Drive-relative
486
            $up = (string) $this->getUserPath();
487
            $ud = (string) $this->getDrive($up);
488
            if ($ud !== null) {
0 ignored issues
show
introduced by
The condition $ud !== null is always true.
Loading history...
489
                return (string) $ud . $path;
490
            }
491
492
            return (string) $up . $path; //User dir is a UNC path
493
        }
494
495
        if ($pl === 2) { // Directory-relative
496
            $up = (string) $this->getUserPath();
497
            $ud = (string) $this->getDrive($up);
498
            if (($ud !== null) && StringHelper::startsWith($ud, $path)) {
499
                return (string) ($up . $this->slashify(substr($path, 2)));
500
            }
501
            $drive = (string) $path[0];
502
            $dir = (string) $this->getDriveDirectory($drive);
503
504
            if ($dir !== null) {
0 ignored issues
show
introduced by
The condition $dir !== null is always true.
Loading history...
505
                /* When resolving a directory-relative path that refers to a
506
                drive other than the current drive, insist that the caller
507
                have read permission on the result */
508
                $p = (string) $drive . (':' . $dir . $this->slashify(substr($path, 2)));
509
510
                if (!$this->checkAccess(new File($p))) {
511
                    throw new IOException("Can't resolve path $p");
512
                }
513
514
                return $p;
515
            }
516
517
            return (string) $drive . ':' . $this->slashify(substr($path, 2)); //fake it
518
        }
519
520
        throw new InvalidArgumentException("Unresolvable path: " . $path);
521
    }
522
523
    /* -- most of the following is mapped to the functions mapped th php natives in FileSystem */
524
525
    /* -- Basic infrastructure -- */
526
527
    /**
528
     * compares file paths lexicographically
529
     *
530
     * @param File $f1
531
     * @param File $f2
532
     * @return int
533
     */
534
    public function compare(File $f1, File $f2)
535
    {
536
        $f1Path = $f1->getPath();
537
        $f2Path = $f2->getPath();
538
539
        return strcasecmp((string) $f1Path, (string) $f2Path);
540
    }
541
542
    /**
543
     * On Windows platforms, PHP will mangle non-ASCII characters, see http://bugs.php.net/bug.php?id=47096
544
     *
545
     * @param  $strPath
546
     * @return mixed|string
547
     */
548
    private function fixEncoding($strPath)
549
    {
550
        $charSet = trim(strstr(setlocale(LC_CTYPE, ''), '.'), '.');
551
        if ($charSet === 'utf8') {
552
            return $strPath;
553
        }
554
        $codepage = 'CP' . $charSet;
555
        if (function_exists('iconv')) {
556
            $strPath = iconv('UTF-8', $codepage . '//IGNORE', $strPath);
557
        } elseif (function_exists('mb_convert_encoding')) {
558
            $strPath = mb_convert_encoding($strPath, $codepage, 'UTF-8');
559
        }
560
        return $strPath;
561
    }
562
}
563