mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
* nfs, smb: apply menu setting changes without a restart The clients took their settings at startup and after config_load only. Their numbers (NFS version, ports, timeout, read-ahead, connection count, SMB auth mode) are copies, so a menu change waited for a restart. Each change now hands the clients the current values. SMB keys its pools on the whole configuration, so its next connection uses the new values. The NFS pool matched a free connection by export alone, so a changed server, version or port could ride an old one. New settings now close the idle connections and advance a generation; a slot whose generation or server differs connects again. The pool is kept rather than replaced, so nfs_init_cfg() cannot free it under a thread that has just taken a slot. On version 4 an NFS port of 0 means 2049 and the mount port is unused; the sublabels say so. vfs_threads_test changes the ports while a prefetcher holds a slot. * nfs, smb: log why a network directory listing failed A failed file browser listing of an nfs:// or smb:// directory showed "Directory not found" and logged nothing. It now logs one error line with the URL, the settings the client ran with and the client's reason. The NFS client's reasons say what failed: the step and the server's status (v4.2 mount: LOOKUP roms: NFS4ERR_NOENT), the address a connect was refused at or timed out on, and RPC rejects such as PROG_MISMATCH or AUTH_TOOWEAK. A refusal (PERM, ACCESS, WRONGSEC, or a v3 MOUNT ACCES) adds that a Linux export needs 'insecure' for a client port above 1023, as this client's always is. A failed walk or redial keeps its own reason. Failures that never reach a server record one too: no export, no server in the URL, no SMB server set. SMB gets smb_get_last_error() to match nfs_get_last_error(). nfs_error_test checks the reasons without a server and against fake_nfs42.py. * nfs: open files keep the settings they were opened with An open nfs:// file or directory stored only its server and export, so after a settings change its next read reconnected with the new version and ports: a v3 handle sent over v4, or the stream's server asked on another server's port. Open files and directories now keep the version, ports and timeout they were opened with and reconnect with those. Each pool slot carries the parameters it connected with and is reused only on an exact match, which replaces the settings generation. Read-ahead is client-side and still follows the settings on every take. nfs_error_test reads an open file on across a port and version change. * smb: list a server's shares when no share is set Browsing smb://server with no share configured never listed anything. retro_vfs_opendir_impl rejected any SMB handle whose dir was NULL, which is exactly the handle retro_vfs_opendir_smb returns for a share list, and leaked it. The backend already reads and closes that handle, so the dispatcher now accepts it. The UWP dispatcher does the same and takes an entry's directory flag from the listing instead of a stat. vfs_test checks the share list through the dispatcher as well. * nfs: a file opened over a private connection keeps its export With no NFS Export set, the export comes from the URL and is found when connecting. When every pooled connection is busy, as it is once a few read-only streams' prefetchers hold one each, the open goes over a private connection and the file or directory handle recorded an empty export. Every later borrow then mounted "": refused on v3; on v4 the pseudo root, where the path under the export is not found, so such a stream never got its prefetcher. Record the export the connection was made with, through a new rnfs_get_export(). nfs_error_test opens a file by URL while a prefetcher holds the only slot. * nfs, smb: settings as immutable copies; pools keyed by the copy nfs_init_cfg() and smb_init_cfg() copy the settings they are given, strings included, and publish the copy through vfs_cfg_snapshot.h: a reader holds the copy it got between enter and leave, and a copy that was replaced is freed once nothing holds it - retro_retire.h's epochs where the pointer atomics are lock-free, a reference count under a short flag elsewhere. Menu edits may rewrite the settings in place while pool, prefetch and task threads read them. NFS resolves a URL against the held copy's export without copying it, and applies the timeout and read-ahead on every take: neither is part of what a connection must match. SMB pools remember the copy they were last matched to, so a take on that copy and share finds its pool without building or comparing a key. A READDIR failure names the result's own status. nfs_cfg_race_test and smb_cfg_race_test free the caller's struct right after handing it over, then resolve against it, and hand one struct over repeatedly while four threads stat and list; they need no server and run ahead of the server matrices in local_unfs3_test.sh and local_samba_test.sh. --------- Co-authored-by: LibretroAdmin <[email protected]>
4459 lines
140 KiB
C
4459 lines
140 KiB
C
/* Copyright (C) 2010-2020 The RetroArch team
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* The following license statement only applies to this file (vfs_implementation.c).
|
|
* ---------------------------------------------------------------------------------------
|
|
*
|
|
* Permission is hereby granted, free of charge,
|
|
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <retro_posix_source.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <limits.h> /* INT_MAX, LONG_MAX -- both C89 */
|
|
#include <sys/types.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
# ifdef _MSC_VER
|
|
# define setmode _setmode
|
|
# endif
|
|
#include <sys/stat.h>
|
|
# ifdef _XBOX
|
|
# include <xtl.h>
|
|
# define INVALID_FILE_ATTRIBUTES -1
|
|
# else
|
|
|
|
# include <fcntl.h>
|
|
# include <direct.h>
|
|
# include <windows.h>
|
|
# endif
|
|
# include <io.h>
|
|
#else
|
|
# if defined(PSP)
|
|
# include <pspiofilemgr.h>
|
|
# endif
|
|
# include <sys/types.h>
|
|
# include <sys/stat.h>
|
|
# if !defined(VITA)
|
|
# include <dirent.h>
|
|
# endif
|
|
# include <unistd.h>
|
|
# if defined(WIIU)
|
|
# include <malloc.h>
|
|
# endif
|
|
#endif
|
|
|
|
#include <fcntl.h>
|
|
#if defined(__linux__)
|
|
#include <linux/falloc.h>
|
|
#endif
|
|
|
|
/* TODO: Some things are duplicated but I'm really afraid of breaking other platforms by touching this */
|
|
#if defined(VITA)
|
|
# include <psp2/io/fcntl.h>
|
|
# include <psp2/io/dirent.h>
|
|
# include <psp2/io/stat.h>
|
|
# include <psp2/rtc.h>
|
|
#elif !defined(_WIN32)
|
|
# if defined(PSP)
|
|
# include <pspiofilemgr.h>
|
|
# endif
|
|
# include <sys/types.h>
|
|
# include <sys/stat.h>
|
|
# include <dirent.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(__QNX__) || defined(PSP)
|
|
#include <unistd.h> /* stat() is defined here */
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#ifdef __MACH__
|
|
#include <TargetConditionals.h>
|
|
#endif
|
|
#endif
|
|
#ifdef __HAIKU__
|
|
#include <kernel/image.h>
|
|
#endif
|
|
#ifndef __MACH__
|
|
#include <compat/strl.h>
|
|
#include <compat/posix_string.h>
|
|
#endif
|
|
#include <compat/strcasestr.h>
|
|
#include <retro_miscellaneous.h>
|
|
#include <encodings/utf.h>
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef _XBOX
|
|
#if defined(_MSC_VER) && _MSC_VER <= 1200
|
|
#define INVALID_FILE_ATTRIBUTES ((DWORD)-1)
|
|
#endif
|
|
#endif
|
|
#elif defined(VITA)
|
|
#define SCE_ERROR_ERRNO_EEXIST 0x80010011
|
|
#include <psp2/io/fcntl.h>
|
|
#include <psp2/io/dirent.h>
|
|
#include <psp2/io/stat.h>
|
|
#else
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
|
|
#if defined(PSP)
|
|
#include <pspkernel.h>
|
|
#endif
|
|
|
|
#if defined(__PS3__) || defined(__PSL1GHT__)
|
|
#define FS_SUCCEEDED 0
|
|
#define FS_TYPE_DIR 1
|
|
#ifdef __PSL1GHT__
|
|
#include <lv2/sysfs.h>
|
|
#ifndef O_RDONLY
|
|
#define O_RDONLY SYS_O_RDONLY
|
|
#endif
|
|
#ifndef O_WRONLY
|
|
#define O_WRONLY SYS_O_WRONLY
|
|
#endif
|
|
#ifndef O_CREAT
|
|
#define O_CREAT SYS_O_CREAT
|
|
#endif
|
|
#ifndef O_TRUNC
|
|
#define O_TRUNC SYS_O_TRUNC
|
|
#endif
|
|
#ifndef O_RDWR
|
|
#define O_RDWR SYS_O_RDWR
|
|
#endif
|
|
#else
|
|
#include <cell/cell_fs.h>
|
|
#ifndef O_RDONLY
|
|
#define O_RDONLY CELL_FS_O_RDONLY
|
|
#endif
|
|
#ifndef O_WRONLY
|
|
#define O_WRONLY CELL_FS_O_WRONLY
|
|
#endif
|
|
#ifndef O_CREAT
|
|
#define O_CREAT CELL_FS_O_CREAT
|
|
#endif
|
|
#ifndef O_TRUNC
|
|
#define O_TRUNC CELL_FS_O_TRUNC
|
|
#endif
|
|
#ifndef O_RDWR
|
|
#define O_RDWR CELL_FS_O_RDWR
|
|
#endif
|
|
#ifndef sysFsStat
|
|
#define sysFsStat cellFsStat
|
|
#endif
|
|
#ifndef sysFSDirent
|
|
#define sysFSDirent CellFsDirent
|
|
#endif
|
|
#ifndef sysFsOpendir
|
|
#define sysFsOpendir cellFsOpendir
|
|
#endif
|
|
#ifndef sysFsReaddir
|
|
#define sysFsReaddir cellFsReaddir
|
|
#endif
|
|
#ifndef sysFSDirent
|
|
#define sysFSDirent CellFsDirent
|
|
#endif
|
|
#ifndef sysFsClosedir
|
|
#define sysFsClosedir cellFsClosedir
|
|
#endif
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(VITA)
|
|
#define FIO_S_ISDIR SCE_S_ISDIR
|
|
#endif
|
|
|
|
#if defined(__QNX__) || defined(PSP)
|
|
#include <unistd.h> /* stat() is defined here */
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
#define ATLEAST_VC2005
|
|
#endif
|
|
#endif
|
|
|
|
#include <vfs/vfs_implementation.h>
|
|
#include <libretro.h>
|
|
#if defined(HAVE_MMAP)
|
|
#include <memmap.h>
|
|
#include <sys/mman.h>
|
|
#endif
|
|
#include <encodings/utf.h>
|
|
#include <compat/fopen_utf8.h>
|
|
#include <file/file_path.h>
|
|
#include <string/stdstring.h>
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_MMAP) && defined(HAVE_THREADS)
|
|
#include <retro_atomic.h>
|
|
#include <rthreads/rthreads.h>
|
|
#include <rthreads/retro_eventcount.h>
|
|
#endif
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_MMAP)
|
|
static void vfs_win32_readahead(libretro_vfs_implementation_file *stream,
|
|
uint64_t pos, uint64_t len);
|
|
#ifdef HAVE_THREADS
|
|
static void vfs_win32_warm_free(void *data);
|
|
#endif
|
|
#endif
|
|
|
|
/* VFS API v5 metadata operations (read-only state, modification time)
|
|
* are implemented on the platforms whose libc exposes chmod()/utimes()
|
|
* with a permission model behind them, and on Win32 via attributes.
|
|
* Everywhere else they report failure rather than pretend. */
|
|
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) \
|
|
|| defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) \
|
|
|| defined(__HAIKU__) || defined(__QNX__) || defined(ANDROID) \
|
|
|| defined(__sun__)
|
|
#define VFS_HAVE_POSIX_METADATA 1
|
|
#include <sys/time.h>
|
|
#endif
|
|
|
|
/* fstatat(): POSIX.1-2008. glibc, musl, bionic, the BSDs and Haiku have
|
|
* it; QNX 6.5 does not, ORBIS's FreeBSD-derived libc does not, and on
|
|
* Apple it arrived with the 10.10 SDK, which the PowerPC cross SDK
|
|
* predates. Everyone else takes the join+stat path in dirent_stat. */
|
|
#if defined(VFS_HAVE_POSIX_METADATA) && !defined(__QNX__) && !defined(ORBIS)
|
|
#if defined(__APPLE__)
|
|
#include <AvailabilityMacros.h>
|
|
#if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
|
|
#define VFS_HAVE_FSTATAT 1
|
|
#elif !defined(MAC_OS_X_VERSION_MIN_REQUIRED) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
|
|
#define VFS_HAVE_FSTATAT 1
|
|
#endif
|
|
#else
|
|
#define VFS_HAVE_FSTATAT 1
|
|
#endif
|
|
#endif
|
|
/* clonefile(): APFS constant-time copy. Needs the 10.12+ SDK and a
|
|
* deployment target that has the symbol; the PowerPC and other legacy
|
|
* SDKs have neither, and __has_include keeps them from even looking. */
|
|
#if defined(__APPLE__) && !defined(VFS_COPY_NO_FASTPATH)
|
|
#include <AvailabilityMacros.h>
|
|
#if defined(__has_include)
|
|
#if __has_include(<sys/clonefile.h>) \
|
|
&& defined(MAC_OS_X_VERSION_MIN_REQUIRED) \
|
|
&& MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
|
|
#include <sys/clonefile.h>
|
|
#define VFS_HAVE_CLONEFILE 1
|
|
#endif
|
|
#endif
|
|
#endif
|
|
/* copy_file_range() through syscall(): the glibc wrapper is only
|
|
* declared under _GNU_SOURCE, which standalone consumers of this file
|
|
* need not define, and the raw syscall number has been in the kernel
|
|
* headers since 4.5. Define VFS_COPY_NO_FASTPATH to force the
|
|
* portable loop (used by the samples to test that path on a host
|
|
* that would otherwise never take it). */
|
|
#if defined(__linux__) && !defined(VFS_COPY_NO_FASTPATH)
|
|
#include <sys/syscall.h>
|
|
#if defined(SYS_copy_file_range)
|
|
#define VFS_HAVE_COPY_FILE_RANGE 1
|
|
#endif
|
|
#endif
|
|
|
|
/* Windows FILETIME is 100 ns ticks since 1601-01-01; Unix time is
|
|
* seconds since 1970-01-01. Both conversions run in uint64_t so the
|
|
* epoch offset can never be a signed-overflow UB site. */
|
|
#if defined(_WIN32)
|
|
#define VFS_FILETIME_EPOCH_DIFF 116444736000000000ULL
|
|
#define VFS_FILETIME_TICKS_PER_S 10000000ULL
|
|
static int64_t vfs_filetime_to_unix(const FILETIME *ft)
|
|
{
|
|
uint64_t t = ((uint64_t)ft->dwHighDateTime << 32) | (uint64_t)ft->dwLowDateTime;
|
|
if (t < VFS_FILETIME_EPOCH_DIFF)
|
|
return -(int64_t)((VFS_FILETIME_EPOCH_DIFF - t) / VFS_FILETIME_TICKS_PER_S);
|
|
return (int64_t)((t - VFS_FILETIME_EPOCH_DIFF) / VFS_FILETIME_TICKS_PER_S);
|
|
}
|
|
|
|
/* FILETIME covers 1601-01-01 .. ~30828 AD. Anything outside is clamped
|
|
* to the nearest end rather than wrapped; -unix_s is never formed, so
|
|
* INT64_MIN is safe. */
|
|
#define VFS_FILETIME_MIN_UNIX (-(int64_t)(VFS_FILETIME_EPOCH_DIFF / VFS_FILETIME_TICKS_PER_S))
|
|
#define VFS_FILETIME_MAX_UNIX ((int64_t)((0x7fffffffffffffffULL - VFS_FILETIME_EPOCH_DIFF) / VFS_FILETIME_TICKS_PER_S))
|
|
static void vfs_unix_to_filetime(int64_t unix_s, FILETIME *ft)
|
|
{
|
|
uint64_t t;
|
|
if (unix_s <= VFS_FILETIME_MIN_UNIX)
|
|
t = 0;
|
|
else if (unix_s >= VFS_FILETIME_MAX_UNIX)
|
|
t = 0x7fffffffffffffffULL;
|
|
else if (unix_s < 0)
|
|
t = VFS_FILETIME_EPOCH_DIFF - ((uint64_t)0 - (uint64_t)unix_s) * VFS_FILETIME_TICKS_PER_S;
|
|
else
|
|
t = VFS_FILETIME_EPOCH_DIFF + (uint64_t)unix_s * VFS_FILETIME_TICKS_PER_S;
|
|
ft->dwLowDateTime = (DWORD)(t & 0xffffffffULL);
|
|
ft->dwHighDateTime = (DWORD)(t >> 32);
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_CDROM
|
|
#include <vfs/vfs_implementation_cdrom.h>
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
#include <vfs/vfs_implementation_saf.h>
|
|
#endif
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
#include "vfs_implementation_smb.h"
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
#include "vfs_implementation_nfs.h"
|
|
#endif
|
|
|
|
#if (defined(_POSIX_C_SOURCE) && (_POSIX_C_SOURCE - 0) >= 200112) || (defined(__POSIX_VISIBLE) && __POSIX_VISIBLE >= 200112) || (defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112) || (defined(__USE_LARGEFILE) && __USE_LARGEFILE) || (defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64)
|
|
#ifndef HAVE_64BIT_OFFSETS
|
|
#define HAVE_64BIT_OFFSETS
|
|
#endif
|
|
#endif
|
|
|
|
#define RFILE_HINT_UNBUFFERED (1 << 8)
|
|
|
|
/* Largest count handed to one fread/fwrite/read/write call. bionic's
|
|
* stdio routes counts through an int-typed callback (aborting via
|
|
* FORTIFY above INT_MAX) and one Linux read()/write() syscall moves at
|
|
* most MAX_RW_COUNT; both fit comfortably under 1 GiB. */
|
|
#define VFS_STDIO_IO_CHUNK_MAX (1024 * 1024 * 1024)
|
|
|
|
/* 64-bit seek on the descriptor path.
|
|
*
|
|
* The buffered path seeks with _fseeki64/fseeko. The descriptor path
|
|
* - the one RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS selects, and
|
|
* the only one a memory-mapped file ever uses - seeked with lseek()
|
|
* and off_t. off_t is 32 bits on every Windows build, 64-bit ones
|
|
* included (long is 32-bit there), and on any Unix built without
|
|
* large-file support. A file of 2 GiB or more therefore could not be
|
|
* seeked, and because retro_vfs_file_open_impl() sizes a new handle by
|
|
* seeking to its end, the failure landed on stream->size and
|
|
* stream->mapsize at open: the whole file came back as size 0 or a
|
|
* negative one, and the mapping was skipped. Disc images are exactly
|
|
* the files that cross that line.
|
|
*
|
|
* The width is settled at compile time, in this order:
|
|
*
|
|
* Windows SetFilePointer with the high-order dword, on the
|
|
* handle behind the descriptor. Deliberately not the
|
|
* CRT's _lseeki64: that arrived with Visual C++ 4 and is
|
|
* exported by the msvcrt.dll of that era onwards, but
|
|
* Windows 95 shipped without an msvcrt.dll at all and
|
|
* which one a 9x box ended up with came down to what an
|
|
* installer happened to drop there. SetFilePointer has
|
|
* no such history - it is the original Win32 seek, in
|
|
* kernel32 since NT 3.1 and Windows 95 RTM, and it is
|
|
* what the CRT's own _lseeki64 calls underneath. (Not
|
|
* SetFilePointerEx, which is XP and up and would be the
|
|
* one call here that 9x could not make.) The Xbox XDKs
|
|
* have it too, alongside the _get_osfhandle this file
|
|
* already uses for the Win32 mapping path, so no console
|
|
* needs a case of its own. Note that on the original
|
|
* Xbox the buffered path is stuck at 32 bits regardless:
|
|
* that CRT has fseek(long) and no _fseeki64, so a large
|
|
* file there is reachable only through this path.
|
|
*
|
|
* INVALID_SET_FILE_POINTER is also a legitimate low
|
|
* dword for a file this size, hence the SetLastError
|
|
* dance rather than a bare comparison.
|
|
*
|
|
* Store/UWP targets, where SetFilePointer is outside the
|
|
* app API partition, take _lseeki64 instead - every CRT
|
|
* new enough to build for that target has it.
|
|
*
|
|
* Android lseek64. Bionic declares it in <unistd.h> at every
|
|
* API level with no feature macro required, which is the
|
|
* point: _FILE_OFFSET_BITS=64 is not usable on Android
|
|
* before API 24, where asking for it removes mmap() from
|
|
* the ABI rather than widening it.
|
|
*
|
|
* otherwise lseek with off_t, whose width is the build's business
|
|
* and not this file's. An earlier version of this tried
|
|
* to widen it here by defining _FILE_OFFSET_BITS and
|
|
* _LARGEFILE64_SOURCE above the includes, which was wrong
|
|
* twice: this file is #included into griffin.c as the
|
|
* 424th member of that translation unit, so "before every
|
|
* header" is not a promise it can keep, and the macros it
|
|
* set were read further down as capability tests -
|
|
* _LARGEFILE64_SOURCE selecting struct stat64/stat64(),
|
|
* which 3DS, PSP, DJGPP and Darwin do not have, and
|
|
* _FILE_OFFSET_BITS forcing HAVE_64BIT_OFFSETS and its
|
|
* fseeko/ftello onto every non-Windows target. A build
|
|
* wanting a wider off_t passes -D_FILE_OFFSET_BITS=64
|
|
* itself, which is the only place it can be set
|
|
* consistently for a whole program anyway. Where off_t
|
|
* is narrow, an offset it cannot represent is refused
|
|
* here rather than silently truncated.
|
|
*/
|
|
/* 64-bit seek on the buffered path.
|
|
*
|
|
* The existing ladder asked for _fseeki64 only under ATLEAST_VC2005
|
|
* (an _MSC_VER test, so never true for MinGW) and otherwise for
|
|
* fseeko under HAVE_64BIT_OFFSETS, whose POSIX feature macros no
|
|
* Windows toolchain defines by itself. A MinGW build that did not
|
|
* happen to pass -D_FILE_OFFSET_BITS=64 therefore fell all the way
|
|
* through to fseek(fp, (long)offset, whence) and could not seek a
|
|
* stream past 2 GiB - the same failure as the descriptor path, in the
|
|
* path callers reach without asking for any hint at all. Measured on
|
|
* an unfixed tree, both Windows targets: size -1073741824, seeks
|
|
* failing, tell reporting the truncation.
|
|
*
|
|
* Windows tiers, widest provenance last:
|
|
*
|
|
* _fseeki64 MSVC 2005 and up, and MinGW/MinGW-w64, which
|
|
* declare it and import it from msvcrt.
|
|
* fgetpos/ C89, in every CRT that has ever existed, including
|
|
* fsetpos the 2002 Xbox one - and on Windows fpos_t is a
|
|
* 64-bit scalar (__int64) rather than the struct the
|
|
* standard permits, so it carries the whole range.
|
|
* This is the tier VC6 and the Xbox XDK land on.
|
|
* SEEK_END needs a length, taken from the handle
|
|
* with GetFileSize rather than from a CRT call, for
|
|
* the same reason SetFilePointer was chosen below.
|
|
* fseek Anything else: unchanged behaviour, with an
|
|
* unrepresentable offset refused rather than
|
|
* silently truncated.
|
|
*/
|
|
#if defined(_WIN32)
|
|
#if defined(ATLEAST_VC2005) || defined(__MINGW32__)
|
|
#define RETRO_VFS_HAVE_FSEEKI64 1
|
|
#elif !defined(__STDC__) && defined(_INTEGRAL_MAX_BITS) && _INTEGRAL_MAX_BITS >= 64
|
|
#define RETRO_VFS_HAVE_FPOS64 1
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(_WIN32) && !defined(RETRO_VFS_HAVE_FSEEKI64) && defined(RETRO_VFS_HAVE_FPOS64)
|
|
static int64_t retro_vfs_fp_length64(FILE *fp)
|
|
{
|
|
HANDLE h;
|
|
DWORD lo, hi = 0;
|
|
|
|
fflush(fp);
|
|
h = (HANDLE)_get_osfhandle(_fileno(fp));
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return -1;
|
|
|
|
SetLastError(NO_ERROR);
|
|
lo = GetFileSize(h, &hi);
|
|
if (lo == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
|
|
return -1;
|
|
|
|
return (int64_t)(((uint64_t)hi << 32) | (uint64_t)lo);
|
|
}
|
|
#endif
|
|
|
|
static int retro_vfs_fp_seek64(FILE *fp, int64_t offset, int whence)
|
|
{
|
|
#if defined(RETRO_VFS_HAVE_FSEEKI64)
|
|
return _fseeki64(fp, offset, whence);
|
|
#elif defined(RETRO_VFS_HAVE_FPOS64)
|
|
fpos_t pos;
|
|
|
|
switch (whence)
|
|
{
|
|
case SEEK_SET:
|
|
break;
|
|
case SEEK_CUR:
|
|
if (fgetpos(fp, &pos) != 0)
|
|
return -1;
|
|
offset += (int64_t)pos;
|
|
break;
|
|
case SEEK_END:
|
|
{
|
|
const int64_t len = retro_vfs_fp_length64(fp);
|
|
if (len < 0)
|
|
return -1;
|
|
offset += len;
|
|
}
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
if (offset < 0)
|
|
return -1;
|
|
pos = (fpos_t)offset;
|
|
return fsetpos(fp, &pos) == 0 ? 0 : -1;
|
|
#elif defined(HAVE_64BIT_OFFSETS)
|
|
return fseeko(fp, (off_t)offset, whence);
|
|
#else
|
|
if (sizeof(long) < sizeof(int64_t))
|
|
{
|
|
if ( offset > (int64_t)LONG_MAX
|
|
|| offset < -(int64_t)LONG_MAX - 1)
|
|
return -1;
|
|
}
|
|
return fseek(fp, (long)offset, whence) != 0 ? -1 : 0;
|
|
#endif
|
|
}
|
|
|
|
static int64_t retro_vfs_fp_tell64(FILE *fp)
|
|
{
|
|
#if defined(RETRO_VFS_HAVE_FSEEKI64)
|
|
return _ftelli64(fp);
|
|
#elif defined(RETRO_VFS_HAVE_FPOS64)
|
|
fpos_t pos;
|
|
if (fgetpos(fp, &pos) != 0)
|
|
return -1;
|
|
return (int64_t)pos;
|
|
#elif defined(HAVE_64BIT_OFFSETS)
|
|
return ftello(fp);
|
|
#else
|
|
return ftell(fp);
|
|
#endif
|
|
}
|
|
|
|
static int64_t retro_vfs_fd_seek64(int fd, int64_t offset, int whence)
|
|
{
|
|
#if defined(_WIN32) && (!defined(WINAPI_FAMILY) || defined(_CRT_USE_WINAPI_FAMILY_DESKTOP_APP))
|
|
HANDLE h = (HANDLE)_get_osfhandle(fd);
|
|
LONG hi;
|
|
DWORD lo;
|
|
DWORD method;
|
|
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return -1;
|
|
|
|
switch (whence)
|
|
{
|
|
case SEEK_SET:
|
|
method = FILE_BEGIN;
|
|
break;
|
|
case SEEK_CUR:
|
|
method = FILE_CURRENT;
|
|
break;
|
|
case SEEK_END:
|
|
method = FILE_END;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
|
|
/* Two's complement split: a negative offset relative to CURRENT or
|
|
* END arrives as a sign-extended high dword, which is what
|
|
* SetFilePointer's signed PLONG wants. */
|
|
hi = (LONG)(offset >> 32);
|
|
SetLastError(NO_ERROR);
|
|
lo = SetFilePointer(h, (LONG)(DWORD)(offset & 0xFFFFFFFFu), &hi, method);
|
|
if (lo == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR)
|
|
return -1;
|
|
|
|
return (int64_t)(((uint64_t)(DWORD)hi << 32) | (uint64_t)lo);
|
|
#elif defined(_WIN32)
|
|
return (int64_t)_lseeki64(fd, (__int64)offset, whence);
|
|
#elif defined(__ANDROID__)
|
|
return (int64_t)lseek64(fd, (off64_t)offset, whence);
|
|
#elif defined(VITA)
|
|
/* SCE_SEEK_SET/CUR/END are 0/1/2, the same values as SEEK_*. */
|
|
{
|
|
SceOff pos = sceIoLseek(fd, (SceOff)offset, whence);
|
|
return (pos < 0) ? -1 : (int64_t)pos;
|
|
}
|
|
#else
|
|
/* Compile-time: the cast is lossless exactly when off_t is wide
|
|
* enough, and the guard costs nothing when it is (the comparison
|
|
* folds away). */
|
|
if (sizeof(off_t) < sizeof(int64_t))
|
|
{
|
|
if ( offset > (int64_t)LONG_MAX
|
|
|| offset < -(int64_t)LONG_MAX - 1)
|
|
return -1;
|
|
}
|
|
return (int64_t)lseek(fd, (off_t)offset, whence);
|
|
#endif
|
|
}
|
|
|
|
/* Keeps a cold path that needs a PATH_MAX_LENGTH scratch buffer out of
|
|
* a caller that would otherwise not need a frame at all. Under -Os the
|
|
* compiler is already optimizing for size and the forced outlining only
|
|
* buys a call, so it is disabled there. */
|
|
#if defined(__OPTIMIZE_SIZE__)
|
|
#define VFS_NOINLINE
|
|
#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
|
|
#define VFS_NOINLINE __attribute__((noinline))
|
|
#elif defined(_MSC_VER)
|
|
#define VFS_NOINLINE __declspec(noinline)
|
|
#else
|
|
#define VFS_NOINLINE
|
|
#endif
|
|
|
|
#ifdef HAVE_CDROM
|
|
static int path_is_cdrom(const char *p)
|
|
{
|
|
return (p
|
|
&& p[0] == 'c' && p[1] == 'd' && p[2] == 'r'
|
|
&& p[3] == 'o' && p[4] == 'm' && p[5] == ':'
|
|
&& p[6] == '/' && p[7] == '/' && p[8] != '\0');
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
static int path_is_smb(const char *p)
|
|
{
|
|
return (p
|
|
&& p[0] == 's' && p[1] == 'm' && p[2] == 'b'
|
|
&& p[3] == ':' && p[4] == '/' && p[5] == '/'
|
|
&& p[6] != '\0');
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
static int path_is_nfs(const char *p)
|
|
{
|
|
return (p
|
|
&& p[0] == 'n' && p[1] == 'f' && p[2] == 's'
|
|
&& p[3] == ':' && p[4] == '/' && p[5] == '/'
|
|
&& p[6] != '\0');
|
|
}
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
static int path_is_saf(const char *p)
|
|
{
|
|
return (p
|
|
&& p[0] == 's' && p[1] == 'a' && p[2] == 'f'
|
|
&& p[3] == ':' && p[4] == '/' && p[5] == '/'
|
|
&& p[6] != '\0');
|
|
}
|
|
#endif
|
|
|
|
int64_t retro_vfs_file_seek_internal(
|
|
libretro_vfs_implementation_file *stream,
|
|
int64_t offset, int whence)
|
|
{
|
|
if (!stream)
|
|
return -1;
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
return retro_vfs_file_seek_cdrom(stream, offset, whence);
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return retro_vfs_file_seek_smb(stream, offset, whence);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return retro_vfs_file_seek_nfs(stream, offset, whence);
|
|
#endif
|
|
return retro_vfs_fp_seek64(stream->fp, offset, whence);
|
|
}
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
/* Need to check stream->mapped because this function is
|
|
* called in filestream_open() */
|
|
if (stream->mapped && (stream->hints &
|
|
RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
{
|
|
/* fseek() returns error on under/overflow but
|
|
* allows cursor > EOF for
|
|
read-only file descriptors. */
|
|
/* The file position must never be negative. */
|
|
switch (whence)
|
|
{
|
|
case SEEK_SET:
|
|
if (offset < 0)
|
|
return -1;
|
|
|
|
stream->mappos = offset;
|
|
break;
|
|
|
|
case SEEK_CUR:
|
|
if ((int64_t)stream->mappos + offset < 0)
|
|
return -1;
|
|
|
|
stream->mappos += offset;
|
|
break;
|
|
|
|
case SEEK_END:
|
|
/* RETRO_VFS_SEEK_POSITION_END states offset should be negative.
|
|
* However, this is impractical because we would be forcing the
|
|
* end of file to always be off by one.
|
|
*/
|
|
if (offset > 0 || (int64_t)stream->mapsize + offset < 0)
|
|
return -1;
|
|
|
|
stream->mappos = stream->mapsize + offset;
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
return retro_vfs_fd_seek64(stream->fd, offset, whence) == -1 ? -1 : 0;
|
|
}
|
|
|
|
/**
|
|
* retro_vfs_file_open_impl:
|
|
* @path : path to file
|
|
* @mode : file mode to use when opening (read/write)
|
|
* @hints :
|
|
*
|
|
* Opens a file for reading or writing, depending on the requested mode.
|
|
* Returns a pointer to an RFILE if opened successfully, otherwise NULL.
|
|
**/
|
|
|
|
libretro_vfs_implementation_file *retro_vfs_file_open_impl(
|
|
const char *path, unsigned mode, unsigned hints)
|
|
{
|
|
int flags = 0;
|
|
const char *mode_str = NULL;
|
|
libretro_vfs_implementation_file *stream =
|
|
(libretro_vfs_implementation_file*)
|
|
calloc(1, sizeof(*stream));
|
|
|
|
if (!stream)
|
|
return NULL;
|
|
|
|
stream->fd = -1;
|
|
stream->hints = hints;
|
|
stream->scheme = VFS_SCHEME_NONE;
|
|
|
|
#ifdef VFS_FRONTEND
|
|
if ( path
|
|
&& path[0] == 'v'
|
|
&& path[1] == 'f'
|
|
&& path[2] == 's'
|
|
&& path[3] == 'o'
|
|
&& path[4] == 'n'
|
|
&& path[5] == 'l'
|
|
&& path[6] == 'y'
|
|
&& path[7] == ':'
|
|
&& path[8] == '/'
|
|
&& path[9] == '/')
|
|
path += sizeof("vfsonly://")-1;
|
|
#endif
|
|
|
|
#ifdef HAVE_CDROM
|
|
if (path_is_cdrom(path))
|
|
{
|
|
/* The sector cache and cue sheet state only exist for cdrom://
|
|
* handles, so they are allocated here rather than carried by
|
|
* every open file. Zeroed, because the old inline member was
|
|
* zeroed by the calloc() above and the cdrom paths rely on
|
|
* that (cue_buf NULL, last_frame_valid false, cur_track 0). */
|
|
if (!(stream->cdrom = (vfs_cdrom_t*)calloc(1, sizeof(*stream->cdrom))))
|
|
{
|
|
free(stream);
|
|
return NULL;
|
|
}
|
|
path += sizeof("cdrom://")-1;
|
|
stream->scheme = VFS_SCHEME_CDROM;
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (path_is_smb(path))
|
|
{
|
|
stream->scheme = VFS_SCHEME_SMB;
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (path_is_nfs(path))
|
|
stream->scheme = VFS_SCHEME_NFS;
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(path))
|
|
{
|
|
stream->scheme = VFS_SCHEME_SAF;
|
|
}
|
|
#endif
|
|
|
|
if (path)
|
|
stream->orig_path = strdup(path);
|
|
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
/* Only a descriptor can be mapped: plain files, and SAF, which the
|
|
* descriptor branch opens. cdrom://, smb:// and nfs:// streams are
|
|
* opened by their own backends in the buffered branch and read
|
|
* through them, unmapped. */
|
|
if ( (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)
|
|
&& mode == RETRO_VFS_FILE_ACCESS_READ
|
|
&& ( stream->scheme == VFS_SCHEME_NONE
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
|| stream->scheme == VFS_SCHEME_SAF
|
|
#endif
|
|
))
|
|
stream->hints |= RFILE_HINT_UNBUFFERED;
|
|
else
|
|
#endif
|
|
stream->hints &= ~RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS;
|
|
|
|
#ifdef VFS_HAVE_DESCRIPTOR_IO
|
|
/* A read-once-and-close stream wants the descriptor path for the
|
|
* opposite reason a mapped one does: not to keep the bytes around,
|
|
* but to avoid a buffer it will never reuse. Deliberately after
|
|
* the block above and never touching the mapping hint, so a
|
|
* stream asking for both still maps and behaves exactly as before.
|
|
*
|
|
* Restricted to VFS_SCHEME_NONE: the descriptor branch below knows
|
|
* about plain files and (on Android) SAF, and has no case for
|
|
* cdrom:// or smb://, which have their own read/seek entry points
|
|
* that only the buffered branch dispatches to. */
|
|
if ( (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK)
|
|
&& mode == RETRO_VFS_FILE_ACCESS_READ
|
|
&& stream->scheme == VFS_SCHEME_NONE)
|
|
stream->hints |= RFILE_HINT_UNBUFFERED;
|
|
#ifdef VFS_HAVE_DESCRIPTOR_WRITE
|
|
/* A write-once stream is the same shape: every byte it will ever
|
|
* write arrives in one call, so a stdio buffer only adds a copy
|
|
* and a flush. */
|
|
if ( (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK)
|
|
&& ( mode == RETRO_VFS_FILE_ACCESS_WRITE
|
|
|| mode == RETRO_VFS_FILE_ACCESS_READ_WRITE)
|
|
&& stream->scheme == VFS_SCHEME_NONE)
|
|
stream->hints |= RFILE_HINT_UNBUFFERED;
|
|
#endif
|
|
#endif
|
|
|
|
switch (mode)
|
|
{
|
|
case RETRO_VFS_FILE_ACCESS_READ:
|
|
mode_str = "rb";
|
|
|
|
flags = O_RDONLY;
|
|
#ifdef _WIN32
|
|
flags |= O_BINARY;
|
|
#endif
|
|
break;
|
|
|
|
case RETRO_VFS_FILE_ACCESS_WRITE:
|
|
mode_str = "wb";
|
|
|
|
flags = O_WRONLY | O_CREAT | O_TRUNC;
|
|
#if defined(_WIN32)
|
|
flags |= O_BINARY;
|
|
#endif
|
|
break;
|
|
|
|
case RETRO_VFS_FILE_ACCESS_READ_WRITE:
|
|
mode_str = "w+b";
|
|
flags = O_RDWR | O_CREAT | O_TRUNC;
|
|
#if defined(_WIN32)
|
|
flags |= O_BINARY;
|
|
#endif
|
|
break;
|
|
|
|
case RETRO_VFS_FILE_ACCESS_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING:
|
|
case RETRO_VFS_FILE_ACCESS_READ_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING:
|
|
mode_str = "r+b";
|
|
|
|
flags = O_RDWR;
|
|
#if defined(_WIN32)
|
|
flags |= O_BINARY;
|
|
#endif
|
|
break;
|
|
|
|
default:
|
|
goto error;
|
|
}
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
FILE *fp;
|
|
switch (stream->scheme)
|
|
{
|
|
#ifdef HAVE_CDROM
|
|
case VFS_SCHEME_CDROM:
|
|
retro_vfs_file_open_cdrom(stream, path, mode, hints);
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
if (!stream->fh || stream->fh == INVALID_HANDLE_VALUE)
|
|
goto error;
|
|
#elif defined(__APPLE__)
|
|
if (!stream->iokit_mmc)
|
|
goto error;
|
|
#else
|
|
if (!stream->fp)
|
|
goto error;
|
|
#endif
|
|
break;
|
|
#endif
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
case VFS_SCHEME_SMB:
|
|
if (!retro_vfs_file_open_smb(stream, path, mode, hints))
|
|
goto error;
|
|
break;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
case VFS_SCHEME_NFS:
|
|
if (!retro_vfs_file_open_nfs(stream, path, mode, hints))
|
|
goto error;
|
|
break;
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
case VFS_SCHEME_SAF:
|
|
{
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
int fd;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, path))
|
|
goto error;
|
|
fd = retro_vfs_file_open_saf(saf_split_result.tree, saf_split_result.path, mode);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
if (fd == -1)
|
|
goto error;
|
|
stream->fp = fdopen(fd, mode_str);
|
|
if (!stream->fp)
|
|
{
|
|
close(fd);
|
|
goto error;
|
|
}
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
if (!(fp = (FILE*)fopen_utf8(path, mode_str)))
|
|
{
|
|
#if TARGET_OS_IPHONE
|
|
if (errno == EEXIST)
|
|
{
|
|
retro_vfs_file_remove_impl(path);
|
|
fp = (FILE*)fopen_utf8(path, mode_str);
|
|
}
|
|
if (!fp)
|
|
#endif
|
|
goto error;
|
|
}
|
|
stream->fp = fp;
|
|
break;
|
|
}
|
|
|
|
/* Regarding setvbuf:
|
|
*
|
|
* https://www.freebsd.org/cgi/man.cgi?query=setvbuf&apropos=0&sektion=0&manpath=FreeBSD+11.1-RELEASE&arch=default&format=html
|
|
*
|
|
* If the size argument is not zero but buf is NULL,
|
|
* a buffer of the given size will be allocated immediately, and
|
|
* released on close. This is an extension to ANSI C.
|
|
*
|
|
* Since C89 does not support specifying a NULL buffer
|
|
* with a non-zero size, we create and track our own buffer for it.
|
|
*/
|
|
/* stdio sizes a file stream's buffer from st_blksize, which the
|
|
* filesystem reports and which is 4 KiB on every platform
|
|
* checked. Callers here write in much smaller pieces than that
|
|
* - a JSON writer flushing a kilobyte at a time, for instance -
|
|
* so that default decides how many syscalls a file costs.
|
|
*
|
|
* Measured writing 8 MiB in 1 KiB pieces, default against
|
|
* 64 KiB:
|
|
*
|
|
* desktop Linux 14.02 ms -> 3.05 ms reads 1.68 -> 0.75 ms
|
|
* macOS 9.35 ms -> 1.58 ms reads 1.59 -> 0.80 ms
|
|
*
|
|
* Sixteen times fewer syscalls for the same bytes, and the
|
|
* dearer a syscall is the more that is worth - which is why the
|
|
* platforms with the slowest storage are the ones that already
|
|
* did this, and with the largest buffers.
|
|
*
|
|
* This is deliberately not gated on a platform list. Such a
|
|
* list has to be maintained, is wrong the moment a target is
|
|
* added, and would leave that target silently on 4 KiB forever -
|
|
* which is exactly the state everything but the two consoles
|
|
* below was in. The rest of this codebase does not gate it
|
|
* either: config_file.c and verbosity.c both buffer
|
|
* unconditionally.
|
|
*
|
|
* The memory is one allocation per open file, released when the
|
|
* file closes, and a handful of files are open at once. Where
|
|
* even that is too much the allocation simply fails and the C
|
|
* library default is used, so a platform under real pressure
|
|
* degrades rather than breaks. A platform wanting a different
|
|
* size says so, as the two below do.
|
|
*
|
|
* It is malloc rather than calloc because nothing ever reads
|
|
* these bytes before stdio writes them - the buffer is handed
|
|
* straight to setvbuf and otherwise only freed, and the WiiU
|
|
* path below has always used non-zeroing memalign. Zeroing it
|
|
* was the single dearest part of opening a small file: 3000
|
|
* opens of 256-byte files measured 2.9-3.1 us each with the
|
|
* zeroing and 1.8 us without, so scan-shaped and thumbnail-
|
|
* shaped workloads - thousands of opens, few bytes each - spent
|
|
* more time clearing buffers than reading files. */
|
|
#if defined(_3DS)
|
|
if (stream->scheme != VFS_SCHEME_CDROM)
|
|
{
|
|
stream->buf = (char*)malloc(0x10000);
|
|
if (stream->fp)
|
|
setvbuf(stream->fp, stream->buf, _IOFBF, 0x10000);
|
|
}
|
|
#elif defined(WIIU)
|
|
if (stream->scheme != VFS_SCHEME_CDROM)
|
|
{
|
|
const int bufsize = 128 * 1024;
|
|
stream->buf = (char*)memalign(0x40, bufsize);
|
|
if (stream->fp)
|
|
setvbuf(stream->fp, stream->buf, _IOFBF, bufsize);
|
|
}
|
|
#else
|
|
if (stream->scheme != VFS_SCHEME_CDROM)
|
|
{
|
|
const int bufsize = 64 * 1024;
|
|
/* NULL rather than a buffer of our own, so the C library
|
|
* allocates and owns it.
|
|
*
|
|
* Who owns the buffer is not a detail on every library.
|
|
* Apple's fread() has a fast path that reads a large request
|
|
* straight into the caller's buffer instead of copying it
|
|
* through the stream, and gates it on __SMBF - "this buffer
|
|
* is mine" - which setvbuf() clears when handed a buffer
|
|
* from outside (Libc stdio/FreeBSD/setvbuf.c sets __SMBF
|
|
* only on the branch that allocates, and fread.c tests it;
|
|
* see also radars 5980080 and 6180417). So the 64 KiB
|
|
* buffer added here for write throughput was disqualifying
|
|
* the read fast path on every Apple target, and measured at
|
|
* roughly half the whole-file read speed on macOS.
|
|
*
|
|
* The size argument still applies, so writes keep the buffer
|
|
* they were given. A library that will not allocate for a
|
|
* NULL buffer leaves the stream on its own default, which is
|
|
* slower for small writes but correct - the same outcome the
|
|
* malloc failing used to produce. */
|
|
if (stream->fp)
|
|
setvbuf(stream->fp, NULL, _IOFBF, bufsize);
|
|
}
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
switch (stream->scheme)
|
|
{
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
case VFS_SCHEME_SAF:
|
|
{
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, path))
|
|
goto error;
|
|
stream->fd = retro_vfs_file_open_saf(saf_split_result.tree, saf_split_result.path, mode);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
{
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
char *path_local = utf8_to_local_string_alloc(path);
|
|
stream->fd = open(path_local, flags, 0);
|
|
if (path_local)
|
|
free(path_local);
|
|
}
|
|
else
|
|
{
|
|
wchar_t * path_wide = utf8_to_utf16_string_alloc(path);
|
|
stream->fd = _wopen(path_wide, flags, 0);
|
|
if (path_wide)
|
|
free(path_wide);
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
char *path_local = utf8_to_local_string_alloc(path);
|
|
stream->fd = open(path_local, flags, 0);
|
|
if (path_local)
|
|
free(path_local);
|
|
#else
|
|
wchar_t * path_wide = utf8_to_utf16_string_alloc(path);
|
|
stream->fd = _wopen(path_wide, flags, 0);
|
|
if (path_wide)
|
|
free(path_wide);
|
|
#endif
|
|
#elif defined(VITA)
|
|
{
|
|
int sce_flags = 0;
|
|
SceUID uid;
|
|
|
|
/* The POSIX flags above are for the open() targets. */
|
|
(void)flags;
|
|
|
|
switch (mode)
|
|
{
|
|
case RETRO_VFS_FILE_ACCESS_READ:
|
|
sce_flags = SCE_O_RDONLY;
|
|
break;
|
|
case RETRO_VFS_FILE_ACCESS_WRITE:
|
|
sce_flags = SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC;
|
|
break;
|
|
case RETRO_VFS_FILE_ACCESS_READ_WRITE:
|
|
sce_flags = SCE_O_RDWR | SCE_O_CREAT | SCE_O_TRUNC;
|
|
break;
|
|
default:
|
|
sce_flags = SCE_O_RDWR;
|
|
break;
|
|
}
|
|
|
|
/* SceUID error codes are negative; the descriptor
|
|
* path only ever tests for -1. */
|
|
uid = sceIoOpen(path, sce_flags, 0777);
|
|
stream->fd = (uid < 0) ? -1 : (int)uid;
|
|
}
|
|
#else
|
|
stream->fd = open(path, flags, S_IRUSR | S_IWUSR);
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (stream->fd == -1)
|
|
goto error;
|
|
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)
|
|
{
|
|
stream->mappos = 0;
|
|
stream->mapped = NULL;
|
|
|
|
retro_vfs_file_seek_internal(stream, 0, SEEK_END);
|
|
|
|
stream->mapsize = retro_vfs_file_tell_impl(stream);
|
|
if (stream->mapsize == (uint64_t)-1)
|
|
goto error;
|
|
|
|
retro_vfs_file_seek_internal(stream, 0, SEEK_SET);
|
|
|
|
#if defined(HAVE_MMAP)
|
|
/* mmap() takes a size_t. On a 32-bit host a file larger than
|
|
* the address space would truncate to its low bits on the way
|
|
* in - and a truncated length can still map successfully,
|
|
* leaving a short mapping while mapsize goes on claiming the
|
|
* whole file, so every read past the cut would fault instead
|
|
* of coming up short. Refuse the mapping and let the
|
|
* descriptor path below serve the file, exactly as a failed
|
|
* mmap() does. Folds away where size_t is 64 bits. */
|
|
if (stream->mapsize > (uint64_t)(size_t)-1)
|
|
{
|
|
stream->mapped = NULL;
|
|
stream->hints &= ~RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS;
|
|
}
|
|
else if ((stream->mapped = (uint8_t*)mmap((void*)0,
|
|
(size_t)stream->mapsize, PROT_READ, MAP_SHARED, stream->fd, 0)) == MAP_FAILED)
|
|
{
|
|
stream->mapped = NULL;
|
|
stream->hints &= ~RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS;
|
|
}
|
|
#else
|
|
{
|
|
/* Win32 file mapping. A zero-length file cannot be mapped
|
|
* (CreateFileMapping rejects it), and a failed mapping of
|
|
* any kind - including a >4GB file on a 32-bit process,
|
|
* where MapViewOfFile cannot find address space - degrades
|
|
* to the ordinary descriptor path by dropping the hint,
|
|
* exactly as the POSIX branch does. */
|
|
HANDLE fh = (HANDLE)_get_osfhandle(stream->fd);
|
|
stream->map_handle = NULL;
|
|
if (fh != INVALID_HANDLE_VALUE && stream->mapsize > 0)
|
|
stream->map_handle = CreateFileMapping(fh, NULL,
|
|
PAGE_READONLY, 0, 0, NULL);
|
|
if (stream->map_handle)
|
|
{
|
|
stream->mapped = (uint8_t*)MapViewOfFile(stream->map_handle,
|
|
FILE_MAP_READ, 0, 0, 0);
|
|
if (!stream->mapped)
|
|
{
|
|
CloseHandle(stream->map_handle);
|
|
stream->map_handle = NULL;
|
|
}
|
|
}
|
|
if (!stream->mapped)
|
|
stream->hints &= ~RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS;
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
{
|
|
retro_vfs_file_seek_cdrom(stream, 0, SEEK_END);
|
|
|
|
stream->size = retro_vfs_file_tell_impl(stream);
|
|
|
|
retro_vfs_file_seek_cdrom(stream, 0, SEEK_SET);
|
|
}
|
|
else
|
|
#endif
|
|
/* A truncating open leaves a zero-length file positioned at 0,
|
|
* so the probe below - a seek to the end, a tell and a seek back,
|
|
* each a system call - would only ever confirm that. */
|
|
if ( mode != RETRO_VFS_FILE_ACCESS_WRITE
|
|
&& mode != RETRO_VFS_FILE_ACCESS_READ_WRITE)
|
|
{
|
|
retro_vfs_file_seek_internal(stream, 0, SEEK_END);
|
|
|
|
stream->size = retro_vfs_file_tell_impl(stream);
|
|
|
|
retro_vfs_file_seek_internal(stream, 0, SEEK_SET);
|
|
}
|
|
return stream;
|
|
|
|
error:
|
|
retro_vfs_file_close_impl(stream);
|
|
return NULL;
|
|
}
|
|
|
|
int retro_vfs_file_close_impl(libretro_vfs_implementation_file *stream)
|
|
{
|
|
int result = 0;
|
|
if (!stream)
|
|
return -1;
|
|
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
{
|
|
result = retro_vfs_file_close_cdrom(stream);
|
|
goto end;
|
|
}
|
|
#endif
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
{
|
|
retro_vfs_file_close_smb(stream);
|
|
goto netend;
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
{
|
|
retro_vfs_file_close_nfs(stream);
|
|
goto netend;
|
|
}
|
|
#endif
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
if (stream->fp)
|
|
if (fclose(stream->fp) != 0)
|
|
result = -1;
|
|
}
|
|
else
|
|
{
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->mapped && (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
{
|
|
#if defined(HAVE_MMAP)
|
|
munmap(stream->mapped, stream->mapsize);
|
|
#else
|
|
#ifdef HAVE_THREADS
|
|
if (stream->warm)
|
|
vfs_win32_warm_free(stream->warm);
|
|
stream->warm = NULL;
|
|
#endif
|
|
UnmapViewOfFile(stream->mapped);
|
|
if (stream->map_handle)
|
|
{
|
|
CloseHandle(stream->map_handle);
|
|
stream->map_handle = NULL;
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (stream->fd >= 0)
|
|
{
|
|
#if defined(VITA)
|
|
if (sceIoClose((SceUID)stream->fd) < 0)
|
|
result = -1;
|
|
#else
|
|
/* Only a negative return is a failure: on PS2, ps2sdk's close()
|
|
* passes iomanX's result through, which is the closed descriptor
|
|
* number on success. */
|
|
if (close(stream->fd) < 0)
|
|
result = -1;
|
|
#endif
|
|
}
|
|
#ifdef HAVE_CDROM
|
|
end:
|
|
/* Reached both by the goto above and by fall-through from the
|
|
* non-cdrom path, where stream->cdrom is NULL. */
|
|
if (stream->cdrom)
|
|
{
|
|
if (stream->cdrom->cue_buf)
|
|
free(stream->cdrom->cue_buf);
|
|
free(stream->cdrom);
|
|
stream->cdrom = NULL;
|
|
}
|
|
#endif
|
|
#if defined(HAVE_SMBCLIENT) || defined(HAVE_NFSCLIENT)
|
|
netend:
|
|
#endif
|
|
|
|
if (stream->buf)
|
|
free(stream->buf);
|
|
|
|
if (stream->orig_path)
|
|
free(stream->orig_path);
|
|
|
|
free(stream);
|
|
|
|
return result;
|
|
}
|
|
|
|
int retro_vfs_file_error_impl(libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (!stream)
|
|
return -1;
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
return retro_vfs_file_error_cdrom(stream);
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return retro_vfs_file_error_smb(stream);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return retro_vfs_file_error_nfs(stream);
|
|
#endif
|
|
if (!stream->fp)
|
|
return -1;
|
|
return ferror(stream->fp);
|
|
}
|
|
|
|
int64_t retro_vfs_file_size_impl(libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (stream)
|
|
return stream->size;
|
|
return 0;
|
|
}
|
|
|
|
/* Deallocate a range without changing the file's length. See
|
|
* filestream_punch_hole for why this is a capability rather than a
|
|
* guarantee: most backends cannot do it, and callers fall back to writing
|
|
* zeroes. Returns 0 on success, -1 otherwise. */
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
/* The pieces of winioctl.h that retro_vfs_file_punch_hole_impl needs,
|
|
* declared here rather than by including that header.
|
|
*
|
|
* winioctl.h defines the storage class GUIDs, and in a unity build --
|
|
* griffin.c, where this file is compiled alongside the D3D headers that
|
|
* set INITGUID -- those definitions collide with the ones already emitted
|
|
* in the same translation unit, which is twenty-odd C2374 "redefinition;
|
|
* multiple initialization" errors on MSVC. Nothing here needs a GUID.
|
|
*
|
|
* The two control codes are the documented CTL_CODE expansions:
|
|
* FSCTL_SET_SPARSE = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 49, METHOD_BUFFERED, FILE_SPECIAL_ACCESS)
|
|
* FSCTL_SET_ZERO_DATA = CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 50, METHOD_BUFFERED, FILE_WRITE_DATA)
|
|
* They carry their own names so a translation unit that also sees
|
|
* winioctl.h holds one definition of each. */
|
|
#define RA_FSCTL_SET_SPARSE 0x000900c4
|
|
#define RA_FSCTL_SET_ZERO_DATA 0x000980c8
|
|
#ifndef FILE_ZERO_DATA_INFORMATION_DEFINED
|
|
#define FILE_ZERO_DATA_INFORMATION_DEFINED
|
|
typedef struct _RETRO_FILE_ZERO_DATA_INFORMATION
|
|
{
|
|
LARGE_INTEGER FileOffset;
|
|
LARGE_INTEGER BeyondFinalZero;
|
|
} RETRO_FILE_ZERO_DATA_INFORMATION;
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
/* FILE_NAME_NORMALIZED came in with the Vista SDK, the same generation as
|
|
* GetFinalPathNameByHandleW itself. The function is resolved by name at
|
|
* run time below precisely so that older toolchains still build this
|
|
* file; the flag has to clear the same bar, so supply the documented
|
|
* value when the SDK does not. */
|
|
#ifndef FILE_NAME_NORMALIZED
|
|
#define FILE_NAME_NORMALIZED 0x0
|
|
#endif
|
|
#endif
|
|
|
|
/* Allocation unit of the filesystem holding this file: the smallest span
|
|
* retro_vfs_file_punch_hole_impl can actually free. 0 when unknown.
|
|
*
|
|
* This exists so a caller never has to reach for the descriptor itself.
|
|
* On NTFS this is the compression unit rather than the cluster, because
|
|
* that is what the filesystem actually deallocates in.
|
|
*
|
|
* PCSX2's ATA backend derived both the cluster size and the filesystem
|
|
* name by calling
|
|
* GetFinalPathNameByHandle on a HANDLE it pulled out of a FILE*, which is
|
|
* the one thing that kept it off the VFS -- the question is about the
|
|
* filesystem, not the file, and the backend that owns the handle is the
|
|
* right place to answer it. */
|
|
int64_t retro_vfs_file_get_sparse_granularity_impl(
|
|
libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (!stream)
|
|
return 0;
|
|
|
|
/* Excludes __WINRT__ for the same reason features_cpu.c does: the probe
|
|
* below reaches kernel32 through GetModuleHandle, and that entry point is
|
|
* not in the Windows Store API partition -- dylib_proc() says as much and
|
|
* refuses to call it there. A UWP build reports "unknown" and skips
|
|
* sparse handling, which is already the documented behaviour for any
|
|
* system that cannot resolve these names. */
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
|
|
{
|
|
/* GetFinalPathNameByHandleW and GetVolumeInformationByHandleW are
|
|
* Vista and later. Importing them statically costs more than the
|
|
* feature is worth: the loader resolves imports before main, so a
|
|
* binary that names them will not start at all on 9x or XP, and
|
|
* mingw does not even declare them below _WIN32_WINNT 0x0600, which
|
|
* is what the i686 MXE build targets. Resolved by name instead, so
|
|
* older systems simply report "unknown" and skip sparse handling. */
|
|
typedef DWORD (WINAPI *final_path_t)(HANDLE, LPWSTR, DWORD, DWORD);
|
|
typedef BOOL (WINAPI *vol_info_t)(HANDLE, LPWSTR, DWORD, LPDWORD,
|
|
LPDWORD, LPDWORD, LPWSTR, DWORD);
|
|
typedef BOOL (WINAPI *vol_path_t)(LPCWSTR, LPWSTR, DWORD);
|
|
typedef BOOL (WINAPI *disk_free_t)(LPCWSTR, LPDWORD, LPDWORD,
|
|
LPDWORD, LPDWORD);
|
|
|
|
static final_path_t p_final_path = NULL;
|
|
static vol_info_t p_vol_info = NULL;
|
|
static vol_path_t p_vol_path = NULL;
|
|
static disk_free_t p_disk_free = NULL;
|
|
static int resolved = 0;
|
|
|
|
DWORD sectors_per_cluster = 0;
|
|
DWORD bytes_per_sector = 0;
|
|
DWORD tmp1 = 0;
|
|
DWORD tmp2 = 0;
|
|
DWORD len;
|
|
HANDLE handle = stream->fh;
|
|
wchar_t *final_path;
|
|
wchar_t root[MAX_PATH + 1];
|
|
wchar_t fs_name[MAX_PATH + 1];
|
|
int64_t cluster;
|
|
|
|
if (!resolved)
|
|
{
|
|
HMODULE k32 = GetModuleHandleA("kernel32.dll");
|
|
|
|
resolved = 1;
|
|
if (k32)
|
|
{
|
|
p_final_path = (final_path_t)GetProcAddress(k32,
|
|
"GetFinalPathNameByHandleW");
|
|
p_vol_info = (vol_info_t)GetProcAddress(k32,
|
|
"GetVolumeInformationByHandleW");
|
|
/* Wide GetVolumePathName is 2000 and later, and 9x does not
|
|
* export it at all -- a static import would stop the binary
|
|
* loading there, so every one of these is resolved by name. */
|
|
p_vol_path = (vol_path_t)GetProcAddress(k32,
|
|
"GetVolumePathNameW");
|
|
p_disk_free = (disk_free_t)GetProcAddress(k32,
|
|
"GetDiskFreeSpaceW");
|
|
}
|
|
}
|
|
|
|
if (!p_final_path || !p_vol_path || !p_disk_free)
|
|
return 0;
|
|
|
|
if (!handle || handle == INVALID_HANDLE_VALUE)
|
|
{
|
|
if (!stream->fp)
|
|
return 0;
|
|
handle = (HANDLE)_get_osfhandle(_fileno(stream->fp));
|
|
if (handle == INVALID_HANDLE_VALUE)
|
|
return 0;
|
|
}
|
|
|
|
/* The volume that matters is the one the file really lives on, so
|
|
* the path has to be resolved through any junctions first. */
|
|
len = p_final_path(handle, NULL, 0, FILE_NAME_NORMALIZED);
|
|
if (len == 0)
|
|
return 0;
|
|
|
|
final_path = (wchar_t*)calloc(len + 1, sizeof(wchar_t));
|
|
if (!final_path)
|
|
return 0;
|
|
|
|
if (p_final_path(handle, final_path, len, FILE_NAME_NORMALIZED) == 0)
|
|
{
|
|
free(final_path);
|
|
return 0;
|
|
}
|
|
|
|
/* GetVolumePathNameW gives the mount point, which is what
|
|
* GetDiskFreeSpaceW wants, and unlike splitting the string by hand
|
|
* it copes with a path mounted on a folder rather than a letter. */
|
|
if (!p_vol_path(final_path, root, MAX_PATH))
|
|
{
|
|
free(final_path);
|
|
return 0;
|
|
}
|
|
free(final_path);
|
|
|
|
if (!p_disk_free(root, §ors_per_cluster, &bytes_per_sector,
|
|
&tmp1, &tmp2))
|
|
return 0;
|
|
|
|
cluster = (int64_t)sectors_per_cluster * (int64_t)bytes_per_sector;
|
|
|
|
/* On NTFS the cluster size is not the answer. A sparse file is
|
|
* deallocated in compression units, which are 16 clusters, capped
|
|
* at 64K -- so a 4K-cluster volume, the common case, frees in 64K
|
|
* chunks and punching a single 4K cluster frees nothing at all.
|
|
* Other filesystems deallocate by cluster, so the cluster size
|
|
* stands. Without the volume query, assume the conservative case. */
|
|
if (!p_vol_info)
|
|
return cluster;
|
|
|
|
if (p_vol_info(handle, NULL, 0, NULL, NULL, NULL, fs_name, MAX_PATH)
|
|
&& !wcscmp(fs_name, L"NTFS"))
|
|
{
|
|
const int64_t unit = cluster * 16;
|
|
return (unit > 65536) ? 65536 : unit;
|
|
}
|
|
|
|
return cluster;
|
|
}
|
|
#elif !defined(_WIN32) && !defined(VITA) && !defined(PSP) && !defined(PS2) && !defined(ORBIS) && !defined(GEKKO) && !defined(_3DS)
|
|
{
|
|
struct stat st;
|
|
int fd = stream->fd;
|
|
|
|
if (fd < 0)
|
|
{
|
|
if (!stream->fp)
|
|
return 0;
|
|
fd = fileno(stream->fp);
|
|
if (fd < 0)
|
|
return 0;
|
|
}
|
|
|
|
if (fstat(fd, &st) != 0)
|
|
return 0;
|
|
if (st.st_blksize > 0)
|
|
return (int64_t)st.st_blksize;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int retro_vfs_file_punch_hole_impl(libretro_vfs_implementation_file *stream,
|
|
int64_t offset, int64_t len)
|
|
{
|
|
if (!stream || offset < 0 || len <= 0)
|
|
return -1;
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
{
|
|
RETRO_FILE_ZERO_DATA_INFORMATION zero_info;
|
|
DWORD returned = 0;
|
|
HANDLE handle = stream->fh;
|
|
|
|
if (!handle || handle == INVALID_HANDLE_VALUE)
|
|
{
|
|
if (!stream->fp)
|
|
return -1;
|
|
handle = (HANDLE)_get_osfhandle(_fileno(stream->fp));
|
|
if (handle == INVALID_HANDLE_VALUE)
|
|
return -1;
|
|
}
|
|
|
|
/* The file has to be marked sparse before a zero-data range does
|
|
* anything; on a non-sparse file the call succeeds and writes real
|
|
* zeroes, which is correct but saves nothing. Marking is idempotent. */
|
|
{
|
|
DWORD tmp = 0;
|
|
DeviceIoControl(handle, RA_FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &tmp, NULL);
|
|
}
|
|
|
|
zero_info.FileOffset.QuadPart = offset;
|
|
zero_info.BeyondFinalZero.QuadPart = offset + len;
|
|
|
|
if (!DeviceIoControl(handle, RA_FSCTL_SET_ZERO_DATA, &zero_info,
|
|
sizeof(zero_info), NULL, 0, &returned, NULL))
|
|
return -1;
|
|
return 0;
|
|
}
|
|
/* fallocate() sits behind _GNU_SOURCE in glibc (it is gated on __USE_GNU),
|
|
* so whether it is *declared* depends on the consumer's feature-test
|
|
* macros rather than on anything this file controls. <linux/falloc.h>
|
|
* supplies FALLOC_FL_PUNCH_HOLE either way, so testing the flag alone
|
|
* leaves a consumer that does not define _GNU_SOURCE calling an
|
|
* undeclared function -- a warning today, an error on stricter
|
|
* compilers, and wrong argument passing for the off_t pair on 32-bit.
|
|
* Require the declaration too, and fall through to the unsupported
|
|
* path when it is absent. uClibc declares it only from uClibc-ng 1.0
|
|
* on, and only with its Linux-specific API enabled. */
|
|
#elif defined(__linux__) && defined(FALLOC_FL_PUNCH_HOLE) \
|
|
&& (defined(_GNU_SOURCE) || defined(__USE_GNU)) \
|
|
&& (!defined(__UCLIBC__) || (defined(__UCLIBC_LINUX_SPECIFIC__) \
|
|
&& defined(__UCLIBC_MAJOR__) && __UCLIBC_MAJOR__ >= 1))
|
|
{
|
|
int fd = stream->fd;
|
|
|
|
if (fd < 0)
|
|
{
|
|
if (!stream->fp)
|
|
return -1;
|
|
fd = fileno(stream->fp);
|
|
if (fd < 0)
|
|
return -1;
|
|
}
|
|
|
|
if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
|
|
(off_t)offset, (off_t)len) != 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
#else
|
|
/* No hole punching on this platform. */
|
|
(void)offset;
|
|
(void)len;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
int64_t retro_vfs_file_truncate_impl(libretro_vfs_implementation_file *stream, int64_t len)
|
|
{
|
|
#ifdef _WIN32
|
|
/* _chsize takes a long and silently truncates lengths > LONG_MAX
|
|
* (2 GiB on Windows) -- present on all Windows CRTs including
|
|
* VC6. _chsize_s takes __int64 and was added in the Secure CRT
|
|
* (VS 2005, _MSC_VER 1400). Prefer the 64-bit variant when
|
|
* available, and on older MSVC / MinGW with legacy msvcrt fall
|
|
* back to _chsize only for lengths that fit in long -- return
|
|
* an error for larger lengths rather than silently truncating
|
|
* the file. */
|
|
#if defined(_MSC_VER) && _MSC_VER >= 1400
|
|
if (stream && stream->fp && _chsize_s(_fileno(stream->fp), len) == 0)
|
|
{
|
|
stream->size = len;
|
|
return 0;
|
|
}
|
|
#else
|
|
if (stream && stream->fp && len >= 0 && len <= (int64_t)LONG_MAX
|
|
&& _chsize(_fileno(stream->fp), (long)len) == 0)
|
|
{
|
|
stream->size = len;
|
|
return 0;
|
|
}
|
|
#endif
|
|
#elif !defined(VITA) && !defined(PSP) && !defined(PS2) && !defined(ORBIS)
|
|
if (stream && stream->fp && ftruncate(fileno(stream->fp), (off_t)len) == 0)
|
|
{
|
|
stream->size = len;
|
|
return 0;
|
|
}
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
int64_t retro_vfs_file_tell_impl(libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (!stream)
|
|
return -1;
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
return retro_vfs_file_tell_cdrom(stream);
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return retro_vfs_file_tell_smb(stream);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return retro_vfs_file_tell_nfs(stream);
|
|
#endif
|
|
return retro_vfs_fp_tell64(stream->fp);
|
|
}
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
/* Need to check stream->mapped because this function
|
|
* is called in filestream_open() */
|
|
if (stream->mapped && (stream->hints &
|
|
RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
return stream->mappos;
|
|
#endif
|
|
return retro_vfs_fd_seek64(stream->fd, 0, SEEK_CUR);
|
|
}
|
|
|
|
int64_t retro_vfs_file_seek_impl(libretro_vfs_implementation_file *stream,
|
|
int64_t offset, int seek_position)
|
|
{
|
|
return retro_vfs_file_seek_internal(stream, offset, seek_position);
|
|
}
|
|
|
|
int64_t retro_vfs_file_view_impl(libretro_vfs_implementation_file *stream,
|
|
uint64_t len, const uint8_t **ptr)
|
|
{
|
|
if (!stream || !ptr || len > (uint64_t)INT64_MAX)
|
|
return -1;
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return retro_vfs_file_view_smb(stream, len, ptr);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return retro_vfs_file_view_nfs(stream, len, ptr);
|
|
#endif
|
|
return -1;
|
|
}
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->mapped
|
|
&& (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
{
|
|
if (stream->mappos >= stream->mapsize)
|
|
return stream->mappos == stream->mapsize ? 0 : -1;
|
|
if (len > stream->mapsize - stream->mappos)
|
|
len = stream->mapsize - stream->mappos;
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_MMAP)
|
|
vfs_win32_readahead(stream, stream->mappos, len);
|
|
#endif
|
|
*ptr = &stream->mapped[stream->mappos];
|
|
stream->mappos += len;
|
|
return (int64_t)len;
|
|
}
|
|
#endif
|
|
return -1;
|
|
}
|
|
|
|
int64_t retro_vfs_file_read_impl(libretro_vfs_implementation_file *stream,
|
|
void *s, uint64_t len)
|
|
{
|
|
if (!stream || !s)
|
|
return -1;
|
|
|
|
/* A length with the top bit set is a negative int64 that arrived
|
|
* through the unsigned parameter - no legitimate caller reads 8EiB.
|
|
* The stdio path below hands len to fread() unclamped, where bionic
|
|
* FORTIFY turns it into a process abort (observed in the field);
|
|
* fail the read instead, like the mapped path's clamp below already
|
|
* does for its overflow case. */
|
|
if (len > (uint64_t)INT64_MAX)
|
|
return -1;
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
return retro_vfs_file_read_cdrom(stream, s, len);
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return retro_vfs_file_read_smb(stream, s, len);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return retro_vfs_file_read_nfs(stream, s, len);
|
|
#endif
|
|
/* bionic's stdio dispatches fread through the FILE's int-typed
|
|
* read callback: a single count above INT_MAX truncates to a
|
|
* negative int inside libc, __sread sign-extends it back into
|
|
* read(), and FORTIFY aborts the process ("read: count ... >
|
|
* SSIZE_MAX"). Chunk the transfer so no single libc call sees
|
|
* a count any stdio cannot take. */
|
|
{
|
|
uint8_t *p = (uint8_t*)s;
|
|
uint64_t total = 0;
|
|
while (len != 0)
|
|
{
|
|
size_t _chunk = (len > VFS_STDIO_IO_CHUNK_MAX)
|
|
? (size_t)VFS_STDIO_IO_CHUNK_MAX : (size_t)len;
|
|
size_t _got = fread(p, 1, _chunk, stream->fp);
|
|
total += _got;
|
|
if (_got < _chunk)
|
|
break;
|
|
p += _got;
|
|
len -= _got;
|
|
}
|
|
return (int64_t)total;
|
|
}
|
|
}
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)
|
|
{
|
|
if (stream->mappos >= stream->mapsize)
|
|
{
|
|
/* At or past EOF: 0 bytes is the correct return for
|
|
* fread-style semantics on a legitimate read that reached
|
|
* EOF, -1 if we were already past EOF (corrupt state). */
|
|
if (stream->mappos == stream->mapsize)
|
|
return 0;
|
|
return -1;
|
|
}
|
|
|
|
/* Clamp len against the remaining mapped bytes. Done as an
|
|
* unsigned subtraction *before* computing mappos+len to avoid
|
|
* integer overflow: mappos+len can wrap past mapsize when
|
|
* both operands are large uint64_t values, defeating the
|
|
* naive "mappos + len > mapsize" bound check. */
|
|
{
|
|
uint64_t remaining = stream->mapsize - stream->mappos;
|
|
if (len > remaining)
|
|
len = remaining;
|
|
}
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_MMAP)
|
|
vfs_win32_readahead(stream, stream->mappos, len);
|
|
#endif
|
|
memcpy(s, &stream->mapped[stream->mappos], (size_t)len);
|
|
stream->mappos += len;
|
|
|
|
return (int64_t)len;
|
|
}
|
|
#endif
|
|
|
|
/* One read() moves at most MAX_RW_COUNT (INT_MAX & PAGE_MASK) on
|
|
* Linux and returns short past it, so a large request needs the
|
|
* same chunk loop as the stdio path to honor the full count. */
|
|
{
|
|
uint8_t *p = (uint8_t*)s;
|
|
uint64_t total = 0;
|
|
while (len != 0)
|
|
{
|
|
size_t _chunk = (len > VFS_STDIO_IO_CHUNK_MAX)
|
|
? (size_t)VFS_STDIO_IO_CHUNK_MAX : (size_t)len;
|
|
#if defined(VITA)
|
|
ssize_t _got = (ssize_t)sceIoRead((SceUID)stream->fd, p, (SceSize)_chunk);
|
|
#else
|
|
ssize_t _got = read(stream->fd, p, _chunk);
|
|
#endif
|
|
if (_got < 0)
|
|
return (total != 0) ? (int64_t)total : -1;
|
|
total += (uint64_t)_got;
|
|
if ((size_t)_got < _chunk)
|
|
break;
|
|
p += _got;
|
|
len -= (uint64_t)_got;
|
|
}
|
|
return (int64_t)total;
|
|
}
|
|
}
|
|
|
|
int64_t retro_vfs_file_write_impl(libretro_vfs_implementation_file *stream, const void *s, uint64_t len)
|
|
{
|
|
int64_t pos = 0;
|
|
ssize_t ret = -1;
|
|
|
|
if (!stream)
|
|
return -1;
|
|
|
|
if ((stream->hints & RFILE_HINT_UNBUFFERED) == 0)
|
|
{
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
{
|
|
pos = retro_vfs_file_tell_nfs(stream);
|
|
ret = retro_vfs_file_write_nfs(stream, s, len);
|
|
if (ret != -1 && pos + ret > stream->size)
|
|
stream->size = pos + ret;
|
|
return ret;
|
|
}
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
{
|
|
pos = retro_vfs_file_tell_smb(stream);
|
|
ret = retro_vfs_file_write_smb(stream, s, len);
|
|
if (ret != -1 && pos + ret > stream->size)
|
|
stream->size = pos + ret;
|
|
return ret;
|
|
}
|
|
#endif
|
|
pos = retro_vfs_file_tell_impl(stream);
|
|
/* Same int-typed stdio callback contract as the read side:
|
|
* chunk so no single fwrite count exceeds what bionic takes. */
|
|
{
|
|
const uint8_t *p = (const uint8_t*)s;
|
|
uint64_t total = 0;
|
|
while (len != 0)
|
|
{
|
|
size_t _chunk = (len > VFS_STDIO_IO_CHUNK_MAX)
|
|
? (size_t)VFS_STDIO_IO_CHUNK_MAX : (size_t)len;
|
|
size_t _put = fwrite(p, 1, _chunk, stream->fp);
|
|
total += _put;
|
|
if (_put < _chunk)
|
|
break;
|
|
p += _put;
|
|
len -= _put;
|
|
}
|
|
ret = (ssize_t)total;
|
|
}
|
|
|
|
if (ret > 0 && pos + ret > stream->size)
|
|
stream->size = pos + ret;
|
|
|
|
return ret;
|
|
}
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)
|
|
return -1;
|
|
#endif
|
|
|
|
pos = retro_vfs_file_tell_impl(stream);
|
|
/* write() shares read()'s MAX_RW_COUNT clamp; loop for the full
|
|
* count. */
|
|
{
|
|
const uint8_t *p = (const uint8_t*)s;
|
|
uint64_t total = 0;
|
|
while (len != 0)
|
|
{
|
|
size_t _chunk = (len > VFS_STDIO_IO_CHUNK_MAX)
|
|
? (size_t)VFS_STDIO_IO_CHUNK_MAX : (size_t)len;
|
|
#if defined(VITA)
|
|
ssize_t _put = (ssize_t)sceIoWrite((SceUID)stream->fd, p, (SceSize)_chunk);
|
|
#else
|
|
ssize_t _put = write(stream->fd, p, _chunk);
|
|
#endif
|
|
if (_put < 0)
|
|
{
|
|
if (total == 0)
|
|
return -1;
|
|
break;
|
|
}
|
|
total += (uint64_t)_put;
|
|
if ((size_t)_put < _chunk)
|
|
break;
|
|
p += _put;
|
|
len -= (uint64_t)_put;
|
|
}
|
|
ret = (ssize_t)total;
|
|
}
|
|
|
|
if (ret != -1 && pos + ret > stream->size)
|
|
stream->size = pos + ret;
|
|
|
|
return ret;
|
|
}
|
|
|
|
int retro_vfs_file_flush_impl(libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (!stream)
|
|
return -1;
|
|
#ifdef HAVE_CDROM
|
|
if (stream->scheme == VFS_SCHEME_CDROM)
|
|
return 0;
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (stream->scheme == VFS_SCHEME_SMB)
|
|
return 0;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (stream->scheme == VFS_SCHEME_NFS)
|
|
return 0;
|
|
#endif
|
|
if (stream->fp && fflush(stream->fp) == 0)
|
|
return 0;
|
|
return -1;
|
|
}
|
|
|
|
int retro_vfs_file_remove_impl(const char *path)
|
|
{
|
|
if (path && *path)
|
|
{
|
|
int ret = -1;
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(path))
|
|
{
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, path))
|
|
return -1;
|
|
ret = retro_vfs_file_remove_saf(saf_split_result.tree, saf_split_result.path);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
/* Win32 (no Xbox) */
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
char *path_local = NULL;
|
|
if ((path_local = utf8_to_local_string_alloc(path)))
|
|
{
|
|
/* We need to check if path is a directory */
|
|
if ((retro_vfs_stat_impl(path, NULL) & RETRO_VFS_STAT_IS_DIRECTORY) != 0)
|
|
ret = _rmdir(path_local);
|
|
else
|
|
ret = remove(path_local);
|
|
free(path_local);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wchar_t *path_wide = NULL;
|
|
if ((path_wide = utf8_to_utf16_string_alloc(path)))
|
|
{
|
|
/* We need to check if path is a directory */
|
|
if ((retro_vfs_stat_impl(path, NULL) & RETRO_VFS_STAT_IS_DIRECTORY) != 0)
|
|
ret = _wrmdir(path_wide);
|
|
else
|
|
ret = _wremove(path_wide);
|
|
free(path_wide);
|
|
}
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
char *path_local = NULL;
|
|
if ((path_local = utf8_to_local_string_alloc(path)))
|
|
{
|
|
/* We need to check if path is a directory */
|
|
if ((retro_vfs_stat_impl(path, NULL) & RETRO_VFS_STAT_IS_DIRECTORY) != 0)
|
|
ret = _rmdir(path_local);
|
|
else
|
|
ret = remove(path_local);
|
|
free(path_local);
|
|
}
|
|
#else
|
|
wchar_t *path_wide = NULL;
|
|
if ((path_wide = utf8_to_utf16_string_alloc(path)))
|
|
{
|
|
/* We need to check if path is a directory */
|
|
if ((retro_vfs_stat_impl(path, NULL) & RETRO_VFS_STAT_IS_DIRECTORY) != 0)
|
|
ret = _wrmdir(path_wide);
|
|
else
|
|
ret = _wremove(path_wide);
|
|
free(path_wide);
|
|
}
|
|
#endif
|
|
#else
|
|
ret = remove(path);
|
|
#endif
|
|
if (ret == 0)
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX) && (defined(LEGACY_WIN32_RUNTIME) || defined(LEGACY_WIN32))
|
|
/* Replace @new_local with @old_local where MoveFileEx() is not
|
|
* available (Windows 9x): a destination in the way is moved aside,
|
|
* the source renamed into place, and the moved-aside copy removed
|
|
* only then - or put back if the second rename fails. Both paths
|
|
* are in the local code page. */
|
|
static int win32_rename_replace_local(const char *old_local,
|
|
const char *new_local)
|
|
{
|
|
size_t _len;
|
|
char *aside;
|
|
int ret;
|
|
|
|
if (rename(old_local, new_local) == 0)
|
|
return 0;
|
|
|
|
_len = strlen(new_local);
|
|
if (!(aside = (char*)malloc(_len + sizeof(".old"))))
|
|
return -1;
|
|
memcpy(aside, new_local, _len);
|
|
memcpy(aside + _len, ".old", sizeof(".old"));
|
|
|
|
ret = -1;
|
|
remove(aside); /* a leftover from an earlier run */
|
|
if (rename(new_local, aside) == 0)
|
|
{
|
|
if (rename(old_local, new_local) == 0)
|
|
{
|
|
remove(aside);
|
|
ret = 0;
|
|
}
|
|
else
|
|
rename(aside, new_local);
|
|
}
|
|
|
|
free(aside);
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
int retro_vfs_file_rename_impl(const char *old_path, const char *new_path)
|
|
{
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(old_path) && path_is_saf(new_path))
|
|
{
|
|
int ret;
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result_old, saf_split_result_new;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result_old, old_path))
|
|
return -1;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result_new, new_path))
|
|
{
|
|
free(saf_split_result_old.path);
|
|
free(saf_split_result_old.tree);
|
|
return -1;
|
|
}
|
|
ret = retro_vfs_file_rename_saf(saf_split_result_old.tree, saf_split_result_old.path, saf_split_result_new.tree, saf_split_result_new.path);
|
|
free(saf_split_result_new.path);
|
|
free(saf_split_result_new.tree);
|
|
free(saf_split_result_old.path);
|
|
free(saf_split_result_old.tree);
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
/* Win32 (no Xbox) */
|
|
int ret = -1;
|
|
|
|
if (!old_path || !*old_path || !new_path || !*new_path)
|
|
return -1;
|
|
|
|
/* Neither rename() nor _wrename() can replace an existing
|
|
* destination on Windows, which breaks writing to a temporary and
|
|
* renaming it over the original. MoveFileExW with
|
|
* MOVEFILE_REPLACE_EXISTING gives the POSIX overwrite behaviour.
|
|
* It is unsupported on 9x, so the local-encoding path moves the
|
|
* destination aside and renames into its place. */
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
char *old_path_local = utf8_to_local_string_alloc(old_path);
|
|
|
|
if (old_path_local)
|
|
{
|
|
char *new_path_local = utf8_to_local_string_alloc(new_path);
|
|
|
|
if (new_path_local)
|
|
{
|
|
ret = win32_rename_replace_local(old_path_local, new_path_local);
|
|
free(new_path_local);
|
|
}
|
|
|
|
free(old_path_local);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
wchar_t *old_path_wide = utf8_to_utf16_string_alloc(old_path);
|
|
|
|
if (old_path_wide)
|
|
{
|
|
wchar_t *new_path_wide = utf8_to_utf16_string_alloc(new_path);
|
|
|
|
if (new_path_wide)
|
|
{
|
|
if (MoveFileExW(old_path_wide, new_path_wide,
|
|
MOVEFILE_REPLACE_EXISTING))
|
|
ret = 0;
|
|
free(new_path_wide);
|
|
}
|
|
|
|
free(old_path_wide);
|
|
}
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
{
|
|
char *old_path_local = utf8_to_local_string_alloc(old_path);
|
|
|
|
if (old_path_local)
|
|
{
|
|
char *new_path_local = utf8_to_local_string_alloc(new_path);
|
|
|
|
if (new_path_local)
|
|
{
|
|
ret = win32_rename_replace_local(old_path_local, new_path_local);
|
|
free(new_path_local);
|
|
}
|
|
|
|
free(old_path_local);
|
|
}
|
|
}
|
|
#else
|
|
{
|
|
wchar_t *old_path_wide = utf8_to_utf16_string_alloc(old_path);
|
|
|
|
if (old_path_wide)
|
|
{
|
|
wchar_t *new_path_wide = utf8_to_utf16_string_alloc(new_path);
|
|
|
|
if (new_path_wide)
|
|
{
|
|
if (MoveFileExW(old_path_wide, new_path_wide,
|
|
MOVEFILE_REPLACE_EXISTING))
|
|
ret = 0;
|
|
free(new_path_wide);
|
|
}
|
|
|
|
free(old_path_wide);
|
|
}
|
|
}
|
|
#endif
|
|
return ret;
|
|
|
|
#elif defined(_XBOX)
|
|
/* MoveFileEx() is not in the XDK and the CRT rename() refuses an
|
|
* existing destination, so a destination in the way is moved
|
|
* aside with MoveFile(), the source moved into place, and the
|
|
* moved-aside copy deleted only then - or put back if the second
|
|
* move fails. */
|
|
{
|
|
size_t _len;
|
|
char *aside;
|
|
int ret;
|
|
|
|
if (!old_path || !*old_path || !new_path || !*new_path)
|
|
return -1;
|
|
|
|
if (GetFileAttributes(new_path) == INVALID_FILE_ATTRIBUTES)
|
|
return MoveFile(old_path, new_path) ? 0 : -1;
|
|
|
|
_len = strlen(new_path);
|
|
if (!(aside = (char*)malloc(_len + sizeof(".old"))))
|
|
return -1;
|
|
memcpy(aside, new_path, _len);
|
|
memcpy(aside + _len, ".old", sizeof(".old"));
|
|
|
|
ret = -1;
|
|
DeleteFile(aside); /* a leftover from an earlier run */
|
|
if (MoveFile(new_path, aside))
|
|
{
|
|
if (MoveFile(old_path, new_path))
|
|
{
|
|
DeleteFile(aside);
|
|
ret = 0;
|
|
}
|
|
else
|
|
MoveFile(aside, new_path);
|
|
}
|
|
|
|
free(aside);
|
|
return ret;
|
|
}
|
|
#elif defined(VITA) || defined(PSP)
|
|
/* rename() here means "replace": the Win32 branch above says so
|
|
* with MOVEFILE_REPLACE_EXISTING and POSIX says so by definition,
|
|
* and the write-to-temporary-then-rename pattern - playlists, the
|
|
* config file, core info - is built on it. The kernel's own
|
|
* sceIoRename() refuses an existing destination on both PSP and
|
|
* Vita. pspsdk's rename() passes that refusal through; Vita's
|
|
* newlib bridges the gap by sceIoRemove()ing the destination
|
|
* first and renaming after. Between those two calls nothing is
|
|
* on disk, and if the rename then fails the caller's temporary
|
|
* is discarded on top: the file that was being replaced is
|
|
* simply gone.
|
|
*
|
|
* So call the kernel directly and, when the destination is in the
|
|
* way, move it aside rather than delete it. The old file is only
|
|
* removed once its replacement is in place, and is put back if
|
|
* the replacement cannot be. */
|
|
{
|
|
SceIoStat st;
|
|
size_t _len;
|
|
char *aside;
|
|
int ret;
|
|
|
|
if (!old_path || !*old_path || !new_path || !*new_path)
|
|
return -1;
|
|
|
|
if (sceIoRename(old_path, new_path) >= 0)
|
|
return 0;
|
|
|
|
/* Only worth trying when there is a destination to move aside. */
|
|
if (sceIoGetstat(new_path, &st) < 0)
|
|
return -1;
|
|
|
|
_len = strlen(new_path);
|
|
if (!(aside = (char*)malloc(_len + sizeof(".old"))))
|
|
return -1;
|
|
memcpy(aside, new_path, _len);
|
|
memcpy(aside + _len, ".old", sizeof(".old"));
|
|
|
|
ret = -1;
|
|
/* A leftover aside from an earlier run is removed only when it
|
|
* is in the way: a lookup of a missing name scans the directory. */
|
|
if ( sceIoRename(new_path, aside) >= 0
|
|
|| ( sceIoRemove(aside) >= 0
|
|
&& sceIoRename(new_path, aside) >= 0))
|
|
{
|
|
if (sceIoRename(old_path, new_path) >= 0)
|
|
{
|
|
sceIoRemove(aside);
|
|
ret = 0;
|
|
}
|
|
else
|
|
sceIoRename(aside, new_path);
|
|
}
|
|
|
|
free(aside);
|
|
return ret;
|
|
}
|
|
#elif defined(_3DS) || defined(PS2) || defined(WIIU) || defined(GEKKO) || defined(__PSL1GHT__) || defined(__PS3__) || defined(SWITCH)
|
|
/* The filesystems here refuse to rename onto an existing entry -
|
|
* FS on 3DS and Switch, FatFs behind bdmfs_fatfs on PS2, libfat
|
|
* on Wii and GameCube, LV2's sys_fs_rename on PS3 - and libnx,
|
|
* ps2sdk, libfat and PSL1GHT pass that refusal through, while libctru
|
|
* clears the way by deleting the destination first, leaving
|
|
* nothing on disk until the rename lands. Wii U's FSRename()
|
|
* is taken to behave the same way. So when a destination is in
|
|
* the way, move it aside before calling rename() at all: it is
|
|
* removed only once its replacement is in place, and put back
|
|
* if the replacement cannot be. */
|
|
{
|
|
struct stat st;
|
|
size_t _len;
|
|
char *aside;
|
|
int ret;
|
|
|
|
if (!old_path || !*old_path || !new_path || !*new_path)
|
|
return -1;
|
|
|
|
if (stat(new_path, &st) != 0)
|
|
return rename(old_path, new_path) == 0 ? 0 : -1;
|
|
|
|
_len = strlen(new_path);
|
|
if (!(aside = (char*)malloc(_len + sizeof(".old"))))
|
|
return -1;
|
|
memcpy(aside, new_path, _len);
|
|
memcpy(aside + _len, ".old", sizeof(".old"));
|
|
|
|
ret = -1;
|
|
/* A leftover aside from an earlier run is removed only when it
|
|
* is in the way: a lookup of a missing name scans the directory. */
|
|
if ( rename(new_path, aside) == 0
|
|
|| ( remove(aside) == 0
|
|
&& rename(new_path, aside) == 0))
|
|
{
|
|
if (rename(old_path, new_path) == 0)
|
|
{
|
|
remove(aside);
|
|
ret = 0;
|
|
}
|
|
else
|
|
rename(aside, new_path);
|
|
}
|
|
|
|
free(aside);
|
|
return ret;
|
|
}
|
|
#else
|
|
/* Every other platform */
|
|
if (!old_path || !*old_path || !new_path || !*new_path)
|
|
return -1;
|
|
return rename(old_path, new_path) == 0 ? 0 : -1;
|
|
#endif
|
|
}
|
|
|
|
const char *retro_vfs_file_get_path_impl(
|
|
libretro_vfs_implementation_file *stream)
|
|
{
|
|
if (!stream)
|
|
return NULL;
|
|
return stream->orig_path;
|
|
}
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_MMAP)
|
|
/* PrefetchVirtualMemory is Windows 8 and later; resolved by name so the
|
|
* binary still loads where it is absent. */
|
|
typedef struct
|
|
{
|
|
PVOID VirtualAddress;
|
|
SIZE_T NumberOfBytes;
|
|
} vfs_win32_memory_range_t;
|
|
typedef BOOL (WINAPI *vfs_prefetch_t)(HANDLE, ULONG_PTR,
|
|
vfs_win32_memory_range_t*, ULONG);
|
|
|
|
#ifdef VFS_WIN32_PREFETCH_HOOK
|
|
/* A test build points the call at its own stand-in (see
|
|
* samples/file/vfs/vfs_prefetch_offthread_test.c); nothing else
|
|
* defines this. */
|
|
static BOOL WINAPI vfs_win32_prefetch_hooked(HANDLE process,
|
|
ULONG_PTR count, vfs_win32_memory_range_t *ranges, ULONG flags)
|
|
{
|
|
(void)process;
|
|
(void)count;
|
|
(void)flags;
|
|
return VFS_WIN32_PREFETCH_HOOK(ranges->VirtualAddress,
|
|
(size_t)ranges->NumberOfBytes);
|
|
}
|
|
#endif
|
|
|
|
static vfs_prefetch_t vfs_win32_p_prefetch;
|
|
/* 0 unresolved, 1 PrefetchVirtualMemory, 2 page toucher, 3 nothing.
|
|
* Either of the first two runs on the stream's read-ahead thread, never
|
|
* on the caller: PrefetchVirtualMemory is not documented to return
|
|
* before its reads complete, and the caller is usually a frame. */
|
|
static int vfs_win32_prefetch_mode;
|
|
|
|
/* RETRO_VFS_PREFETCH=touch|none overrides the choice, for tests */
|
|
static int vfs_win32_prefetch_resolve(void)
|
|
{
|
|
if (!vfs_win32_prefetch_mode)
|
|
{
|
|
const char *force = getenv("RETRO_VFS_PREFETCH");
|
|
int mode = 2;
|
|
|
|
if (force && !*force)
|
|
force = NULL;
|
|
if (!force)
|
|
{
|
|
#ifdef VFS_WIN32_PREFETCH_HOOK
|
|
vfs_win32_p_prefetch = vfs_win32_prefetch_hooked;
|
|
#else
|
|
HMODULE k32 = GetModuleHandleA("kernel32.dll");
|
|
if (k32)
|
|
vfs_win32_p_prefetch = (vfs_prefetch_t)GetProcAddress(k32,
|
|
"PrefetchVirtualMemory");
|
|
#endif
|
|
}
|
|
if (vfs_win32_p_prefetch)
|
|
mode = 1;
|
|
if (force && !strcmp(force, "none"))
|
|
mode = 3;
|
|
#ifndef HAVE_THREADS
|
|
/* no thread to run either on */
|
|
mode = 3;
|
|
#endif
|
|
vfs_win32_prefetch_mode = mode;
|
|
}
|
|
return vfs_win32_prefetch_mode;
|
|
}
|
|
|
|
/* Distance read ahead of a sequential reader, how far past the last
|
|
* read's end the next read may start and still count as sequential,
|
|
* and the unit ranges are published and touched in. */
|
|
#define VFS_RA_AHEAD (4u * 1024u * 1024u)
|
|
#define VFS_RA_GAP (256u * 1024u)
|
|
#define VFS_WARM_UNIT (64u * 1024u)
|
|
|
|
#ifdef HAVE_THREADS
|
|
/* The stream's read-ahead thread. The caller publishes a range as unit
|
|
* indices and a generation and returns; the thread brings in what of it
|
|
* is not already done, with PrefetchVirtualMemory or, before Windows 8,
|
|
* by touching a byte per page so the fault brings each page into the
|
|
* process as a mapped read would. A torn pair only warms a stale range
|
|
* once, and is clamped. */
|
|
typedef struct vfs_win32_warm
|
|
{
|
|
uint64_t size;
|
|
const volatile uint8_t *base;
|
|
sthread_t *thread;
|
|
size_t page;
|
|
retro_eventcount_t ec;
|
|
retro_atomic_int_t lo;
|
|
retro_atomic_int_t hi;
|
|
retro_atomic_int_t gen;
|
|
retro_atomic_int_t stop;
|
|
int mode; /* 1 or 2, as resolved */
|
|
int req_lo; /* reader side only */
|
|
int req_hi;
|
|
} vfs_win32_warm_t;
|
|
|
|
static void vfs_win32_warm_thread(void *data)
|
|
{
|
|
vfs_win32_warm_t *w = (vfs_win32_warm_t*)data;
|
|
int seen = 0;
|
|
int cur = 0;
|
|
int hi = 0;
|
|
unsigned sink = 0;
|
|
|
|
for (;;)
|
|
{
|
|
int g;
|
|
|
|
if (retro_atomic_load_acquire_int(&w->stop))
|
|
break;
|
|
g = retro_atomic_load_acquire_int(&w->gen);
|
|
if (g != seen)
|
|
{
|
|
int lo = retro_atomic_load_acquire_int(&w->lo);
|
|
seen = g;
|
|
hi = retro_atomic_load_acquire_int(&w->hi);
|
|
/* a range that goes on from the last continues where that one
|
|
* got to, so no byte is asked for twice */
|
|
if (cur < lo || cur >= hi)
|
|
cur = lo;
|
|
}
|
|
if (cur < hi)
|
|
{
|
|
uint64_t o = (uint64_t)cur * VFS_WARM_UNIT;
|
|
uint64_t end = (uint64_t)hi * VFS_WARM_UNIT;
|
|
if (end > w->size)
|
|
end = w->size;
|
|
if (o >= end)
|
|
{
|
|
cur = hi;
|
|
continue;
|
|
}
|
|
if (w->mode == 1)
|
|
{
|
|
vfs_win32_memory_range_t range;
|
|
range.VirtualAddress = (PVOID)(w->base + o);
|
|
range.NumberOfBytes = (SIZE_T)(end - o);
|
|
vfs_win32_p_prefetch(GetCurrentProcess(), 1, &range, 0);
|
|
cur = hi;
|
|
}
|
|
else
|
|
{
|
|
/* a unit at a time, so a new range is seen between units */
|
|
if (end > o + VFS_WARM_UNIT)
|
|
end = o + VFS_WARM_UNIT;
|
|
for (; o < end; o += w->page)
|
|
sink += w->base[o];
|
|
cur++;
|
|
}
|
|
continue;
|
|
}
|
|
{
|
|
int key = retro_eventcount_prepare_wait(&w->ec);
|
|
if ( retro_atomic_load_acquire_int(&w->stop)
|
|
|| retro_atomic_load_acquire_int(&w->gen) != seen)
|
|
retro_eventcount_cancel_wait(&w->ec);
|
|
else
|
|
retro_eventcount_commit_wait(&w->ec, key);
|
|
}
|
|
}
|
|
(void)sink;
|
|
}
|
|
|
|
static void vfs_win32_warm_free(void *data)
|
|
{
|
|
vfs_win32_warm_t *w = (vfs_win32_warm_t*)data;
|
|
if (w->thread)
|
|
{
|
|
retro_atomic_store_release_int(&w->stop, 1);
|
|
retro_eventcount_notify(&w->ec);
|
|
sthread_join(w->thread);
|
|
retro_eventcount_free(&w->ec);
|
|
}
|
|
free(w);
|
|
}
|
|
|
|
static void vfs_win32_warm(libretro_vfs_implementation_file *stream,
|
|
size_t lo_byte, size_t hi_byte, size_t page, int mode)
|
|
{
|
|
vfs_win32_warm_t *w = (vfs_win32_warm_t*)stream->warm;
|
|
int lo, hi;
|
|
|
|
if (!w)
|
|
{
|
|
if (!(w = (vfs_win32_warm_t*)calloc(1, sizeof(*w))))
|
|
return;
|
|
stream->warm = w;
|
|
w->base = stream->mapped;
|
|
w->size = stream->mapsize;
|
|
w->page = page;
|
|
w->mode = mode;
|
|
retro_atomic_int_init(&w->lo, 0);
|
|
retro_atomic_int_init(&w->hi, 0);
|
|
retro_atomic_int_init(&w->gen, 0);
|
|
retro_atomic_int_init(&w->stop, 0);
|
|
if (retro_eventcount_init(&w->ec))
|
|
{
|
|
if (!(w->thread = sthread_create(vfs_win32_warm_thread, w)))
|
|
retro_eventcount_free(&w->ec);
|
|
}
|
|
}
|
|
/* a failed start stays failed: the hint is dropped from then on */
|
|
if (!w->thread)
|
|
return;
|
|
lo = (int)(lo_byte / VFS_WARM_UNIT);
|
|
hi = (int)((hi_byte + VFS_WARM_UNIT - 1) / VFS_WARM_UNIT);
|
|
if (lo >= w->req_lo && hi <= w->req_hi)
|
|
return;
|
|
w->req_lo = lo;
|
|
w->req_hi = hi;
|
|
retro_atomic_store_release_int(&w->lo, lo);
|
|
retro_atomic_store_release_int(&w->hi, hi);
|
|
retro_atomic_fetch_add_int(&w->gen, 1);
|
|
retro_eventcount_notify(&w->ec);
|
|
}
|
|
#endif
|
|
|
|
/* A mapped read that follows on from the last one hints the
|
|
* VFS_RA_AHEAD bytes past its end each time half of the hinted span is
|
|
* used up. The read's own bytes are not in the hint: the read is about
|
|
* to fault them in itself. The view starts no read-ahead on Windows. */
|
|
static void vfs_win32_readahead(libretro_vfs_implementation_file *stream,
|
|
uint64_t pos, uint64_t len)
|
|
{
|
|
uint64_t end = pos + len;
|
|
int seq = pos >= stream->ra_last
|
|
&& pos - stream->ra_last <= VFS_RA_GAP;
|
|
|
|
stream->ra_last = end;
|
|
/* a jump starts over, so the next read in order hints at once */
|
|
if (!seq)
|
|
stream->ra_next = 0;
|
|
/* nothing left ahead: a read to the end, a whole-file load */
|
|
if (!seq || end <= stream->ra_next || end >= stream->mapsize)
|
|
return;
|
|
stream->ra_next = end + VFS_RA_AHEAD / 2;
|
|
retro_vfs_file_prefetch_impl(stream, end, VFS_RA_AHEAD);
|
|
}
|
|
#endif
|
|
|
|
void retro_vfs_file_prefetch_impl(
|
|
libretro_vfs_implementation_file *stream, uint64_t offset,
|
|
uint64_t len)
|
|
{
|
|
if (!stream || !len)
|
|
return;
|
|
if (stream->scheme != VFS_SCHEME_NONE)
|
|
return;
|
|
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
if (stream->mapped
|
|
&& (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
{
|
|
size_t page;
|
|
size_t lo, hi;
|
|
|
|
if (offset >= stream->mapsize)
|
|
return;
|
|
if (len > stream->mapsize - offset)
|
|
len = stream->mapsize - offset;
|
|
#if defined(HAVE_MMAP)
|
|
{
|
|
long sz = sysconf(_SC_PAGESIZE);
|
|
page = sz > 0 ? (size_t)sz : 0;
|
|
}
|
|
#else
|
|
{
|
|
SYSTEM_INFO si;
|
|
GetSystemInfo(&si);
|
|
page = si.dwPageSize;
|
|
}
|
|
#endif
|
|
if (!page)
|
|
return;
|
|
lo = (size_t)offset & ~(page - 1);
|
|
hi = (size_t)(offset + len);
|
|
#if defined(HAVE_MMAP)
|
|
/* Older libc feature profiles may expose only the POSIX advice API.
|
|
* Prefetch is advisory: leave it a no-op if neither API is exposed. */
|
|
#if defined(MADV_WILLNEED)
|
|
madvise(stream->mapped + lo, hi - lo, MADV_WILLNEED);
|
|
#elif defined(POSIX_MADV_WILLNEED)
|
|
posix_madvise(stream->mapped + lo, hi - lo, POSIX_MADV_WILLNEED);
|
|
#endif
|
|
#else
|
|
{
|
|
const int mode = vfs_win32_prefetch_resolve();
|
|
#ifdef HAVE_THREADS
|
|
if (mode == 1 || mode == 2)
|
|
vfs_win32_warm(stream, lo, hi, page, mode);
|
|
#else
|
|
(void)mode;
|
|
(void)lo;
|
|
(void)hi;
|
|
#endif
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
#if defined(POSIX_FADV_WILLNEED) && !defined(_WIN32)
|
|
if (stream->fd >= 0)
|
|
posix_fadvise(stream->fd, (off_t)offset, (off_t)len,
|
|
POSIX_FADV_WILLNEED);
|
|
#endif
|
|
}
|
|
|
|
const uint8_t *retro_vfs_file_get_mapped_ptr_impl(
|
|
libretro_vfs_implementation_file *stream, int64_t *len)
|
|
{
|
|
if (len)
|
|
*len = 0;
|
|
#ifdef VFS_HAVE_FILE_MAPPING
|
|
/* Gate on the hint as well as the pointer, matching the read and
|
|
* seek paths: those consult 'mapped' only under the hint, so the
|
|
* map is authoritative for the file contents only when the hint
|
|
* put it there. */
|
|
if ( stream
|
|
&& stream->mapped
|
|
&& (stream->hints & RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS))
|
|
{
|
|
if (len)
|
|
*len = (int64_t)stream->mapsize;
|
|
return stream->mapped;
|
|
}
|
|
#else
|
|
(void)stream;
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
#if defined(_WIN32) && !defined(VITA) && !defined(__PSL1GHT__) && !defined(__PS3__)
|
|
#if defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
/* Use _stat64 explicitly to match the struct _stat64 buffer the caller
|
|
* declares. The bare _stat is a macro that expands to _stat64i32 on
|
|
* VS2005+ (or _stat32 with _USE_32BIT_TIME_T), neither of which match
|
|
* struct _stat64 -- passing the wrong struct silently truncates
|
|
* st_size. _stat64 has been in MSVC since VS2003 (_MSC_VER >= 1300)
|
|
* and is provided by mingw-w64. VC6 has no 64-bit time_t at all;
|
|
* _stati64 is the only match. */
|
|
/* GetFileAttributesEx rather than GetFileAttributes: same availability
|
|
* (Win98/NT4+), and it also hands back the last-write FILETIME, which
|
|
* unlike the CRT's st_mtime can represent dates before 1970. */
|
|
static int vfs_stat_win32_ansi(const char *path,
|
|
struct _stat64 *stat_buf, DWORD *file_info, FILETIME *mtime_ft)
|
|
{
|
|
WIN32_FILE_ATTRIBUTE_DATA ad;
|
|
char *path_local = utf8_to_local_string_alloc(path);
|
|
|
|
if (!path_local)
|
|
return 0;
|
|
|
|
if (!GetFileAttributesExA(path_local, GetFileExInfoStandard, &ad))
|
|
{
|
|
free(path_local);
|
|
return 0;
|
|
}
|
|
*file_info = ad.dwFileAttributes;
|
|
*mtime_ft = ad.ftLastWriteTime;
|
|
|
|
#if defined(_MSC_VER) && _MSC_VER < 1300
|
|
if (_stati64(path_local, (struct _stati64*)stat_buf) != 0)
|
|
#else
|
|
if (_stat64(path_local, stat_buf) != 0)
|
|
#endif
|
|
{
|
|
free(path_local);
|
|
return 0;
|
|
}
|
|
|
|
free(path_local);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
#if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
static int vfs_stat_win32_wide(const char *path,
|
|
struct _stat64 *stat_buf, DWORD *file_info, FILETIME *mtime_ft)
|
|
{
|
|
WIN32_FILE_ATTRIBUTE_DATA ad;
|
|
wchar_t *path_wide = utf8_to_utf16_string_alloc(path);
|
|
|
|
if (!path_wide)
|
|
return 0;
|
|
|
|
if (!GetFileAttributesExW(path_wide, GetFileExInfoStandard, &ad))
|
|
{
|
|
free(path_wide);
|
|
return 0;
|
|
}
|
|
*file_info = ad.dwFileAttributes;
|
|
*mtime_ft = ad.ftLastWriteTime;
|
|
|
|
if (_wstat64(path_wide, stat_buf) != 0)
|
|
{
|
|
free(path_wide);
|
|
return 0;
|
|
}
|
|
|
|
free(path_wide);
|
|
return 1;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
/* One platform ladder serves stat, stat_64, get_mtime and the POSIX
|
|
* fallback of dirent_stat. @mtime is filled (seconds since the Unix
|
|
* epoch) where the platform reports one; the SMB and SAF backends do
|
|
* not carry it through their stat helpers yet, so it is left untouched
|
|
* there and callers treat -1 from get_mtime as "unavailable". */
|
|
static int retro_vfs_stat_full(const char *path, int64_t *size, int64_t *mtime)
|
|
{
|
|
int ret = RETRO_VFS_STAT_IS_VALID;
|
|
|
|
if (!path || !*path)
|
|
return 0;
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (path_is_smb(path))
|
|
return retro_vfs_stat_smb(path, size);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (path_is_nfs(path))
|
|
return retro_vfs_stat_nfs(path, size);
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(path))
|
|
{
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, path))
|
|
return 0;
|
|
ret = retro_vfs_stat_saf(saf_split_result.tree, saf_split_result.path, size);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
return ret;
|
|
}
|
|
#endif
|
|
|
|
{
|
|
#if defined(VITA)
|
|
/* Vita / PSP */
|
|
SceIoStat stat_buf;
|
|
int dir_ret;
|
|
char path_buf[PATH_MAX_LENGTH];
|
|
size_t _len = strlcpy(path_buf, path, sizeof(path_buf));
|
|
if (_len > 0 && path_buf[_len-1] == '/')
|
|
path_buf[_len-1] = '\0';
|
|
|
|
dir_ret = sceIoGetstat(path_buf, &stat_buf);
|
|
if (dir_ret < 0)
|
|
return 0;
|
|
|
|
if (size)
|
|
*size = (int64_t)stat_buf.st_size;
|
|
if (mtime)
|
|
{
|
|
time_t t = 0;
|
|
sceRtcGetTime_t(&stat_buf.st_mtime, &t);
|
|
*mtime = (int64_t)t;
|
|
}
|
|
|
|
if (FIO_S_ISDIR(stat_buf.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (!(stat_buf.st_mode & SCE_S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
/* Lowlevel Lv2 */
|
|
sysFSStat stat_buf;
|
|
|
|
if (sysFsStat(path, &stat_buf) < 0)
|
|
return 0;
|
|
|
|
if (size)
|
|
*size = (int64_t)stat_buf.st_size;
|
|
if (mtime)
|
|
*mtime = (int64_t)stat_buf.st_mtime;
|
|
|
|
if ((stat_buf.st_mode & S_IFMT) == S_IFDIR)
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (!(stat_buf.st_mode & S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
#elif defined(_WIN32)
|
|
/* Windows
|
|
* Older MSVC _stat may fail on directory paths
|
|
* with a trailing backslash */
|
|
struct _stat64 stat_buf;
|
|
FILETIME mtime_ft;
|
|
char path_buf[PATH_MAX_LENGTH];
|
|
const char *stat_path = path;
|
|
DWORD file_info;
|
|
size_t _len = strlcpy(path_buf, path, sizeof(path_buf));
|
|
|
|
if (_len > 0 && _len < sizeof(path_buf))
|
|
{
|
|
while (_len > 0 &&
|
|
(path_buf[_len - 1] == '\\' || path_buf[_len - 1] == '/'))
|
|
{
|
|
/* Keep drive roots like "C:\" intact */
|
|
if (_len == 3 &&
|
|
((((path_buf[0] >= 'A') && (path_buf[0] <= 'Z')) ||
|
|
((path_buf[0] >= 'a') && (path_buf[0] <= 'z'))) &&
|
|
path_buf[1] == ':' && path_buf[2] == '\\'))
|
|
break;
|
|
|
|
path_buf[--_len] = '\0';
|
|
}
|
|
|
|
stat_path = path_buf;
|
|
}
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
if (!vfs_stat_win32_ansi(stat_path, &stat_buf, &file_info, &mtime_ft))
|
|
return 0;
|
|
}
|
|
else if (!vfs_stat_win32_wide(stat_path, &stat_buf, &file_info, &mtime_ft))
|
|
return 0;
|
|
#elif defined(LEGACY_WIN32)
|
|
if (!vfs_stat_win32_ansi(stat_path, &stat_buf, &file_info, &mtime_ft))
|
|
return 0;
|
|
#else
|
|
if (!vfs_stat_win32_wide(stat_path, &stat_buf, &file_info, &mtime_ft))
|
|
return 0;
|
|
#endif
|
|
|
|
if (size)
|
|
*size = (int64_t)stat_buf.st_size;
|
|
if (mtime)
|
|
*mtime = vfs_filetime_to_unix(&mtime_ft);
|
|
|
|
if (file_info & FILE_ATTRIBUTE_DIRECTORY)
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (file_info & FILE_ATTRIBUTE_READONLY)
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
#elif defined(GEKKO)
|
|
/* On GEKKO platforms, paths cannot have
|
|
* trailing slashes - we must therefore
|
|
* remove them */
|
|
size_t _len;
|
|
char path_buf[PATH_MAX_LENGTH];
|
|
struct stat stat_buf;
|
|
|
|
_len = strlcpy(path_buf, path, sizeof(path_buf));
|
|
if (_len > 0 && path_buf[_len - 1] == '/')
|
|
path_buf[_len - 1] = '\0';
|
|
|
|
if (stat(path_buf, &stat_buf) < 0)
|
|
return 0;
|
|
|
|
if (size)
|
|
*size = (int64_t)stat_buf.st_size;
|
|
if (mtime)
|
|
*mtime = (int64_t)stat_buf.st_mtime;
|
|
|
|
if (S_ISDIR(stat_buf.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (S_ISCHR(stat_buf.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_CHARACTER_SPECIAL;
|
|
if (!(stat_buf.st_mode & S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
#else
|
|
/* Every other platform */
|
|
/* _LARGEFILE64_SOURCE is a request for the LFS64 API, not evidence
|
|
* that it exists: Darwin, the 3DS and PSP toolchains, DJGPP and musl
|
|
* from 1.2.4 have no stat64 to offer whoever asks. Paired with a libc
|
|
* known to carry it, so that a build defining the macro for its own
|
|
* reasons cannot turn this into a compile error. */
|
|
#if defined(_LARGEFILE64_SOURCE) \
|
|
&& (defined(__GLIBC__) || defined(__ANDROID__) || defined(__UCLIBC__))
|
|
struct stat64 stat_buf;
|
|
if (stat64(path, &stat_buf) < 0)
|
|
return 0;
|
|
#else
|
|
struct stat stat_buf;
|
|
|
|
if (stat(path, &stat_buf) < 0)
|
|
return 0;
|
|
#endif
|
|
|
|
if (size)
|
|
*size = (int64_t)stat_buf.st_size;
|
|
if (mtime)
|
|
*mtime = (int64_t)stat_buf.st_mtime;
|
|
|
|
if (S_ISDIR(stat_buf.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (S_ISCHR(stat_buf.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_CHARACTER_SPECIAL;
|
|
if (!(stat_buf.st_mode & S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
#endif
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int retro_vfs_stat_64_impl(const char *path, int64_t *size)
|
|
{
|
|
return retro_vfs_stat_full(path, size, NULL);
|
|
}
|
|
|
|
int retro_vfs_get_mtime_impl(const char *path, int64_t *mtime)
|
|
{
|
|
int64_t t = 0;
|
|
int flags;
|
|
bool got_it;
|
|
|
|
if (!mtime)
|
|
return -1;
|
|
|
|
/* The SMB/SAF stat helpers leave @mtime untouched; a sentinel that
|
|
* no real file system reports tells those cases apart from a genuine
|
|
* timestamp. */
|
|
t = INT64_MIN;
|
|
flags = retro_vfs_stat_full(path, NULL, &t);
|
|
got_it = (flags != 0) && (t != INT64_MIN);
|
|
if (!got_it)
|
|
return -1;
|
|
*mtime = t;
|
|
return 0;
|
|
}
|
|
|
|
int retro_vfs_stat_impl(const char *path, int32_t *size)
|
|
{
|
|
int64_t size64 = 0;
|
|
int ret = retro_vfs_stat_64_impl(path, size ? &size64 : NULL);
|
|
|
|
/* If a file is larger than 2 GiB, size64 holds the correct value
|
|
* but a naked (int32_t) cast would truncate -- worse, on files in
|
|
* (INT32_MAX, UINT32_MAX] the high bit wraps and callers see a
|
|
* negative size that they may interpret as an error. Saturate to
|
|
* INT_MAX so a caller using the legacy API gets a clamped-large
|
|
* value rather than a corrupted one, and migrate to
|
|
* retro_vfs_stat_64_impl for files that need the real size.
|
|
* INT_MAX is used instead of INT32_MAX for C89 / VC6 portability
|
|
* (stdint.h's INT32_MAX is a C99 addition; INT_MAX is C89). int
|
|
* is 32-bit on all MSVC targets including VC6, so INT_MAX ==
|
|
* INT32_MAX everywhere this code runs. */
|
|
if (size)
|
|
*size = (size64 > (int64_t)INT_MAX) ? INT_MAX : (int32_t)size64;
|
|
|
|
return ret;
|
|
}
|
|
|
|
#if defined(VITA)
|
|
#define path_mkdir_err(ret) (((ret) == SCE_ERROR_ERRNO_EEXIST))
|
|
#elif defined(PSP) || defined(PS2) || defined(_3DS) || defined(WIIU) || defined(SWITCH)
|
|
#define path_mkdir_err(ret) ((ret) == -1)
|
|
#else
|
|
#define path_mkdir_err(ret) ((ret) < 0 && errno == EEXIST)
|
|
#endif
|
|
|
|
int retro_vfs_mkdir_impl(const char *dir)
|
|
{
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(dir))
|
|
{
|
|
int ret;
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, dir))
|
|
return -1;
|
|
ret = retro_vfs_mkdir_saf(saf_split_result.tree, saf_split_result.path);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
return ret;
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
#if defined(_WIN32)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
int ret = -1;
|
|
|
|
if (win32_needs_local_encoding())
|
|
ret = _mkdir(dir);
|
|
else
|
|
{
|
|
wchar_t *dir_w = utf8_to_utf16_string_alloc(dir);
|
|
|
|
if (dir_w)
|
|
{
|
|
ret = _wmkdir(dir_w);
|
|
free(dir_w);
|
|
}
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
int ret = _mkdir(dir);
|
|
#else
|
|
wchar_t *dir_w = utf8_to_utf16_string_alloc(dir);
|
|
int ret = -1;
|
|
|
|
if (dir_w)
|
|
{
|
|
ret = _wmkdir(dir_w);
|
|
free(dir_w);
|
|
}
|
|
#endif
|
|
#elif TARGET_OS_IPHONE
|
|
int ret = mkdir(dir, 0755);
|
|
#elif defined(VITA)
|
|
int ret = sceIoMkdir(dir, 0777);
|
|
#elif defined(__QNX__)
|
|
int ret = mkdir(dir, 0777);
|
|
#elif defined(GEKKO) || defined(WIIU)
|
|
/* On GEKKO platforms, mkdir() fails if
|
|
* the path has a trailing slash. We must
|
|
* therefore remove it. */
|
|
int ret = -1;
|
|
char *dir_buf = strdup(dir);
|
|
|
|
if (dir_buf)
|
|
{
|
|
size_t _len = strlen(dir_buf);
|
|
|
|
if (_len > 0)
|
|
if (dir_buf[_len - 1] == '/')
|
|
dir_buf[_len - 1] = '\0';
|
|
|
|
ret = mkdir(dir_buf, 0750);
|
|
|
|
free(dir_buf);
|
|
}
|
|
#else
|
|
int ret = mkdir(dir, 0750);
|
|
#endif
|
|
|
|
if (path_mkdir_err(ret))
|
|
return -2;
|
|
return ret < 0 ? -1 : 0;
|
|
}
|
|
}
|
|
|
|
/* Removes the empty directory @dir. 0 on success, -1 otherwise -
|
|
* including a directory that is not empty, and platforms with no
|
|
* directory removal here, where an empty directory is simply left. */
|
|
int retro_vfs_rmdir_impl(const char *dir)
|
|
{
|
|
if (!dir || !*dir)
|
|
return -1;
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(dir))
|
|
{
|
|
int ret;
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, dir))
|
|
return -1;
|
|
ret = retro_vfs_file_remove_saf(saf_split_result.tree,
|
|
saf_split_result.path);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
return ret == 0 ? 0 : -1;
|
|
}
|
|
#endif
|
|
{
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
int ret = -1;
|
|
|
|
if (win32_needs_local_encoding())
|
|
ret = _rmdir(dir);
|
|
else
|
|
{
|
|
wchar_t *dir_w = utf8_to_utf16_string_alloc(dir);
|
|
if (dir_w)
|
|
{
|
|
ret = _wrmdir(dir_w);
|
|
free(dir_w);
|
|
}
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
int ret = _rmdir(dir);
|
|
#else
|
|
int ret = -1;
|
|
wchar_t *dir_w = utf8_to_utf16_string_alloc(dir);
|
|
|
|
if (dir_w)
|
|
{
|
|
ret = _wrmdir(dir_w);
|
|
free(dir_w);
|
|
}
|
|
#endif
|
|
#elif defined(VITA)
|
|
int ret = sceIoRmdir(dir);
|
|
#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) \
|
|
|| defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) \
|
|
|| defined(__HAIKU__) || defined(__QNX__) || defined(__EMSCRIPTEN__)
|
|
int ret = rmdir(dir);
|
|
#else
|
|
/* No directory removal wired up for this platform yet */
|
|
int ret = -1;
|
|
#endif
|
|
return ret < 0 ? -1 : 0;
|
|
}
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
/* Worst-case UTF-8 for a find-data name, terminator included.
|
|
*
|
|
* W: cFileName is WCHAR[MAX_PATH]. A BMP unit is at most 3 UTF-8 bytes;
|
|
* a surrogate pair is 2 units for 4 bytes, so 3 per unit bounds both.
|
|
*
|
|
* A: cFileName is CHAR[MAX_PATH] in the local codepage. One SBCS byte
|
|
* is one BMP character, so it is bounded by the same 3; DBCS spends two
|
|
* bytes per character and is cheaper still.
|
|
*
|
|
* The old code decoded back over cFileName itself, which is 520 bytes on
|
|
* the W path and 260 on the A path - short of this bound whenever the
|
|
* name is not ASCII, so a 174-character CJK name (255 is legal on NTFS)
|
|
* was silently strlcpy()-truncated. It also destroyed the source
|
|
* encoding in place, making the function return garbage if ever called
|
|
* twice on one entry. */
|
|
#define VFS_WIN32_NAME_UTF8_MAX (MAX_PATH * 3 + 1)
|
|
#endif
|
|
|
|
#ifdef VFS_FRONTEND
|
|
struct retro_vfs_dir_handle
|
|
#else
|
|
struct libretro_vfs_implementation_dir
|
|
#endif
|
|
{
|
|
char* orig_path;
|
|
#if defined(_WIN32)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
/* Unlike every other site, the difference here is a type, not a
|
|
* call: the handle carries the find-data across retro_readdir()
|
|
* calls. A union costs the larger of the two (the W form, by the
|
|
* width of cFileName) and lets both live in one handle. */
|
|
union
|
|
{
|
|
WIN32_FIND_DATA a;
|
|
WIN32_FIND_DATAW w;
|
|
} entry;
|
|
#elif defined(LEGACY_WIN32)
|
|
WIN32_FIND_DATA entry;
|
|
#else
|
|
WIN32_FIND_DATAW entry;
|
|
#endif
|
|
HANDLE directory;
|
|
bool next;
|
|
char path[PATH_MAX_LENGTH];
|
|
char name_utf8[VFS_WIN32_NAME_UTF8_MAX];
|
|
#elif defined(VITA)
|
|
SceUID directory;
|
|
SceIoDirent entry;
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
int error;
|
|
int directory;
|
|
sysFSDirent entry;
|
|
#else
|
|
DIR *directory;
|
|
const struct dirent *entry;
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
libretro_vfs_implementation_saf_dir *saf_directory;
|
|
#endif
|
|
#ifdef HAVE_SMBCLIENT
|
|
smb_dir_handle* smb_handle;
|
|
char smb_path[PATH_MAX_LENGTH];
|
|
bool smb_is_dir;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
nfs_dir_handle* nfs_handle;
|
|
const char *nfs_name;
|
|
bool nfs_is_dir;
|
|
#endif
|
|
};
|
|
|
|
static bool dirent_check_err(libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
#if defined(_WIN32)
|
|
return (rdir->directory == INVALID_HANDLE_VALUE);
|
|
#elif defined(VITA) || defined(ORBIS)
|
|
return (rdir->directory < 0);
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
return (rdir->error != FS_SUCCEEDED);
|
|
#else
|
|
return !(rdir->directory);
|
|
#endif
|
|
}
|
|
|
|
int retro_vfs_restrict_permissions_impl(const char *path)
|
|
{
|
|
#if defined(_WIN32) || defined(VITA) || defined(PSP) || defined(PS2) \
|
|
|| defined(__PS3__) || defined(GEKKO) || defined(WIIU) \
|
|
|| defined(_3DS) || defined(HAVE_LIBNX) || defined(__CELLOS_LV2__) \
|
|
|| defined(ORBIS) || defined(DJGPP) || defined(EMSCRIPTEN) \
|
|
|| defined(ANDROID) || TARGET_OS_IPHONE || TARGET_OS_TV
|
|
/* No per-user permission model the frontend controls: Windows
|
|
* profile directories and mobile app sandboxes are already
|
|
* private, consoles are single-user. */
|
|
(void)path;
|
|
return 0;
|
|
#else
|
|
return chmod(path, S_IRUSR | S_IWUSR) == 0 ? 0 : -1;
|
|
#endif
|
|
}
|
|
|
|
int retro_vfs_set_readonly_impl(const char *path, int readonly)
|
|
{
|
|
if (!path || !*path)
|
|
return -1;
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
{
|
|
DWORD attrs;
|
|
int ret = -1;
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
#endif
|
|
#if defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
{
|
|
char *path_local = utf8_to_local_string_alloc(path);
|
|
if (!path_local)
|
|
return -1;
|
|
attrs = GetFileAttributes(path_local);
|
|
if (attrs != INVALID_FILE_ATTRIBUTES)
|
|
{
|
|
if (readonly)
|
|
attrs |= FILE_ATTRIBUTE_READONLY;
|
|
else
|
|
attrs &= ~FILE_ATTRIBUTE_READONLY;
|
|
ret = SetFileAttributes(path_local, attrs) ? 0 : -1;
|
|
}
|
|
free(path_local);
|
|
}
|
|
#endif
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
}
|
|
else
|
|
#endif
|
|
#if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
{
|
|
wchar_t *path_wide = utf8_to_utf16_string_alloc(path);
|
|
if (!path_wide)
|
|
return -1;
|
|
attrs = GetFileAttributesW(path_wide);
|
|
if (attrs != INVALID_FILE_ATTRIBUTES)
|
|
{
|
|
if (readonly)
|
|
attrs |= FILE_ATTRIBUTE_READONLY;
|
|
else
|
|
attrs &= ~FILE_ATTRIBUTE_READONLY;
|
|
ret = SetFileAttributesW(path_wide, attrs) ? 0 : -1;
|
|
}
|
|
free(path_wide);
|
|
}
|
|
#endif
|
|
return ret;
|
|
}
|
|
#elif defined(VITA)
|
|
{
|
|
SceIoStat st;
|
|
if (sceIoGetstat(path, &st) < 0)
|
|
return -1;
|
|
/* Owner write bit only: SCE_S_IWOTH is deprecated in the SDK and
|
|
* the contract is "the current user cannot write". */
|
|
if (readonly)
|
|
st.st_mode &= ~SCE_S_IWUSR;
|
|
else
|
|
st.st_mode |= SCE_S_IWUSR;
|
|
return sceIoChstat(path, &st, SCE_CST_MODE) < 0 ? -1 : 0;
|
|
}
|
|
#elif defined(VFS_HAVE_POSIX_METADATA)
|
|
{
|
|
struct stat st;
|
|
mode_t mode;
|
|
if (stat(path, &st) < 0)
|
|
return -1;
|
|
mode = st.st_mode & 07777;
|
|
if (readonly)
|
|
mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
|
|
else
|
|
mode |= S_IWUSR;
|
|
return chmod(path, mode) == 0 ? 0 : -1;
|
|
}
|
|
#else
|
|
/* No permission model reachable from here. */
|
|
(void)readonly;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
int retro_vfs_set_mtime_impl(const char *path, int64_t mtime)
|
|
{
|
|
if (!path || !*path)
|
|
return -1;
|
|
|
|
#if defined(_WIN32) && !defined(_XBOX)
|
|
{
|
|
HANDLE h = INVALID_HANDLE_VALUE;
|
|
FILETIME ft;
|
|
BOOL ok;
|
|
|
|
vfs_unix_to_filetime(mtime, &ft);
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
#endif
|
|
#if defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
{
|
|
char *path_local = utf8_to_local_string_alloc(path);
|
|
if (!path_local)
|
|
return -1;
|
|
h = CreateFile(path_local, FILE_WRITE_ATTRIBUTES,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
|
free(path_local);
|
|
}
|
|
#endif
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
}
|
|
else
|
|
#endif
|
|
#if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
{
|
|
wchar_t *path_wide = utf8_to_utf16_string_alloc(path);
|
|
if (!path_wide)
|
|
return -1;
|
|
h = CreateFileW(path_wide, FILE_WRITE_ATTRIBUTES,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
|
free(path_wide);
|
|
}
|
|
#endif
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return -1;
|
|
/* Creation and access times are left alone. */
|
|
ok = SetFileTime(h, NULL, NULL, &ft);
|
|
CloseHandle(h);
|
|
return ok ? 0 : -1;
|
|
}
|
|
#elif defined(VITA)
|
|
{
|
|
SceIoStat st;
|
|
time_t t = (time_t)mtime;
|
|
if (sceIoGetstat(path, &st) < 0)
|
|
return -1;
|
|
sceRtcSetTime_t(&st.st_mtime, t);
|
|
return sceIoChstat(path, &st, SCE_CST_MT) < 0 ? -1 : 0;
|
|
}
|
|
#elif defined(VFS_HAVE_POSIX_METADATA)
|
|
{
|
|
struct stat st;
|
|
struct timeval tv[2];
|
|
if (stat(path, &st) < 0)
|
|
return -1;
|
|
/* Keep the access time; only the modification time changes. */
|
|
tv[0].tv_sec = st.st_atime;
|
|
tv[0].tv_usec = 0;
|
|
tv[1].tv_sec = (time_t)mtime;
|
|
tv[1].tv_usec = 0;
|
|
return utimes(path, tv) == 0 ? 0 : -1;
|
|
}
|
|
#else
|
|
(void)mtime;
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
/* Create @dir and every missing directory above it. Same shape as
|
|
* path_mkdir(), kept local so this file does not grow a link-time
|
|
* dependency on file_path_io.c for standalone consumers. Works from
|
|
* the bottom up: try @dir, on failure make its parent and retry, so a
|
|
* drive root ("C:"), a UNC share prefix or a doubled separator is just
|
|
* a rung that fails harmlessly instead of a component we try to
|
|
* create. @dir is modified in place and restored before returning.
|
|
* Returns 0 when @dir exists afterwards. */
|
|
static int vfs_copy_mkdir_parents(char *dir)
|
|
{
|
|
char *sep;
|
|
int ret = retro_vfs_mkdir_impl(dir);
|
|
if (ret != -1)
|
|
return 0; /* 0 created, -2 already there */
|
|
/* Strip the last component (ignoring a trailing separator) and
|
|
* recurse; stop at the top of the string. */
|
|
sep = dir + strlen(dir);
|
|
while (sep > dir && (sep[-1] == '/' || sep[-1] == '\\'))
|
|
sep--;
|
|
while (sep > dir && sep[-1] != '/' && sep[-1] != '\\')
|
|
sep--;
|
|
while (sep > dir && (sep[-1] == '/' || sep[-1] == '\\'))
|
|
sep--;
|
|
if (sep == dir)
|
|
return -1;
|
|
{
|
|
char c = *sep;
|
|
*sep = '\0';
|
|
vfs_copy_mkdir_parents(dir);
|
|
*sep = c;
|
|
}
|
|
return retro_vfs_mkdir_impl(dir) != -1 ? 0 : -1;
|
|
}
|
|
|
|
/* ---- copy: begin / step / close --------------------------------------
|
|
*
|
|
* A copy is a resumable state machine that the caller advances. There
|
|
* is no thread in here and no lock: begin() does the up-front checks
|
|
* and opens both ends, step() moves at most the bytes it was asked to
|
|
* and returns, close() releases everything and removes a partial dst.
|
|
* Whoever wants the transfer off their own thread (RetroArch's task
|
|
* queue, a core's worker) makes that decision, not the VFS.
|
|
*
|
|
* Memory: the handle, its two path strings, the two open files, and on
|
|
* the portable path one transfer buffer (1 MiB, 64 KiB if that
|
|
* allocation fails). The Linux path moves bytes with copy_file_range
|
|
* at explicit offsets, so it needs no buffer at all and resumes exactly
|
|
* where the last step stopped. On APFS a same-volume copy is a
|
|
* clonefile() in begin(): O(1), no bytes move, step() reports DONE.
|
|
*
|
|
* Speed: with a large budget a step is the same kernel primitive a
|
|
* blocking copy would issue, so throughput is that of the primitive;
|
|
* with a small budget it is bounded latency. The caller picks. */
|
|
|
|
/* The samples build with VFS_COPY_DEBUG so a CI lane shows which path
|
|
* each step took and what the platform answered; never set in a
|
|
* frontend build. */
|
|
#if defined(VFS_COPY_DEBUG)
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#define VFS_COPY_DBG(...) fprintf(stderr, "[vfs-copy] " __VA_ARGS__)
|
|
#else
|
|
#define VFS_COPY_DBG(...) do { } while (0)
|
|
#endif
|
|
|
|
/* Transfer buffer for the portable path, which is the one consoles,
|
|
* SAF, SMB and CDROM take -- the places with the least memory to
|
|
* spare. Measured on a 512 MiB copy: 16 KiB and 64 KiB are clearly
|
|
* slower, and everything from 128 KiB to 1 MiB is the same within
|
|
* run-to-run noise, so 128 KiB is the ceiling and small files get
|
|
* only what they need. The kernel fast paths allocate nothing. */
|
|
#define VFS_COPY_BUF_MAX (128 * 1024)
|
|
#define VFS_COPY_BUF_MIN (16 * 1024)
|
|
/* Largest single kernel request per step; the loop inside a step
|
|
* issues as many as the budget allows. */
|
|
#define VFS_COPY_KERNEL_REQ ((size_t)16 * 1024 * 1024)
|
|
/* Default step when the caller passes 0: bounded enough for a frame
|
|
* loop on fast media, large enough that a poll-per-frame caller still
|
|
* moves hundreds of MB/s. */
|
|
#define VFS_COPY_DEFAULT_STEP ((int64_t)4 * 1024 * 1024)
|
|
|
|
struct retro_vfs_copy_handle
|
|
{
|
|
char *src;
|
|
char *dst;
|
|
int64_t total;
|
|
int64_t done;
|
|
int status; /* RETRO_VFS_COPY_RUNNING / DONE / FAILED */
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
int in_fd; /* -1 when the kernel path is not in use */
|
|
int out_fd;
|
|
#endif
|
|
/* Portable path: both ends through the VFS so either may be any
|
|
* backend (SAF, SMB, CDROM, native). */
|
|
libretro_vfs_implementation_file *in;
|
|
libretro_vfs_implementation_file *out;
|
|
char *buf;
|
|
size_t buf_len;
|
|
};
|
|
|
|
static void vfs_copy_handle_free(struct retro_vfs_copy_handle *h)
|
|
{
|
|
if (!h)
|
|
return;
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
if (h->in_fd >= 0) close(h->in_fd);
|
|
if (h->out_fd >= 0) close(h->out_fd);
|
|
#endif
|
|
if (h->in) retro_vfs_file_close_impl(h->in);
|
|
if (h->out) retro_vfs_file_close_impl(h->out);
|
|
free(h->buf);
|
|
free(h->src);
|
|
free(h->dst);
|
|
free(h);
|
|
}
|
|
|
|
/* Opens the portable path's two ends and buffer. With @resume the
|
|
* destination is opened in place (no truncate) and both ends are
|
|
* positioned at h->done, for taking over a copy another path started.
|
|
* Returns 0 or -1. */
|
|
static int vfs_copy_open_portable(struct retro_vfs_copy_handle *h, bool resume)
|
|
{
|
|
/* No point in a buffer larger than what is left to copy. */
|
|
{
|
|
int64_t left = h->total - h->done;
|
|
h->buf_len = VFS_COPY_BUF_MAX;
|
|
if (left > 0 && left < (int64_t)h->buf_len)
|
|
h->buf_len = (size_t)left;
|
|
if (h->buf_len < VFS_COPY_BUF_MIN)
|
|
h->buf_len = VFS_COPY_BUF_MIN;
|
|
}
|
|
if (!(h->buf = (char*)malloc(h->buf_len)))
|
|
{
|
|
h->buf_len = VFS_COPY_BUF_MIN;
|
|
if (!(h->buf = (char*)malloc(h->buf_len)))
|
|
return -1;
|
|
}
|
|
h->in = retro_vfs_file_open_impl(h->src, RETRO_VFS_FILE_ACCESS_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
|
|
if (!h->in)
|
|
return -1;
|
|
h->out = retro_vfs_file_open_impl(h->dst,
|
|
resume ? (RETRO_VFS_FILE_ACCESS_READ_WRITE | RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING)
|
|
: RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
if (!h->out)
|
|
return -1;
|
|
if (resume && h->done > 0)
|
|
{
|
|
if ( retro_vfs_file_seek_impl(h->in, h->done, RETRO_VFS_SEEK_POSITION_START) < 0
|
|
|| retro_vfs_file_seek_impl(h->out, h->done, RETRO_VFS_SEEK_POSITION_START) < 0)
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* One portable step: up to @budget bytes through the buffer.
|
|
* Returns the new status. */
|
|
static int vfs_copy_step_portable(struct retro_vfs_copy_handle *h, int64_t budget)
|
|
{
|
|
while (budget > 0)
|
|
{
|
|
size_t want = h->buf_len;
|
|
int64_t n;
|
|
if ((int64_t)want > budget)
|
|
want = (size_t)budget;
|
|
n = retro_vfs_file_read_impl(h->in, h->buf, want);
|
|
VFS_COPY_DBG("portable: want %lu -> read %lld (done %lld budget %lld)\n",
|
|
(unsigned long)want, (long long)n, (long long)h->done, (long long)budget);
|
|
if (n < 0)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
if (n == 0)
|
|
{
|
|
/* EOF: close dst so a DONE report means "complete and closed". */
|
|
libretro_vfs_implementation_file *out = h->out;
|
|
h->out = NULL;
|
|
if (retro_vfs_file_close_impl(out) != 0)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
return RETRO_VFS_COPY_DONE;
|
|
}
|
|
if (retro_vfs_file_write_impl(h->out, h->buf, (uint64_t)n) != n)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
h->done += n;
|
|
budget -= n;
|
|
}
|
|
return RETRO_VFS_COPY_RUNNING;
|
|
}
|
|
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
/* A kernel that hands back more bytes than it was asked for cannot be
|
|
* held to a step budget. Real Linux never does; sandboxed kernels
|
|
* (gVisor was observed copying to EOF regardless of len) do. Once
|
|
* seen, this process stops offering it work: every later copy takes
|
|
* the portable path, which is bounded by construction. */
|
|
static bool vfs_cfr_untrusted = false;
|
|
|
|
/* Kernel path: copy_file_range at explicit offsets, resumable from
|
|
* h->done with no state in the kernel between steps. Returns 1 if it
|
|
* is in use after this call, 0 if the kernel declined before any byte
|
|
* moved (caller falls back to the portable path), -1 on error. */
|
|
static int vfs_copy_open_linux(struct retro_vfs_copy_handle *h)
|
|
{
|
|
if (vfs_cfr_untrusted)
|
|
{
|
|
VFS_COPY_DBG("open: kernel path retired, portable\n");
|
|
return 0;
|
|
}
|
|
h->in_fd = open(h->src, O_RDONLY | O_CLOEXEC);
|
|
if (h->in_fd < 0)
|
|
return 0;
|
|
h->out_fd = open(h->dst, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0666);
|
|
if (h->out_fd < 0)
|
|
{
|
|
close(h->in_fd);
|
|
h->in_fd = -1;
|
|
return 0;
|
|
}
|
|
posix_fadvise(h->in_fd, 0, 0, POSIX_FADV_SEQUENTIAL);
|
|
/* Reserve the extent so the copy lands contiguously; harmless if
|
|
* the file system cannot (FAT, tmpfs). */
|
|
if (h->total > 0)
|
|
posix_fallocate(h->out_fd, 0, (off_t)h->total);
|
|
return 1;
|
|
}
|
|
|
|
static int vfs_copy_step_linux(struct retro_vfs_copy_handle *h, int64_t budget)
|
|
{
|
|
while (budget > 0 && h->done < h->total)
|
|
{
|
|
int64_t left = h->total - h->done;
|
|
size_t want = (size_t)(left < (int64_t)VFS_COPY_KERNEL_REQ ? left : (int64_t)VFS_COPY_KERNEL_REQ);
|
|
long long off_in = (long long)h->done;
|
|
long long off_out = (long long)h->done;
|
|
ssize_t n;
|
|
if ((int64_t)want > budget)
|
|
want = (size_t)budget;
|
|
/* Every argument widened to long: syscall() reads its varargs
|
|
* as longs, and an int or unsigned in a register may carry
|
|
* whatever was in its upper half. */
|
|
#if defined(VFS_COPY_TEST_FAKE_CFR)
|
|
/* Test-only stand-in for a kernel that ignores len and copies to
|
|
* EOF (what gVisor was seen doing): the samples build with this
|
|
* to exercise the distrust-and-resume path on a real kernel. */
|
|
{
|
|
char tmp[65536];
|
|
ssize_t got, total_fake = 0;
|
|
/* 1: copy to EOF. 2: copy three times what was asked, so the
|
|
* resume-in-place path (kernel left the copy part-way) runs. */
|
|
while ((VFS_COPY_TEST_FAKE_CFR == 1 || (size_t)total_fake < 3 * want)
|
|
&& (got = pread(h->in_fd, tmp, sizeof(tmp), (off_t)(off_in + total_fake))) > 0)
|
|
{
|
|
if (pwrite(h->out_fd, tmp, (size_t)got, (off_t)(off_out + total_fake)) != got)
|
|
{ got = -1; break; }
|
|
total_fake += got;
|
|
}
|
|
n = got < 0 ? -1 : total_fake;
|
|
}
|
|
#else
|
|
n = (ssize_t)syscall(SYS_copy_file_range, (long)h->in_fd, (long)&off_in,
|
|
(long)h->out_fd, (long)&off_out, (long)want, 0L);
|
|
#endif
|
|
VFS_COPY_DBG("copy_file_range: want %lu at %lld -> %ld errno %d (budget %lld total %lld)\n",
|
|
(unsigned long)want, (long long)h->done, (long)n, n < 0 ? errno : 0,
|
|
(long long)budget, (long long)h->total);
|
|
if (n < 0)
|
|
{
|
|
if (h->done == 0 && (errno == EXDEV || errno == ENOSYS
|
|
|| errno == EINVAL || errno == EOPNOTSUPP || errno == EPERM))
|
|
{
|
|
/* Kernel cannot do this pair: hand over to the portable
|
|
* path from offset 0. The fds go; the VFS opens its own. */
|
|
close(h->in_fd);
|
|
close(h->out_fd);
|
|
h->in_fd = h->out_fd = -1;
|
|
if (vfs_copy_open_portable(h, false) != 0)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
return vfs_copy_step_portable(h, budget);
|
|
}
|
|
return RETRO_VFS_COPY_FAILED;
|
|
}
|
|
if (n == 0)
|
|
break; /* src shrank under us; what we have is the file */
|
|
if ((size_t)n > want)
|
|
{
|
|
/* Bytes are on disk, so account for them, but this kernel
|
|
* does not honour len: no further kernel steps, here or in
|
|
* any later copy. */
|
|
vfs_cfr_untrusted = true;
|
|
h->done += n;
|
|
if (h->done > h->total)
|
|
h->done = h->total;
|
|
break;
|
|
}
|
|
h->done += n;
|
|
budget -= n;
|
|
}
|
|
if (vfs_cfr_untrusted && h->done < h->total)
|
|
{
|
|
/* Continue this copy on the portable path from where the
|
|
* kernel left it. */
|
|
close(h->in_fd);
|
|
close(h->out_fd);
|
|
h->in_fd = h->out_fd = -1;
|
|
if (vfs_copy_open_portable(h, true) != 0)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
return RETRO_VFS_COPY_RUNNING;
|
|
}
|
|
if (h->done >= h->total || budget > 0)
|
|
{
|
|
int out_fd = h->out_fd;
|
|
h->out_fd = -1;
|
|
/* fallocate may have reserved more than src turned out to hold */
|
|
if (ftruncate(out_fd, (off_t)h->done) != 0 || close(out_fd) != 0)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
return RETRO_VFS_COPY_DONE;
|
|
}
|
|
return RETRO_VFS_COPY_RUNNING;
|
|
}
|
|
#endif
|
|
|
|
struct retro_vfs_copy_handle *retro_vfs_copy_begin_impl(
|
|
const char *src, const char *dst, unsigned flags)
|
|
{
|
|
struct retro_vfs_copy_handle *h = NULL;
|
|
int64_t src_size = 0;
|
|
int sflags, dflags;
|
|
bool native = true;
|
|
|
|
if (!src || !*src || !dst || !*dst)
|
|
return NULL;
|
|
#if defined(_WIN32)
|
|
if (string_is_equal_case_insensitive(src, dst))
|
|
return NULL;
|
|
#else
|
|
if (string_is_equal(src, dst))
|
|
return NULL;
|
|
#endif
|
|
|
|
sflags = retro_vfs_stat_full(src, &src_size, NULL);
|
|
if ( !(sflags & RETRO_VFS_STAT_IS_VALID)
|
|
|| (sflags & RETRO_VFS_STAT_IS_DIRECTORY)
|
|
|| (sflags & RETRO_VFS_STAT_IS_CHARACTER_SPECIAL))
|
|
return NULL;
|
|
|
|
dflags = retro_vfs_stat_full(dst, NULL, NULL);
|
|
if (dflags & RETRO_VFS_STAT_IS_VALID)
|
|
{
|
|
if (dflags & RETRO_VFS_STAT_IS_DIRECTORY)
|
|
return NULL;
|
|
if (!(flags & RETRO_VFS_COPY_OVERWRITE))
|
|
return NULL;
|
|
/* cp -f semantics: a stale read-only dst must not defeat an
|
|
* explicit overwrite, and a fresh inode is what the kernel
|
|
* paths want anyway. Windows refuses to delete a read-only
|
|
* file, so clear the attribute and try once more. */
|
|
if (retro_vfs_file_remove_impl(dst) != 0)
|
|
{
|
|
if ( !(dflags & RETRO_VFS_STAT_IS_READONLY)
|
|
|| retro_vfs_set_readonly_impl(dst, 0) != 0
|
|
|| retro_vfs_file_remove_impl(dst) != 0)
|
|
return NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Parent directory of dst, if dst has one. */
|
|
const char *last = strrchr(dst, '/');
|
|
const char *bs = strrchr(dst, '\\');
|
|
if (bs > last)
|
|
last = bs;
|
|
if (last && last > dst)
|
|
{
|
|
size_t n = (size_t)(last - dst);
|
|
char *dst_dir;
|
|
int parent_ok;
|
|
if (n >= PATH_MAX_LENGTH || !(dst_dir = (char*)malloc(n + 1)))
|
|
return NULL;
|
|
memcpy(dst_dir, dst, n);
|
|
dst_dir[n] = '\0';
|
|
/* mkdir reports "exists" for a file of that name too, so
|
|
* confirm the parent really is a directory. */
|
|
parent_ok = vfs_copy_mkdir_parents(dst_dir) == 0
|
|
&& (retro_vfs_stat_full(dst_dir, NULL, NULL) & RETRO_VFS_STAT_IS_DIRECTORY);
|
|
free(dst_dir);
|
|
if (!parent_ok)
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
if (!(h = (struct retro_vfs_copy_handle*)calloc(1, sizeof(*h))))
|
|
return NULL;
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
h->in_fd = h->out_fd = -1;
|
|
#endif
|
|
h->src = strdup(src);
|
|
h->dst = strdup(dst);
|
|
h->total = src_size;
|
|
h->status = RETRO_VFS_COPY_RUNNING;
|
|
if (!h->src || !h->dst)
|
|
goto fail;
|
|
|
|
/* Fast paths only when both ends are native. */
|
|
#if defined(HAVE_SMBCLIENT)
|
|
if (path_is_smb(src) || path_is_smb(dst))
|
|
native = false;
|
|
#endif
|
|
#if defined(HAVE_NFSCLIENT)
|
|
if (path_is_nfs(src) || path_is_nfs(dst))
|
|
native = false;
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (path_is_saf(src) || path_is_saf(dst))
|
|
native = false;
|
|
#endif
|
|
|
|
#if defined(VFS_HAVE_CLONEFILE)
|
|
/* APFS same-volume: a clone. Constant time, no bytes move, and
|
|
* the result is a complete independent file, so the copy is DONE
|
|
* before the first step. Any failure (other volume, HFS+, network)
|
|
* just means we copy bytes like everyone else. */
|
|
if (native && clonefile(src, dst, 0) == 0)
|
|
{
|
|
h->done = h->total;
|
|
h->status = RETRO_VFS_COPY_DONE;
|
|
return h;
|
|
}
|
|
#endif
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
if (native)
|
|
{
|
|
int r = vfs_copy_open_linux(h);
|
|
if (r < 0)
|
|
goto fail;
|
|
if (r == 1)
|
|
return h;
|
|
/* r == 0: could not even open natively; portable path. */
|
|
}
|
|
#endif
|
|
(void)native;
|
|
if (vfs_copy_open_portable(h, false) != 0)
|
|
goto fail;
|
|
return h;
|
|
|
|
fail:
|
|
vfs_copy_handle_free(h);
|
|
retro_vfs_file_remove_impl(dst);
|
|
return NULL;
|
|
}
|
|
|
|
int retro_vfs_copy_step_impl(struct retro_vfs_copy_handle *h,
|
|
int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total)
|
|
{
|
|
if (!h)
|
|
return RETRO_VFS_COPY_FAILED;
|
|
if (h->status == RETRO_VFS_COPY_RUNNING)
|
|
{
|
|
int64_t budget = max_bytes > 0 ? max_bytes : VFS_COPY_DEFAULT_STEP;
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
if (h->in_fd >= 0)
|
|
h->status = vfs_copy_step_linux(h, budget);
|
|
else
|
|
#endif
|
|
h->status = vfs_copy_step_portable(h, budget);
|
|
if (h->status == RETRO_VFS_COPY_FAILED)
|
|
{
|
|
/* Release the ends now so the partial dst can go; the
|
|
* handle itself lives until close(). */
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
if (h->in_fd >= 0) { close(h->in_fd); h->in_fd = -1; }
|
|
if (h->out_fd >= 0) { close(h->out_fd); h->out_fd = -1; }
|
|
#endif
|
|
if (h->in) { retro_vfs_file_close_impl(h->in); h->in = NULL; }
|
|
if (h->out) { retro_vfs_file_close_impl(h->out); h->out = NULL; }
|
|
retro_vfs_file_remove_impl(h->dst);
|
|
}
|
|
}
|
|
if (bytes_done)
|
|
*bytes_done = h->done;
|
|
if (bytes_total)
|
|
*bytes_total = h->total;
|
|
return h->status;
|
|
}
|
|
|
|
int retro_vfs_copy_close_impl(struct retro_vfs_copy_handle *h)
|
|
{
|
|
int status;
|
|
if (!h)
|
|
return -1;
|
|
status = h->status;
|
|
if (status == RETRO_VFS_COPY_RUNNING)
|
|
{
|
|
/* Cancelled: drop both ends first (a Win32 dst cannot be removed
|
|
* while open), then the partial file. */
|
|
#if defined(VFS_HAVE_COPY_FILE_RANGE)
|
|
if (h->in_fd >= 0) { close(h->in_fd); h->in_fd = -1; }
|
|
if (h->out_fd >= 0) { close(h->out_fd); h->out_fd = -1; }
|
|
#endif
|
|
if (h->in) { retro_vfs_file_close_impl(h->in); h->in = NULL; }
|
|
if (h->out) { retro_vfs_file_close_impl(h->out); h->out = NULL; }
|
|
retro_vfs_file_remove_impl(h->dst);
|
|
}
|
|
vfs_copy_handle_free(h);
|
|
return status == RETRO_VFS_COPY_DONE ? 0 : -1;
|
|
}
|
|
|
|
libretro_vfs_implementation_dir *retro_vfs_opendir_impl(
|
|
const char *name, bool include_hidden)
|
|
{
|
|
#if defined(_WIN32)
|
|
char path_buf[1024];
|
|
size_t _len;
|
|
#if defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
char *path_local = NULL;
|
|
#endif
|
|
#if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
wchar_t *path_wide = NULL;
|
|
#endif
|
|
#endif
|
|
libretro_vfs_implementation_dir *rdir;
|
|
|
|
/* Reject NULL or empty string paths*/
|
|
if (!name || (*name == 0))
|
|
return NULL;
|
|
|
|
/*Allocate RDIR struct. Tidied later with retro_closedir*/
|
|
if (!(rdir = (libretro_vfs_implementation_dir*)
|
|
calloc(1, sizeof(*rdir))))
|
|
return NULL;
|
|
|
|
rdir->orig_path = strdup(name);
|
|
if (rdir->orig_path == NULL)
|
|
{
|
|
free(rdir);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (path_is_smb(name))
|
|
{
|
|
smb_dir_handle *dh = retro_vfs_opendir_smb(name, include_hidden);
|
|
if (!dh)
|
|
{
|
|
free(rdir->orig_path);
|
|
free(rdir);
|
|
return NULL;
|
|
}
|
|
rdir->smb_handle = dh;
|
|
rdir->smb_path[0] = '\0';
|
|
rdir->smb_is_dir = false;
|
|
return rdir;
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
rdir->nfs_handle = NULL;
|
|
if (path_is_nfs(name))
|
|
{
|
|
nfs_dir_handle *dh = retro_vfs_opendir_nfs(name, include_hidden);
|
|
if (!dh)
|
|
{
|
|
free(rdir->orig_path);
|
|
free(rdir);
|
|
return NULL;
|
|
}
|
|
rdir->nfs_handle = dh;
|
|
rdir->nfs_name = "";
|
|
rdir->nfs_is_dir = false;
|
|
return rdir;
|
|
}
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
rdir->saf_directory = NULL;
|
|
|
|
if (path_is_saf(name))
|
|
{
|
|
struct libretro_vfs_implementation_saf_path_split_result saf_split_result;
|
|
if (!retro_vfs_path_split_saf(&saf_split_result, name))
|
|
{
|
|
free(rdir->orig_path);
|
|
free(rdir);
|
|
return NULL;
|
|
}
|
|
rdir->saf_directory = retro_vfs_opendir_saf(saf_split_result.tree, saf_split_result.path, include_hidden);
|
|
free(saf_split_result.path);
|
|
free(saf_split_result.tree);
|
|
if (rdir->saf_directory == NULL)
|
|
{
|
|
free(rdir->orig_path);
|
|
free(rdir);
|
|
return NULL;
|
|
}
|
|
return rdir;
|
|
}
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
_len = strlcpy(path_buf, name, sizeof(path_buf));
|
|
/* Non-NT platforms don't like extra slashes in the path */
|
|
if (path_buf[_len - 1] != '\\')
|
|
path_buf [_len++] = '\\';
|
|
|
|
path_buf[_len ] = '*';
|
|
path_buf[_len + 1] = '\0';
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
path_local = utf8_to_local_string_alloc(path_buf);
|
|
rdir->directory = FindFirstFileA(path_local, &rdir->entry.a);
|
|
if (path_local)
|
|
free(path_local);
|
|
}
|
|
else
|
|
{
|
|
path_wide = utf8_to_utf16_string_alloc(path_buf);
|
|
rdir->directory = FindFirstFileW(path_wide, &rdir->entry.w);
|
|
if (path_wide)
|
|
free(path_wide);
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
path_local = utf8_to_local_string_alloc(path_buf);
|
|
rdir->directory = FindFirstFile(path_local, &rdir->entry);
|
|
if (path_local)
|
|
free(path_local);
|
|
#else
|
|
path_wide = utf8_to_utf16_string_alloc(path_buf);
|
|
rdir->directory = FindFirstFileW(path_wide, &rdir->entry);
|
|
if (path_wide)
|
|
free(path_wide);
|
|
#endif
|
|
|
|
#elif defined(VITA)
|
|
rdir->directory = sceIoDopen(name);
|
|
#elif defined(_3DS)
|
|
rdir->directory = opendir(name);
|
|
rdir->entry = NULL;
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
rdir->error = sysFsOpendir(name, &rdir->directory);
|
|
#else
|
|
rdir->directory = opendir(name);
|
|
rdir->entry = NULL;
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
/* Same field, same offset in both arms of the union - but C wants
|
|
* one of them named, so follow whichever the handle is using. */
|
|
if (win32_needs_local_encoding())
|
|
{
|
|
if (include_hidden)
|
|
rdir->entry.a.dwFileAttributes |= FILE_ATTRIBUTE_HIDDEN;
|
|
else
|
|
rdir->entry.a.dwFileAttributes &= ~FILE_ATTRIBUTE_HIDDEN;
|
|
}
|
|
else
|
|
{
|
|
if (include_hidden)
|
|
rdir->entry.w.dwFileAttributes |= FILE_ATTRIBUTE_HIDDEN;
|
|
else
|
|
rdir->entry.w.dwFileAttributes &= ~FILE_ATTRIBUTE_HIDDEN;
|
|
}
|
|
#else
|
|
if (include_hidden)
|
|
rdir->entry.dwFileAttributes |= FILE_ATTRIBUTE_HIDDEN;
|
|
else
|
|
rdir->entry.dwFileAttributes &= ~FILE_ATTRIBUTE_HIDDEN;
|
|
#endif
|
|
#else
|
|
(void)include_hidden;
|
|
#endif
|
|
|
|
if (rdir->directory && !dirent_check_err(rdir))
|
|
return rdir;
|
|
|
|
retro_vfs_closedir_impl(rdir);
|
|
return NULL;
|
|
}
|
|
|
|
bool retro_vfs_readdir_impl(libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (rdir->smb_handle)
|
|
{
|
|
struct smbc_dirent *de = retro_vfs_readdir_smb(rdir->smb_handle);
|
|
if (!de)
|
|
return false;
|
|
strlcpy(rdir->smb_path, de->name, sizeof(rdir->smb_path));
|
|
rdir->smb_is_dir = (de->type == RETRO_SMB_DIRENT_DIR);
|
|
return true;
|
|
}
|
|
/* If we opened an SMB path but failed, do not fall through to native readdir */
|
|
if (path_is_smb(rdir->orig_path))
|
|
return false;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (rdir->nfs_handle)
|
|
{
|
|
struct nfs_dirent *de = retro_vfs_readdir_nfs(rdir->nfs_handle);
|
|
if (!de)
|
|
return false;
|
|
rdir->nfs_name = de->name;
|
|
rdir->nfs_is_dir = (de->type == RETRO_NFS_DIRENT_DIR);
|
|
return true;
|
|
}
|
|
if (path_is_nfs(rdir->orig_path))
|
|
return false;
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (rdir->saf_directory != NULL)
|
|
return retro_vfs_readdir_saf(rdir->saf_directory);
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
if (rdir->next)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
{
|
|
if (win32_needs_local_encoding())
|
|
return (FindNextFileA(rdir->directory, &rdir->entry.a) != 0);
|
|
return (FindNextFileW(rdir->directory, &rdir->entry.w) != 0);
|
|
}
|
|
#elif defined(LEGACY_WIN32)
|
|
return (FindNextFile(rdir->directory, &rdir->entry) != 0);
|
|
#else
|
|
return (FindNextFileW(rdir->directory, &rdir->entry) != 0);
|
|
#endif
|
|
|
|
rdir->next = true;
|
|
return (rdir->directory != INVALID_HANDLE_VALUE);
|
|
#elif defined(VITA)
|
|
return (sceIoDread(rdir->directory, &rdir->entry) > 0);
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
uint64_t nread;
|
|
rdir->error = sysFsReaddir(rdir->directory, &rdir->entry, &nread);
|
|
return (nread != 0);
|
|
#else
|
|
return ((rdir->entry = readdir(rdir->directory)) != NULL);
|
|
#endif
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
#if defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
/* Which codepages actually need converting is a question for
|
|
* encodings/, so ask it there rather than restate it here; on targets
|
|
* where the answer is "none" this allocates nothing. */
|
|
static const char *vfs_win32_name_local(
|
|
libretro_vfs_implementation_dir *rdir, const char *src)
|
|
{
|
|
if (!local_to_utf8_string(src, rdir->name_utf8, sizeof(rdir->name_utf8)))
|
|
return NULL;
|
|
return rdir->name_utf8;
|
|
}
|
|
#endif
|
|
|
|
#if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME)
|
|
/* utf16_conv_utf8() writes without an output bound and does not
|
|
* terminate; VFS_WIN32_NAME_UTF8_MAX is what makes the first safe and
|
|
* the store below covers the second. Taking it directly is what drops
|
|
* the per-entry allocation - utf16_to_utf8_string_alloc() and
|
|
* utf16_to_char_string() both malloc internally. */
|
|
static const char *vfs_win32_name_utf16(
|
|
libretro_vfs_implementation_dir *rdir, const wchar_t *src)
|
|
{
|
|
char *name;
|
|
size_t out_len = 0;
|
|
size_t in_len = 0;
|
|
|
|
while (src[in_len])
|
|
in_len++;
|
|
|
|
if (utf16_conv_utf8((uint8_t*)rdir->name_utf8, &out_len,
|
|
(const uint16_t*)src, in_len))
|
|
{
|
|
rdir->name_utf8[out_len] = '\0';
|
|
return rdir->name_utf8;
|
|
}
|
|
|
|
/* Unpaired surrogate: utf16_conv_utf8() stops at one, and NTFS
|
|
* permits them in names. WideCharToMultiByte() substitutes U+FFFD
|
|
* and carries on, so fall back to it rather than hand back a name
|
|
* truncated at the bad unit. Rare enough that the allocation this
|
|
* costs does not matter. */
|
|
if (!(name = utf16_to_utf8_string_alloc(src)))
|
|
return NULL;
|
|
strlcpy(rdir->name_utf8, name, sizeof(rdir->name_utf8));
|
|
free(name);
|
|
return rdir->name_utf8;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
const char *retro_vfs_dirent_get_name_impl(libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (rdir->smb_handle)
|
|
return rdir->smb_path;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (rdir->nfs_handle)
|
|
return rdir->nfs_name;
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (rdir->saf_directory != NULL)
|
|
return retro_vfs_dirent_get_name_saf(rdir->saf_directory);
|
|
else
|
|
#endif
|
|
{
|
|
#if defined(_WIN32)
|
|
#if defined(LEGACY_WIN32_RUNTIME)
|
|
if (win32_needs_local_encoding())
|
|
return vfs_win32_name_local(rdir, rdir->entry.a.cFileName);
|
|
return vfs_win32_name_utf16(rdir, rdir->entry.w.cFileName);
|
|
#elif defined(LEGACY_WIN32)
|
|
return vfs_win32_name_local(rdir, rdir->entry.cFileName);
|
|
#else
|
|
return vfs_win32_name_utf16(rdir, rdir->entry.cFileName);
|
|
#endif
|
|
#elif defined(VITA) || defined(__PSL1GHT__) || defined(__PS3__)
|
|
return rdir->entry.d_name;
|
|
#else
|
|
if (!rdir || !rdir->entry)
|
|
return NULL;
|
|
return rdir->entry->d_name;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if !defined(_WIN32) && !defined(VITA) && !defined(__PSL1GHT__) && !defined(__PS3__)
|
|
/* Split out of retro_vfs_dirent_is_dir_impl() so that the d_type test
|
|
* there does not carry this one's frame. Filesystems that populate
|
|
* d_type - ext4, APFS, NTFS - answer from the dirent alone and never
|
|
* reach this. The joined path is on the heap: next to the stat it is
|
|
* cheap, and it keeps the frame within the 2 KiB thread-stack budget. */
|
|
static VFS_NOINLINE bool retro_vfs_dirent_is_dir_stat(
|
|
libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
struct stat buf;
|
|
bool is_dir;
|
|
char *path = (char*)malloc(PATH_MAX_LENGTH);
|
|
|
|
if (!path)
|
|
return false;
|
|
fill_pathname_join_special(path, rdir->orig_path,
|
|
retro_vfs_dirent_get_name_impl(rdir), PATH_MAX_LENGTH);
|
|
is_dir = stat(path, &buf) == 0 && S_ISDIR(buf.st_mode);
|
|
free(path);
|
|
return is_dir;
|
|
}
|
|
#endif
|
|
|
|
bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (rdir->smb_handle)
|
|
return rdir->smb_is_dir;
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (rdir->nfs_handle)
|
|
return rdir->nfs_is_dir;
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (rdir->saf_directory != NULL)
|
|
return retro_vfs_dirent_is_dir_saf(rdir->saf_directory);
|
|
else
|
|
#endif
|
|
{
|
|
#if defined(_WIN32)
|
|
const WIN32_FIND_DATA *entry = (const WIN32_FIND_DATA*)&rdir->entry;
|
|
return entry->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
|
|
#elif defined(VITA)
|
|
const SceIoDirent *entry = (const SceIoDirent*)&rdir->entry;
|
|
return SCE_S_ISDIR(entry->d_stat.st_mode);
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
sysFSDirent *entry = (sysFSDirent*)&rdir->entry;
|
|
return (entry->d_type == FS_TYPE_DIR);
|
|
#else
|
|
#if defined(DT_DIR)
|
|
const struct dirent *entry = (const struct dirent*)rdir->entry;
|
|
if (entry->d_type == DT_DIR)
|
|
return true;
|
|
/* This can happen on certain file systems. */
|
|
if (!(entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK))
|
|
return false;
|
|
#endif
|
|
/* dirent struct doesn't have d_type, do it the slow way ... */
|
|
return retro_vfs_dirent_is_dir_stat(rdir);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* The join-and-stat fallback: one full-path stat per entry, which is
|
|
* exactly what a caller without dirent_stat would do itself. Split out
|
|
* so the fast paths do not carry its frame, and the joined path is on
|
|
* the heap to keep this one within the 2 KiB thread-stack budget. Only
|
|
* compiled where some branch of dirent_stat reaches it. */
|
|
#if defined(HAVE_SMBCLIENT) || defined(HAVE_NFSCLIENT) || (defined(ANDROID) && defined(HAVE_SAF)) \
|
|
|| !(defined(_WIN32) || defined(VITA) \
|
|
|| defined(VFS_HAVE_FSTATAT))
|
|
static VFS_NOINLINE int retro_vfs_dirent_stat_slow(
|
|
libretro_vfs_implementation_dir *rdir, int64_t *size, int64_t *mtime)
|
|
{
|
|
char *path;
|
|
int flags;
|
|
const char *name = retro_vfs_dirent_get_name_impl(rdir);
|
|
if (!name || !rdir->orig_path || !(path = (char*)malloc(PATH_MAX_LENGTH)))
|
|
return 0;
|
|
fill_pathname_join_special(path, rdir->orig_path, name, PATH_MAX_LENGTH);
|
|
flags = retro_vfs_stat_full(path, size, mtime);
|
|
free(path);
|
|
return flags;
|
|
}
|
|
#endif
|
|
|
|
int retro_vfs_dirent_stat_impl(libretro_vfs_implementation_dir *rdir,
|
|
int64_t *size, int64_t *mtime)
|
|
{
|
|
if (!rdir)
|
|
return 0;
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (rdir->smb_handle)
|
|
return retro_vfs_dirent_stat_slow(rdir, size, mtime);
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (rdir->nfs_handle)
|
|
return retro_vfs_dirent_stat_slow(rdir, size, mtime);
|
|
#endif
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (rdir->saf_directory != NULL)
|
|
return retro_vfs_dirent_stat_slow(rdir, size, mtime);
|
|
else
|
|
#endif
|
|
{
|
|
#if defined(_WIN32)
|
|
/* Everything is already in the find data; no I/O at all. */
|
|
const WIN32_FIND_DATA *entry = (const WIN32_FIND_DATA*)&rdir->entry;
|
|
int ret = RETRO_VFS_STAT_IS_VALID;
|
|
if (size)
|
|
*size = (int64_t)(((uint64_t)entry->nFileSizeHigh << 32)
|
|
| (uint64_t)entry->nFileSizeLow);
|
|
if (mtime)
|
|
*mtime = vfs_filetime_to_unix(&entry->ftLastWriteTime);
|
|
if (entry->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (entry->dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
return ret;
|
|
#elif defined(VITA)
|
|
const SceIoDirent *entry = (const SceIoDirent*)&rdir->entry;
|
|
int ret = RETRO_VFS_STAT_IS_VALID;
|
|
if (size)
|
|
*size = (int64_t)entry->d_stat.st_size;
|
|
if (mtime)
|
|
{
|
|
time_t t = 0;
|
|
sceRtcGetTime_t(&entry->d_stat.st_mtime, &t);
|
|
*mtime = (int64_t)t;
|
|
}
|
|
if (SCE_S_ISDIR(entry->d_stat.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (!(entry->d_stat.st_mode & SCE_S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
return ret;
|
|
#elif defined(VFS_HAVE_FSTATAT)
|
|
/* fstatat on the open directory: no path join, no lookup from
|
|
* the root, one inode read. */
|
|
const struct dirent *entry = (const struct dirent*)rdir->entry;
|
|
struct stat st;
|
|
int ret = RETRO_VFS_STAT_IS_VALID;
|
|
if (!entry || fstatat(dirfd(rdir->directory), entry->d_name, &st, 0) < 0)
|
|
return 0;
|
|
if (size)
|
|
*size = (int64_t)st.st_size;
|
|
if (mtime)
|
|
*mtime = (int64_t)st.st_mtime;
|
|
if (S_ISDIR(st.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_DIRECTORY;
|
|
if (S_ISCHR(st.st_mode))
|
|
ret |= RETRO_VFS_STAT_IS_CHARACTER_SPECIAL;
|
|
if (!(st.st_mode & S_IWUSR))
|
|
ret |= RETRO_VFS_STAT_IS_READONLY;
|
|
return ret;
|
|
#else
|
|
return retro_vfs_dirent_stat_slow(rdir, size, mtime);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
int retro_vfs_closedir_impl(libretro_vfs_implementation_dir *rdir)
|
|
{
|
|
int ret = 0;
|
|
|
|
if (!rdir)
|
|
return -1;
|
|
|
|
#ifdef HAVE_SMBCLIENT
|
|
if (rdir->smb_handle)
|
|
{
|
|
retro_vfs_closedir_smb(rdir->smb_handle);
|
|
rdir->smb_handle = NULL;
|
|
}
|
|
#endif
|
|
#ifdef HAVE_NFSCLIENT
|
|
if (rdir->nfs_handle)
|
|
{
|
|
retro_vfs_closedir_nfs(rdir->nfs_handle);
|
|
rdir->nfs_handle = NULL;
|
|
}
|
|
#endif
|
|
|
|
#if defined(ANDROID) && defined(HAVE_SAF)
|
|
if (rdir->saf_directory != NULL)
|
|
ret = retro_vfs_closedir_saf(rdir->saf_directory);
|
|
else
|
|
#endif
|
|
{
|
|
#if defined(_WIN32)
|
|
if (rdir->directory != INVALID_HANDLE_VALUE)
|
|
FindClose(rdir->directory);
|
|
#elif defined(VITA)
|
|
sceIoDclose(rdir->directory);
|
|
#elif defined(__PSL1GHT__) || defined(__PS3__)
|
|
rdir->error = sysFsClosedir(rdir->directory);
|
|
#else
|
|
if (rdir->directory)
|
|
closedir(rdir->directory);
|
|
#endif
|
|
}
|
|
|
|
if (rdir->orig_path)
|
|
free(rdir->orig_path);
|
|
free(rdir);
|
|
return ret;
|
|
}
|