WindowsFileSystem::getSeparator()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

37
        /** @scrutinizer ignore-call */ 
38
        $this->slash = self::getSeparator();
Loading history...
38 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

38
        /** @scrutinizer ignore-call */ 
39
        $this->semicolon = self::getPathSeparator();
Loading history...
39 1
        $this->altSlash = ('\\' === $this->slash) ? '/' : '\\';
40
    }
41
42
    /**
43
     * @param $c
44
     *
45
     * @return bool
46
     */
47
    public function isSlash($c)
48
    {
49
        return ('\\' == $c) || ('/' == $c);
50
    }
51
52
    /**
53
     * @param $c
54
     *
55
     * @return bool
56
     */
57
    public function isLetter($c)
58
    {
59
        return ((ord($c) >= ord('a')) && (ord($c) <= ord('z')))
60
            || ((ord($c) >= ord('A')) && (ord($c) <= ord('Z')));
61
    }
62
63
    /**
64
     * @param $p
65
     *
66
     * @return string
67
     */
68
    public function slashify($p)
69
    {
70
        if ((strlen($p) > 0) && ($p[0] != $this->slash)) {
71
            return $this->slash . $p;
72
        }
73
74
        return $p;
75
    }
76
77
    // -- Normalization and construction --
78
79
    /**
80
     * @return string
81
     */
82 1
    public function getSeparator()
83
    {
84
        // the ascii value of is the \
85 1
        return chr(92);
86
    }
87
88
    /**
89
     * @return string
90
     */
91 1
    public function getPathSeparator()
92
    {
93 1
        return ';';
94
    }
95
96
    /**
97
     * A normal Win32 pathname contains no duplicate slashes, except possibly
98
     * for a UNC prefix, and does not end with a slash.  It may be the empty
99
     * string.  Normalized Win32 pathnames have the convenient property that
100
     * the length of the prefix almost uniquely identifies the type of the path
101
     * and whether it is absolute or relative:.
102
     *
103
     *    0  relative to both drive and directory
104
     *    1  drive-relative (begins with '\\')
105
     *    2  absolute UNC (if first char is '\\'), else directory-relative (has form "z:foo")
106
     *    3  absolute local pathname (begins with "z:\\")
107
     *
108
     * @param  $strPath
109
     * @param  $len
110
     * @param  $sb
111
     *
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
     * Check that the given pathname is normal.  If not, invoke the real
155
     * normalizer on the part of the pathname that requires normalization.
156
     * This way we iterate through the whole pathname string only once.
157
     *
158
     * @param string $strPath
159
     *
160
     * @return string
161
     */
162
    public function normalize($strPath)
163
    {
164
        $strPath = $this->fixEncoding($strPath);
165
166
        if ($this->isPharArchive($strPath)) {
167
            return str_replace('\\', '/', $strPath);
168
        }
169
170
        $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

170
        $n = strlen(/** @scrutinizer ignore-type */ $strPath);
Loading history...
171
        $slash = $this->slash;
172
        $altSlash = $this->altSlash;
173
        $prev = 0;
174
        for ($i = 0; $i < $n; ++$i) {
175
            $c = $strPath[$i];
176
            if ($c === $altSlash) {
177
                return $this->normalizer($strPath, $n, ($prev === $slash) ? $i - 1 : $i);
178
            }
179
            if (($c === $slash) && ($prev === $slash) && ($i > 1)) {
180
                return $this->normalizer($strPath, $n, $i - 1);
181
            }
182
            if ((':' === $c) && ($i > 1)) {
183
                return $this->normalizer($strPath, $n, 0);
184
            }
185
            $prev = $c;
186
        }
187
        if ($prev === $slash) {
188
            return $this->normalizer($strPath, $n, $n - 1);
189
        }
190
191
        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...
192
    }
193
194
    /**
195
     * @param string $strPath
196
     *
197
     * @return int
198
     */
199
    public function prefixLength($strPath)
200
    {
201
        if ($this->isPharArchive($strPath)) {
202
            return 0;
203
        }
204
205
        $path = (string) $strPath;
206
        $slash = (string) $this->slash;
207
        $n = (int) strlen($path);
208
        if (0 === $n) {
209
            return 0;
210
        }
211
        $c0 = $path[0];
212
        $c1 = ($n > 1) ? $path[1] :
213
            0;
214
        if ($c0 === $slash) {
215
            if ($c1 === $slash) {
216
                return 2; // absolute UNC pathname "\\\\foo"
217
            }
218
219
            return 1; // drive-relative "\\foo"
220
        }
221
222
        if ($this->isLetter($c0) && (':' === $c1)) {
223
            if (($n > 2) && ($path[2]) === $slash) {
224
                return 3; // Absolute local pathname "z:\\foo" */
225
            }
226
227
            return 2; // Directory-relative "z:foo"
228
        }
229
230
        return 0; // Completely relative
231
    }
232
233
    /**
234
     * @param string $parent
235
     * @param string $child
236
     *
237
     * @return string
238
     */
239
    public function resolve($parent, $child)
240
    {
241
        $parent = (string) $parent;
242
        $child = (string) $child;
243
        $slash = (string) $this->slash;
244
245
        $pn = (int) strlen($parent);
246
        if (0 === $pn) {
247
            return $child;
248
        }
249
        $cn = (int) strlen($child);
250
        if (0 === $cn) {
251
            return $parent;
252
        }
253
254
        $c = $child;
255
        if (($cn > 1) && ($c[0] === $slash)) {
256
            if ($c[1] === $slash) {
257
                // drop prefix when child is a UNC pathname
258
                $c = substr($c, 2);
259
            } else {
260
                //Drop prefix when child is drive-relative */
261
                $c = substr($c, 1);
262
            }
263
        }
264
265
        $p = $parent;
266
        if ($p[$pn - 1] === $slash) {
267
            $p = substr($p, 0, $pn - 1);
268
        }
269
270
        return $p . $this->slashify($c);
271
    }
272
273
    /**
274
     * @return string
275
     */
276
    public function getDefaultParent()
277
    {
278
        return (string) ('' . $this->slash);
279
    }
280
281
    /**
282
     * @param string $strPath
283
     *
284
     * @return string
285
     */
286
    public function fromURIPath($strPath)
287
    {
288
        $p = (string) $strPath;
289
        if ((strlen($p) > 2) && (':' === $p[2])) {
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
302
        return (string) $p;
303
    }
304
305
    // -- Path operations --
306
307
    /**
308
     * @return bool
309
     */
310
    public function isAbsolute(File $f)
311
    {
312
        $pl = (int) $f->getPrefixLength();
313
        $p = (string) $f->getPath();
314
315
        return ((2 === $pl) && ($p[0] === $this->slash)) || (3 === $pl) || (1 === $pl && $p[0] === $this->slash);
316
    }
317
318
    public function resolveFile(File $f)
319
    {
320
        $path = $f->getPath();
321
        $pl = (int) $f->getPrefixLength();
322
323
        if ((2 === $pl) && ($path[0] === $this->slash)) {
324
            return $path; // UNC
325
        }
326
327
        if (3 === $pl) {
328
            return $path; // Absolute local
329
        }
330
331
        if (0 === $pl) {
332
            if ($this->isPharArchive($path)) {
333
                return $path;
334
            }
335
336
            return (string) ($this->getUserPath() . $this->slashify($path)); //Completely relative
337
        }
338
339
        if (1 === $pl) { // Drive-relative
340
            $up = (string) $this->getUserPath();
341
            $ud = (string) $this->getDrive($up);
342
            if (null !== $ud) {
0 ignored issues
show
introduced by
The condition null !== $ud is always true.
Loading history...
343
                return (string) $ud . $path;
344
            }
345
346
            return (string) $up . $path; //User dir is a UNC path
347
        }
348
349
        if (2 === $pl) { // Directory-relative
350
            $up = (string) $this->getUserPath();
351
            $ud = (string) $this->getDrive($up);
352
            if ((null !== $ud) && StringHelper::startsWith($ud, $path)) {
353
                return (string) ($up . $this->slashify(substr($path, 2)));
354
            }
355
            $drive = (string) $path[0];
356
            $dir = (string) $this->getDriveDirectory($drive);
357
358
            if (null !== $dir) {
0 ignored issues
show
introduced by
The condition null !== $dir is always true.
Loading history...
359
                /* When resolving a directory-relative path that refers to a
360
                drive other than the current drive, insist that the caller
361
                have read permission on the result */
362
                $p = (string) $drive . (':' . $dir . $this->slashify(substr($path, 2)));
363
364
                if (!$this->checkAccess(new File($p))) {
365
                    throw new IOException("Can't resolve path {$p}");
366
                }
367
368
                return $p;
369
            }
370
371
            return (string) $drive . ':' . $this->slashify(substr($path, 2)); //fake it
372
        }
373
374
        throw new InvalidArgumentException('Unresolvable path: ' . $path);
375
    }
376
377
    // -- most of the following is mapped to the functions mapped th php natives in FileSystem
378
379
    // -- Basic infrastructure --
380
381
    /**
382
     * compares file paths lexicographically.
383
     *
384
     * @return int
385
     */
386
    public function compare(File $f1, File $f2)
387
    {
388
        $f1Path = $f1->getPath();
389
        $f2Path = $f2->getPath();
390
391
        return strcasecmp((string) $f1Path, (string) $f2Path);
392
    }
393
394
    /**
395
     * Normalize the given pathname, whose length is len, starting at the given
396
     * offset; everything before this offset is already normal.
397
     *
398
     * @param  $strPath
399
     * @param  $len
400
     * @param  $offset
401
     *
402
     * @return string
403
     */
404
    protected function normalizer($strPath, $len, $offset)
405
    {
406
        if (0 == $len) {
407
            return $strPath;
408
        }
409
        if ($offset < 3) {
410
            $offset = 0; //Avoid fencepost cases with UNC pathnames
411
        }
412
        $src = 0;
413
        $slash = $this->slash;
414
        $sb = '';
415
416
        if (0 == $offset) {
417
            // Complete normalization, including prefix
418
            $src = $this->normalizePrefix($strPath, $len, $sb);
419
        } else {
420
            // Partial normalization
421
            $src = $offset;
422
            $sb .= substr($strPath, 0, $offset);
423
        }
424
425
        // Remove redundant slashes from the remainder of the path, forcing all
426
        // slashes into the preferred slash
427
        while ($src < $len) {
428
            $c = $strPath[$src++];
429
            if ($this->isSlash($c)) {
430
                while (($src < $len) && $this->isSlash($strPath[$src])) {
431
                    ++$src;
432
                }
433
                if ($src === $len) {
434
                    // Check for trailing separator
435
                    $sn = (int) strlen($sb);
436
                    if ((2 == $sn) && (':' === $sb[1])) {
437
                        // "z:\\"
438
                        $sb .= $slash;
439
440
                        break;
441
                    }
442
                    if (0 === $sn) {
443
                        // "\\"
444
                        $sb .= $slash;
445
446
                        break;
447
                    }
448
                    if ((1 === $sn) && ($this->isSlash($sb[0]))) {
449
                        /* "\\\\" is not collapsed to "\\" because "\\\\" marks
450
                        the beginning of a UNC pathname.  Even though it is
451
                        not, by itself, a valid UNC pathname, we leave it as
452
                        is in order to be consistent with the win32 APIs,
453
                        which treat this case as an invalid UNC pathname
454
                        rather than as an alias for the root directory of
455
                        the current drive. */
456
                        $sb .= $slash;
457
458
                        break;
459
                    }
460
                    // Path does not denote a root directory, so do not append
461
                    // trailing slash
462
                    break;
463
                }
464
465
                $sb .= $slash;
466
            } else {
467
                $sb .= $c;
468
            }
469
        }
470
471
        return (string) $sb;
472
    }
473
474
    /**
475
     * @param  $d
476
     *
477
     * @return int
478
     */
479
    private function getDriveIndex($d)
480
    {
481
        $d = (string) $d[0];
482
        if ((ord($d) >= ord('a')) && (ord($d) <= ord('z'))) {
483
            return ord($d) - ord('a');
484
        }
485
        if ((ord($d) >= ord('A')) && (ord($d) <= ord('Z'))) {
486
            return ord($d) - ord('A');
487
        }
488
489
        return -1;
490
    }
491
492
    /**
493
     * @param  $strPath
494
     *
495
     * @return bool
496
     */
497
    private function isPharArchive($strPath)
498
    {
499
        return 0 === strpos($strPath, 'phar://');
500
    }
501
502
    /**
503
     * @param $drive
504
     *
505
     * @return null|mixed
506
     */
507
    private function getDriveDirectory($drive)
508
    {
509
        $drive = (string) $drive[0];
510
        $i = (int) $this->getDriveIndex($drive);
511
        if ($i < 0) {
512
            return null;
513
        }
514
515
        $s = (self::$driveDirCache[$i] ?? null);
516
517
        if (null !== $s) {
518
            return $s;
519
        }
520
521
        $s = $this->getDriveDirectory($i + 1);
522
        self::$driveDirCache[$i] = $s;
523
524
        return $s;
525
    }
526
527
    /**
528
     * @return string
529
     */
530
    private function getUserPath()
531
    {
532
        //For both compatibility and security, we must look this up every time
533
        return (string) $this->normalize(Phing::getProperty('user.dir'));
534
    }
535
536
    /**
537
     * @param $path
538
     *
539
     * @return null|string
540
     */
541
    private function getDrive($path)
542
    {
543
        $path = (string) $path;
544
        $pl = $this->prefixLength($path);
545
546
        return (3 === $pl) ? substr($path, 0, 2) : null;
547
    }
548
549
    /**
550
     * On Windows platforms, PHP will mangle non-ASCII characters, see http://bugs.php.net/bug.php?id=47096.
551
     *
552
     * @param  $strPath
553
     *
554
     * @return mixed|string
555
     */
556
    private function fixEncoding($strPath)
557
    {
558
        $charSet = trim(strstr(setlocale(LC_CTYPE, ''), '.'), '.');
559
        if ('utf8' === $charSet) {
560
            return $strPath;
561
        }
562
        $codepage = 'CP' . $charSet;
563
        if (function_exists('iconv')) {
564
            $strPath = iconv('UTF-8', $codepage . '//IGNORE', $strPath);
565
        } elseif (function_exists('mb_convert_encoding')) {
566
            $strPath = mb_convert_encoding($strPath, $codepage, 'UTF-8');
567
        }
568
569
        return $strPath;
570
    }
571
}
572