Passed
Push — test ( 018ff3...9962ad )
by Tom
02:40
created

LibFs::normalizePath()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 16
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines;
6
7
use UnexpectedValueException;
8
9
/**
10
 * Class LibFs - Low level file-system utility functions
11
 *
12
 * @covers \Ktomk\Pipelines\LibFs
13
 */
14
class LibFs
15
{
16
    /**
17
     * @param string $path
18
     * @param string $mode [optional]
19
     *
20
     * @return bool
21
     */
22 3
    public static function canFopen($path, $mode = null)
23
    {
24 3
        if (null === $mode) {
25 1
            $mode = 'rb';
26
        }
27
28 3
        $handle = @fopen($path, $mode);
29 3
        if (false === $handle) {
30 2
            return false;
31
        }
32
33
        /** @scrutinizer ignore-unhandled */
34 2
        @fclose($handle);
35
36 2
        return true;
37
    }
38
39
    /**
40
     * locate (readable) file by basename upward all parent directories
41
     *
42
     * @param string $basename
43
     * @param string $directory [optional] directory to operate from, defaults
44
     *               to "." (relative path of present working directory)
45
     *
46
     * @return null|string
47
     */
48 4
    public static function fileLookUp($basename, $directory = null)
49
    {
50 4
        if ('' === $directory || null === $directory) {
51 1
            $directory = '.';
52
        }
53
54
        for (
55 4
            $dirName = $directory, $old = null;
56 4
            $old !== $dirName;
57 2
            $old = $dirName, $dirName = dirname($dirName)
58
        ) {
59 4
            $test = $dirName . '/' . $basename;
60 4
            if (self::isReadableFile($test)) {
61 3
                return $test;
62
            }
63
        }
64
65 1
        return null;
66
    }
67
68
    /**
69
     * check if path is absolute
70
     *
71
     * @param string $path
72
     *
73
     * @return bool
74
     */
75 8
    public static function isAbsolutePath($path)
76
    {
77
        // TODO: a variant with PHP stream wrapper prefix support
78
        /* @see isStreamUri */
79
80 8
        $count = strspn($path, '/', 0, 3) % 2;
81
82 8
        return (bool)$count;
83
    }
84
85
    /**
86
     * check if path is basename
87
     *
88
     * @param string $path
89
     *
90
     * @return bool
91
     */
92 5
    public static function isBasename($path)
93
    {
94 5
        if (in_array($path, array('', '.', '..'), true)) {
95 1
            return false;
96
        }
97
98 4
        if (false !== strpos($path, '/')) {
99 3
            return false;
100
        }
101
102 1
        return true;
103
    }
104
105
    /**
106
     * see 2.2 Standards permit the exclusion of bad filenames / POSIX.1-2008
107
     *
108
     * @link https://dwheeler.com/essays/fixing-unix-linux-filenames.html
109
     *
110
     * @param string $filename
111
     *
112
     * @return bool
113
     */
114 10
    public static function isPortableFilename($filename)
115
    {
116
        # A-Z, a-z, 0-9, <period>, <underscore>, and <hyphen>)
117 10
        $result = preg_match('(^(?!-)[A-Za-z0-9._-]+$)', $filename);
118 10
        if (false === $result) {
119
            // @codeCoverageIgnoreStart
120
            throw new UnexpectedValueException('preg_match pattern failed');
121
            // @codeCoverageIgnoreEnd
122
        }
123
124 10
        return 1 === $result;
125
    }
126
127
    /**
128
     * @param string $path
129
     *
130
     * @return bool
131
     */
132 12
    public static function isReadableFile($path)
133
    {
134 12
        if (!is_file($path)) {
135 5
            return false;
136
        }
137
138 10
        return is_readable($path) ?: self::canFopen($path, 'rb');
139
    }
140
141
    /**
142
     * similar to a readable file, a readable path allows stream-urls
143
     * as well, e.g. php://stdin or php://fd/0 but only for local
144
     * streams.
145
     *
146
     * @param string $path
147
     *
148
     * @return bool
149
     */
150 1
    public static function isReadableStream($path)
151
    {
152 1
        if (self::isReadableFile($path)) {
153 1
            return true;
154
        }
155
156 1
        if (!is_string($path)) {
0 ignored issues
show
introduced by
The condition is_string($path) is always true.
Loading history...
157 1
            return false;
158
        }
159
160 1
        return self::canFopen($path, 'rb');
161
    }
162
163
    /**
164
     * map a file path to a stream path (if applicable)
165
     *
166
     * @param string $file
167
     * @param null|string $dashRepresent
168
     *
169
     * @return string
170
     */
171 1
    public static function mapStream($file, $dashRepresent = 'php://stdin')
172
    {
173 1
        if (self::isStreamUri($file)) {
174 1
            return $file;
175
        }
176
177 1
        $path = $file;
178 1
        (null !== $dashRepresent) && ('-' === $file) && $path = $dashRepresent;
179
180
        /* @link https://bugs.php.net/bug.php?id=53465 */
181 1
        $path = preg_replace('(^/(?:proc/self|dev)/(fd/\d+))', 'php://\1', $path);
182
183 1
        return stream_is_local($path) ? $path : $file;
184
    }
185
186
    /**
187
     * @param string $path
188
     *
189
     * @return bool
190
     */
191 3
    public static function isStreamUri($path)
192
    {
193 3
        $scheme = parse_url($path, PHP_URL_SCHEME);
194 3
        if (null === $scheme) {
195 2
            return false;
196
        }
197
198 2
        return in_array($scheme, stream_get_wrappers(), true);
199
    }
200
201
    /**
202
     * create directory if not yet exists
203
     *
204
     * @param string $path
205
     * @param int $mode [optional]
206
     *
207
     * @return string
208
     */
209 7
    public static function mkDir($path, $mode = 0777)
210
    {
211 7
        if (!is_dir($path)) {
212
            /** @noinspection NestedPositiveIfStatementsInspection */
213 6
            if (!mkdir($path, $mode, true) && !is_dir($path)) {
214
                // @codeCoverageIgnoreStart
215
                throw new \RuntimeException(
216
                    sprintf('Directory "%s" was not created', $path)
217
                );
218
                // @codeCoverageIgnoreEnd
219
            }
220
        }
221
222 7
        return $path;
223
    }
224
225
    /**
226
     * Normalize a path as/if common in PHP
227
     *
228
     * E.g. w/ phar:// in front which means w/ stream wrappers in
229
     * mind.
230
     *
231
     * @param string $path
232
     *
233
     * @return string
234
     */
235 5
    public static function normalizePath($path)
236
    {
237 5
        $buffer = $path;
238
239 5
        $scheme = '';
240
        // TODO support for all supported stream wrappers (w/ absolute/relative notation?)
241
        /* @see isStreamUri */
242
        /* @see isAbsolutePath */
243 5
        if (0 === strpos($buffer, 'phar://') || 0 === strpos($buffer, 'file://')) {
244 4
            $scheme = substr($buffer, 0, 7);
245 4
            $buffer = substr($buffer, 7);
246
        }
247
248 5
        $normalized = self::normalizePathSegments($buffer);
249
250 5
        return $scheme . $normalized;
251
    }
252
253
    /**
254
     * Resolve relative path segments in a path on it's own
255
     *
256
     * This is not realpath, not resolving any links.
257
     *
258
     * @param string $path
259
     *
260
     * @return string
261
     */
262 18
    public static function normalizePathSegments($path)
263
    {
264 18
        if ('' === $path) {
265 1
            return $path;
266
        }
267
268 17
        $buffer = $path;
269
270 17
        $prefix = '';
271 17
        $len = strspn($buffer, '/');
272 17
        if (0 < $len) {
273 9
            $prefix = substr($buffer, 0, $len);
274 9
            $buffer = substr($buffer, $len);
275
        }
276
277 17
        $buffer = rtrim($buffer, '/');
278
279 17
        if (in_array($buffer, array('', '.'), true)) {
280 4
            return $prefix;
281
        }
282
283 13
        $pos = strpos($buffer, '/');
284 13
        if (false === $pos) {
285 2
            return $prefix . $buffer;
286
        }
287
288 11
        $buffer = preg_replace('~/+~', '/', $buffer);
289
290 11
        $segments = explode('/', $buffer);
291 11
        $stack = array();
292 11
        foreach ($segments as $segment) {
293 11
            $i = count($stack) - 1;
294 11
            if ('.' === $segment) {
295 2
                continue;
296
            }
297
298 11
            if ('..' !== $segment) {
299 11
                $stack[] = $segment;
300
301 11
                continue;
302
            }
303
304 9
            if (($i > -1) && '..' !== $stack[$i]) {
305 9
                array_pop($stack);
306
307 9
                continue;
308
            }
309
310 1
            $stack[] = $segment;
311
        }
312
313 11
        return $prefix . implode('/', $stack);
314
    }
315
316
    /**
317
     * rename a file
318
     *
319
     * @param string $old
320
     * @param string $new
321
     *
322
     * @return string new file-name
323
     */
324 1
    public static function rename($old, $new)
325
    {
326 1
        if (!@rename($old, $new)) {
327 1
            throw new \RuntimeException(sprintf('Failed to rename "%s" to "%s"', $old, $new));
328
        }
329
330 1
        return $new;
331
    }
332
333
    /**
334
     * @param string $file
335
     *
336
     * @return string
337
     */
338 5
    public static function rm($file)
339
    {
340 5
        if (self::isReadableFile($file)) {
341 5
            unlink($file);
342
        }
343
344 5
        return $file;
345
    }
346
347
    /**
348
     * @param string $dir
349
     *
350
     * @throws UnexpectedValueException
351
     *
352
     * @return void
353
     */
354 3
    public static function rmDir($dir)
355
    {
356 3
        $result = @lstat($dir);
357 3
        if (false === $result) {
358 1
            return;
359
        }
360
361 3
        $dirs = array();
362 3
        $dirs[] = $dir;
363 3
        for ($i = 0; isset($dirs[$i]); $i++) {
364 3
            $current = $dirs[$i];
365 3
            $result = @scandir($current);
366 3
            if (false === $result) {
367 1
                throw new UnexpectedValueException(sprintf('Failed to open directory: %s', $current));
368
            }
369 2
            $files = array_diff($result, array('.', '..'));
370 2
            foreach ($files as $file) {
371 1
                $path = "${current}/${file}";
372 1
                if (is_dir($path)) {
373 1
                    $dirs[] = $path;
374 1
                } elseif (is_file($path)) {
375 1
                    self::rm($path);
376
                }
377
            }
378
        }
379
380 2
        while (null !== ($pop = array_pop($dirs))) {
381
            /* @scrutinizer ignore-unhandled */
382 2
            @rmdir($pop);
383
        }
384 2
    }
385
386
    /**
387
     * create symbolic link, recreate if it exists
388
     *
389
     * @param string $target
390
     * @param string $link
391
     *
392
     * @return void
393
     */
394
    public static function symlink($target, $link)
395
    {
396 1
        self::unlink($link);
397 1
        symlink($target, $link);
398 1
    }
399
400
    /**
401
     * @param string $link
402
     *
403
     * @return void
404
     */
405
    public static function unlink($link)
406
    {
407 1
        if (is_link($link)) {
408 1
            unlink($link);
409
        }
410 1
    }
411
}
412