/src/mpv/misc/path_utils.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Path utility functions |
3 | | * |
4 | | * This file is part of mpv. |
5 | | * |
6 | | * mpv is free software; you can redistribute it and/or |
7 | | * modify it under the terms of the GNU Lesser General Public |
8 | | * License as published by the Free Software Foundation; either |
9 | | * version 2.1 of the License, or (at your option) any later version. |
10 | | * |
11 | | * mpv is distributed in the hope that it will be useful, |
12 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | * GNU Lesser General Public License for more details. |
15 | | * |
16 | | * You should have received a copy of the GNU Lesser General Public |
17 | | * License along with mpv. If not, see <http://www.gnu.org/licenses/>. |
18 | | */ |
19 | | |
20 | | #include <assert.h> |
21 | | #include <stdio.h> |
22 | | #include <stdlib.h> |
23 | | #include <string.h> |
24 | | #include <stdbool.h> |
25 | | #include <sys/types.h> |
26 | | #include <sys/stat.h> |
27 | | #include <errno.h> |
28 | | |
29 | | #include "config.h" |
30 | | |
31 | | #include "mpv_talloc.h" |
32 | | #include "common/common.h" |
33 | | #include "osdep/io.h" |
34 | | #include "misc/ctype.h" |
35 | | #include "misc/path_utils.h" |
36 | | |
37 | | #if HAVE_DOS_PATHS |
38 | | #include <windows.h> |
39 | | #include <pathcch.h> |
40 | | #endif |
41 | | |
42 | | char *mp_basename(const char *path) |
43 | 3.26M | { |
44 | 3.26M | char *s; |
45 | | |
46 | | #if HAVE_DOS_PATHS |
47 | | if (!mp_is_url(bstr0(path))) { |
48 | | s = strrchr(path, '\\'); |
49 | | if (s) |
50 | | path = s + 1; |
51 | | s = strrchr(path, ':'); |
52 | | if (s) |
53 | | path = s + 1; |
54 | | } |
55 | | #endif |
56 | 3.26M | s = strrchr(path, '/'); |
57 | 3.26M | return s ? s + 1 : (char *)path; |
58 | 3.26M | } |
59 | | |
60 | | struct bstr mp_dirname(const char *path) |
61 | 75.5k | { |
62 | 75.5k | struct bstr ret = { |
63 | 75.5k | (uint8_t *)path, mp_basename(path) - path |
64 | 75.5k | }; |
65 | 75.5k | if (ret.len == 0) |
66 | 0 | return bstr0("."); |
67 | 75.5k | return ret; |
68 | 75.5k | } |
69 | | |
70 | | |
71 | | #if HAVE_DOS_PATHS |
72 | | static const char mp_path_separators[] = "\\/"; |
73 | | #else |
74 | | static const char mp_path_separators[] = "/"; |
75 | | #endif |
76 | | |
77 | | // Mutates path and removes a trailing '/' (or '\' on Windows) |
78 | | void mp_path_strip_trailing_separator(char *path) |
79 | 0 | { |
80 | 0 | size_t len = strlen(path); |
81 | 0 | if (len > 0 && strchr(mp_path_separators, path[len - 1])) |
82 | 0 | path[len - 1] = '\0'; |
83 | 0 | } |
84 | | |
85 | | char *mp_splitext(const char *path, bstr *root) |
86 | 143k | { |
87 | 143k | mp_assert(path); |
88 | 143k | int skip = (*path == '.'); // skip leading dot for "hidden" unix files |
89 | 143k | const char *split = strrchr(path + skip, '.'); |
90 | 143k | if (!split || !split[1] || strchr(split, '/')) |
91 | 89.6k | return NULL; |
92 | 53.5k | if (root) |
93 | 0 | *root = (bstr){(char *)path, split - path}; |
94 | 53.5k | return (char *)split + 1; |
95 | 143k | } |
96 | | |
97 | | bool mp_path_is_absolute(struct bstr path) |
98 | 670k | { |
99 | 670k | if (path.len && strchr(mp_path_separators, path.start[0])) |
100 | 12.4k | return true; |
101 | | |
102 | | #if HAVE_DOS_PATHS |
103 | | // Note: "X:filename" is a path relative to the current working directory |
104 | | // of drive X, and thus is not an absolute path. It needs to be |
105 | | // followed by \ or /. |
106 | | if (path.len >= 3 && path.start[1] == ':' && |
107 | | strchr(mp_path_separators, path.start[2])) |
108 | | return true; |
109 | | #endif |
110 | | |
111 | 658k | return false; |
112 | 670k | } |
113 | | |
114 | | char *mp_path_join_bstr(void *talloc_ctx, struct bstr p1, struct bstr p2) |
115 | 585k | { |
116 | 585k | if (p1.len == 0) |
117 | 243k | return bstrdup0(talloc_ctx, p2); |
118 | 342k | if (p2.len == 0) |
119 | 1.75k | return bstrdup0(talloc_ctx, p1); |
120 | | |
121 | 340k | if (mp_path_is_absolute(p2)) |
122 | 1.65k | return bstrdup0(talloc_ctx, p2); |
123 | | |
124 | 338k | bool have_separator = strchr(mp_path_separators, p1.start[p1.len - 1]); |
125 | | #if HAVE_DOS_PATHS |
126 | | // "X:" only => path relative to "X:" current working directory. |
127 | | if (p1.len == 2 && p1.start[1] == ':') |
128 | | have_separator = true; |
129 | | #endif |
130 | | |
131 | 338k | return talloc_asprintf(talloc_ctx, "%.*s%s%.*s", BSTR_P(p1), |
132 | 340k | have_separator ? "" : "/", BSTR_P(p2)); |
133 | 340k | } |
134 | | |
135 | | char *mp_path_join(void *talloc_ctx, const char *p1, const char *p2) |
136 | 333k | { |
137 | 333k | return mp_path_join_bstr(talloc_ctx, bstr0(p1), bstr0(p2)); |
138 | 333k | } |
139 | | |
140 | | char *mp_getcwd(void *talloc_ctx) |
141 | 319k | { |
142 | 319k | char *e_wd = getenv("PWD"); |
143 | 319k | if (e_wd) |
144 | 319k | return talloc_strdup(talloc_ctx, e_wd); |
145 | | |
146 | 0 | char *wd = talloc_array(talloc_ctx, char, 20); |
147 | 0 | while (getcwd(wd, talloc_get_size(wd)) == NULL) { |
148 | 0 | if (errno != ERANGE) { |
149 | 0 | talloc_free(wd); |
150 | 0 | return NULL; |
151 | 0 | } |
152 | 0 | wd = talloc_realloc(talloc_ctx, wd, char, talloc_get_size(wd) * 2); |
153 | 0 | } |
154 | 0 | return wd; |
155 | 0 | } |
156 | | |
157 | | char *mp_normalize_path(void *talloc_ctx, const char *path) |
158 | 342k | { |
159 | 342k | if (!path) |
160 | 0 | return NULL; |
161 | | |
162 | 342k | if (mp_is_url(bstr0(path))) |
163 | 11.6k | return talloc_strdup(talloc_ctx, path); |
164 | | |
165 | 330k | void *tmp = talloc_new(NULL); |
166 | 330k | if (!mp_path_is_absolute(bstr0(path))) { |
167 | 319k | char *cwd = mp_getcwd(tmp); |
168 | 319k | if (!cwd) { |
169 | 0 | talloc_free(tmp); |
170 | 0 | return NULL; |
171 | 0 | } |
172 | 319k | path = mp_path_join(tmp, cwd, path); |
173 | 319k | } |
174 | | |
175 | | #if HAVE_DOS_PATHS |
176 | | wchar_t *pathw = mp_from_utf8(tmp, path); |
177 | | wchar_t *read = pathw, *write = pathw; |
178 | | wchar_t prev = '\0'; |
179 | | // preserve leading double backslashes |
180 | | if (read[0] == '\\' && read[1] == '\\') { |
181 | | prev = '\\'; |
182 | | write += 2; |
183 | | read += 2; |
184 | | } |
185 | | wchar_t curr; |
186 | | while ((curr = *read)) { |
187 | | if (curr == '/') |
188 | | curr = '\\'; |
189 | | if (curr != '\\' || prev != '\\') |
190 | | *write++ = curr; |
191 | | prev = curr; |
192 | | read++; |
193 | | } |
194 | | *write = '\0'; |
195 | | size_t max_size = wcslen(pathw) + 1; |
196 | | wchar_t *pathc = talloc_array(tmp, wchar_t, max_size); |
197 | | HRESULT hr = PathCchCanonicalizeEx(pathc, max_size, pathw, PATHCCH_ALLOW_LONG_PATHS); |
198 | | char *ret = SUCCEEDED(hr) ? mp_to_utf8(talloc_ctx, pathc) : talloc_strdup(talloc_ctx, path); |
199 | | talloc_free(tmp); |
200 | | return ret; |
201 | | #else |
202 | 330k | char *result = talloc_strdup(tmp, ""); |
203 | 330k | const char *next; |
204 | 330k | const char *end = path + strlen(path); |
205 | | |
206 | 2.10M | for (const char *ptr = path; ptr < end; ptr = next + 1) { |
207 | 1.77M | next = memchr(ptr, '/', end - ptr); |
208 | 1.77M | if (next == NULL) |
209 | 315k | next = end; |
210 | | |
211 | 1.77M | switch (next - ptr) { |
212 | 394k | case 0: |
213 | 394k | continue; |
214 | 107k | case 1: |
215 | 107k | if (ptr[0] == '.') |
216 | 4.64k | continue; |
217 | 102k | break; |
218 | 102k | case 2: |
219 | | // Normalizing symlink/.. results in a wrong path: if the |
220 | | // current working directory is /tmp/foo, and it is a symlink to |
221 | | // /usr/bin, mpv ../file.mkv opens /usr/file.mkv, so we can't |
222 | | // normalize the path to /tmp/file.mkv. Resolve symlinks to fix |
223 | | // this. Otherwise we don't use realpath so users can use |
224 | | // symlinks e.g. to hide how media files are distributed over |
225 | | // real storage and move them while still resuming playback as |
226 | | // long as the symlinked path doesn't change. |
227 | 70.0k | if (ptr[0] == '.' && ptr[1] == '.') { |
228 | 1.12k | char *tmp_result = realpath(path, NULL); |
229 | 1.12k | result = talloc_strdup(talloc_ctx, tmp_result); |
230 | 1.12k | free(tmp_result); |
231 | 1.12k | talloc_free(tmp); |
232 | 1.12k | return result; |
233 | 1.12k | } |
234 | 1.77M | } |
235 | | |
236 | 1.37M | result = talloc_strdup_append_buffer(result, "/"); |
237 | 1.37M | result = talloc_strndup_append_buffer(result, ptr, next - ptr); |
238 | 1.37M | } |
239 | | |
240 | 329k | result = talloc_steal(talloc_ctx, result); |
241 | 329k | talloc_free(tmp); |
242 | 329k | return result; |
243 | 330k | #endif |
244 | 330k | } |
245 | | |
246 | | bool mp_path_exists(const char *path) |
247 | 398k | { |
248 | 398k | struct stat st; |
249 | 398k | return path && stat(path, &st) == 0; |
250 | 398k | } |
251 | | |
252 | | bool mp_path_isdir(const char *path) |
253 | 0 | { |
254 | 0 | struct stat st; |
255 | 0 | return stat(path, &st) == 0 && S_ISDIR(st.st_mode); |
256 | 0 | } |
257 | | |
258 | | // Return false if it's considered a normal local filesystem path. |
259 | | bool mp_is_url(bstr path) |
260 | 5.26M | { |
261 | 5.26M | int proto = bstr_find0(path, "://"); |
262 | 5.26M | if (proto < 1) |
263 | 1.31M | return false; |
264 | | // Per RFC3986, the first character of the protocol must be alphabetic. |
265 | | // The rest must be alphanumeric plus -, + and . |
266 | 20.4M | for (int i = 0; i < proto; i++) { |
267 | 16.5M | unsigned char c = path.start[i]; |
268 | 16.5M | if ((i == 0 && !mp_isalpha(c)) || |
269 | 16.5M | (!mp_isalnum(c) && c != '.' && c != '-' && c != '+')) |
270 | 86.6k | { |
271 | 86.6k | return false; |
272 | 86.6k | } |
273 | 16.5M | } |
274 | 3.85M | return true; |
275 | 3.94M | } |
276 | | |
277 | | // Return the protocol part of path, e.g. "http" if path is "http://...". |
278 | | // On success, out_url (if not NULL) is set to the part after the "://". |
279 | | bstr mp_split_proto(bstr path, bstr *out_url) |
280 | 727k | { |
281 | 727k | if (!mp_is_url(path)) |
282 | 366k | return (bstr){0}; |
283 | 360k | bstr r; |
284 | 360k | bstr_split_tok(path, "://", &r, out_url ? out_url : &(bstr){0}); |
285 | 360k | return r; |
286 | 727k | } |
287 | | |
288 | | void mp_mkdirp(const char *dir) |
289 | 173 | { |
290 | 173 | if (mp_path_exists(dir)) |
291 | 171 | return; |
292 | | |
293 | 2 | char *path = talloc_strdup(NULL, dir); |
294 | 2 | char *cdir = path + 1; |
295 | | |
296 | 4 | while (cdir) { |
297 | 2 | cdir = strchr(cdir, '/'); |
298 | 2 | if (cdir) |
299 | 0 | *cdir = 0; |
300 | | |
301 | 2 | mkdir(path, 0777); |
302 | | |
303 | 2 | if (cdir) |
304 | 0 | *cdir++ = '/'; |
305 | 2 | } |
306 | | |
307 | 2 | talloc_free(path); |
308 | 2 | } |