Path::makeRelative()   C
last analyzed

Complexity

Conditions 13
Paths 8

Size

Total Lines 56
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 25
nc 8
nop 2
dl 0
loc 56
rs 6.6166
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the webmozart/path-util package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Utils;
13
14
use DaveLiddament\StaticAnalysisResultsBaseliner\Domain\Common\InvalidPathException;
15
use InvalidArgumentException;
16
use Webmozart\Assert\Assert;
17
18
/**
19
 * Contains utility methods for handling path strings.
20
 *
21
 * The methods in this class are able to deal with both UNIX and Windows paths
22
 * with both forward and backward slashes. All methods return normalized parts
23
 * containing only forward slashes and no excess "." and ".." segments.
24
 *
25
 * @since  1.0
26
 *
27
 * @author Bernhard Schussek <[email protected]>
28
 * @author Thomas Schulz <[email protected]>
29
 */
30
final class Path
31
{
32
    /**
33
     * The number of buffer entries that triggers a cleanup operation.
34
     */
35
    private const CLEANUP_THRESHOLD = 1250;
36
37
    /**
38
     * The buffer size after the cleanup operation.
39
     */
40
    private const CLEANUP_SIZE = 1000;
41
42
    /**
43
     * Buffers input/output of {@link canonicalize()}.
44
     *
45
     * @var array<string,string>
46
     */
47
    private static $buffer = [];
48
49
    /**
50
     * The size of the buffer.
51
     *
52
     * @var int
53
     */
54
    private static $bufferSize = 0;
55
56
    /**
57
     * Canonicalizes the given path.
58
     *
59
     * During normalization, all slashes are replaced by forward slashes ("/").
60
     * Furthermore, all "." and ".." segments are removed as far as possible.
61
     * ".." segments at the beginning of relative paths are not removed.
62
     *
63
     * ```php
64
     * echo Path::canonicalize("\webmozart\puli\..\css\style.css");
65
     * // => /webmozart/css/style.css
66
     *
67
     * echo Path::canonicalize("../css/./style.css");
68
     * // => ../css/style.css
69
     * ```
70
     *
71
     * This method is able to deal with both UNIX and Windows paths.
72
     *
73
     * @throws InvalidPathException
74
     */
75
    public static function canonicalize(string $path): string
76
    {
77
        if ('' === $path) {
78
            return '';
79
        }
80
81
        // This method is called by many other methods in this class. Buffer
82
        // the canonicalized paths to make up for the severe performance
83
        // decrease.
84
        if (isset(self::$buffer[$path])) {
85
            return self::$buffer[$path];
86
        }
87
88
        // Replace "~" with user's home directory.
89
        if ('~' === $path[0]) {
90
            $path = static::getHomeDirectory().substr($path, 1);
91
        }
92
93
        $path = str_replace('\\', '/', $path);
94
95
        [$root, $pathWithoutRoot] = self::split($path);
96
97
        $parts = explode('/', $pathWithoutRoot);
98
        $canonicalParts = [];
99
100
        // Collapse "." and "..", if possible
101
        foreach ($parts as $part) {
102
            if ('.' === $part || '' === $part) {
103
                continue;
104
            }
105
106
            // Collapse ".." with the previous part, if one exists
107
            // Don't collapse ".." if the previous part is also ".."
108
            if ('..' === $part && count($canonicalParts) > 0
109
                    && '..' !== $canonicalParts[count($canonicalParts) - 1]) {
110
                array_pop($canonicalParts);
111
112
                continue;
113
            }
114
115
            // Only add ".." prefixes for relative paths
116
            if ('..' !== $part || '' === $root) {
117
                $canonicalParts[] = $part;
118
            }
119
        }
120
121
        // Add the root directory again
122
        self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
123
        ++self::$bufferSize;
124
125
        // Clean up regularly to prevent memory leaks
126
        if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
127
            self::$buffer = array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
128
            self::$bufferSize = self::CLEANUP_SIZE;
129
        }
130
131
        return $canonicalPath;
132
    }
133
134
    /**
135
     * Returns canonical path of the user's home directory.
136
     *
137
     * Supported operating systems:
138
     *
139
     *  - UNIX
140
     *  - Windows8 and upper
141
     *
142
     * If your operation system or environment isn't supported, an exception is thrown.
143
     *
144
     * The result is a canonical path.
145
     *
146
     * @throws InvalidPathException If your operation system or environment isn't supported
147
     */
148
    public static function getHomeDirectory(): string
149
    {
150
        // For UNIX support
151
        $home = getenv('HOME');
152
        if (false !== $home && '' !== $home) {
153
            return static::canonicalize($home);
154
        }
155
156
        // For >= Windows8 support
157
        $homeDrive = getenv('HOMEDRIVE');
158
        $homePath = getenv('HOMEPATH');
159
160
        if (false !== $homeDrive && '' !== $homeDrive && false !== $homePath && '' !== $homePath) {
161
            return static::canonicalize($homeDrive.$homePath);
162
        }
163
164
        throw InvalidPathException::operatingSystemNotSupported();
165
    }
166
167
    /**
168
     * Returns whether a path is absolute.
169
     */
170
    public static function isAbsolute(string $path): bool
171
    {
172
        if ('' === $path) {
173
            return false;
174
        }
175
176
        // Strip scheme
177
        if (false !== ($pos = strpos($path, '://'))) {
178
            $path = substr($path, $pos + 3);
179
        }
180
181
        // UNIX root "/" or "\" (Windows style)
182
        if ('/' === $path[0] || '\\' === $path[0]) {
183
            return true;
184
        }
185
186
        // Windows root
187
        if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
188
            // Special case: "C:"
189
            if (2 === strlen($path)) {
190
                return true;
191
            }
192
193
            // Normal case: "C:/ or "C:\"
194
            if ('/' === $path[2] || '\\' === $path[2]) {
195
                return true;
196
            }
197
        }
198
199
        return false;
200
    }
201
202
    /**
203
     * Turns a relative path into an absolute path.
204
     *
205
     * Usually, the relative path is appended to the given base path. Dot
206
     * segments ("." and "..") are removed/collapsed and all slashes turned
207
     * into forward slashes.
208
     *
209
     * ```php
210
     * echo Path::makeAbsolute("../style.css", "/webmozart/puli/css");
211
     * // => /webmozart/puli/style.css
212
     * ```
213
     *
214
     * If an absolute path is passed, that path is returned unless its root
215
     * directory is different than the one of the base path. In that case, an
216
     * exception is thrown.
217
     *
218
     * ```php
219
     * Path::makeAbsolute("/style.css", "/webmozart/puli/css");
220
     * // => /style.css
221
     *
222
     * Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css");
223
     * // => C:/style.css
224
     *
225
     * Path::makeAbsolute("C:/style.css", "/webmozart/puli/css");
226
     * // InvalidArgumentException
227
     * ```
228
     *
229
     * If the base path is not an absolute path, an exception is thrown.
230
     *
231
     * The result is a canonical path.
232
     *
233
     * @throws \InvalidArgumentException if the base path is not absolute or if
234
     *                                  the given path is an absolute path with
235
     *                                  a different root than the base path
236
     * @throws InvalidPathException
237
     */
238
    public static function makeAbsolute(string $path, string $basePath): string
239
    {
240
        Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s');
241
242
        if (!static::isAbsolute($basePath)) {
243
            throw new \InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath));
244
        }
245
246
        if (static::isAbsolute($path)) {
247
            return static::canonicalize($path);
248
        }
249
250
        if (false !== ($pos = strpos($basePath, '://'))) {
251
            $scheme = substr($basePath, 0, $pos + 3);
252
            $basePath = substr($basePath, $pos + 3);
253
        } else {
254
            $scheme = '';
255
        }
256
257
        return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
258
    }
259
260
    /**
261
     * Turns a path into a relative path.
262
     *
263
     * The relative path is created relative to the given base path:
264
     *
265
     * ```php
266
     * echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli");
267
     * // => ../style.css
268
     * ```
269
     *
270
     * If a relative path is passed and the base path is absolute, the relative
271
     * path is returned unchanged:
272
     *
273
     * ```php
274
     * Path::makeRelative("style.css", "/webmozart/puli/css");
275
     * // => style.css
276
     * ```
277
     *
278
     * If both paths are relative, the relative path is created with the
279
     * assumption that both paths are relative to the same directory:
280
     *
281
     * ```php
282
     * Path::makeRelative("style.css", "webmozart/puli/css");
283
     * // => ../../../style.css
284
     * ```
285
     *
286
     * If both paths are absolute, their root directory must be the same,
287
     * otherwise an exception is thrown:
288
     *
289
     * ```php
290
     * Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli");
291
     * // InvalidArgumentException
292
     * ```
293
     *
294
     * If the passed path is absolute, but the base path is not, an exception
295
     * is thrown as well:
296
     *
297
     * ```php
298
     * Path::makeRelative("/webmozart/style.css", "webmozart/puli");
299
     * // InvalidArgumentException
300
     * ```
301
     *
302
     * If the base path is not an absolute path, an exception is thrown.
303
     *
304
     * The result is a canonical path.
305
     *
306
     * @throws \InvalidArgumentException if the base path is not absolute or if
307
     *                                  the given path has a different root
308
     *                                  than the base path
309
     * @throws InvalidPathException
310
     */
311
    public static function makeRelative(string $path, string $basePath): string
312
    {
313
        $path = static::canonicalize($path);
314
        $basePath = static::canonicalize($basePath);
315
316
        [$root, $relativePath] = self::split($path);
317
        [$baseRoot, $relativeBasePath] = self::split($basePath);
318
319
        // If the base path is given as absolute path and the path is already
320
        // relative, consider it to be relative to the given absolute path
321
        // already
322
        if ('' === $root && '' !== $baseRoot) {
323
            // If base path is already in its root
324
            if ('' === $relativeBasePath) {
325
                $relativePath = ltrim($relativePath, './\\');
326
            }
327
328
            return $relativePath;
329
        }
330
331
        // If the passed path is absolute, but the base path is not, we
332
        // cannot generate a relative path
333
        if ('' !== $root && '' === $baseRoot) {
334
            throw new \InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
335
        }
336
337
        // Fail if the roots of the two paths are different
338
        if (('' !== $baseRoot) && ($root !== $baseRoot)) {
339
            throw new \InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
340
        }
341
342
        if ('' === $relativeBasePath) {
343
            return $relativePath;
344
        }
345
346
        // Build a "../../" prefix with as many "../" parts as necessary
347
        $parts = explode('/', $relativePath);
348
        $baseParts = explode('/', $relativeBasePath);
349
        $dotDotPrefix = '';
350
351
        // Once we found a non-matching part in the prefix, we need to add
352
        // "../" parts for all remaining parts
353
        $match = true;
354
355
        foreach ($baseParts as $i => $basePart) {
356
            if ($match && isset($parts[$i]) && $basePart === $parts[$i]) {
357
                unset($parts[$i]);
358
359
                continue;
360
            }
361
362
            $match = false;
363
            $dotDotPrefix .= '../';
364
        }
365
366
        return rtrim($dotDotPrefix.implode('/', $parts), '/');
367
    }
368
369
    /**
370
     * Joins two or more path strings.
371
     *
372
     * The result is a canonical path.
373
     *
374
     * @param array<int,string> $paths path parts as parameters or array
375
     *
376
     * @throws InvalidPathException
377
     */
378
    public static function join(array $paths): string
379
    {
380
        $finalPath = null;
381
        $wasScheme = false;
382
383
        foreach ($paths as $path) {
384
            if ('' === $path) {
385
                continue;
386
            }
387
388
            if (null === $finalPath) {
389
                // For first part we keep slashes, like '/top', 'C:\' or 'phar://'
390
                $finalPath = $path;
391
                $wasScheme = str_contains($path, '://');
392
                continue;
393
            }
394
395
            // Only add slash if previous part didn't end with '/' or '\'
396
            if (!in_array(substr($finalPath, -1), ['/', '\\'], true)) {
0 ignored issues
show
Bug introduced by
$finalPath of type void is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

396
            if (!in_array(substr(/** @scrutinizer ignore-type */ $finalPath, -1), ['/', '\\'], true)) {
Loading history...
397
                $finalPath .= '/';
398
            }
399
400
            // If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim
401
            $finalPath .= $wasScheme ? $path : ltrim($path, '/');
402
            $wasScheme = false;
403
        }
404
405
        if (null === $finalPath) {
406
            return '';
407
        }
408
409
        return self::canonicalize($finalPath);
410
    }
411
412
    /**
413
     * Returns whether a path is a base path of another path.
414
     *
415
     * Dot segments ("." and "..") are removed/collapsed and all slashes turned
416
     * into forward slashes.
417
     *
418
     * ```php
419
     * Path::isBasePath('/webmozart', '/webmozart/css');
420
     * // => true
421
     *
422
     * Path::isBasePath('/webmozart', '/webmozart');
423
     * // => true
424
     *
425
     * Path::isBasePath('/webmozart', '/webmozart/..');
426
     * // => false
427
     *
428
     * Path::isBasePath('/webmozart', '/puli');
429
     * // => false
430
     * ```
431
     *
432
     * @throws InvalidPathException
433
     */
434
    public static function isBasePath(string $basePath, string $ofPath): bool
435
    {
436
        $basePath = self::canonicalize($basePath);
437
        $ofPath = self::canonicalize($ofPath);
438
439
        // Append slashes to prevent false positives when two paths have
440
        // a common prefix, for example /base/foo and /base/foobar.
441
        // Don't append a slash for the root "/", because then that root
442
        // won't be discovered as common prefix ("//" is not a prefix of
443
        // "/foobar/").
444
        return str_starts_with($ofPath.'/', rtrim($basePath, '/').'/');
445
    }
446
447
    /**
448
     * Splits a part into its root directory and the remainder.
449
     *
450
     * If the path has no root directory, an empty root directory will be
451
     * returned.
452
     *
453
     * If the root directory is a Windows style partition, the resulting root
454
     * will always contain a trailing slash.
455
     *
456
     * list ($root, $path) = Path::split("C:/webmozart")
457
     * // => array("C:/", "webmozart")
458
     *
459
     * list ($root, $path) = Path::split("C:")
460
     * // => array("C:/", "")
461
     *
462
     * @return array{string,string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{string,string} at position 2 could not be parsed: Expected ':' at position 2, but found 'string'.
Loading history...
463
     */
464
    private static function split(string $path): array
465
    {
466
        if ('' === $path) {
467
            return ['', ''];
468
        }
469
470
        // Remember scheme as part of the root, if any
471
        if (false !== ($pos = strpos($path, '://'))) {
472
            $root = substr($path, 0, $pos + 3);
473
            $path = substr($path, $pos + 3);
474
        } else {
475
            $root = '';
476
        }
477
478
        $length = strlen($path);
479
480
        // Remove and remember root directory
481
        if ('/' === $path[0]) {
482
            $root .= '/';
483
            $path = $length > 1 ? substr($path, 1) : '';
484
        } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
485
            if (2 === $length) {
486
                // Windows special case: "C:"
487
                $root .= $path.'/';
488
                $path = '';
489
            } elseif ('/' === $path[2]) {
490
                // Windows normal case: "C:/"..
491
                $root .= substr($path, 0, 3);
492
                $path = $length > 3 ? substr($path, 3) : '';
493
            }
494
        }
495
496
        return [$root, $path];
497
    }
498
499
    /**
500
     * @codeCoverageIgnore
501
     */
502
    private function __construct()
503
    {
504
    }
505
}
506