Coverage Report

Created: 2026-05-16 07:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/misc/path_utils.c
Line
Count
Source
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
const char *mp_basename(const char *path)
43
4.65M
{
44
4.65M
    const 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
4.65M
    s = strrchr(path, '/');
57
4.65M
    return s ? s + 1 : path;
58
4.65M
}
59
60
struct bstr mp_dirname(const char *path)
61
78.7k
{
62
78.7k
    struct bstr ret = {
63
78.7k
        (uint8_t *)path, mp_basename(path) - path
64
78.7k
    };
65
78.7k
    if (ret.len == 0)
66
0
        return bstr0(".");
67
78.7k
    return ret;
68
78.7k
}
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
151k
{
87
151k
    mp_assert(path);
88
151k
    const char *bn = mp_basename(path);
89
90
    // Skip all leading dots, not just for "hidden" unix files, otherwise we
91
    // end up splitting a part of the filename sans leading dot.
92
151k
    bn += strspn(bn, ".");
93
94
151k
    const char *split = strrchr(bn, '.');
95
151k
    if (!split || !split[1])
96
101k
        return NULL;
97
50.1k
    if (root)
98
0
        *root = (bstr){(char *)path, split - path};
99
50.1k
    return (char *)split + 1;
100
151k
}
101
102
char *mp_strip_ext(void *talloc_ctx, const char *s)
103
0
{
104
0
    bstr root;
105
0
    return mp_splitext(s, &root) ? bstrto0(talloc_ctx, root) : talloc_strdup(talloc_ctx, s);
106
0
}
107
108
bool mp_path_is_absolute(struct bstr path)
109
667k
{
110
667k
    if (path.len && strchr(mp_path_separators, path.start[0]))
111
41.9k
        return true;
112
113
#if HAVE_DOS_PATHS
114
    // Note: "X:filename" is a path relative to the current working directory
115
    //       of drive X, and thus is not an absolute path. It needs to be
116
    //       followed by \ or /.
117
    if (path.len >= 3 && path.start[1] == ':' &&
118
        strchr(mp_path_separators, path.start[2]))
119
        return true;
120
#endif
121
122
625k
    return false;
123
667k
}
124
125
char *mp_path_join_bstr(void *talloc_ctx, struct bstr p1, struct bstr p2)
126
624k
{
127
624k
    if (p1.len == 0)
128
277k
        return bstrdup0(talloc_ctx, p2);
129
347k
    if (p2.len == 0)
130
6.14k
        return bstrdup0(talloc_ctx, p1);
131
132
341k
    if (mp_path_is_absolute(p2))
133
1.47k
        return bstrdup0(talloc_ctx, p2);
134
135
341k
    bool have_separator = strchr(mp_path_separators, p1.start[p1.len - 1]);
136
#if HAVE_DOS_PATHS
137
    // "X:" only => path relative to "X:" current working directory.
138
    if (p1.len == 2 && p1.start[1] == ':')
139
        have_separator = true;
140
#endif
141
142
340k
    return talloc_asprintf(talloc_ctx, "%.*s%s%.*s", BSTR_P(p1),
143
341k
                           have_separator ? "" : "/", BSTR_P(p2));
144
341k
}
145
146
char *mp_path_join(void *talloc_ctx, const char *p1, const char *p2)
147
297k
{
148
297k
    return mp_path_join_bstr(talloc_ctx, bstr0(p1), bstr0(p2));
149
297k
}
150
151
char *mp_getcwd(void *talloc_ctx)
152
285k
{
153
285k
    char *e_wd = getenv("PWD");
154
285k
    if (e_wd)
155
285k
        return talloc_strdup(talloc_ctx, e_wd);
156
157
0
    char *wd = talloc_array(talloc_ctx, char, 20);
158
0
    while (getcwd(wd, talloc_get_size(wd)) == NULL) {
159
0
        if (errno != ERANGE) {
160
0
            talloc_free(wd);
161
0
            return NULL;
162
0
        }
163
0
        wd = talloc_realloc(talloc_ctx, wd, char, talloc_get_size(wd) * 2);
164
0
    }
165
0
    return wd;
166
0
}
167
168
char *mp_normalize_path(void *talloc_ctx, const char *path)
169
466k
{
170
466k
    if (!path)
171
0
        return NULL;
172
173
466k
    if (mp_is_url(bstr0(path)) || !strcmp(path, "-"))
174
140k
        return talloc_strdup(talloc_ctx, path);
175
176
326k
    void *tmp = talloc_new(NULL);
177
326k
    char *result;
178
179
326k
    if (!mp_path_is_absolute(bstr0(path))) {
180
285k
        char *cwd = mp_getcwd(tmp);
181
285k
        if (!cwd) {
182
0
            result = talloc_strdup(talloc_ctx, path);
183
0
            goto exit;
184
0
        }
185
285k
        path = mp_path_join(tmp, cwd, path);
186
285k
    }
187
188
#if HAVE_DOS_PATHS
189
    wchar_t *pathw = mp_from_utf8(tmp, path);
190
    wchar_t *read = pathw, *write = pathw;
191
    wchar_t prev = '\0';
192
    // preserve leading double backslashes
193
    if (read[0] == '\\' && read[1] == '\\') {
194
        prev = '\\';
195
        write += 2;
196
        read += 2;
197
    }
198
    wchar_t curr;
199
    while ((curr = *read)) {
200
        if (curr == '/')
201
            curr = '\\';
202
        if (curr != '\\' || prev != '\\')
203
            *write++ = curr;
204
        prev = curr;
205
        read++;
206
    }
207
    *write = '\0';
208
    size_t max_size = wcslen(pathw) + 1;
209
    wchar_t *pathc = talloc_array(tmp, wchar_t, max_size);
210
    HRESULT hr = PathCchCanonicalizeEx(pathc, max_size, pathw, PATHCCH_ALLOW_LONG_PATHS);
211
    result = SUCCEEDED(hr) ? mp_to_utf8(talloc_ctx, pathc) : talloc_strdup(talloc_ctx, path);
212
#else
213
326k
    result = talloc_strdup(tmp, "");
214
326k
    const char *next;
215
326k
    const char *end = path + strlen(path);
216
217
1.96M
    for (const char *ptr = path; ptr < end; ptr = next + 1) {
218
1.63M
        next = memchr(ptr, '/', end - ptr);
219
1.63M
        if (next == NULL)
220
281k
            next = end;
221
222
1.63M
        switch (next - ptr) {
223
401k
            case 0:
224
401k
                continue;
225
104k
            case 1:
226
104k
                if (ptr[0] == '.')
227
3.50k
                    continue;
228
101k
                break;
229
101k
            case 2:
230
                // Normalizing symlink/.. results in a wrong path: if the
231
                // current working directory is /tmp/foo, and it is a symlink to
232
                // /usr/bin, mpv ../file.mkv opens /usr/file.mkv, so we can't
233
                // normalize the path to /tmp/file.mkv. Resolve symlinks to fix
234
                // this. Otherwise we don't use realpath so users can use
235
                // symlinks e.g. to hide how media files are distributed over
236
                // real storage and move them while still resuming playback as
237
                // long as the symlinked path doesn't change.
238
53.5k
                if (ptr[0] == '.' && ptr[1] == '.') {
239
1.91k
                    char *tmp_result = realpath(path, NULL);
240
1.91k
                    result = talloc_strdup(talloc_ctx,
241
1.91k
                                           tmp_result ? tmp_result : path);
242
1.91k
                    free(tmp_result);
243
1.91k
                    goto exit;
244
1.91k
                }
245
1.63M
        }
246
247
1.23M
        result = talloc_strdup_append_buffer(result, "/");
248
1.23M
        result = talloc_strndup_append_buffer(result, ptr, next - ptr);
249
1.23M
    }
250
251
324k
    result = talloc_steal(talloc_ctx, result);
252
324k
#endif
253
254
326k
exit:
255
326k
    talloc_free(tmp);
256
326k
    return result;
257
324k
}
258
259
bool mp_path_exists(const char *path)
260
352k
{
261
352k
    struct stat st;
262
352k
    return path && stat(path, &st) == 0;
263
352k
}
264
265
bool mp_path_isdir(const char *path)
266
0
{
267
0
    struct stat st;
268
0
    return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
269
0
}
270
271
// Return false if it's considered a normal local filesystem path.
272
bool mp_is_url(bstr path)
273
6.49M
{
274
6.49M
    int proto = bstr_find0(path, "://");
275
6.49M
    if (proto < 1)
276
1.12M
        return false;
277
    // Per RFC3986, the first character of the protocol must be alphabetic.
278
    // The rest must be alphanumeric plus -, + and .
279
26.3M
    for (int i = 0; i < proto; i++) {
280
21.0M
        unsigned char c = path.start[i];
281
21.0M
        if ((i == 0 && !mp_isalpha(c)) ||
282
21.0M
            (!mp_isalnum(c) && c != '.' && c != '-' && c != '+'))
283
89.3k
        {
284
89.3k
            return false;
285
89.3k
        }
286
21.0M
    }
287
5.27M
    return true;
288
5.36M
}
289
290
// Return the protocol part of path, e.g. "http" if path is "http://...".
291
// On success, out_url (if not NULL) is set to the part after the "://".
292
bstr mp_split_proto(bstr path, bstr *out_url)
293
940k
{
294
940k
    if (!mp_is_url(path))
295
336k
        return (bstr){0};
296
603k
    bstr r;
297
603k
    bstr_split_tok(path, "://", &r, out_url ? out_url : &(bstr){0});
298
603k
    return r;
299
940k
}
300
301
void mp_mkdirp(const char *dir)
302
842
{
303
842
    if (mp_path_exists(dir))
304
810
        return;
305
306
32
    char *path = talloc_strdup(NULL, dir);
307
32
    char *cdir = path + 1;
308
309
260
    while (cdir) {
310
228
        cdir = strchr(cdir, '/');
311
228
        if (cdir)
312
196
            *cdir = 0;
313
314
228
        mkdir(path, 0777);
315
316
228
        if (cdir)
317
196
            *cdir++ = '/';
318
228
    }
319
320
32
    talloc_free(path);
321
32
}