Path::changeExtension()   C
last analyzed

Complexity

Conditions 8
Paths 12

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 36
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 12
nop 2
1
<?php
2
3
namespace Roukmoute\IO;
4
5
use DusanKasan\Knapsack\Collection;
6
7
/**
8
 * Provides methods for processing directory strings.
9
 * The methods will handle most string operations.
10
 */
11
class Path
12
{
13
    /**
14
     * Platform specific directory separator character.
15
     * This is backslash ('\') on Windows, slash ('/') on Unix, and colon (':') on Mac.
16
     */
17
    const DIRECTORY_SEPARATOR_CHAR = '\\';
18
19
    /**
20
     * Platform specific alternate directory separator character.
21
     * This is backslash ('\') on Unix, and slash ('/') on Windows and MacOS.
22
     */
23
    const ALTDIRECTORY_SEPARATOR_CHAR = '/';
24
25
    /**
26
     * Platform specific volume separator character.
27
     * This is colon (':') on Windows and MacOS, and slash ('/') on Unix.
28
     *
29
     * This is mostly useful for parsing paths like
30
     * "c:\windows" or "MacVolume:System Folder".
31
     */
32
    const VOLUME_SEPARATOR_CHAR = ':';
33
34
    const INVALID_PATH_ASCII = [
35
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
36
        11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
37
        21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
38
        31, 34, 60, 62, 124
39
    ];
40
41
    /**
42
     * Changes the extension of a path string.
43
     *
44
     * If path is null, the function returns null.
45
     *
46
     * @param string $path      The path information to modify.
47
     *                          The path cannot contain any of the characters
48
     *                          defined in invalidPathChars.
49
     *                          If it does not contain a file extension,
50
     *                          the new file extension is appended to the path.
51
     * @param string $extension The new extension (with or without a leading period).
52
     *                          Specify null to remove an existing extension from path.
53
     *                          If it is null, any existing extension is removed from path.
54
     *
55
     * @return string           A file path with the same root, directory,
56
     *                          and base name parts as path,
57
     *                          but with the file extension changed
58
     *                          to the specified extension.
59
     */
60
    public static function changeExtension(string $path, string $extension = null): string
61
    {
62
        self::checkInvalidPathChars($path);
63
64
        $newPath = $path;
65
        $pathLength = mb_strlen($path);
66
67
        for ($i = $pathLength; --$i >= 0;) {
68
            $character = $path[$i];
69
70
            if ($character == '.') {
71
                $newPath = mb_substr($newPath, 0, $i);
72
                break;
73
            }
74
75
            if (in_array(
76
                $character,
77
                [
78
                    self::DIRECTORY_SEPARATOR_CHAR,
79
                    self::ALTDIRECTORY_SEPARATOR_CHAR,
80
                    self::VOLUME_SEPARATOR_CHAR,
81
                ]
82
            )) {
83
                break;
84
            }
85
        }
86
87
        if ($extension !== null && $pathLength !== 0) {
88
            if (mb_strlen($extension) === 0 || $extension[0] !== '.') {
89
                $newPath .= ".";
90
            }
91
            $newPath .= $extension;
92
        }
93
94
        return $newPath;
95
    }
96
97
    /**
98
     * Gets an array containing the characters that are not allowed in path names.
99
     */
100
    public static function invalidPathChars(): array
101
    {
102
        return Collection::from(self::INVALID_PATH_ASCII)
103
                         ->map(
104
                             function (string $ascii) {
105
                                 return chr($ascii);
106
                             }
107
                         )
108
                         ->toArray();
109
    }
110
111
    /**
112
     * Gets a value indicating whether the specified path string contains a root.
113
     *
114
     * A path is considered rooted if it starts with a backslash ("\")
115
     * or a drive letter and a colon (":").
116
     *
117
     * @param string $path  The path to test.
118
     *
119
     * @return bool         true if path contains a root; otherwise, false.
120
     */
121
    public static function isPathRooted(string $path): bool
122
    {
123
        if ($path != null) {
124
            self::checkInvalidPathChars($path);
125
126
            $pathLength = mb_strlen($path);
127
            if (($pathLength >= 1 &&
128
                    (
129
                        $path[0] == self::DIRECTORY_SEPARATOR_CHAR
130
                        || $path[0] == self::ALTDIRECTORY_SEPARATOR_CHAR
131
                    )
132
                )
133
                || ($pathLength >= 2 && $path[1] == self::VOLUME_SEPARATOR_CHAR)
134
            ) {
135
                return true;
136
            }
137
        }
138
139
        return false;
140
    }
141
142
    /**
143
     * Combines two strings into a path.
144
     *
145
     * @param string $path1 The first path to combine.
146
     * @param string $path2 The second path to combine.
147
     *
148
     * @return string       The combined paths.
149
     *                      If one of the specified paths is a zero-length string,
150
     *                      this method returns the other path.
151
     *                      If path2 contains an absolute path,
152
     *                      this method returns path2.
153
     */
154
    public static function combine(string $path1, string $path2): string
155
    {
156
        self::hasIllegalCharacter($path1);
157
        self::hasIllegalCharacter($path2);
158
159
        if (mb_strlen($path2) == 0) {
160
            return $path1;
161
        }
162
163
        if (mb_strlen($path1) == 0) {
164
            return $path2;
165
        }
166
167
        if (self::isPathRooted($path2)) {
168
            return $path2;
169
        }
170
171
        if (!in_array(
172
            $path1[mb_strlen($path1) - 1],
173
            [
174
                self::DIRECTORY_SEPARATOR_CHAR,
175
                self::ALTDIRECTORY_SEPARATOR_CHAR,
176
                self::VOLUME_SEPARATOR_CHAR,
177
            ]
178
        )
179
        ) {
180
            return "$path1" . DIRECTORY_SEPARATOR . "$path2";
181
        }
182
183
        return $path1 . $path2;
184
    }
185
186
    /**
187
     * Check if contains one or more of the invalid characters
188
     * defined in invalidPathChars.
189
     */
190
    private static function checkInvalidPathChars(string $path): void
191
    {
192
        if (self::hasIllegalCharacter($path)) {
193
            throw new \InvalidArgumentException("'$path': This file name is not valid.");
194
        }
195
    }
196
197
    /**
198
     * Indicates if the given path contains invalid characters.
199
     * (", <, >, or any ASCII char whose integer representation
200
     * is in the range of 0 through 31)
201
     */
202
    private static function hasIllegalCharacter(string $path): bool
203
    {
204
        return Collection::from(self::INVALID_PATH_ASCII)
205
                         ->some(
206
                             function (string $ascii) use ($path): bool {
207
                                 return strpos($path, chr($ascii));
208
                             }
209
                         );
210
    }
211
}
212