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 | 4 | public static function getPathSeparator(): string |
|
58 | { |
||
59 | 4 | if (null === self::$pathSeparator) { |
|
60 | self::$pathSeparator = FileSystem::getFileSystem()->getPathSeparator(); |
||
61 | } |
||
62 | |||
63 | 4 | return self::$pathSeparator; |
|
64 | } |
||
65 | |||
66 | /** |
||
67 | * @throws IOException |
||
68 | */ |
||
69 | 891 | public static function getSeparator(): string |
|
70 | { |
||
71 | 891 | if (null === self::$separator) { |
|
72 | self::$separator = FileSystem::getFileSystem()->getSeparator(); |
||
73 | } |
||
74 | |||
75 | 891 | 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
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 | 37 | public static function getChainedReader(Reader $in, &$filterChains, Project $project) |
|
117 | { |
||
118 | 37 | if (!empty($filterChains)) { |
|
119 | 28 | $crh = new ChainReaderHelper(); |
|
120 | 28 | $crh->setBufferSize(65536); // 64k buffer, but isn't being used (yet?) |
|
121 | 28 | $crh->setPrimaryReader($in); |
|
122 | 28 | $crh->setFilterChains($filterChains); |
|
123 | 28 | $crh->setProject($project); |
|
124 | |||
125 | 28 | return $crh->getAssembledReader(); |
|
126 | } |
||
127 | |||
128 | 10 | 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 | 49 | 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 | 49 | $overwrite |
|
155 | 39 | || !$destFile->exists() |
|
156 | 49 | || $destFile->lastModified() < $sourceFile->lastModified() - $granularity |
|
157 | ) { |
||
158 | 48 | if ($destFile->exists() && ($destFile->isFile() || $destFile->isLink())) { |
|
159 | 1 | $destFile->delete(); |
|
160 | } |
||
161 | |||
162 | // ensure that parent dir of dest file exists! |
||
163 | 48 | $parent = $destFile->getParentFile(); |
|
164 | 48 | 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 | 3 | $dirMode = ($preservePermissions ? $sourceFile->getParentFile()->getMode() : $mode); |
|
170 | |||
171 | 3 | $parent->mkdirs($dirMode); |
|
172 | } |
||
173 | |||
174 | 48 | if ((is_array($filterChains)) && (!empty($filterChains))) { |
|
175 | 21 | $in = self::getChainedReader(new BufferedReader(new FileReader($sourceFile)), $filterChains, $project); |
|
176 | 21 | $out = new BufferedWriter(new FileWriter($destFile)); |
|
177 | |||
178 | // New read() methods returns a big buffer. |
||
179 | 21 | while (-1 !== ($buffer = $in->read())) { // -1 indicates EOF |
|
180 | 21 | $out->write($buffer); |
|
181 | } |
||
182 | |||
183 | 21 | if (null !== $in) { |
|
184 | 21 | $in->close(); |
|
185 | } |
||
186 | 21 | if (null !== $out) { |
|
187 | 21 | $out->close(); |
|
188 | } |
||
189 | |||
190 | // Set/Copy the permissions on the target |
||
191 | 21 | if (true === $preservePermissions) { |
|
192 | 21 | $destFile->setMode($sourceFile->getMode()); |
|
193 | } |
||
194 | } else { |
||
195 | // simple copy (no filtering) |
||
196 | 27 | $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 | 27 | if (false === $preservePermissions) { |
|
201 | $destFile->setMode(FileUtils::getDefaultFileCreationMask()); |
||
202 | } |
||
203 | } |
||
204 | |||
205 | 48 | if ($preserveLastModified && !$destFile->isLink()) { |
|
206 | 2 | $destFile->setLastModified($sourceFile->lastModified()); |
|
207 | } |
||
208 | } |
||
209 | 49 | } |
|
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 | 1 | } |
|
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 | 478 | 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 | 478 | $fs = FileSystem::getFileSystem(); |
|
265 | |||
266 | 478 | $filename = str_replace(['\\', '/'], $fs->getSeparator(), $filename); |
|
267 | |||
268 | // deal with absolute files |
||
269 | if ( |
||
270 | 478 | StringHelper::startsWith($fs->getSeparator(), $filename) |
|
271 | 453 | || (strlen($filename) >= 2 |
|
272 | 453 | && Character::isLetter($filename[0]) |
|
273 | 478 | && ':' === $filename[1]) |
|
274 | ) { |
||
275 | 106 | return new File($this->normalize($filename)); |
|
276 | } |
||
277 | |||
278 | 453 | if (strlen($filename) >= 2 && Character::isLetter($filename[0]) && ':' === $filename[1]) { |
|
279 | return new File($this->normalize($filename)); |
||
280 | } |
||
281 | |||
282 | 453 | $helpFile = new File($file->getAbsolutePath()); |
|
283 | |||
284 | 453 | $tok = strtok($filename, $fs->getSeparator()); |
|
285 | 453 | while (false !== $tok) { |
|
286 | 453 | $part = $tok; |
|
287 | 453 | if ('..' === $part) { |
|
288 | 171 | $parentFile = $helpFile->getParent(); |
|
289 | 171 | 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 | 171 | $helpFile = new File($parentFile); |
|
295 | 453 | } elseif ('.' !== $part) { |
|
296 | 427 | $helpFile = new File($helpFile, $part); |
|
297 | } |
||
298 | 453 | $tok = strtok($fs->getSeparator()); |
|
299 | } |
||
300 | |||
301 | 453 | 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 | 875 | public function normalize(string $path): string |
|
320 | { |
||
321 | 875 | $dissect = $this->dissect($path); |
|
322 | 875 | $sep = self::getSeparator(); |
|
323 | |||
324 | 875 | $s = []; |
|
325 | 875 | $s[] = $dissect[0]; |
|
326 | 875 | $tok = strtok($dissect[1], $sep); |
|
327 | 875 | while (false !== $tok) { |
|
328 | 872 | $thisToken = $tok; |
|
329 | 872 | if ('.' === $thisToken) { |
|
330 | $tok = strtok($sep); |
||
331 | |||
332 | continue; |
||
333 | } |
||
334 | |||
335 | 872 | if ('..' === $thisToken) { |
|
336 | 1 | if (count($s) < 2) { |
|
337 | // using '..' in path that is too short |
||
338 | throw new IOException("Cannot resolve path: {$path}"); |
||
339 | } |
||
340 | |||
341 | 1 | array_pop($s); |
|
342 | } else { // plain component |
||
343 | 872 | $s[] = $thisToken; |
|
344 | } |
||
345 | 872 | $tok = strtok($sep); |
|
346 | } |
||
347 | |||
348 | 875 | $sb = ''; |
|
349 | 875 | foreach ($s as $i => $v) { |
|
350 | 875 | if ($i > 1) { |
|
351 | // not before the filesystem root and not after it, since root |
||
352 | // already contains one |
||
353 | 872 | $sb .= $sep; |
|
354 | } |
||
355 | 875 | $sb .= $v; |
|
356 | } |
||
357 | |||
358 | 875 | $path = $sb; |
|
359 | 875 | if (true === $this->dosWithDrive) { |
|
360 | $path = str_replace('/', '\\', $path); |
||
361 | } |
||
362 | |||
363 | 875 | 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 | 875 | public function dissect(string $path): array |
|
375 | { |
||
376 | 875 | $sep = self::getSeparator(); |
|
377 | 875 | $path = str_replace(['\\', '/'], $sep, $path); |
|
378 | |||
379 | // make sure we are dealing with an absolute path |
||
380 | if ( |
||
381 | 875 | !StringHelper::startsWith($sep, $path) |
|
382 | && !(strlen($path) >= 2 |
||
383 | && Character::isLetter($path[0]) |
||
384 | 875 | && ':' === $path[1]) |
|
385 | ) { |
||
386 | throw new BuildException("{$path} is not an absolute path"); |
||
387 | } |
||
388 | |||
389 | 875 | $this->dosWithDrive = false; |
|
390 | 875 | $root = null; |
|
391 | |||
392 | // Eliminate consecutive slashes after the drive spec |
||
393 | |||
394 | 875 | if (strlen($path) >= 2 && Character::isLetter($path[0]) && ':' === $path[1]) { |
|
395 | $this->dosWithDrive = true; |
||
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 | 875 | if (1 === strlen($path)) { |
|
422 | 3 | $root = $sep; |
|
423 | 3 | $path = ''; |
|
424 | } else { |
||
425 | 872 | if ($path[1] === $sep) { |
|
426 | // UNC drive |
||
427 | $root = $sep . $sep; |
||
428 | $path = substr($path, 2); |
||
429 | } else { |
||
430 | 872 | $root = $sep; |
|
431 | 872 | $path = substr($path, 1); |
|
432 | } |
||
433 | } |
||
434 | } |
||
435 | |||
436 | 875 | 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; |
||
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 |