Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
50 / 50 |
|
100.00% |
3 / 3 |
CRAP | |
100.00% |
1 / 1 |
RelPath | |
100.00% |
50 / 50 |
|
100.00% |
3 / 3 |
27 | |
100.00% |
1 / 1 |
splitPath | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
11 | |||
getRelativePath | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 | |||
joinPath | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | /** |
3 | * Copyright (c) 2015 Ori Livneh <ori@wikimedia.org> |
4 | * |
5 | * Permission is hereby granted, free of charge, to any person obtaining |
6 | * a copy of this software and associated documentation files (the |
7 | * "Software"), to deal in the Software without restriction, including |
8 | * without limitation the rights to use, copy, modify, merge, publish, |
9 | * distribute, sublicense, and/or sell copies of the Software, and to |
10 | * permit persons to whom the Software is furnished to do so, subject to |
11 | * the following conditions: |
12 | * |
13 | * The above copyright notice and this permission notice shall be |
14 | * included in all copies or substantial portions of the Software. |
15 | * |
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
19 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
20 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
22 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
23 | * |
24 | * @file |
25 | * @author Ori Livneh <ori@wikimedia.org> |
26 | */ |
27 | |
28 | namespace Wikimedia; |
29 | |
30 | /** |
31 | * Utilities for computing a relative filepath between two paths. |
32 | */ |
33 | class RelPath { |
34 | /** |
35 | * Split a path into path components. |
36 | * |
37 | * @param string $path File path. |
38 | * @return string[] Array of path components. |
39 | */ |
40 | public static function splitPath( string $path ): array { |
41 | $fragments = []; |
42 | |
43 | while ( true ) { |
44 | $cur = dirname( $path ); |
45 | if ( $cur[0] === DIRECTORY_SEPARATOR ) { |
46 | // dirname() on Windows sometimes returns a leading backslash, but other |
47 | // times it retains the leading forward slash. Slashes other than the leading one |
48 | // are returned as-is, and therefore do not need to be touched. |
49 | // Furthermore, don't break on *nix where \ is allowed in file/directory names. |
50 | $cur[0] = '/'; |
51 | } |
52 | |
53 | if ( $cur === $path || ( $cur === '.' && basename( $path ) === $path ) ) { |
54 | break; |
55 | } |
56 | |
57 | $fragment = trim( substr( $path, strlen( $cur ) ), '/' ); |
58 | |
59 | if ( !$fragments ) { |
60 | $fragments[] = $fragment; |
61 | } elseif ( $fragment === '..' && basename( $cur ) !== '..' ) { |
62 | $cur = dirname( $cur ); |
63 | } elseif ( $fragment !== '.' ) { |
64 | $fragments[] = $fragment; |
65 | } |
66 | |
67 | $path = $cur; |
68 | } |
69 | |
70 | if ( $path !== '' ) { |
71 | $fragments[] = trim( $path, '/' ); |
72 | } |
73 | |
74 | return array_reverse( $fragments ); |
75 | } |
76 | |
77 | /** |
78 | * Return a relative filepath to $path, either from the current directory or from |
79 | * an optional start directory. Both paths must be absolute. |
80 | * |
81 | * @param string $path File path. |
82 | * @param string|null $start Start directory. Optional; if not specified, the current |
83 | * working directory will be used. |
84 | * @return string|false Relative path, or false if input was invalid. |
85 | */ |
86 | public static function getRelativePath( string $path, ?string $start = null ) { |
87 | if ( $start === null ) { |
88 | // @codeCoverageIgnoreStart |
89 | $start = getcwd(); |
90 | } |
91 | // @codeCoverageIgnoreEnd |
92 | |
93 | if ( strpos( $path, '/' ) !== 0 || strpos( $start, '/' ) !== 0 ) { |
94 | return false; |
95 | } |
96 | |
97 | $pathParts = self::splitPath( $path ); |
98 | $countPathParts = count( $pathParts ); |
99 | |
100 | $startParts = self::splitPath( $start ); |
101 | $countStartParts = count( $startParts ); |
102 | |
103 | $commonLength = min( $countPathParts, $countStartParts ); |
104 | for ( $i = 0; $i < $commonLength; $i++ ) { |
105 | if ( $startParts[$i] !== $pathParts[$i] ) { |
106 | break; |
107 | } |
108 | } |
109 | |
110 | $relList = ( $countStartParts > $i ) |
111 | ? array_fill( 0, $countStartParts - $i, '..' ) |
112 | : []; |
113 | |
114 | $relList = array_merge( $relList, array_slice( $pathParts, $i ) ); |
115 | |
116 | return implode( '/', $relList ) ?: '.'; |
117 | } |
118 | |
119 | /** |
120 | * Join two path components. |
121 | * |
122 | * This can be used to expand a path relative to a given base path. |
123 | * The given path may also be absolute, in which case it is returned |
124 | * directly. |
125 | * |
126 | * @code |
127 | * RelPath::joinPath('/srv/foo', 'bar'); # '/srv/foo/bar' |
128 | * RelPath::joinPath('/srv/foo', './bar'); # '/srv/foo/bar' |
129 | * RelPath::joinPath('/srv//foo', '../baz'); # '/srv/baz' |
130 | * RelPath::joinPath('/srv/foo', '/var/quux/'); # '/var/quux/' |
131 | * @endcode |
132 | * |
133 | * This function is similar to `os.path.join()` in Python, |
134 | * and `path.join()` in Node.js. |
135 | * |
136 | * @param string $base Base path. |
137 | * @param string $path File $path to join to $base path. |
138 | * @return string|false Combined path, or false if input was invalid. |
139 | */ |
140 | public static function joinPath( string $base, string $path ) { |
141 | if ( strpos( $path, '/' ) === 0 ) { |
142 | // $path is absolute. |
143 | return $path; |
144 | } |
145 | |
146 | if ( strpos( $base, '/' ) !== 0 ) { |
147 | // $base is relative. |
148 | return false; |
149 | } |
150 | |
151 | $pathParts = self::splitPath( $path ); |
152 | $resultParts = self::splitPath( $base ); |
153 | |
154 | // @phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition |
155 | while ( ( $part = array_shift( $pathParts ) ) !== null ) { |
156 | switch ( $part ) { |
157 | case '.': |
158 | break; |
159 | case '..': |
160 | if ( count( $resultParts ) > 1 ) { |
161 | array_pop( $resultParts ); |
162 | } |
163 | break; |
164 | default: |
165 | $resultParts[] = $part; |
166 | break; |
167 | } |
168 | } |
169 | |
170 | return implode( '/', $resultParts ); |
171 | } |
172 | } |