FileUtils   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Test Coverage

Coverage 71.26%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 85
eloc 180
dl 0
loc 486
ccs 124
cts 174
cp 0.7126
rs 2
c 1
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getTempDir() 0 3 1
A getDefaultFileCreationMask() 0 9 2
A getSeparator() 0 7 2
A getPathSeparator() 0 7 2
C resolveFile() 0 42 12
B createTempFile() 0 36 7
B contentEquals() 0 18 7
A getChainedReader() 0 13 2
D copyFile() 0 65 19
B renameFile() 0 23 8
C dissect() 0 63 15
B normalize() 0 45 8

How to fix   Complexity   

Complex Class

Complex classes like FileUtils 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 FileUtils, and based on these observations, apply Extract Interface, too.

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 Exception;
24
use Phing\Exception\BuildException;
25
use Phing\Filter\ChainReaderHelper;
26
use Phing\Phing;
27
use Phing\Project;
28
use Phing\Util\Character;
29
use Phing\Util\StringHelper;
30
31
/**
32
 * File utility class.
33
 * - handles os independent stuff etc
34
 * - mapper stuff
35
 * - filter stuff.
36
 */
37
class FileUtils
38
{
39
    /**
40
     * path separator string, static, obtained from FileSystem (; or :).
41
     */
42
    private static $pathSeparator;
43
44
    /**
45
     * separator string, static, obtained from FileSystem.
46
     */
47
    private static $separator;
48
49
    /**
50
     * @var false
51
     */
52
    private $dosWithDrive;
53
54
    /**
55
     * @throws IOException
56
     */
57 6
    public static function getPathSeparator(): string
58
    {
59 6
        if (null === self::$pathSeparator) {
60
            self::$pathSeparator = FileSystem::getFileSystem()->getPathSeparator();
61
        }
62
63 6
        return self::$pathSeparator;
64
    }
65
66
    /**
67
     * @throws IOException
68
     */
69 938
    public static function getSeparator(): string
70
    {
71 938
        if (null === self::$separator) {
72
            self::$separator = FileSystem::getFileSystem()->getSeparator();
73
        }
74
75 938
        return self::$separator;
76
    }
77
78
    /**
79
     * Returns the path to the temp directory.
80
     *
81
     * @return string
82
     */
83 9
    public static function getTempDir()
84
    {
85 9
        return Phing::getProperty('php.tmpdir');
86
    }
87
88
    /**
89
     * Returns the default file/dir creation mask value
90
     * (The mask value is prepared w.r.t the current user's file-creation mask value).
91
     *
92
     * @param bool $dirmode Directory creation mask to select
93
     *
94
     * @return int Creation Mask in octal representation
95
     */
96
    public static function getDefaultFileCreationMask($dirmode = false): int
97
    {
98
        // Preparing the creation mask base permission
99
        $permission = (true === $dirmode) ? 0777 : 0666;
100
101
        // Default mask information
102
        $defaultmask = sprintf('%03o', ($permission & ($permission - (int) sprintf('%04o', umask()))));
103
104
        return octdec($defaultmask);
0 ignored issues
show
Bug Best Practice introduced by
The expression return octdec($defaultmask) could return the type double which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
105
    }
106
107
    /**
108
     * Returns a new Reader with filterchains applied.  If filterchains are empty,
109
     * simply returns passed reader.
110
     *
111
     * @param Reader $in            reader to modify (if appropriate)
112
     * @param array  &$filterChains filter chains to apply
113
     *
114
     * @return Reader assembled Reader (w/ filter chains)
115
     */
116 41
    public static function getChainedReader(Reader $in, &$filterChains, Project $project)
117
    {
118 41
        if (!empty($filterChains)) {
119 31
            $crh = new ChainReaderHelper();
120 31
            $crh->setBufferSize(65536); // 64k buffer, but isn't being used (yet?)
121 31
            $crh->setPrimaryReader($in);
122 31
            $crh->setFilterChains($filterChains);
123 31
            $crh->setProject($project);
124
125 31
            return $crh->getAssembledReader();
126
        }
127
128 11
        return $in;
129
    }
130
131
    /**
132
     * Copies a file using filter chains.
133
     *
134
     * @param bool  $overwrite
135
     * @param bool  $preserveLastModified
136
     * @param array $filterChains
137
     * @param int   $mode
138
     * @param bool  $preservePermissions
139
     *
140
     * @throws IOException
141
     */
142 52
    public function copyFile(
143
        File $sourceFile,
144
        File $destFile,
145
        Project $project,
146
        $overwrite = false,
147
        $preserveLastModified = true,
148
        &$filterChains = null,
149
        $mode = 0755,
150
        $preservePermissions = true,
151
        int $granularity = 0
152
    ) {
153
        if (
154 52
            $overwrite
155 41
            || !$destFile->exists()
156 52
            || $destFile->lastModified() < $sourceFile->lastModified() - $granularity
157
        ) {
158 51
            if ($destFile->exists() && ($destFile->isFile() || $destFile->isLink())) {
159 1
                $destFile->delete();
160
            }
161
162
            // ensure that parent dir of dest file exists!
163 51
            $parent = $destFile->getParentFile();
164 51
            if (null !== $parent && !$parent->exists()) {
165
                // Setting source directory permissions to target
166
                // (On permissions preservation, the target directory permissions
167
                // will be inherited from the source directory, otherwise the 'mode'
168
                // will be used)
169 6
                $dirMode = ($preservePermissions ? $sourceFile->getParentFile()->getMode() : $mode);
170
171 6
                $parent->mkdirs($dirMode);
172
            }
173
174 51
            if ((is_array($filterChains)) && (!empty($filterChains))) {
175 23
                $in = self::getChainedReader(new BufferedReader(new FileReader($sourceFile)), $filterChains, $project);
176 23
                $out = new BufferedWriter(new FileWriter($destFile));
177
178
                // New read() methods returns a big buffer.
179 23
                while (-1 !== ($buffer = $in->read())) { // -1 indicates EOF
180 23
                    $out->write($buffer);
181
                }
182
183 23
                if (null !== $in) {
184 23
                    $in->close();
185
                }
186 23
                if (null !== $out) {
187 23
                    $out->close();
188
                }
189
190
                // Set/Copy the permissions on the target
191 23
                if (true === $preservePermissions) {
192 23
                    $destFile->setMode($sourceFile->getMode());
193
                }
194
            } else {
195
                // simple copy (no filtering)
196 28
                $sourceFile->copyTo($destFile);
197
198
                // By default, PHP::Copy also copies the file permissions. Therefore,
199
                // re-setting the mode with the "user file-creation mask" information.
200 28
                if (false === $preservePermissions) {
201
                    $destFile->setMode(FileUtils::getDefaultFileCreationMask());
202
                }
203
            }
204
205 51
            if ($preserveLastModified && !$destFile->isLink()) {
206 2
                $destFile->setLastModified($sourceFile->lastModified());
207
            }
208
        }
209
    }
210
211
    /**
212
     * Attempts to rename a file from a source to a destination.
213
     * If overwrite is set to true, this method overwrites existing file even if the destination file is newer.
214
     * Otherwise, the source file is renamed only if the destination file is older than it.
215
     *
216
     * @param mixed $overwrite
217
     *
218
     * @throws IOException
219
     */
220 1
    public function renameFile(File $sourceFile, File $destFile, $overwrite = false): void
221
    {
222
        // ensure that parent dir of dest file exists!
223 1
        $parent = $destFile->getParentFile();
224 1
        if (null !== $parent) {
225 1
            if (!$parent->exists()) {
226
                $parent->mkdirs();
227
            }
228
        }
229
230 1
        if ($overwrite || !$destFile->exists() || $destFile->lastModified() < $sourceFile->lastModified()) {
231 1
            if ($destFile->exists()) {
232
                try {
233
                    $destFile->delete();
234
                } catch (Exception $e) {
235
                    throw new BuildException(
236
                        'Unable to remove existing file ' . $destFile->__toString() . ': ' . $e->getMessage()
237
                    );
238
                }
239
            }
240
        }
241
242 1
        $sourceFile->renameTo($destFile);
243
    }
244
245
    /**
246
     * Interpret the filename as a file relative to the given file -
247
     * unless the filename already represents an absolute filename.
248
     *
249
     * @param File   $file     the "reference" file for relative paths. This
250
     *                         instance must be an absolute file and must
251
     *                         not contain ./ or ../ sequences (same for \
252
     *                         instead of /).
253
     * @param string $filename a file name
254
     *
255
     * @throws IOException
256
     *
257
     * @return File A PhingFile object pointing to an absolute file that doesn't contain ./ or ../ sequences
258
     *              and uses the correct separator for the current platform.
259
     */
260 500
    public function resolveFile(File $file, string $filename): File
261
    {
262
        // remove this and use the static class constant File::separator
263
        // as soon as ZE2 is ready
264 500
        $fs = FileSystem::getFileSystem();
265
266 500
        $filename = str_replace(['\\', '/'], $fs->getSeparator(), $filename);
267
268
        // deal with absolute files
269
        if (
270 500
            StringHelper::startsWith($fs->getSeparator(), $filename)
271 500
            || (strlen($filename) >= 2
272 500
                && Character::isLetter($filename[0])
273 500
                && ':' === $filename[1])
274
        ) {
275 110
            return new File($this->normalize($filename));
276
        }
277
278 475
        if (strlen($filename) >= 2 && Character::isLetter($filename[0]) && ':' === $filename[1]) {
279
            return new File($this->normalize($filename));
280
        }
281
282 475
        $helpFile = new File($file->getAbsolutePath());
283
284 475
        $tok = strtok($filename, $fs->getSeparator());
285 475
        while (false !== $tok) {
286 475
            $part = $tok;
287 475
            if ('..' === $part) {
288 174
                $parentFile = $helpFile->getParent();
289 174
                if (null === $parentFile) {
290
                    $msg = "The file or path you specified ({$filename}) is invalid relative to " . $file->getPath();
291
292
                    throw new IOException($msg);
293
                }
294 174
                $helpFile = new File($parentFile);
295 475
            } elseif ('.' !== $part) {
296 445
                $helpFile = new File($helpFile, $part);
297
            }
298 475
            $tok = strtok($fs->getSeparator());
299
        }
300
301 475
        return new File($helpFile->getAbsolutePath());
302
    }
303
304
    /**
305
     * Normalize the given absolute path.
306
     *
307
     * This includes:
308
     *   - Uppercase the drive letter if there is one.
309
     *   - Remove redundant slashes after the drive spec.
310
     *   - resolve all ./, .\, ../ and ..\ sequences.
311
     *   - DOS style paths that start with a drive letter will have
312
     *     \ as the separator.
313
     *
314
     * @param string $path path to normalize
315
     *
316
     * @throws IOException
317
     * @throws BuildException
318
     */
319 921
    public function normalize(string $path): string
320
    {
321 921
        $dissect = $this->dissect($path);
322 921
        $sep = self::getSeparator();
323
324 921
        $s = [];
325 921
        $s[] = $dissect[0];
326 921
        $tok = strtok($dissect[1], $sep);
327 921
        while (false !== $tok) {
328 918
            $thisToken = $tok;
329 918
            if ('.' === $thisToken) {
330
                $tok = strtok($sep);
331
332
                continue;
333
            }
334
335 918
            if ('..' === $thisToken) {
336
                if (count($s) < 2) {
337
                    // using '..' in path that is too short
338
                    throw new IOException("Cannot resolve path: {$path}");
339
                }
340
341
                array_pop($s);
342
            } else { // plain component
343 918
                $s[] = $thisToken;
344
            }
345 918
            $tok = strtok($sep);
346
        }
347
348 921
        $sb = '';
349 921
        foreach ($s as $i => $v) {
350 921
            if ($i > 1) {
351
                // not before the filesystem root and not after it, since root
352
                // already contains one
353 918
                $sb .= $sep;
354
            }
355 921
            $sb .= $v;
356
        }
357
358 921
        $path = $sb;
359 921
        if (true === $this->dosWithDrive) {
360
            $path = str_replace('/', '\\', $path);
361
        }
362
363 921
        return $path;
364
    }
365
366
    /**
367
     * Dissect the specified absolute path.
368
     *
369
     * @throws BuildException
370
     * @throws IOException
371
     *
372
     * @return array {root, remainig path}
373
     */
374 921
    public function dissect(string $path): array
375
    {
376 921
        $sep = self::getSeparator();
377 921
        $path = str_replace(['\\', '/'], $sep, $path);
378
379
        // make sure we are dealing with an absolute path
380
        if (
381 921
            !StringHelper::startsWith($sep, $path)
382 921
            && !(strlen($path) >= 2
383 921
                && Character::isLetter($path[0])
384 921
                && ':' === $path[1])
385
        ) {
386
            throw new BuildException("{$path} is not an absolute path");
387
        }
388
389 921
        $this->dosWithDrive = false;
390 921
        $root = null;
391
392
        // Eliminate consecutive slashes after the drive spec
393
394 921
        if (strlen($path) >= 2 && Character::isLetter($path[0]) && ':' === $path[1]) {
395
            $this->dosWithDrive = true;
0 ignored issues
show
Documentation Bug introduced by
The property $dosWithDrive was declared of type false, but true is of type true. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
396
397
            $ca = str_replace('/', '\\', $path);
398
399
            $path = strtoupper($ca[0]) . ':';
400
401
            for ($i = 2, $_i = strlen($ca); $i < $_i; ++$i) {
402
                if (
403
                    ('\\' !== $ca[$i])
404
                    || ('\\' === $ca[$i]
405
                        && '\\' !== $ca[$i - 1])
406
                ) {
407
                    $path .= $ca[$i];
408
                }
409
            }
410
411
            $path = str_replace('\\', $sep, $path);
412
413
            if (2 === strlen($path)) {
414
                $root = $path;
415
                $path = '';
416
            } else {
417
                $root = substr($path, 0, 3);
418
                $path = substr($path, 3);
419
            }
420
        } else {
421 921
            if (1 === strlen($path)) {
422 3
                $root = $sep;
423 3
                $path = '';
424
            } else {
425 918
                if ($path[1] === $sep) {
426
                    // UNC drive
427
                    $root = $sep . $sep;
428
                    $path = substr($path, 2);
429
                } else {
430 918
                    $root = $sep;
431 918
                    $path = substr($path, 1);
432
                }
433
            }
434
        }
435
436 921
        return [$root, $path];
437
    }
438
439
    /**
440
     * Create a temporary file in a given directory.
441
     *
442
     * <p>The file denoted by the returned abstract pathname did not
443
     * exist before this method was invoked, any subsequent invocation
444
     * of this method will yield a different file name.</p>
445
     *
446
     * @param string $prefix       prefix before the random number
447
     * @param string $suffix       file extension; include the '.'.
448
     * @param File   $parentDir    directory to create the temporary file in;
449
     *                             sys_get_temp_dir() used if not specified
450
     * @param bool   $deleteOnExit whether to set the tempfile for deletion on
451
     *                             normal exit
452
     * @param bool   $createFile   true if the file must actually be created. If false
453
     *                             chances exist that a file with the same name is
454
     *                             created in the time between invoking this method
455
     *                             and the moment the file is actually created. If
456
     *                             possible set to true.
457
     *
458
     * @throws BuildException
459
     *
460
     * @return File a File reference to the new temporary file
461
     */
462 1
    public function createTempFile(
463
        $prefix,
464
        $suffix,
465
        File $parentDir = null,
466
        $deleteOnExit = false,
467
        $createFile = false
468
    ): File {
469 1
        $result = null;
470 1
        $parent = (null === $parentDir) ? self::getTempDir() : $parentDir->getPath();
471
472 1
        if ($createFile) {
473
            try {
474
                $directory = new File($parent);
475
                // quick but efficient hack to create a unique filename ;-)
476
                $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
477
                do {
478
                    $result = new File($directory, $prefix . substr(md5(time()), 0, 8) . $suffix);
479
                } while (file_exists($result->getPath()));
480
481
                $fs = FileSystem::getFileSystem();
482
                $fs->createNewFile($result->getPath());
483
                $fs->lock($result);
484
            } catch (IOException $e) {
485
                throw new BuildException('Could not create tempfile in ' . $parent, $e);
486
            }
487
        } else {
488
            do {
489 1
                $result = new File($parent, $prefix . substr(md5((string) time()), 0, 8) . $suffix);
490 1
            } while ($result->exists());
491
        }
492
493 1
        if ($deleteOnExit) {
494
            $result->deleteOnExit();
495
        }
496
497 1
        return $result;
498
    }
499
500
    /**
501
     * @throws IOException
502
     *
503
     * @return bool whether contents of two files is the same
504
     */
505 14
    public function contentEquals(File $file1, File $file2): bool
506
    {
507 14
        if (!($file1->exists() && $file2->exists())) {
508 2
            return false;
509
        }
510
511 13
        if (!($file1->canRead() && $file2->canRead())) {
512
            return false;
513
        }
514
515 13
        if ($file1->isDirectory() || $file2->isDirectory()) {
516 1
            return false;
517
        }
518
519 13
        $c1 = file_get_contents($file1->getAbsolutePath());
520 13
        $c2 = file_get_contents($file2->getAbsolutePath());
521
522 13
        return trim($c1) === trim($c2);
523
    }
524
}
525