Coverage Report

Created: 2025-08-28 07:26

/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
}