Passed
Push — master ( 972120...1c77fc )
by Michiel
08:19
created

WindowsFileSystem   F

Complexity

Total Complexity 107

Size/Duplication

Total Lines 537
Duplicated Lines 0 %

Test Coverage

Coverage 4.23%

Importance

Changes 0
Metric Value
eloc 200
dl 0
loc 537
rs 2
c 0
b 0
f 0
ccs 9
cts 213
cp 0.0423
wmc 107

22 Methods

Rating   Name   Duplication   Size   Complexity  
B normalizePrefix() 0 37 9
A getSeparator() 0 4 1
A __construct() 0 5 2
A getPathSeparator() 0 3 1
A isSlash() 0 3 2
A slashify() 0 7 3
A isLetter() 0 4 4
A _getDrive() 0 6 2
C resolveFile() 0 57 13
A fixEncoding() 0 13 4
B resolve() 0 32 7
A _isPharArchive() 0 3 1
B prefixLength() 0 32 10
A _driveIndex() 0 11 5
B fromURIPath() 0 17 7
B normalize() 0 30 11
A getDefaultParent() 0 3 1
A compare() 0 6 1
A isAbsolute() 0 6 5
A _getUserPath() 0 4 1
C normalizer() 0 64 14
A _getDriveDirectory() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like WindowsFileSystem often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WindowsFileSystem, and based on these observations, apply Extract Interface, too.

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
 * @package   phing.system.io
28
 */
29
class WindowsFileSystem extends FileSystem
30
{
31
    protected $slash;
32
    protected $altSlash;
33
    protected $semicolon;
34
35
    private static $driveDirCache = [];
36
37
    /**
38
     *
39
     */
40 1
    public function __construct()
41
    {
42 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

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

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

245
        $n = strlen(/** @scrutinizer ignore-type */ $strPath);
Loading history...
246
        $slash = $this->slash;
247
        $altSlash = $this->altSlash;
248
        $prev = 0;
249
        for ($i = 0; $i < $n; $i++) {
250
            $c = $strPath[$i];
251
            if ($c === $altSlash) {
252
                return $this->normalizer($strPath, $n, ($prev === $slash) ? $i - 1 : $i);
253
            }
254
            if (($c === $slash) && ($prev === $slash) && ($i > 1)) {
255
                return $this->normalizer($strPath, $n, $i - 1);
256
            }
257
            if (($c === ':') && ($i > 1)) {
258
                return $this->normalizer($strPath, $n, 0);
259
            }
260
            $prev = $c;
261
        }
262
        if ($prev === $slash) {
263
            return $this->normalizer($strPath, $n, $n - 1);
264
        }
265
266
        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...
267
    }
268
269
    /**
270
     * @param string $strPath
271
     * @return int
272
     */
273
    public function prefixLength($strPath)
274
    {
275
        if ($this->_isPharArchive($strPath)) {
276
            return 0;
277
        }
278
279
        $path = (string) $strPath;
280
        $slash = (string) $this->slash;
281
        $n = (int) strlen($path);
282
        if ($n === 0) {
283
            return 0;
284
        }
285
        $c0 = $path[0];
286
        $c1 = ($n > 1) ? $path[1] :
287
            0;
288
        if ($c0 === $slash) {
289
            if ($c1 === $slash) {
290
                return 2; // absolute UNC pathname "\\\\foo"
291
            }
292
293
            return 1; // drive-relative "\\foo"
294
        }
295
296
        if ($this->isLetter($c0) && ($c1 === ':')) {
297
            if (($n > 2) && ($path[2]) === $slash) {
298
                return 3; // Absolute local pathname "z:\\foo" */
299
            }
300
301
            return 2; // Directory-relative "z:foo"
302
        }
303
304
        return 0; // Completely relative
305
    }
306
307
    /**
308
     * @param string $parent
309
     * @param string $child
310
     * @return string
311
     */
312
    public function resolve($parent, $child)
313
    {
314
        $parent = (string) $parent;
315
        $child = (string) $child;
316
        $slash = (string) $this->slash;
317
318
        $pn = (int) strlen($parent);
319
        if ($pn === 0) {
320
            return $child;
321
        }
322
        $cn = (int) strlen($child);
323
        if ($cn === 0) {
324
            return $parent;
325
        }
326
327
        $c = $child;
328
        if (($cn > 1) && ($c[0] === $slash)) {
329
            if ($c[1] === $slash) {
330
                // drop prefix when child is a UNC pathname
331
                $c = substr($c, 2);
332
            } else {
333
                //Drop prefix when child is drive-relative */
334
                $c = substr($c, 1);
335
            }
336
        }
337
338
        $p = $parent;
339
        if ($p[$pn - 1] === $slash) {
340
            $p = substr($p, 0, $pn - 1);
341
        }
342
343
        return $p . $this->slashify($c);
344
    }
345
346
    /**
347
     * @return string
348
     */
349
    public function getDefaultParent()
350
    {
351
        return (string) ("" . $this->slash);
352
    }
353
354
    /**
355
     * @param string $strPath
356
     * @return string
357
     */
358
    public function fromURIPath($strPath)
359
    {
360
        $p = (string) $strPath;
361
        if ((strlen($p) > 2) && ($p[2] === ':')) {
362
            // "/c:/foo" --> "c:/foo"
363
            $p = substr($p, 1);
364
365
            // "c:/foo/" --> "c:/foo", but "c:/" --> "c:/"
366
            if ((strlen($p) > 3) && StringHelper::endsWith('/', $p)) {
367
                $p = substr($p, 0, strlen($p) - 1);
368
            }
369
        } elseif ((strlen($p) > 1) && StringHelper::endsWith('/', $p)) {
370
            // "/foo/" --> "/foo"
371
            $p = substr($p, 0, strlen($p) - 1);
372
        }
373
374
        return (string) $p;
375
    }
376
377
    /* -- Path operations -- */
378
379
    /**
380
     * @param File $f
381
     * @return bool
382
     */
383
    public function isAbsolute(File $f)
384
    {
385
        $pl = (int) $f->getPrefixLength();
386
        $p = (string) $f->getPath();
387
388
        return ((($pl === 2) && ($p[0] === $this->slash)) || ($pl === 3) || ($pl === 1 && $p[0] === $this->slash));
389
    }
390
391
    /**
392
     * private
393
     *
394
     * @param  $d
395
     * @return int
396
     */
397
    public function _driveIndex($d)
398
    {
399
        $d = (string) $d[0];
400
        if ((ord($d) >= ord('a')) && (ord($d) <= ord('z'))) {
401
            return ord($d) - ord('a');
402
        }
403
        if ((ord($d) >= ord('A')) && (ord($d) <= ord('Z'))) {
404
            return ord($d) - ord('A');
405
        }
406
407
        return -1;
408
    }
409
410
    /**
411
     * private
412
     *
413
     * @param  $strPath
414
     * @return bool
415
     */
416
    public function _isPharArchive($strPath)
417
    {
418
        return (strpos($strPath, 'phar://') === 0);
419
    }
420
421
    /**
422
     * @param $drive
423
     * @return null
424
     */
425
    public function _getDriveDirectory($drive)
426
    {
427
        $drive = (string) $drive[0];
428
        $i = (int) $this->_driveIndex($drive);
429
        if ($i < 0) {
430
            return null;
431
        }
432
433
        $s = (self::$driveDirCache[$i] ?? null);
434
435
        if ($s !== null) {
436
            return $s;
437
        }
438
439
        $s = $this->_getDriveDirectory($i + 1);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $s is correct as $this->_getDriveDirectory($i + 1) targeting Phing\Io\WindowsFileSystem::_getDriveDirectory() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
440
        self::$driveDirCache[$i] = $s;
441
442
        return $s;
443
    }
444
445
    /**
446
     * @return string
447
     */
448
    public function _getUserPath()
449
    {
450
        //For both compatibility and security, we must look this up every time
451
        return (string) $this->normalize(Phing::getProperty("user.dir"));
452
    }
453
454
    /**
455
     * @param $path
456
     * @return null|string
457
     */
458
    public function _getDrive($path)
459
    {
460
        $path = (string) $path;
461
        $pl = $this->prefixLength($path);
462
463
        return ($pl === 3) ? substr($path, 0, 2) : null;
464
    }
465
466
    /**
467
     * @param File $f
468
     */
469
    public function resolveFile(File $f)
470
    {
471
        $path = $f->getPath();
472
        $pl = (int) $f->getPrefixLength();
473
474
        if (($pl === 2) && ($path[0] === $this->slash)) {
475
            return $path; // UNC
476
        }
477
478
        if ($pl === 3) {
479
            return $path; // Absolute local
480
        }
481
482
        if ($pl === 0) {
483
            if ($this->_isPharArchive($path)) {
484
                return $path;
485
            }
486
487
            return (string) ($this->_getUserPath() . $this->slashify($path)); //Completely relative
488
        }
489
490
        if ($pl === 1) { // Drive-relative
491
            $up = (string) $this->_getUserPath();
492
            $ud = (string) $this->_getDrive($up);
493
            if ($ud !== null) {
0 ignored issues
show
introduced by
The condition $ud !== null is always true.
Loading history...
494
                return (string) $ud . $path;
495
            }
496
497
            return (string) $up . $path; //User dir is a UNC path
498
        }
499
500
        if ($pl === 2) { // Directory-relative
501
            $up = (string) $this->_getUserPath();
502
            $ud = (string) $this->_getDrive($up);
503
            if (($ud !== null) && StringHelper::startsWith($ud, $path)) {
504
                return (string) ($up . $this->slashify(substr($path, 2)));
505
            }
506
            $drive = (string) $path[0];
507
            $dir = (string) $this->_getDriveDirectory($drive);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->_getDriveDirectory($drive) targeting Phing\Io\WindowsFileSystem::_getDriveDirectory() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
508
509
            if ($dir !== null) {
0 ignored issues
show
introduced by
The condition $dir !== null is always true.
Loading history...
510
                /* When resolving a directory-relative path that refers to a
511
                drive other than the current drive, insist that the caller
512
                have read permission on the result */
513
                $p = (string) $drive . (':' . $dir . $this->slashify(substr($path, 2)));
514
515
                if (!$this->checkAccess(new File($p))) {
516
                    throw new IOException("Can't resolve path $p");
517
                }
518
519
                return $p;
520
            }
521
522
            return (string) $drive . ':' . $this->slashify(substr($path, 2)); //fake it
523
        }
524
525
        throw new InvalidArgumentException("Unresolvable path: " . $path);
526
    }
527
528
    /* -- most of the following is mapped to the functions mapped th php natives in FileSystem */
529
530
    /* -- Basic infrastructure -- */
531
532
    /**
533
     * compares file paths lexicographically
534
     *
535
     * @param File $f1
536
     * @param File $f2
537
     * @return int
538
     */
539
    public function compare(File $f1, File $f2)
540
    {
541
        $f1Path = $f1->getPath();
542
        $f2Path = $f2->getPath();
543
544
        return strcasecmp((string) $f1Path, (string) $f2Path);
545
    }
546
547
    /**
548
     * On Windows platforms, PHP will mangle non-ASCII characters, see http://bugs.php.net/bug.php?id=47096
549
     *
550
     * @param  $strPath
551
     * @return mixed|string
552
     */
553
    private function fixEncoding($strPath)
554
    {
555
        $charSet = trim(strstr(setlocale(LC_CTYPE, ''), '.'), '.');
556
        if ($charSet === 'utf8') {
557
            return $strPath;
558
        }
559
        $codepage = 'CP' . $charSet;
560
        if (function_exists('iconv')) {
561
            $strPath = iconv('UTF-8', $codepage . '//IGNORE', $strPath);
562
        } elseif (function_exists('mb_convert_encoding')) {
563
            $strPath = mb_convert_encoding($strPath, $codepage, 'UTF-8');
564
        }
565
        return $strPath;
566
    }
567
}
568