/src/mpv/stream/stream_file.c
Line | Count | Source |
1 | | /* |
2 | | * Original authors: Albeu, probably Arpi |
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 "config.h" |
21 | | |
22 | | #include <stdio.h> |
23 | | #include <sys/types.h> |
24 | | #include <sys/stat.h> |
25 | | #include <fcntl.h> |
26 | | #include <errno.h> |
27 | | |
28 | | #ifndef _WIN32 |
29 | | #include <poll.h> |
30 | | #endif |
31 | | |
32 | | #include "osdep/io.h" |
33 | | |
34 | | #include "common/common.h" |
35 | | #include "common/msg.h" |
36 | | #include "misc/thread_tools.h" |
37 | | #include "stream.h" |
38 | | #include "options/m_option.h" |
39 | | #include "options/path.h" |
40 | | |
41 | | #if HAVE_BSD_FSTATFS |
42 | | #include <sys/param.h> |
43 | | #include <sys/mount.h> |
44 | | #endif |
45 | | |
46 | | #if HAVE_LINUX_FSTATFS |
47 | | #include <sys/vfs.h> |
48 | | #endif |
49 | | |
50 | | #ifdef _WIN32 |
51 | | #include <windows.h> |
52 | | #include <winioctl.h> |
53 | | #include <winternl.h> |
54 | | #include <io.h> |
55 | | |
56 | | #ifdef _MSC_VER |
57 | | // Those are defined only in Windows DDK |
58 | | typedef struct _FILE_FS_DEVICE_INFORMATION { |
59 | | DEVICE_TYPE DeviceType; |
60 | | ULONG Characteristics; |
61 | | } FILE_FS_DEVICE_INFORMATION, *PFILE_FS_DEVICE_INFORMATION; |
62 | | |
63 | | typedef enum _FSINFOCLASS { |
64 | | FileFsVolumeInformation = 1, |
65 | | FileFsLabelInformation, // 2 |
66 | | FileFsSizeInformation, // 3 |
67 | | FileFsDeviceInformation, // 4 |
68 | | FileFsAttributeInformation, // 5 |
69 | | FileFsControlInformation, // 6 |
70 | | FileFsFullSizeInformation, // 7 |
71 | | FileFsObjectIdInformation, // 8 |
72 | | FileFsDriverPathInformation, // 9 |
73 | | FileFsVolumeFlagsInformation, // 10 |
74 | | FileFsSectorSizeInformation, // 11 |
75 | | FileFsDataCopyInformation, // 12 |
76 | | FileFsMetadataSizeInformation, // 13 |
77 | | FileFsFullSizeInformationEx, // 14 |
78 | | FileFsGuidInformation, // 15 |
79 | | FileFsMaximumInformation |
80 | | } FS_INFORMATION_CLASS, *PFS_INFORMATION_CLASS; |
81 | | #endif |
82 | | |
83 | | #ifndef FILE_REMOTE_DEVICE |
84 | | #define FILE_REMOTE_DEVICE (0x10) |
85 | | #endif |
86 | | #endif |
87 | | |
88 | | struct priv { |
89 | | int fd; |
90 | | bool close; |
91 | | bool use_poll; |
92 | | bool regular_file; |
93 | | bool appending; |
94 | | int64_t orig_size; |
95 | | struct mp_cancel *cancel; |
96 | | }; |
97 | | |
98 | | // Total timeout = RETRY_TIMEOUT * MAX_RETRIES |
99 | 0 | #define RETRY_TIMEOUT 0.2 |
100 | 548k | #define MAX_RETRIES 10 |
101 | | |
102 | | static int64_t get_size(stream_t *s) |
103 | 632k | { |
104 | 632k | struct priv *p = s->priv; |
105 | 632k | struct stat st; |
106 | 632k | if (fstat(p->fd, &st) == 0) { |
107 | 632k | if (st.st_size <= 0 && !s->seekable) |
108 | 0 | st.st_size = -1; |
109 | 632k | if (st.st_size >= 0) |
110 | 632k | return st.st_size; |
111 | 632k | } |
112 | 0 | return -1; |
113 | 632k | } |
114 | | |
115 | | static int fill_buffer(stream_t *s, void *buffer, int max_len) |
116 | 548k | { |
117 | 548k | struct priv *p = s->priv; |
118 | | |
119 | 548k | #ifndef _WIN32 |
120 | 548k | if (p->use_poll) { |
121 | 0 | int c = mp_cancel_get_fd(p->cancel); |
122 | 0 | struct pollfd fds[2] = { |
123 | 0 | {.fd = p->fd, .events = POLLIN}, |
124 | 0 | {.fd = c, .events = POLLIN}, |
125 | 0 | }; |
126 | 0 | poll(fds, c >= 0 ? 2 : 1, -1); |
127 | 0 | if (fds[1].revents & POLLIN) |
128 | 0 | return -1; |
129 | 0 | } |
130 | 548k | #endif |
131 | | |
132 | 548k | for (int retries = 0; retries < MAX_RETRIES; retries++) { |
133 | 548k | int r = read(p->fd, buffer, max_len); |
134 | 548k | if (r > 0) |
135 | 45.1k | return r; |
136 | | |
137 | | // Try to detect and handle files being appended during playback. |
138 | 503k | int64_t size = get_size(s); |
139 | 503k | if (p->regular_file && size > p->orig_size && !p->appending) { |
140 | 0 | MP_WARN(s, "File is apparently being appended to, will keep " |
141 | 0 | "retrying with timeouts.\n"); |
142 | 0 | p->appending = true; |
143 | 0 | } |
144 | | |
145 | 503k | if (!p->appending || p->use_poll) |
146 | 503k | break; |
147 | | |
148 | 0 | if (mp_cancel_wait(p->cancel, RETRY_TIMEOUT)) |
149 | 0 | break; |
150 | 0 | } |
151 | | |
152 | 503k | return 0; |
153 | 548k | } |
154 | | |
155 | | static int write_buffer(stream_t *s, void *buffer, int len) |
156 | 0 | { |
157 | 0 | struct priv *p = s->priv; |
158 | 0 | return write(p->fd, buffer, len); |
159 | 0 | } |
160 | | |
161 | | static int seek(stream_t *s, int64_t newpos) |
162 | 1.81k | { |
163 | 1.81k | struct priv *p = s->priv; |
164 | 1.81k | return lseek(p->fd, newpos, SEEK_SET) != (off_t)-1; |
165 | 1.81k | } |
166 | | |
167 | | static void s_close(stream_t *s) |
168 | 49.0k | { |
169 | 49.0k | struct priv *p = s->priv; |
170 | 49.0k | if (p->close) |
171 | 4.00k | close(p->fd); |
172 | 49.0k | } |
173 | | |
174 | | // If url is a file:// URL, return the local filename, otherwise return NULL. |
175 | | char *mp_file_url_to_filename(void *talloc_ctx, bstr url) |
176 | 504k | { |
177 | 504k | bstr proto = mp_split_proto(url, &url); |
178 | 504k | if (bstrcasecmp0(proto, "file") != 0) |
179 | 473k | return NULL; |
180 | 31.3k | char *filename = bstrto0(talloc_ctx, url); |
181 | 31.3k | mp_url_unescape_inplace(filename); |
182 | | #if HAVE_DOS_PATHS |
183 | | // extract '/' from '/x:/path' |
184 | | if (filename[0] == '/' && filename[1] && filename[2] == ':') |
185 | | memmove(filename, filename + 1, strlen(filename)); // including \0 |
186 | | #endif |
187 | 31.3k | return filename; |
188 | 504k | } |
189 | | |
190 | | // Return talloc_strdup's filesystem path if local, otherwise NULL. |
191 | | // Unlike mp_file_url_to_filename(), doesn't return NULL if already local. |
192 | | char *mp_file_get_path(void *talloc_ctx, bstr url) |
193 | 0 | { |
194 | 0 | if (mp_split_proto(url, &(bstr){0}).len) { |
195 | 0 | return mp_file_url_to_filename(talloc_ctx, url); |
196 | 0 | } else { |
197 | 0 | return bstrto0(talloc_ctx, url); |
198 | 0 | } |
199 | 0 | } |
200 | | |
201 | | #if HAVE_BSD_FSTATFS |
202 | | static bool check_stream_network(int fd) |
203 | | { |
204 | | struct statfs fs; |
205 | | const char *stypes[] = { "afpfs", "nfs", "smbfs", "webdav", "osxfusefs", |
206 | | "fuse", "fusefs.sshfs", "macfuse", NULL }; |
207 | | if (fstatfs(fd, &fs) == 0) |
208 | | for (int i=0; stypes[i]; i++) |
209 | | if (strcmp(stypes[i], fs.f_fstypename) == 0) |
210 | | return true; |
211 | | return false; |
212 | | |
213 | | } |
214 | | #elif HAVE_LINUX_FSTATFS |
215 | | static bool check_stream_network(int fd) |
216 | 44.5k | { |
217 | 44.5k | struct statfs fs; |
218 | 44.5k | const uint32_t stypes[] = { |
219 | 44.5k | 0x5346414F /*AFS*/, 0x61756673 /*AUFS*/, 0x00C36400 /*CEPH*/, |
220 | 44.5k | 0xFF534D42 /*CIFS*/, 0x73757245 /*CODA*/, 0x19830326 /*FHGFS*/, |
221 | 44.5k | 0x65735546 /*FUSEBLK*/,0x65735543 /*FUSECTL*/,0x1161970 /*GFS*/, |
222 | 44.5k | 0x47504653 /*GPFS*/, 0x6B414653 /*KAFS*/, 0x0BD00BD0 /*LUSTRE*/, |
223 | 44.5k | 0x564C /*NCP*/, 0x6969 /*NFS*/, 0x6E667364 /*NFSD*/, |
224 | 44.5k | 0xAAD7AAEA /*PANFS*/, 0x50495045 /*PIPEFS*/, 0x517B /*SMB*/, |
225 | 44.5k | 0xBEEFDEAD /*SNFS*/, 0xBACBACBC /*VMHGFS*/, 0x7461636f /*OCFS2*/, |
226 | 44.5k | 0xFE534D42 /*SMB2*/, 0x61636673 /*ACFS*/, 0x013111A8 /*IBRIX*/, |
227 | 44.5k | 0 |
228 | 44.5k | }; |
229 | 44.5k | if (fstatfs(fd, &fs) == 0) { |
230 | 1.11M | for (int i=0; stypes[i]; i++) { |
231 | 1.06M | if (stypes[i] == fs.f_type) |
232 | 0 | return true; |
233 | 1.06M | } |
234 | 44.5k | } |
235 | 44.5k | return false; |
236 | | |
237 | 44.5k | } |
238 | | #elif defined(_WIN32) |
239 | | static bool check_stream_network(int fd) |
240 | | { |
241 | | NTSTATUS (NTAPI *pNtQueryVolumeInformationFile)(HANDLE, |
242 | | PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS) = NULL; |
243 | | |
244 | | // NtQueryVolumeInformationFile is an internal Windows function. It has |
245 | | // been present since Windows XP, however this code should fail gracefully |
246 | | // if it's removed from a future version of Windows. |
247 | | HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); |
248 | | pNtQueryVolumeInformationFile = (NTSTATUS (NTAPI*)(HANDLE, |
249 | | PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS)) |
250 | | GetProcAddress(ntdll, "NtQueryVolumeInformationFile"); |
251 | | |
252 | | if (!pNtQueryVolumeInformationFile) |
253 | | return false; |
254 | | |
255 | | HANDLE h = (HANDLE)_get_osfhandle(fd); |
256 | | if (h == INVALID_HANDLE_VALUE) |
257 | | return false; |
258 | | |
259 | | FILE_FS_DEVICE_INFORMATION info = { 0 }; |
260 | | IO_STATUS_BLOCK io; |
261 | | NTSTATUS status = pNtQueryVolumeInformationFile(h, &io, &info, |
262 | | sizeof(info), FileFsDeviceInformation); |
263 | | if (!NT_SUCCESS(status)) |
264 | | return false; |
265 | | |
266 | | return info.DeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM || |
267 | | (info.Characteristics & FILE_REMOTE_DEVICE); |
268 | | } |
269 | | #else |
270 | | static bool check_stream_network(int fd) |
271 | | { |
272 | | return false; |
273 | | } |
274 | | #endif |
275 | | |
276 | | static int open_f(stream_t *stream, const struct stream_open_args *args) |
277 | 52.4k | { |
278 | 52.4k | struct priv *p = talloc_ptrtype(stream, p); |
279 | 52.4k | *p = (struct priv) { |
280 | 52.4k | .fd = -1, |
281 | 52.4k | }; |
282 | 52.4k | stream->priv = p; |
283 | 52.4k | stream->is_local_fs = true; |
284 | | |
285 | 52.4k | bool strict_fs = args->flags & STREAM_LOCAL_FS_ONLY; |
286 | 52.4k | bool write = stream->mode == STREAM_WRITE; |
287 | 52.4k | int m = O_CLOEXEC | (write ? O_RDWR | O_CREAT | O_TRUNC : O_RDONLY); |
288 | | |
289 | 52.4k | char *filename = stream->path; |
290 | 52.4k | char *url = ""; |
291 | 52.4k | if (!strict_fs) { |
292 | 52.4k | char *fn = mp_file_url_to_filename(stream, bstr0(stream->url)); |
293 | 52.4k | if (fn) |
294 | 47 | filename = stream->path = fn; |
295 | 52.4k | url = stream->url; |
296 | 52.4k | } |
297 | | |
298 | 52.4k | bool is_fdclose = strncmp(url, "fdclose://", 10) == 0; |
299 | 52.4k | if (strncmp(url, "fd://", 5) == 0 || is_fdclose) { |
300 | 44.6k | stream->is_local_fs = false; |
301 | 44.6k | char *begin = strstr(stream->url, "://") + 3, *end = NULL; |
302 | 44.6k | p->fd = strtol(begin, &end, 0); |
303 | 44.6k | if (!end || end == begin || end[0] || p->fd < 0) { |
304 | 40 | MP_ERR(stream, "Invalid FD number: %s\n", stream->url); |
305 | 40 | return STREAM_ERROR; |
306 | 40 | } |
307 | 44.5k | #ifdef F_SETFD |
308 | 44.5k | if (fcntl(p->fd, F_GETFD) == -1) { |
309 | 2 | MP_ERR(stream, "Invalid FD: %d\n", p->fd); |
310 | 2 | return STREAM_ERROR; |
311 | 2 | } |
312 | 44.5k | #endif |
313 | 44.5k | if (is_fdclose) |
314 | 30 | p->close = true; |
315 | 44.5k | } else if (!strict_fs && !strcmp(filename, "-")) { |
316 | 531 | stream->is_local_fs = false; |
317 | 531 | if (!write) { |
318 | 531 | MP_INFO(stream, "Reading from stdin...\n"); |
319 | 531 | p->fd = 0; |
320 | 531 | } else { |
321 | 0 | MP_INFO(stream, "Writing to stdout...\n"); |
322 | 0 | p->fd = 1; |
323 | 0 | } |
324 | 7.30k | } else { |
325 | 7.30k | if (bstr_startswith0(bstr0(stream->url), "appending://")) |
326 | 8 | p->appending = true; |
327 | | |
328 | 7.30k | mode_t openmode = S_IRUSR | S_IWUSR; |
329 | 7.30k | #ifndef __MINGW32__ |
330 | 7.30k | openmode |= S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; |
331 | 7.30k | if (!write) |
332 | 7.30k | m |= O_NONBLOCK; |
333 | 7.30k | #endif |
334 | 7.30k | p->fd = open(filename, m | O_BINARY, openmode); |
335 | 7.30k | if (p->fd < 0) { |
336 | 3.33k | MP_ERR(stream, "Cannot open file '%s': %s\n", |
337 | 3.33k | filename, mp_strerror(errno)); |
338 | 3.33k | return STREAM_ERROR; |
339 | 3.33k | } |
340 | 3.97k | p->close = true; |
341 | 3.97k | } |
342 | | |
343 | 49.0k | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
344 | 49.0k | if (p->fd != 42) { |
345 | 4.53k | s_close(stream); |
346 | 4.53k | return STREAM_ERROR; |
347 | 4.53k | } |
348 | 44.5k | #endif |
349 | | |
350 | 44.5k | struct stat st; |
351 | 44.5k | bool is_sock_or_fifo = false; |
352 | 44.5k | if (fstat(p->fd, &st) == 0) { |
353 | 44.5k | if (S_ISDIR(st.st_mode)) { |
354 | 0 | stream->is_directory = true; |
355 | 44.5k | } else if (S_ISREG(st.st_mode)) { |
356 | 44.5k | p->regular_file = true; |
357 | 44.5k | #ifndef _WIN32 |
358 | | // O_NONBLOCK has weird semantics on file locks; remove it. |
359 | 44.5k | int val = fcntl(p->fd, F_GETFL) & ~(unsigned)O_NONBLOCK; |
360 | 44.5k | fcntl(p->fd, F_SETFL, val); |
361 | 44.5k | #endif |
362 | 44.5k | } else { |
363 | 0 | #ifndef __MINGW32__ |
364 | 0 | is_sock_or_fifo = S_ISSOCK(st.st_mode) || S_ISFIFO(st.st_mode); |
365 | 0 | #endif |
366 | 0 | p->use_poll = true; |
367 | 0 | } |
368 | 44.5k | } |
369 | | |
370 | | #ifdef _WIN32 |
371 | | setmode(p->fd, O_BINARY); |
372 | | #endif |
373 | | |
374 | 44.5k | off_t len = lseek(p->fd, 0, SEEK_END); |
375 | 44.5k | lseek(p->fd, 0, SEEK_SET); |
376 | 44.5k | if (len != (off_t)-1) { |
377 | 44.5k | stream->seek = seek; |
378 | 44.5k | stream->seekable = true; |
379 | 44.5k | } |
380 | | |
381 | 44.5k | stream->fast_skip = true; |
382 | 44.5k | stream->fill_buffer = fill_buffer; |
383 | 44.5k | stream->write_buffer = write_buffer; |
384 | 44.5k | stream->get_size = get_size; |
385 | 44.5k | stream->close = s_close; |
386 | | |
387 | 44.5k | if (is_sock_or_fifo || check_stream_network(p->fd)) { |
388 | 0 | stream->streaming = true; |
389 | | #if HAVE_COCOA |
390 | | if (fcntl(p->fd, F_RDAHEAD, 0) < 0) { |
391 | | MP_VERBOSE(stream, "Cannot disable read ahead on file '%s': %s\n", |
392 | | filename, mp_strerror(errno)); |
393 | | } |
394 | | #endif |
395 | 0 | } |
396 | | |
397 | 44.5k | p->orig_size = get_size(stream); |
398 | | |
399 | 44.5k | p->cancel = mp_cancel_new(p); |
400 | 44.5k | if (stream->cancel) |
401 | 26.6k | mp_cancel_set_parent(p->cancel, stream->cancel); |
402 | | |
403 | 44.5k | return STREAM_OK; |
404 | 49.0k | } |
405 | | |
406 | | const stream_info_t stream_info_file = { |
407 | | .name = "file", |
408 | | .open2 = open_f, |
409 | | .protocols = (const char*const[]){ "file", "", "appending", NULL }, |
410 | | .can_write = true, |
411 | | .local_fs = true, |
412 | | .stream_origin = STREAM_ORIGIN_FS, |
413 | | }; |
414 | | |
415 | | const stream_info_t stream_info_fd = { |
416 | | .name = "fd", |
417 | | .open2 = open_f, |
418 | | .protocols = (const char*const[]){ "fd", "fdclose", NULL }, |
419 | | .can_write = true, |
420 | | .stream_origin = STREAM_ORIGIN_UNSAFE, |
421 | | }; |