Passed
Push — main ( 322a86...2c83d5 )
by Siad
06:11
created

FileUtils::renameFile()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 12.6306

Importance

Changes 0
Metric Value
cc 8
eloc 12
nc 12
nop 3
dl 0
loc 23
ccs 7
cts 12
cp 0.5833
crap 12.6306
rs 8.4444
c 0
b 0
f 0
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 Exception;
23
use Phing\Exception\BuildException;
24
use Phing\Filter\ChainReaderHelper;
25
use Phing\Phing;
26
use Phing\Project;
27
use Phing\Util\Character;
28
use Phing\Util\StringHelper;
29
30
/**
31
 * File utility class.
32
 * - handles os independent stuff etc
33
 * - mapper stuff
34
 * - filter stuff
35
 *
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
     * @return string
56
     * @throws IOException
57
     */
58 4
    public static function getPathSeparator(): string
59
    {
60 4
        if (self::$pathSeparator === null) {
61
            self::$pathSeparator = FileSystem::getFileSystem()->getPathSeparator();
62
        }
63 4
        return self::$pathSeparator;
64
    }
65
66
    /**
67
     * @return string
68
     * @throws IOException
69
     */
70 889
    public static function getSeparator(): string
71
    {
72 889
        if (self::$separator === null) {
73
            self::$separator = FileSystem::getFileSystem()->getSeparator();
74
        }
75 889
        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 = ($dirmode === true) ? 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
     * @return Reader  Assembled Reader (w/ filter chains).
114
     */
115 37
    public static function getChainedReader(Reader $in, &$filterChains, Project $project)
116
    {
117 37
        if (!empty($filterChains)) {
118 28
            $crh = new ChainReaderHelper();
119 28
            $crh->setBufferSize(65536); // 64k buffer, but isn't being used (yet?)
120 28
            $crh->setPrimaryReader($in);
121 28
            $crh->setFilterChains($filterChains);
122 28
            $crh->setProject($project);
123 28
            return $crh->getAssembledReader();
124
        }
125
126 10
        return $in;
127
    }
128
129
    /**
130
     * Copies a file using filter chains.
131
     *
132
     * @param File $sourceFile
133
     * @param File $destFile
134
     * @param Project $project
135
     * @param bool $overwrite
136
     * @param bool $preserveLastModified
137
     * @param array $filterChains
138
     * @param int $mode
139
     * @param bool $preservePermissions
140
     * @param int $granularity
141
     * @throws IOException
142
     */
143 49
    public function copyFile(
144
        File $sourceFile,
145
        File $destFile,
146
        Project $project,
147
        $overwrite = false,
148
        $preserveLastModified = true,
149
        &$filterChains = null,
150
        $mode = 0755,
151
        $preservePermissions = true,
152
        int $granularity = 0
153
    ) {
154
        if (
155 49
            $overwrite
156 39
            || !$destFile->exists()
157 49
            || $destFile->lastModified() < $sourceFile->lastModified() - $granularity
158
        ) {
159 48
            if ($destFile->exists() && ($destFile->isFile() || $destFile->isLink())) {
160 1
                $destFile->delete();
161
            }
162
163
            // ensure that parent dir of dest file exists!
164 48
            $parent = $destFile->getParentFile();
165 48
            if ($parent !== null && !$parent->exists()) {
166
                // Setting source directory permissions to target
167
                // (On permissions preservation, the target directory permissions
168
                // will be inherited from the source directory, otherwise the 'mode'
169
                // will be used)
170 3
                $dirMode = ($preservePermissions ? $sourceFile->getParentFile()->getMode() : $mode);
171
172 3
                $parent->mkdirs($dirMode);
173
            }
174
175 48
            if ((is_array($filterChains)) && (!empty($filterChains))) {
176 21
                $in = self::getChainedReader(new BufferedReader(new FileReader($sourceFile)), $filterChains, $project);
177 21
                $out = new BufferedWriter(new FileWriter($destFile));
178
179
                // New read() methods returns a big buffer.
180 21
                while (-1 !== ($buffer = $in->read())) { // -1 indicates EOF
181 21
                    $out->write($buffer);
182
                }
183
184 21
                if ($in !== null) {
185 21
                    $in->close();
186
                }
187 21
                if ($out !== null) {
188 21
                    $out->close();
189
                }
190
191
                // Set/Copy the permissions on the target
192 21
                if ($preservePermissions === true) {
193 21
                    $destFile->setMode($sourceFile->getMode());
194
                }
195
            } else {
196
                // simple copy (no filtering)
197 27
                $sourceFile->copyTo($destFile);
198
199
                // By default, PHP::Copy also copies the file permissions. Therefore,
200
                // re-setting the mode with the "user file-creation mask" information.
201 27
                if ($preservePermissions === false) {
202
                    $destFile->setMode(FileUtils::getDefaultFileCreationMask());
203
                }
204
            }
205
206 48
            if ($preserveLastModified && !$destFile->isLink()) {
207 2
                $destFile->setLastModified($sourceFile->lastModified());
208
            }
209
        }
210 49
    }
211
212
    /**
213
     * Attempts to rename a file from a source to a destination.
214
     * If overwrite is set to true, this method overwrites existing file even if the destination file is newer.
215
     * Otherwise, the source file is renamed only if the destination file is older than it.
216
     *
217
     * @throws IOException
218
     */
219 1
    public function renameFile(File $sourceFile, File $destFile, $overwrite = false): void
220
    {
221
        // ensure that parent dir of dest file exists!
222 1
        $parent = $destFile->getParentFile();
223 1
        if ($parent !== null) {
224 1
            if (!$parent->exists()) {
225
                $parent->mkdirs();
226
            }
227
        }
228
229 1
        if ($overwrite || !$destFile->exists() || $destFile->lastModified() < $sourceFile->lastModified()) {
230 1
            if ($destFile->exists()) {
231
                try {
232
                    $destFile->delete();
233
                } catch (Exception $e) {
234
                    throw new BuildException(
235
                        "Unable to remove existing file " . $destFile->__toString() . ": " . $e->getMessage()
236
                    );
237
                }
238
            }
239
        }
240
241 1
        $sourceFile->renameTo($destFile);
242 1
    }
243
244
    /**
245
     * Interpret the filename as a file relative to the given file -
246
     * unless the filename already represents an absolute filename.
247
     *
248
     * @param File $file the "reference" file for relative paths. This
249
     *                             instance must be an absolute file and must
250
     *                             not contain ./ or ../ sequences (same for \
251
     *                             instead of /).
252
     * @param string $filename a file name
253
     *
254
     * @return File A PhingFile object pointing to an absolute file that doesn't contain ./ or ../ sequences
255
     *                   and uses the correct separator for the current platform.
256
     * @throws IOException
257
     *
258
     */
259 476
    public function resolveFile(File $file, string $filename): File
260
    {
261
        // remove this and use the static class constant File::separator
262
        // as soon as ZE2 is ready
263 476
        $fs = FileSystem::getFileSystem();
264
265 476
        $filename = str_replace(['\\', '/'], $fs->getSeparator(), $filename);
266
267
        // deal with absolute files
268
        if (
269 476
            StringHelper::startsWith($fs->getSeparator(), $filename)
270 451
            || (strlen($filename) >= 2
271 451
                && Character::isLetter($filename[0])
272 476
                && $filename[1] === ':')
273
        ) {
274 105
            return new File($this->normalize($filename));
275
        }
276
277 451
        if (strlen($filename) >= 2 && Character::isLetter($filename[0]) && $filename[1] === ':') {
278
            return new File($this->normalize($filename));
279
        }
280
281 451
        $helpFile = new File($file->getAbsolutePath());
282
283 451
        $tok = strtok($filename, $fs->getSeparator());
284 451
        while ($tok !== false) {
285 451
            $part = $tok;
286 451
            if ($part === '..') {
287 170
                $parentFile = $helpFile->getParent();
288 170
                if ($parentFile === null) {
289
                    $msg = "The file or path you specified ($filename) is invalid relative to " . $file->getPath();
290
                    throw new IOException($msg);
291
                }
292 170
                $helpFile = new File($parentFile);
293 451
            } elseif ($part !== '.') {
294 425
                $helpFile = new File($helpFile, $part);
295
            }
296 451
            $tok = strtok($fs->getSeparator());
297
        }
298
299 451
        return new File($helpFile->getAbsolutePath());
300
    }
301
302
    /**
303
     * Normalize the given absolute path.
304
     *
305
     * This includes:
306
     *   - Uppercase the drive letter if there is one.
307
     *   - Remove redundant slashes after the drive spec.
308
     *   - resolve all ./, .\, ../ and ..\ sequences.
309
     *   - DOS style paths that start with a drive letter will have
310
     *     \ as the separator.
311
     *
312
     * @param string $path Path to normalize.
313
     *
314
     * @return string
315
     * @throws IOException
316
     * @throws BuildException
317
     */
318 873
    public function normalize(string $path): string
319
    {
320 873
        $dissect = $this->dissect($path);
321 873
        $sep = self::getSeparator();
322
323 873
        $s = [];
324 873
        $s[] = $dissect[0];
325 873
        $tok = strtok($dissect[1], $sep);
326 873
        while ($tok !== false) {
327 870
            $thisToken = $tok;
328 870
            if ("." === $thisToken) {
329
                $tok = strtok($sep);
330
                continue;
331
            }
332
333 870
            if (".." === $thisToken) {
334 1
                if (count($s) < 2) {
335
                    // using '..' in path that is too short
336
                    throw new IOException("Cannot resolve path: $path");
337
                }
338
339 1
                array_pop($s);
340
            } else { // plain component
341 870
                $s[] = $thisToken;
342
            }
343 870
            $tok = strtok($sep);
344
        }
345
346 873
        $sb = "";
347 873
        foreach ($s as $i => $v) {
348 873
            if ($i > 1) {
349
                // not before the filesystem root and not after it, since root
350
                // already contains one
351 870
                $sb .= $sep;
352
            }
353 873
            $sb .= $v;
354
        }
355
356 873
        $path = $sb;
357 873
        if ($this->dosWithDrive === true) {
358
            $path = str_replace('/', '\\', $path);
359
        }
360
361 873
        return $path;
362
    }
363
364
    /**
365
     * Dissect the specified absolute path.
366
     * @param string $path
367
     * @return array {root, remainig path}
368
     * @throws BuildException
369
     * @throws IOException
370
     */
371 873
    public function dissect(string $path): array
372
    {
373 873
        $sep = self::getSeparator();
374 873
        $path = str_replace(['\\', '/'], $sep, $path);
375
376
        // make sure we are dealing with an absolute path
377
        if (
378 873
            !StringHelper::startsWith($sep, $path)
379
            && !(strlen($path) >= 2
380
                && Character::isLetter($path[0])
381 873
                && $path[1] === ':')
382
        ) {
383
            throw new BuildException("$path is not an absolute path");
384
        }
385
386 873
        $this->dosWithDrive = false;
387 873
        $root = null;
388
389
        // Eliminate consecutive slashes after the drive spec
390
391 873
        if (strlen($path) >= 2 && Character::isLetter($path[0]) && $path[1] === ':') {
392
            $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...
393
394
            $ca = str_replace('/', '\\', $path);
395
396
            $path = strtoupper($ca[0]) . ':';
397
398
            for ($i = 2, $_i = strlen($ca); $i < $_i; $i++) {
399
                if (
400
                    ($ca[$i] !== '\\')
401
                    || ($ca[$i] === '\\'
402
                        && $ca[$i - 1] !== '\\')
403
                ) {
404
                    $path .= $ca[$i];
405
                }
406
            }
407
408
            $path = str_replace('\\', $sep, $path);
409
410
            if (strlen($path) === 2) {
411
                $root = $path;
412
                $path = "";
413
            } else {
414
                $root = substr($path, 0, 3);
415
                $path = substr($path, 3);
416
            }
417
        } else {
418 873
            if (strlen($path) === 1) {
419 3
                $root = $sep;
420 3
                $path = "";
421
            } else {
422 870
                if ($path[1] === $sep) {
423
                    // UNC drive
424
                    $root = $sep . $sep;
425
                    $path = substr($path, 2);
426
                } else {
427 870
                    $root = $sep;
428 870
                    $path = substr($path, 1);
429
                }
430
            }
431
        }
432
433 873
        return [$root, $path];
434
    }
435
436
    /**
437
     * Create a temporary file in a given directory.
438
     *
439
     * <p>The file denoted by the returned abstract pathname did not
440
     * exist before this method was invoked, any subsequent invocation
441
     * of this method will yield a different file name.</p>
442
     *
443
     * @param string $prefix prefix before the random number.
444
     * @param string $suffix file extension; include the '.'.
445
     * @param File $parentDir Directory to create the temporary file in;
446
     *                                sys_get_temp_dir() used if not specified.
447
     * @param bool $deleteOnExit whether to set the tempfile for deletion on
448
     *                                normal exit.
449
     * @param bool $createFile true if the file must actually be created. If false
450
     *                                chances exist that a file with the same name is
451
     *                                created in the time between invoking this method
452
     *                                and the moment the file is actually created. If
453
     *                                possible set to true.
454
     * @return File            a File reference to the new temporary file.
455
     * @throws BuildException
456
     */
457 1
    public function createTempFile(
458
        $prefix,
459
        $suffix,
460
        File $parentDir = null,
461
        $deleteOnExit = false,
462
        $createFile = false
463
    ): File {
464 1
        $result = null;
465 1
        $parent = ($parentDir === null) ? self::getTempDir() : $parentDir->getPath();
466
467 1
        if ($createFile) {
468
            try {
469
                $directory = new File($parent);
470
                // quick but efficient hack to create a unique filename ;-)
471
                $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
472
                do {
473
                    $result = new File($directory, $prefix . substr(md5(time()), 0, 8) . $suffix);
474
                } while (file_exists($result->getPath()));
475
476
                $fs = FileSystem::getFileSystem();
477
                $fs->createNewFile($result->getPath());
478
                $fs->lock($result);
479
            } catch (IOException $e) {
480
                throw new BuildException("Could not create tempfile in " . $parent, $e);
481
            }
482
        } else {
483
            do {
484 1
                $result = new File($parent, $prefix . substr(md5((string) time()), 0, 8) . $suffix);
485 1
            } while ($result->exists());
486
        }
487
488 1
        if ($deleteOnExit) {
489
            $result->deleteOnExit();
490
        }
491
492 1
        return $result;
493
    }
494
495
    /**
496
     * @param File $file1
497
     * @param File $file2
498
     * @return bool Whether contents of two files is the same.
499
     * @throws IOException
500
     */
501 14
    public function contentEquals(File $file1, File $file2): bool
502
    {
503 14
        if (!($file1->exists() && $file2->exists())) {
504 2
            return false;
505
        }
506
507 13
        if (!($file1->canRead() && $file2->canRead())) {
508
            return false;
509
        }
510
511 13
        if ($file1->isDirectory() || $file2->isDirectory()) {
512 1
            return false;
513
        }
514
515 13
        $c1 = file_get_contents($file1->getAbsolutePath());
516 13
        $c2 = file_get_contents($file2->getAbsolutePath());
517
518 13
        return trim($c1) === trim($c2);
519
    }
520
}
521