diff --git a/.github/workflows/Cross-libc-vfs-samples.yml b/.github/workflows/Cross-libc-vfs-samples.yml index 8e40aedb6d14..cd55d435a0c4 100644 --- a/.github/workflows/Cross-libc-vfs-samples.yml +++ b/.github/workflows/Cross-libc-vfs-samples.yml @@ -89,6 +89,7 @@ jobs: ASAN_OPTIONS=${{ matrix.asan_opts }} ./vfs_bulk_read_test ./vfs_seek_contract_test ./vfs_mapped_ptr_test + ./vfs_v5_metadata_test } # Mapping on (what the platform actually ships), mapping off # with the descriptor path forced (what the consoles have), @@ -158,6 +159,9 @@ jobs: make MMAP=0 ./vfs_bulk_read_test.exe ./vfs_seek_contract_test.exe + # Win32 branches of the v5 metadata ops: attributes, + # SetFileTime, CopyFileW, find-data dirent_stat. + ./vfs_v5_metadata_test.exe # Exercises the Win32 CreateFileMapping/MapViewOfFile # backend, which no other job in the tree reaches. ./vfs_mapped_ptr_test.exe diff --git a/.github/workflows/Linux-libretro-common-samples.yml b/.github/workflows/Linux-libretro-common-samples.yml index 82491b1fb145..23ad947ab3e2 100644 --- a/.github/workflows/Linux-libretro-common-samples.yml +++ b/.github/workflows/Linux-libretro-common-samples.yml @@ -99,6 +99,7 @@ jobs: vfs_large_file_test vfs_seek_contract_test vfs_bulk_read_test + vfs_v5_metadata_test vfs_hybrid_test filestream_rbuf_fault_test cdrom_cuesheet_overflow_test @@ -819,3 +820,22 @@ jobs: UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \ ./vfs_mapped_ptr_test echo "[pass] vfs_mapped_ptr_test (MMAP=0, ASan)" + + - name: Run vfs_v5_metadata_test against a copy_file_range that ignores len + shell: bash + working-directory: libretro-common/samples/file/vfs + run: | + set -eu + # A sandboxed kernel (gVisor on some hosted runners) answers + # copy_file_range by copying to EOF regardless of len. The + # VFS must account the bytes, retire the kernel path for the + # process, and finish the copy in place on the portable path. + # FAKE_CFR=1 is the to-EOF case, FAKE_CFR=2 a partial overshoot + # that leaves the copy part-way and has to be resumed. + for mode in 1 2; do + make clean >/dev/null + make vfs_v5_metadata_test FAKE_CFR=$mode SANITIZER=address,undefined + ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 timeout 120 \ + ./vfs_v5_metadata_test + echo "[pass] vfs_v5_metadata_test (FAKE_CFR=$mode, ASan)" + done diff --git a/libretro-common/file/file_path_io.c b/libretro-common/file/file_path_io.c index 19606d236939..3559ef9920b2 100644 --- a/libretro-common/file/file_path_io.c +++ b/libretro-common/file/file_path_io.c @@ -71,6 +71,12 @@ static retro_vfs_stat_t path_stat32_cb = retro_vfs_stat_impl; static retro_vfs_stat_64_t path_stat64_cb = retro_vfs_stat_64_impl; static retro_vfs_mkdir_t path_mkdir_cb = retro_vfs_mkdir_impl; +/* VFS API v5. NULL when a frontend older than v5 is in use, so the + * wrappers report failure instead of touching the local file system + * behind a foreign frontend's back. */ +static retro_vfs_set_readonly_t path_set_readonly_cb = retro_vfs_set_readonly_impl; +static retro_vfs_get_mtime_t path_get_mtime_cb = retro_vfs_get_mtime_impl; +static retro_vfs_set_mtime_t path_set_mtime_cb = retro_vfs_set_mtime_impl; void path_vfs_init(const struct retro_vfs_interface_info* vfs_info) { @@ -80,6 +86,9 @@ void path_vfs_init(const struct retro_vfs_interface_info* vfs_info) path_stat32_cb = retro_vfs_stat_impl; path_stat64_cb = retro_vfs_stat_64_impl; path_mkdir_cb = retro_vfs_mkdir_impl; + path_set_readonly_cb = retro_vfs_set_readonly_impl; + path_get_mtime_cb = retro_vfs_get_mtime_impl; + path_set_mtime_cb = retro_vfs_set_mtime_impl; if (vfs_info->required_interface_version < PATH_REQUIRED_VFS_VERSION || !vfs_iface) return; @@ -91,6 +100,46 @@ void path_vfs_init(const struct retro_vfs_interface_info* vfs_info) path_stat64_cb = vfs_iface->stat_64; else path_stat64_cb = NULL; + + /* Members a v5 frontend left NULL stay NULL: the wrappers then + * report "unavailable" rather than dereferencing them. */ + path_set_readonly_cb = NULL; + path_get_mtime_cb = NULL; + path_set_mtime_cb = NULL; + if (vfs_info->required_interface_version >= METADATA_REQUIRED_VFS_VERSION) + { + path_set_readonly_cb = vfs_iface->set_readonly; + path_get_mtime_cb = vfs_iface->get_mtime; + path_set_mtime_cb = vfs_iface->set_mtime; + } +} + +bool path_is_readonly(const char *path) +{ + if (path_stat64_cb) + return (path_stat64_cb(path, NULL) & RETRO_VFS_STAT_IS_READONLY) != 0; + return (path_stat32_cb(path, NULL) & RETRO_VFS_STAT_IS_READONLY) != 0; +} + +bool path_set_readonly(const char *path, bool readonly) +{ + if (!path_set_readonly_cb) + return false; + return path_set_readonly_cb(path, readonly ? 1 : 0) == 0; +} + +bool path_get_mtime(const char *path, int64_t *mtime) +{ + if (!path_get_mtime_cb || !mtime) + return false; + return path_get_mtime_cb(path, mtime) == 0; +} + +bool path_set_mtime(const char *path, int64_t mtime) +{ + if (!path_set_mtime_cb) + return false; + return path_set_mtime_cb(path, mtime) == 0; } int path_stat(const char *path) diff --git a/libretro-common/file/retro_dirent.c b/libretro-common/file/retro_dirent.c index da6412ee8322..8bdd1b7051d9 100644 --- a/libretro-common/file/retro_dirent.c +++ b/libretro-common/file/retro_dirent.c @@ -37,6 +37,11 @@ static retro_vfs_readdir_t dirent_readdir_cb = NULL; static retro_vfs_dirent_get_name_t dirent_dirent_get_name_cb = NULL; static retro_vfs_dirent_is_dir_t dirent_dirent_is_dir_cb = NULL; static retro_vfs_closedir_t dirent_closedir_cb = NULL; +static retro_vfs_dirent_stat_t dirent_dirent_stat_cb = NULL; +/* Set when a frontend older than VFS API v5 owns the directory + * handles: the local _impl cannot be used on a foreign handle, so + * retro_dirent_stat() reports "unavailable" instead. */ +static bool dirent_stat_unavailable = false; void dirent_vfs_init(const struct retro_vfs_interface_info* vfs_info) { @@ -47,6 +52,8 @@ void dirent_vfs_init(const struct retro_vfs_interface_info* vfs_info) dirent_dirent_get_name_cb = NULL; dirent_dirent_is_dir_cb = NULL; dirent_closedir_cb = NULL; + dirent_dirent_stat_cb = NULL; + dirent_stat_unavailable = false; vfs_iface = vfs_info->iface; @@ -60,6 +67,14 @@ void dirent_vfs_init(const struct retro_vfs_interface_info* vfs_info) dirent_dirent_get_name_cb = vfs_iface->dirent_get_name; dirent_dirent_is_dir_cb = vfs_iface->dirent_is_dir; dirent_closedir_cb = vfs_iface->closedir; + + /* A frontend that negotiated v5 but left the member NULL is treated + * like an older one: the local _impl must not touch its handles. */ + if (vfs_info->required_interface_version >= DIRENT_STAT_REQUIRED_VFS_VERSION + && vfs_iface->dirent_stat) + dirent_dirent_stat_cb = vfs_iface->dirent_stat; + else + dirent_stat_unavailable = true; } struct RDIR *retro_opendir_include_hidden( @@ -113,6 +128,17 @@ bool retro_dirent_is_dir(struct RDIR *rdir, const char *unused) return retro_vfs_dirent_is_dir_impl((struct retro_vfs_dir_handle *)rdir); } +int retro_dirent_stat(struct RDIR *rdir, int64_t *size, int64_t *mtime) +{ + if (!rdir) + return 0; + if (dirent_dirent_stat_cb) + return dirent_dirent_stat_cb((struct retro_vfs_dir_handle *)rdir, size, mtime); + if (dirent_stat_unavailable) + return 0; + return retro_vfs_dirent_stat_impl((struct retro_vfs_dir_handle *)rdir, size, mtime); +} + void retro_closedir(struct RDIR *rdir) { if (dirent_closedir_cb) diff --git a/libretro-common/include/file/file_path.h b/libretro-common/include/file/file_path.h index f9a58233f207..6cc9392936ed 100644 --- a/libretro-common/include/file/file_path.h +++ b/libretro-common/include/file/file_path.h @@ -37,6 +37,7 @@ RETRO_BEGIN_DECLS #define PATH_REQUIRED_VFS_VERSION 3 #define STAT64_REQUIRED_VFS_VERSION 4 +#define METADATA_REQUIRED_VFS_VERSION 5 void path_vfs_init(const struct retro_vfs_interface_info* vfs_info); @@ -696,6 +697,48 @@ bool path_is_valid(const char *path); **/ bool path_set_private(const char *path); +/** + * path_is_readonly: + * @path : path + * + * Whether the current user cannot write to @path. Reads the + * RETRO_VFS_STAT_IS_READONLY flag, which frontends older than + * VFS API v5 never set, so the answer there is always false. + * + * @return true if @path exists and is read-only. + **/ +bool path_is_readonly(const char *path); + +/** + * path_set_readonly: + * @path : path + * @readonly : true to make read-only, false to make writable + * + * POSIX: toggles the write bits. Windows: FILE_ATTRIBUTE_READONLY. + * + * @return true on success, false if unsupported on this platform, + * the file system, or the negotiated VFS version (< 5). + **/ +bool path_set_readonly(const char *path, bool readonly); + +/** + * path_get_mtime: + * @path : path + * @mtime : receives seconds since 1970-01-01T00:00:00Z + * + * @return true on success, false if unavailable. + **/ +bool path_get_mtime(const char *path, int64_t *mtime); + +/** + * path_set_mtime: + * @path : path + * @mtime : seconds since 1970-01-01T00:00:00Z + * + * @return true on success, false if unsupported or it failed. + **/ +bool path_set_mtime(const char *path, int64_t mtime); + int64_t path_get_size(const char *path); bool is_path_accessible_using_standard_io(const char *path); diff --git a/libretro-common/include/libretro.h b/libretro-common/include/libretro.h index e0028f617e8c..fc56ae83f218 100644 --- a/libretro-common/include/libretro.h +++ b/libretro-common/include/libretro.h @@ -3125,8 +3125,49 @@ struct retro_vfs_dir_handle; */ #define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2) +/** + * Indicates that the current user cannot write to the given path. + * POSIX: the owner write bit is clear. + * Windows/UWP: \c FILE_ATTRIBUTE_READONLY is set. + * Frontends that cannot determine this never set the flag. + * @since VFS API v5 + */ +#define RETRO_VFS_STAT_IS_READONLY (1 << 3) + /** @} */ +/** + * @defgroup RETRO_VFS_COPY Copy Flags + * @since VFS API v5 + * @{ + */ + +/** Replace \c dst if it already exists. Without it an existing \c dst is an error. */ +#define RETRO_VFS_COPY_OVERWRITE (1 << 0) + +/** @} */ + +/** + * @defgroup RETRO_VFS_COPY_STATUS Copy Status + * Values returned by \c retro_vfs_copy_step_t. + * @since VFS API v5 + * @{ + */ +/** The copy is still in progress. */ +#define RETRO_VFS_COPY_RUNNING (0) +/** The copy completed; \c dst is complete and closed. */ +#define RETRO_VFS_COPY_DONE (1) +/** The copy failed or was cancelled; no partial \c dst remains. */ +#define RETRO_VFS_COPY_FAILED (-1) +/** @} */ + +/** + * Opaque handle to an in-progress file copy. + * @see retro_vfs_copy_begin_t + * @since VFS API v5 + */ +struct retro_vfs_copy_handle; + /** * Returns the path that was used to open this file. * @@ -3318,6 +3359,124 @@ typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size); */ typedef int (RETRO_CALLCONV *retro_vfs_stat_64_t)(const char *path, int64_t *size); +/** + * Sets or clears the read-only state of a file or directory. + * + * POSIX: sets or clears the write bits of the mode, leaving the rest intact. + * Windows/UWP: sets or clears \c FILE_ATTRIBUTE_READONLY. + * + * @param path The path to the file or directory. + * @param readonly Non-zero to make the path read-only, + * zero to make it writable. + * @return 0 on success, + * or -1 if \c path does not exist or the platform or file system + * cannot store a read-only state. + * @see path_set_readonly + * @see RETRO_VFS_STAT_IS_READONLY + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_set_readonly_t)(const char *path, int readonly); + +/** + * Gets the last modification time of a file or directory. + * + * @param path The path to the file or directory. + * @param[out] mtime Set to the modification time + * in seconds since 1970-01-01T00:00:00Z. May be negative. + * @return 0 on success, + * or -1 if \c path does not exist or the platform + * cannot report a modification time. + * @see path_get_mtime + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_get_mtime_t)(const char *path, int64_t *mtime); + +/** + * Sets the last modification time of a file or directory. + * + * The frontend rounds to the file system's resolution, + * so a following \c retro_vfs_get_mtime_t may report a different value. + * + * @param path The path to the file or directory. + * @param mtime The modification time in seconds since 1970-01-01T00:00:00Z. + * @return 0 on success, + * or -1 if \c path does not exist or the platform or file system + * does not allow the modification time to be set. + * @see path_set_mtime + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_set_mtime_t)(const char *path, int64_t mtime); + +/** + * Starts copying a single regular file and returns without moving any of it. + * + * A copy is a resumable operation that the caller advances with + * \c retro_vfs_copy_step_t, each step bounded by a byte budget the caller + * chooses. The frontend keeps no thread and holds no lock for it; a caller + * that wants the transfer off its own thread drives the steps from wherever + * it likes. No call in this group ever waits for more than the requested + * step. + * + * \c dst is the full path of the new file, not a directory; missing parent + * directories are created. Metadata (modification time, read-only state) + * of \c dst after the copy is platform-defined. Either path may belong to + * any file system the frontend supports. + * + * Checks that can be made up front (missing or non-regular \c src, + * \c dst is a directory, \c dst exists without \c RETRO_VFS_COPY_OVERWRITE, + * \c src equals \c dst) fail here by returning \c NULL. + * + * @param src The path to the file to copy. Must be a regular file. + * @param dst The full path of the destination file. Must differ from \c src. + * @param flags Bitwise combination of \c RETRO_VFS_COPY flags, or 0. + * @return A handle to poll and close, or \c NULL if the copy could not start. + * @see retro_vfs_copy_step_t + * @see retro_vfs_copy_close_t + * @see filestream_copy_begin + * @see RETRO_VFS_COPY + * @since VFS API v5 + */ +typedef struct retro_vfs_copy_handle *(RETRO_CALLCONV *retro_vfs_copy_begin_t)(const char *src, const char *dst, unsigned flags); + +/** + * Advances a copy started with \c retro_vfs_copy_begin_t by at most + * \c max_bytes and reports its state. + * + * The budget is the caller's latency/throughput dial: a few MiB from a + * frame loop keeps each call short; a very large budget (or repeated calls + * until the status leaves \c RETRO_VFS_COPY_RUNNING) runs the transfer at + * the full speed of the platform's copy primitive with no user-space + * buffer where the kernel can move the bytes itself. + * + * A step never moves more than \c max_bytes, but it may move less, and it + * may report \c RETRO_VFS_COPY_DONE early if the platform completed the + * copy without moving bytes (e.g. a file-system clone). + * + * @param handle The copy. + * @param max_bytes Upper bound on bytes moved by this call; 0 selects a + * frontend default sized for a frame loop (a few MiB). + * @param[out] bytes_done Bytes written to \c dst so far. May be \c NULL. + * @param[out] bytes_total Size of \c src in bytes. May be \c NULL. + * @return One of the \c RETRO_VFS_COPY_STATUS values. + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_copy_step_t)(struct retro_vfs_copy_handle *handle, int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total); + +/** + * Releases a copy handle. + * + * If the copy is still running it is cancelled and the partial \c dst + * removed; nothing is waited for. Must be called exactly once for every + * non-NULL handle from \c retro_vfs_copy_begin_t, whatever + * \c retro_vfs_copy_step_t reported. + * + * @param handle The copy. + * @return 0 if the copy had completed successfully, or -1 if it failed, + * was cancelled, or was still running when closed. + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_copy_close_t)(struct retro_vfs_copy_handle *handle); + /** * Creates a directory at the given path. * @@ -3390,6 +3549,29 @@ typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_v */ typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream); +/** + * Gets information about the directory entry most recently returned by + * \c retro_vfs_readdir_t, without opening it or building its path. + * + * Only valid after a \c retro_vfs_readdir_t call that returned \c true, + * and before the next \c retro_vfs_readdir_t or \c retro_vfs_closedir_t + * call on the same handle. + * + * @param dirstream The directory being enumerated. + * @param[out] size The entry's size in bytes (0 for directories). + * May be \c NULL, in which case this value is ignored. + * @param[out] mtime The entry's modification time + * in seconds since 1970-01-01T00:00:00Z. + * May be \c NULL, in which case this value is ignored. + * @return A bitmask of \c RETRO_VFS_STAT flags for the entry + * (\c RETRO_VFS_STAT_IS_VALID is always set on success), + * or 0 if the frontend cannot provide entry information. + * @see retro_dirent_stat + * @see RETRO_VFS_STAT + * @since VFS API v5 + */ +typedef int (RETRO_CALLCONV *retro_vfs_dirent_stat_t)(struct retro_vfs_dir_handle *dirstream, int64_t *size, int64_t *mtime); + /** * Closes the given directory and release its resources. * @@ -3477,6 +3659,28 @@ struct retro_vfs_interface /* VFS API v4 */ /** @copydoc retro_vfs_stat_64_t */ retro_vfs_stat_64_t stat_64; + + /* VFS API v5 */ + /** @copydoc retro_vfs_set_readonly_t */ + retro_vfs_set_readonly_t set_readonly; + + /** @copydoc retro_vfs_get_mtime_t */ + retro_vfs_get_mtime_t get_mtime; + + /** @copydoc retro_vfs_set_mtime_t */ + retro_vfs_set_mtime_t set_mtime; + + /** @copydoc retro_vfs_copy_begin_t */ + retro_vfs_copy_begin_t copy_begin; + + /** @copydoc retro_vfs_copy_step_t */ + retro_vfs_copy_step_t copy_step; + + /** @copydoc retro_vfs_copy_close_t */ + retro_vfs_copy_close_t copy_close; + + /** @copydoc retro_vfs_dirent_stat_t */ + retro_vfs_dirent_stat_t dirent_stat; }; /** diff --git a/libretro-common/include/retro_dirent.h b/libretro-common/include/retro_dirent.h index d4af7e27d059..ef9b07417dac 100644 --- a/libretro-common/include/retro_dirent.h +++ b/libretro-common/include/retro_dirent.h @@ -42,6 +42,7 @@ RETRO_BEGIN_DECLS * @see retro_vfs_interface_info */ #define DIRENT_REQUIRED_VFS_VERSION 3 +#define DIRENT_STAT_REQUIRED_VFS_VERSION 5 /** * Installs a frontend-provided VFS interface for the dirent functions to use @@ -153,6 +154,23 @@ const char *retro_dirent_get_name(struct RDIR *rdir); */ bool retro_dirent_is_dir(struct RDIR *rdir, const char *unused); +/** + * Gets size, modification time and stat flags for the current dirent + * without opening it or building its path. Free on Windows (already in + * the find data), one fstatat on POSIX. + * + * Only valid after a \c retro_readdir that returned \c true and before + * the next \c retro_readdir or \c retro_closedir on the same handle. + * + * @param rdir The directory being enumerated. + * @param size If non-NULL, receives the entry size in bytes (0 for directories). + * @param mtime If non-NULL, receives seconds since 1970-01-01T00:00:00Z. + * @return A bitmask of \c RETRO_VFS_STAT flags for the entry, + * or 0 if unavailable (including frontends older than VFS API v5). + * @see retro_readdir + */ +int retro_dirent_stat(struct RDIR *rdir, int64_t *size, int64_t *mtime); + /** * Closes an opened \c RDIR that was returned by \c retro_opendir. * diff --git a/libretro-common/include/streams/file_stream.h b/libretro-common/include/streams/file_stream.h index c3026935d448..90c6cd7cbf5e 100644 --- a/libretro-common/include/streams/file_stream.h +++ b/libretro-common/include/streams/file_stream.h @@ -54,6 +54,7 @@ * The minimum version of the VFS interface required by the \c filestream functions. */ #define FILESTREAM_REQUIRED_VFS_VERSION 2 +#define FILESTREAM_COPY_REQUIRED_VFS_VERSION 5 RETRO_BEGIN_DECLS @@ -63,8 +64,6 @@ RETRO_BEGIN_DECLS */ typedef struct RFILE RFILE; -#define FILESTREAM_REQUIRED_VFS_VERSION 2 - /** * Initializes the \c filestream functions to use the VFS interface provided by the frontend. * Optional; if not called, all \c filestream functions @@ -394,15 +393,56 @@ int filestream_delete(const char *path); int filestream_rename(const char *old_path, const char *new_path); /** - * Copies a file to a new location. + * Copies a regular file to a new location, replacing an existing one, + * and does not return until it is done. * - * @param src_path Path to the file to rename. + * Kept for callers that predate VFS API v5. It copies through + * \c filestream_open/read/write and does not use the platform's copy + * primitive; new code should use \c filestream_copy_begin, which does + * and does not block. + * + * @param src_path Path to the file to copy. * @param dst_path The target name and location of the file. * @return 0 if the file was copied successfully, - * or -1 if there was an error. + * or -1 if there was an error (no partial \c dst_path is left behind). + * @see filestream_copy_begin */ int filestream_copy(const char *src_path, const char *dst_path); +/** + * Starts copying a regular file without moving any of it yet. + * Wraps \c retro_vfs_copy_begin_t; see it for the full contract. The + * caller advances the copy with \c filestream_copy_step; no thread is + * involved unless the caller supplies one. + * + * @param src_path Path to the file to copy. Must be a regular file. + * @param dst_path Full path of the destination. Must differ from \c src_path. + * @param flags Bitwise combination of \c RETRO_VFS_COPY flags, or 0. + * @return A handle for \c filestream_copy_step and \c filestream_copy_close, + * or \c NULL if the copy could not start (including when the frontend + * does not offer VFS API v5). + */ +struct retro_vfs_copy_handle *filestream_copy_begin(const char *src_path, const char *dst_path, unsigned flags); + +/** + * Advances a copy started with \c filestream_copy_begin by at most + * \c max_bytes and reports its state. See \c retro_vfs_copy_step_t for + * how to use the budget as a latency/throughput dial. + * + * @param max_bytes Upper bound on bytes moved by this call; 0 selects a + * default sized for a frame loop. + * @return One of the \c RETRO_VFS_COPY_STATUS values. + */ +int filestream_copy_step(struct retro_vfs_copy_handle *handle, int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total); + +/** + * Releases a copy handle, cancelling the copy if it is still running. + * See \c retro_vfs_copy_close_t. + * + * @return 0 if the copy had completed successfully, otherwise -1. + */ +int filestream_copy_close(struct retro_vfs_copy_handle *handle); + /** * Compares and verifies files. * diff --git a/libretro-common/include/vfs/vfs_implementation.h b/libretro-common/include/vfs/vfs_implementation.h index d03b2441344d..39b4c660866d 100644 --- a/libretro-common/include/vfs/vfs_implementation.h +++ b/libretro-common/include/vfs/vfs_implementation.h @@ -104,6 +104,15 @@ int retro_vfs_mkdir_impl(const char *dir); **/ int retro_vfs_restrict_permissions_impl(const char *path); +/* VFS API v5 */ +int retro_vfs_set_readonly_impl(const char *path, int readonly); +int retro_vfs_get_mtime_impl(const char *path, int64_t *mtime); +int retro_vfs_set_mtime_impl(const char *path, int64_t mtime); +struct retro_vfs_copy_handle *retro_vfs_copy_begin_impl(const char *src, const char *dst, unsigned flags); +int retro_vfs_copy_step_impl(struct retro_vfs_copy_handle *handle, int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total); +int retro_vfs_copy_close_impl(struct retro_vfs_copy_handle *handle); +int retro_vfs_dirent_stat_impl(libretro_vfs_implementation_dir *rdir, int64_t *size, int64_t *mtime); + libretro_vfs_implementation_dir *retro_vfs_opendir_impl(const char *dir, bool include_hidden); bool retro_vfs_readdir_impl(libretro_vfs_implementation_dir *dirstream); diff --git a/libretro-common/samples/file/vfs/Makefile b/libretro-common/samples/file/vfs/Makefile index c01ca591d58e..74604da14273 100644 --- a/libretro-common/samples/file/vfs/Makefile +++ b/libretro-common/samples/file/vfs/Makefile @@ -4,6 +4,7 @@ TARGET_TEST2 := vfs_large_file_test TARGET_TEST3 := vfs_seek_contract_test TARGET_TEST4 := vfs_bulk_read_test TARGET_TEST5 := filestream_rbuf_fault_test +TARGET_TEST6 := vfs_v5_metadata_test LIBRETRO_COMM_DIR := ../../.. @@ -13,6 +14,7 @@ COMMON_SOURCES := \ $(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \ $(LIBRETRO_COMM_DIR)/file/file_path.c \ $(LIBRETRO_COMM_DIR)/file/file_path_io.c \ + $(LIBRETRO_COMM_DIR)/file/retro_dirent.c \ $(LIBRETRO_COMM_DIR)/streams/file_stream.c \ $(LIBRETRO_COMM_DIR)/string/rstrtod.c \ $(LIBRETRO_COMM_DIR)/time/rtime.c \ @@ -21,7 +23,8 @@ COMMON_SOURCES := \ COMMON_OBJS := $(COMMON_SOURCES:.c=.o) OBJS := vfs_read_overflow_test.o vfs_mapped_ptr_test.o \ vfs_large_file_test.o vfs_seek_contract_test.o \ - vfs_bulk_read_test.o filestream_rbuf_fault_test.o $(COMMON_OBJS) + vfs_bulk_read_test.o filestream_rbuf_fault_test.o \ + vfs_v5_metadata_test.o $(COMMON_OBJS) # filestream_rbuf_fault_test drives the two arms on which # filestream_rbuf_fill() gives up and hands the caller back to the @@ -72,6 +75,17 @@ ifeq ($(DESCRIPTOR_IO),1) CFLAGS += -DVFS_HAVE_DESCRIPTOR_IO=1 endif +# FAKE_CFR=1 stands in a copy_file_range that copies to EOF whatever +# len it was given (what gVisor was observed doing); FAKE_CFR=2 one +# that copies three times len. Both exercise vfs_v5_metadata_test's +# distrust-and-resume path on a kernel that would never trip it. +ifneq ($(FAKE_CFR),) + CFLAGS += -DVFS_COPY_TEST_FAKE_CFR=$(FAKE_CFR) +endif +# Every sample build narrates the copy stepper's platform calls on +# stderr (which path, what was asked, what came back); a frontend +# build never defines this. +CFLAGS += -DVFS_COPY_DEBUG CFLAGS += -Wall -pedantic -std=gnu99 -g -I$(LIBRETRO_COMM_DIR)/include # The samples workflow passes SANITIZER=address,undefined to every @@ -83,7 +97,7 @@ ifneq ($(SANITIZER),) endif all: $(TARGET) $(TARGET_TEST) $(TARGET_TEST2) $(TARGET_TEST3) $(TARGET_TEST4) \ - $(TARGET_TEST5) + $(TARGET_TEST5) $(TARGET_TEST6) %.o: %.c $(CC) -c -o $@ $< $(CFLAGS) @@ -125,9 +139,18 @@ $(FAULT_FS_OBJ): $(LIBRETRO_COMM_DIR)/streams/file_stream.c \ $(TARGET_TEST5): filestream_rbuf_fault_test.o $(FAULT_OBJS) $(CC) -o $@ $^ $(LDFLAGS) +# vfs_v5_metadata_test covers the VFS API v5 additions (read-only +# state, mtime, begin/step/close copy, dirent_stat) through the path_*, +# filestream_copy* and retro_dirent_stat wrappers. The copy is a +# caller-stepped state machine: no threads, no extra objects. +$(TARGET_TEST6): vfs_v5_metadata_test.o $(COMMON_OBJS) + $(CC) -o $@ $^ $(LDFLAGS) + clean: rm -f $(TARGET) $(TARGET_TEST) $(TARGET_TEST2) $(TARGET_TEST3) \ - $(TARGET_TEST4) $(TARGET_TEST5) $(OBJS) $(FAULT_FS_OBJ) + $(TARGET_TEST4) $(TARGET_TEST5) $(TARGET_TEST6) \ + $(OBJS) $(FAULT_FS_OBJ) + rm -rf v5_meta_dir rm -f large_3gib.bin large_5gib.bin seek_small.bin seek_4gib.bin rm -f rbuf_fault.txt rm -f bulk_select.bin bulk_select_w.bin bulk_roundtrip.bin \ diff --git a/libretro-common/samples/file/vfs/vfs_v5_metadata_test.c b/libretro-common/samples/file/vfs/vfs_v5_metadata_test.c new file mode 100644 index 000000000000..64dd47c44794 --- /dev/null +++ b/libretro-common/samples/file/vfs/vfs_v5_metadata_test.c @@ -0,0 +1,425 @@ +/* Copyright (C) 2010-2026 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (vfs_v5_metadata_test.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. + */ + +/* VFS API v5 contract: read-only state, modification time, copy and + * per-entry stat during enumeration. Runs against the local + * implementation (no frontend), which is the code every frontend + * build links, so it covers the _impl functions and the path_*, + * filestream_copy* and retro_dirent_stat wrappers over them. + * + * Everything lives in one temporary directory created next to the + * binary and removed at the end. Skips (not failures) are reported + * where the host file system cannot store the state under test. */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if !defined(_WIN32) +#include /* geteuid: root ignores mode bits */ +#endif + +#define DIR_NAME "v5_meta_dir" +#define BIG_SIZE (3u * 1024u * 1024u + 17u) /* > any fast-path chunk, odd tail */ + +static int failures = 0; +static int skips = 0; + +#define CHECK(cond, what) do { \ + if (cond) printf(" ok %s\n", what); \ + else { printf(" FAIL %s (%s:%d)\n", what, __FILE__, __LINE__); failures++; } \ +} while (0) + +#define SKIP(what) do { printf(" skip %s\n", what); skips++; } while (0) + +static bool write_pattern(const char *path, size_t len, unsigned seed) +{ + size_t i; + RFILE *f = filestream_open(path, RETRO_VFS_FILE_ACCESS_WRITE, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + unsigned char *buf; + if (!f) + return false; + buf = (unsigned char*)malloc(len ? len : 1); + for (i = 0; i < len; i++) + buf[i] = (unsigned char)((i * 2654435761u + seed) >> 13); + if (filestream_write(f, buf, (int64_t)len) != (int64_t)len) + { + free(buf); + filestream_close(f); + return false; + } + free(buf); + return filestream_close(f) == 0; +} + +static bool files_equal(const char *a, const char *b) +{ + /* filestream_cmp lives in a different unit in some trees; a + * self-contained byte compare keeps this sample's link list short. */ + RFILE *fa = filestream_open(a, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + RFILE *fb = filestream_open(b, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); + unsigned char ba[65536], bb[65536]; + bool same = (fa && fb); + while (same) + { + int64_t na = filestream_read(fa, ba, sizeof(ba)); + int64_t nb = filestream_read(fb, bb, sizeof(bb)); + if (na != nb || na < 0) + same = false; + else if (na == 0) + break; + else if (memcmp(ba, bb, (size_t)na) != 0) + same = false; + } + if (fa) filestream_close(fa); + if (fb) filestream_close(fb); + return same; +} + +static void test_readonly(const char *dir) +{ + char p[512]; + printf("read-only:\n"); + snprintf(p, sizeof(p), "%s/ro.bin", dir); + CHECK(write_pattern(p, 100, 1), "fixture written"); + CHECK(!path_is_readonly(p), "fresh file is writable"); + + if (!path_set_readonly(p, true)) + { + SKIP("set_readonly unsupported on this platform/file system"); + return; + } + CHECK(path_is_readonly(p), "IS_READONLY set after set_readonly(1)"); +#if !defined(_WIN32) + if (geteuid() == 0) + SKIP("open-for-write-denied check (running as root, mode bits are not enforced)"); + else +#endif + { + RFILE *f = filestream_open(p, RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + CHECK(f == NULL, "open for write fails while read-only"); + if (f) filestream_close(f); + } + CHECK(path_set_readonly(p, false), "set_readonly(0) succeeds"); + CHECK(!path_is_readonly(p), "IS_READONLY clear again"); + { + RFILE *f = filestream_open(p, RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); + CHECK(f != NULL, "open for write succeeds again"); + if (f) filestream_close(f); + } + CHECK(!path_set_readonly("does/not/exist.bin", true), "set_readonly on missing path fails"); +} + +static void test_mtime(const char *dir) +{ + char p[512]; + int64_t t = 0, t2 = 0; + printf("mtime:\n"); + snprintf(p, sizeof(p), "%s/mt.bin", dir); + CHECK(write_pattern(p, 10, 2), "fixture written"); + CHECK(path_get_mtime(p, &t), "get_mtime succeeds"); + CHECK(t > 1000000000, "mtime is a plausible recent time"); + CHECK(!path_get_mtime("does/not/exist.bin", &t2), "get_mtime on missing path fails"); + + if (!path_set_mtime(p, 1234567890)) + { + SKIP("set_mtime unsupported on this platform/file system"); + return; + } + CHECK(path_get_mtime(p, &t2), "get_mtime after set"); + /* FAT stores 2 s resolution; allow that slack. */ + CHECK(t2 >= 1234567888 && t2 <= 1234567892, "mtime round-trips (within 2 s)"); + CHECK(path_set_mtime(p, -86400), "negative (pre-1970) mtime accepted"); + CHECK(path_get_mtime(p, &t2) && t2 <= -86398 && t2 >= -86402, "negative mtime round-trips"); +} + +/* Drive a begin/step/close copy to completion with a fixed per-step + * budget, checking that no step overshoots it and that progress is + * monotonic. A small budget exercises resumption across many steps; + * a huge one is the "run it flat out" case. */ +static int overshot_once = 0; + +static int copy_sync_budget(const char *src, const char *dst, unsigned flags, + int64_t budget, unsigned *steps_out) +{ + struct retro_vfs_copy_handle *h = filestream_copy_begin(src, dst, flags); + int st; + int64_t done = 0, total = 0, prev = 0; + unsigned steps = 0; + if (!h) + return -1; + for (;;) + { + st = filestream_copy_step(h, budget, &done, &total); + if (getenv("VFS_V5_TRACE") || (steps < 3 && budget > 0 && budget < 1000000)) + printf(" trace step %u: status %d done %lld total %lld\n", + steps + 1, st, (long long)done, (long long)total); + if (done > total || done < prev) + { + printf(" FAIL step %u: done %lld went backwards or past total %lld\n", + steps + 1, (long long)done, (long long)total); + failures++; + break; + } + if (budget > 0 && done - prev > budget) + { + /* The implementation asked for <= budget; a kernel that hands + * back more than that is a platform quirk (gVisor copies to + * EOF). The VFS stops trusting it from here, which the + * caller below verifies with a second budgeted copy. */ + printf(" note step %u moved %lld for a %lld budget: kernel ignored len\n", + steps + 1, (long long)(done - prev), (long long)budget); + overshot_once++; + } + if (st != RETRO_VFS_COPY_RUNNING) + break; + steps++; + prev = done; + if (steps > 100000000u) + break; + } + if (steps_out) + *steps_out = steps; + return filestream_copy_close(h); +} + +static int copy_sync(const char *src, const char *dst, unsigned flags) +{ + return copy_sync_budget(src, dst, flags, 0, NULL); +} + +static void test_copy(const char *dir) +{ + char src[512], dst[512], sub[512], nested[512]; + printf("copy:\n"); + snprintf(src, sizeof(src), "%s/src.bin", dir); + snprintf(dst, sizeof(dst), "%s/dst.bin", dir); + snprintf(sub, sizeof(sub), "%s/sub", dir); + snprintf(nested, sizeof(nested), "%s/sub/deeper/nested.bin", dir); + + CHECK(write_pattern(src, BIG_SIZE, 3), "3 MiB fixture written"); + CHECK(copy_sync(src, dst, 0) == 0, "copy to new dst completes (default step)"); + CHECK(files_equal(src, dst), "copy is byte-identical"); + CHECK(path_get_size(dst) == (int64_t)BIG_SIZE, "copy has the right size"); + CHECK(!path_is_readonly(dst), "copy is writable"); + + /* Small budget: many resumed steps, none overshooting. */ + { + unsigned steps = 0; + CHECK(copy_sync_budget(src, dst, RETRO_VFS_COPY_OVERWRITE, 100000, &steps) == 0, + "copy with a 100000-byte step budget completes"); + CHECK(files_equal(src, dst), "small-step copy is byte-identical"); + if (overshot_once) + { + /* Once the kernel has been caught ignoring len the VFS does + * not offer it another byte, so this copy should step + * properly. A platform that overshoots here as well is + * ignoring len somewhere the VFS cannot see (the [vfs-copy] + * narration on stderr says where); the copy is still + * correct, so that is reported, not failed. */ + overshot_once = 0; + CHECK(copy_sync_budget(src, dst, RETRO_VFS_COPY_OVERWRITE, 100000, &steps) == 0, + "budgeted copy after a kernel overshoot completes"); + CHECK(files_equal(src, dst), "post-overshoot copy is byte-identical"); + if (overshot_once) + printf(" note this platform ignores len on the portable path too; " + "budget checks skipped\n"); + else + printf(" ok no second overshoot: kernel path retired\n"); + } + if (overshot_once) + printf(" note steps=%u (budgets not honoured by this platform)\n", steps); + else + CHECK(steps >= BIG_SIZE / 100000, "took at least the minimum number of steps"); + } + /* Huge budget: one call moves everything. */ + { + unsigned steps = 0; + CHECK(copy_sync_budget(src, dst, RETRO_VFS_COPY_OVERWRITE, INT64_MAX, &steps) == 0, + "copy with an unbounded budget completes"); + CHECK(files_equal(src, dst), "flat-out copy is byte-identical"); + } + + CHECK(filestream_copy_begin(src, dst, 0) == NULL, "begin onto existing dst without OVERWRITE refused"); + CHECK(files_equal(src, dst), "dst untouched by the refused copy"); + + /* Cancel mid-copy: begin, move a little, close. No partial file may + * remain. (A clone-capable file system may legitimately be DONE + * after begin; then dst must be complete.) */ + { + char cdst[512]; + struct retro_vfs_copy_handle *h; + int rc, st; + int64_t done = 0; + snprintf(cdst, sizeof(cdst), "%s/cancelled.bin", dir); + h = filestream_copy_begin(src, cdst, 0); + CHECK(h != NULL, "begin for cancel test"); + st = filestream_copy_step(h, 65536, &done, NULL); + CHECK(st != RETRO_VFS_COPY_FAILED, "first small step ok"); + rc = filestream_copy_close(h); + if (rc == 0) + CHECK(files_equal(src, cdst), "closed after completion: dst complete"); + else + CHECK(!path_is_valid(cdst), "closed while running: no partial dst"); + filestream_delete(cdst); + } + + CHECK(write_pattern(src, 4096, 4), "fixture replaced with a different one"); + CHECK(copy_sync(src, dst, RETRO_VFS_COPY_OVERWRITE) == 0, "OVERWRITE replaces dst"); + CHECK(files_equal(src, dst) && path_get_size(dst) == 4096, "dst now matches the new source"); + + if (path_set_readonly(dst, true)) + { + CHECK(copy_sync(src, dst, RETRO_VFS_COPY_OVERWRITE) == 0, "OVERWRITE replaces a read-only dst (cp -f)"); + path_set_readonly(dst, false); + } + + CHECK(copy_sync(src, nested, 0) == 0, "copy into a missing directory creates it"); + CHECK(path_is_directory(sub) && files_equal(src, nested), "nested copy landed"); + + CHECK(filestream_copy_begin(src, src, RETRO_VFS_COPY_OVERWRITE) == NULL, "src == dst refused"); + CHECK(path_get_size(src) == 4096, "src not truncated by the refused self-copy"); + CHECK(filestream_copy_begin(dir, dst, RETRO_VFS_COPY_OVERWRITE) == NULL, "directory as src refused"); + CHECK(filestream_copy_begin(src, sub, RETRO_VFS_COPY_OVERWRITE) == NULL, "directory as dst refused"); + CHECK(filestream_copy_begin("does/not/exist.bin", dst, RETRO_VFS_COPY_OVERWRITE) == NULL, "missing src refused"); + + /* No partial file on failure: a dst whose parent is a *file* cannot + * be created, so begin must refuse and leave nothing. */ + { + char bad[512]; + snprintf(bad, sizeof(bad), "%s/src.bin/child.bin", dir); + CHECK(filestream_copy_begin(src, bad, 0) == NULL, "impossible dst refused"); + CHECK(!path_is_valid(bad), "no partial file left behind"); + } + + /* The pre-v5 blocking helper still works and still refuses the + * same things. */ + CHECK(filestream_copy(src, dst) == 0 && files_equal(src, dst), "legacy filestream_copy still copies"); + CHECK(filestream_copy(src, src) != 0, "legacy filestream_copy refuses src == dst"); +} + +static void test_dirent_stat(const char *dir) +{ + char sub[512], f1[512], f2[512]; + struct RDIR *rd; + int seen_f1 = 0, seen_f2 = 0, seen_dir = 0; + printf("dirent_stat:\n"); + snprintf(sub, sizeof(sub), "%s/ds", dir); + snprintf(f1, sizeof(f1), "%s/ds/a-1234.bin", dir); + snprintf(f2, sizeof(f2), "%s/ds/b-ro.bin", dir); + CHECK(path_mkdir(sub), "enumeration dir created"); + CHECK(write_pattern(f1, 1234, 5), "1234-byte file"); + CHECK(write_pattern(f2, 8, 6), "8-byte file"); + { + char d[512]; + snprintf(d, sizeof(d), "%s/ds/childdir", dir); + CHECK(path_mkdir(d), "child directory"); + } + path_set_readonly(f2, true); + + rd = retro_opendir(sub); + CHECK(rd != NULL, "opendir"); + while (rd && retro_readdir(rd)) + { + const char *name = retro_dirent_get_name(rd); + int64_t size = -1, mtime = 0, want_mtime = 0; + int flags; + char full[1024]; + if (!name || !strcmp(name, ".") || !strcmp(name, "..")) + continue; + flags = retro_dirent_stat(rd, &size, &mtime); + snprintf(full, sizeof(full), "%s/%s", sub, name); + + if (!(flags & RETRO_VFS_STAT_IS_VALID)) + { + SKIP("dirent_stat unavailable on this platform"); + continue; + } + CHECK(!!(flags & RETRO_VFS_STAT_IS_DIRECTORY) == retro_dirent_is_dir(rd, NULL), + "IS_DIRECTORY agrees with dirent_is_dir"); + if (!(flags & RETRO_VFS_STAT_IS_DIRECTORY)) + CHECK(size == path_get_size(full), "size agrees with path_get_size"); + if (path_get_mtime(full, &want_mtime)) + CHECK(mtime == want_mtime, "mtime agrees with path_get_mtime"); + CHECK(!!(flags & RETRO_VFS_STAT_IS_READONLY) == path_is_readonly(full), + "IS_READONLY agrees with path_is_readonly"); + + if (!strcmp(name, "a-1234.bin")) { seen_f1++; CHECK(size == 1234, "a-1234.bin is 1234 bytes"); } + if (!strcmp(name, "b-ro.bin")) { seen_f2++; } + if (!strcmp(name, "childdir")) { seen_dir++; CHECK(flags & RETRO_VFS_STAT_IS_DIRECTORY, "childdir flagged as directory"); } + } + if (rd) + retro_closedir(rd); + CHECK(seen_f1 == 1 && seen_f2 == 1 && seen_dir == 1, "all three entries enumerated once"); + path_set_readonly(f2, false); +} + +static void rm_tree(const char *dir) +{ + struct RDIR *rd = retro_opendir(dir); + if (rd) + { + while (retro_readdir(rd)) + { + const char *name = retro_dirent_get_name(rd); + char full[1024]; + if (!name || !strcmp(name, ".") || !strcmp(name, "..")) + continue; + snprintf(full, sizeof(full), "%s/%s", dir, name); + if (retro_dirent_is_dir(rd, NULL)) + rm_tree(full); + else + { + path_set_readonly(full, false); + filestream_delete(full); + } + } + retro_closedir(rd); + } + retro_vfs_mkdir_impl(dir); /* no rmdir in the VFS; leave empty dirs */ +} + +int main(void) +{ + const char *dir = DIR_NAME; + printf("vfs_v5_metadata_test\n"); + rm_tree(dir); + if (!path_mkdir(dir)) + { + printf("cannot create %s\n", dir); + return 1; + } + test_readonly(dir); + test_mtime(dir); + test_copy(dir); + test_dirent_stat(dir); + rm_tree(dir); + printf("%d failure(s), %d skip(s)\n", failures, skips); + return failures ? 1 : 0; +} diff --git a/libretro-common/streams/file_stream.c b/libretro-common/streams/file_stream.c index 05cfbda1e8bc..94c1cc556ef7 100644 --- a/libretro-common/streams/file_stream.c +++ b/libretro-common/streams/file_stream.c @@ -37,6 +37,8 @@ #ifdef _MSC_VER #include #endif +#include +#include #include #include @@ -182,6 +184,13 @@ static retro_vfs_write_t filestream_write_cb = NULL; static retro_vfs_flush_t filestream_flush_cb = NULL; static retro_vfs_remove_t filestream_remove_cb = NULL; static retro_vfs_rename_t filestream_rename_cb = NULL; +/* VFS API v5. NULL when a frontend older than v5 (or one that left + * the members unset) owns the files: the local _impl must not run + * behind its back, so the begin/poll/close wrappers report failure. */ +static retro_vfs_copy_begin_t filestream_copy_begin_cb = NULL; +static retro_vfs_copy_step_t filestream_copy_step_cb = NULL; +static retro_vfs_copy_close_t filestream_copy_close_cb = NULL; +static bool filestream_copy_unavailable = false; /* VFS Initialization */ @@ -202,6 +211,10 @@ void filestream_vfs_init(const struct retro_vfs_interface_info* vfs_info) filestream_flush_cb = NULL; filestream_remove_cb = NULL; filestream_rename_cb = NULL; + filestream_copy_begin_cb = NULL; + filestream_copy_step_cb = NULL; + filestream_copy_close_cb = NULL; + filestream_copy_unavailable = false; if ( (vfs_info->required_interface_version < @@ -221,6 +234,16 @@ void filestream_vfs_init(const struct retro_vfs_interface_info* vfs_info) filestream_flush_cb = vfs_iface->flush; filestream_remove_cb = vfs_iface->remove; filestream_rename_cb = vfs_iface->rename; + + if (vfs_info->required_interface_version >= FILESTREAM_COPY_REQUIRED_VFS_VERSION + && vfs_iface->copy_begin && vfs_iface->copy_step && vfs_iface->copy_close) + { + filestream_copy_begin_cb = vfs_iface->copy_begin; + filestream_copy_step_cb = vfs_iface->copy_step; + filestream_copy_close_cb = vfs_iface->copy_close; + } + else + filestream_copy_unavailable = true; } /* Callback wrappers */ @@ -1618,42 +1641,95 @@ int filestream_rename(const char *old_path, const char *new_path) return retro_vfs_file_rename_impl(old_path, new_path); } -int filestream_copy(const char *src, const char *dst) +/* v1-only frontends: copy through their open/read/write. Large + * heap buffer rather than the historical 256-byte stack one; the + * destination directory is created before the destination is opened, + * which the old order got backwards. */ +static int filestream_copy_loop(const char *src, const char *dst) { - char buf[256] = {0}; - int64_t n = 0; - int ret = 0; - char path_dst[PATH_MAX_LENGTH] = {0}; - - RFILE *fp_src = filestream_open(src, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); - RFILE *fp_dst = filestream_open(dst, RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE); - - if (!fp_src || !fp_dst) - ret = -1; - - if (ret < 0) - goto close; + char *buf = NULL; + size_t buf_len = 256 * 1024; + int64_t n = 0; + int ret = -1; + RFILE *fp_src = NULL; + RFILE *fp_dst = NULL; + char path_dst[PATH_MAX_LENGTH]; + + if (!(buf = (char*)malloc(buf_len))) + return -1; - snprintf(path_dst, sizeof(path_dst), "%s", dst); + strlcpy(path_dst, dst, sizeof(path_dst)); path_basedir(path_dst); - if (!path_is_directory(path_dst)) path_mkdir(path_dst); - while ((n = filestream_read(fp_src, buf, sizeof(buf))) > 0 && ret == 0) + fp_src = filestream_open(src, RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK); + if (!fp_src) + goto end; + fp_dst = filestream_open(dst, RETRO_VFS_FILE_ACCESS_WRITE, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!fp_dst) + goto end; + + while ((n = filestream_read(fp_src, buf, buf_len)) > 0) { if (filestream_write(fp_dst, buf, n) != n) - ret = -1; + goto end; } + ret = (n < 0) ? -1 : 0; -close: +end: if (fp_src) filestream_close(fp_src); if (fp_dst) filestream_close(fp_dst); + if (ret != 0) + filestream_delete(dst); + free(buf); return ret; } +struct retro_vfs_copy_handle *filestream_copy_begin( + const char *src, const char *dst, unsigned flags) +{ + if (filestream_copy_begin_cb) + return filestream_copy_begin_cb(src, dst, flags); + if (filestream_copy_unavailable) + return NULL; + return retro_vfs_copy_begin_impl(src, dst, flags); +} + +int filestream_copy_step(struct retro_vfs_copy_handle *handle, + int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total) +{ + if (filestream_copy_step_cb) + return filestream_copy_step_cb(handle, max_bytes, bytes_done, bytes_total); + if (filestream_copy_unavailable) + return RETRO_VFS_COPY_FAILED; + return retro_vfs_copy_step_impl(handle, max_bytes, bytes_done, bytes_total); +} + +int filestream_copy_close(struct retro_vfs_copy_handle *handle) +{ + if (filestream_copy_close_cb) + return filestream_copy_close_cb(handle); + if (filestream_copy_unavailable) + return -1; + return retro_vfs_copy_close_impl(handle); +} + +/* Pre-v5 helper, kept for its existing callers. Blocks by design; + * new code uses filestream_copy_begin/poll/close. */ +int filestream_copy(const char *src, const char *dst) +{ + if (!src || !*src || !dst || !*dst || string_is_equal(src, dst)) + return -1; + if (!path_is_valid(src) || path_is_directory(src) || path_is_directory(dst)) + return -1; + return filestream_copy_loop(src, dst); +} + int filestream_cmp(const char *src, const char *dst) { int ret = 0; diff --git a/libretro-common/vfs/vfs_hybrid.c b/libretro-common/vfs/vfs_hybrid.c index e3f46e0e8606..483ba8430b32 100644 --- a/libretro-common/vfs/vfs_hybrid.c +++ b/libretro-common/vfs/vfs_hybrid.c @@ -350,6 +350,105 @@ static int hyb_closedir( struct retro_vfs_dir_handle *dh ) { return r; } +/* ---- v5: metadata, copy, dirent_stat ---- */ + +/* Same local-first rule as stat: a native path is answered locally, + a URI (or anything on a sandboxed platform) goes to a frontend + that advertised v5. A frontend older than v5 has not filled these + members and must not be called. */ + +static int hyb_set_readonly( const char *path, int readonly ) { + if ( !hyb_is_uri( path ) ) { + int r = retro_vfs_set_readonly_impl( path, readonly ); + if ( r == 0 || !( hyb_front && HYB_SANDBOXED ) ) + return r; + } + if ( hyb_front && hyb_front_version >= 5 && hyb_front->set_readonly ) + return hyb_front->set_readonly( path, readonly ); + return -1; +} + +static int hyb_get_mtime( const char *path, int64_t *mtime ) { + if ( !hyb_is_uri( path ) ) { + int r = retro_vfs_get_mtime_impl( path, mtime ); + if ( r == 0 || !( hyb_front && HYB_SANDBOXED ) ) + return r; + } + if ( hyb_front && hyb_front_version >= 5 && hyb_front->get_mtime ) + return hyb_front->get_mtime( path, mtime ); + return -1; +} + +static int hyb_set_mtime( const char *path, int64_t mtime ) { + if ( !hyb_is_uri( path ) ) { + int r = retro_vfs_set_mtime_impl( path, mtime ); + if ( r == 0 || !( hyb_front && HYB_SANDBOXED ) ) + return r; + } + if ( hyb_front && hyb_front_version >= 5 && hyb_front->set_mtime ) + return hyb_front->set_mtime( path, mtime ); + return -1; +} + +/* A copy handle must be polled and closed by whichever side began it, + so the wrapper records which one that was. */ +typedef struct { int be; void *h; } hyb_copy_t; + +static struct retro_vfs_copy_handle *hyb_copy_begin( const char *src, const char *dst, unsigned flags ) { + hyb_copy_t *c = (hyb_copy_t *)calloc( 1, sizeof( *c ) ); + if ( !c ) + return NULL; + /* both native: local (fast paths live there). A URI on either + side means at least one end only the frontend can reach. */ + if ( !hyb_is_uri( src ) && !hyb_is_uri( dst ) ) { + c->h = retro_vfs_copy_begin_impl( src, dst, flags ); + if ( c->h || !( hyb_front && HYB_SANDBOXED ) ) { + c->be = HYB_LOCAL; + if ( !c->h ) { free( c ); return NULL; } + return (struct retro_vfs_copy_handle *)c; + } + } + if ( hyb_front && hyb_front_version >= 5 && hyb_front->copy_begin ) { + c->h = hyb_front->copy_begin( src, dst, flags ); + if ( c->h ) { c->be = HYB_FRONT; return (struct retro_vfs_copy_handle *)c; } + } + free( c ); + return NULL; +} + +static int hyb_copy_step( struct retro_vfs_copy_handle *ch, int64_t max_bytes, int64_t *done, int64_t *total ) { + hyb_copy_t *c = (hyb_copy_t *)ch; + if ( !c ) + return RETRO_VFS_COPY_FAILED; + if ( c->be == HYB_LOCAL ) + return retro_vfs_copy_step_impl( (struct retro_vfs_copy_handle *)c->h, max_bytes, done, total ); + return hyb_front->copy_step( (struct retro_vfs_copy_handle *)c->h, max_bytes, done, total ); +} + +static int hyb_copy_close( struct retro_vfs_copy_handle *ch ) { + hyb_copy_t *c = (hyb_copy_t *)ch; + int r; + if ( !c ) + return -1; + if ( c->be == HYB_LOCAL ) + r = retro_vfs_copy_close_impl( (struct retro_vfs_copy_handle *)c->h ); + else + r = hyb_front->copy_close( (struct retro_vfs_copy_handle *)c->h ); + free( c ); + return r; +} + +static int hyb_dirent_stat( struct retro_vfs_dir_handle *dh, int64_t *size, int64_t *mtime ) { + hyb_dir_t *d = (hyb_dir_t *)dh; + if ( !d ) + return 0; + if ( d->be == HYB_LOCAL ) + return retro_vfs_dirent_stat_impl( (libretro_vfs_implementation_dir *)d->h, size, mtime ); + if ( hyb_front_version >= 5 && hyb_front->dirent_stat ) + return hyb_front->dirent_stat( (struct retro_vfs_dir_handle *)d->h, size, mtime ); + return 0; +} + /* the zero-copy sideband: local-backed handles expose their mapping, frontend-backed ones honestly cannot */ static const uint8_t *hyb_mapped_ptr( void *fh, int64_t *len ) { @@ -371,15 +470,22 @@ static struct retro_vfs_interface hyb_iface = { hyb_stat, hyb_mkdir, hyb_opendir, hyb_readdir, hyb_dirent_get_name, hyb_dirent_is_dir, hyb_closedir, /* v4 */ - hyb_stat_64 + hyb_stat_64, + /* v5 */ + hyb_set_readonly, hyb_get_mtime, hyb_set_mtime, + hyb_copy_begin, hyb_copy_step, hyb_copy_close, hyb_dirent_stat }; void vfs_hybrid_init( retro_environment_t env_cb, retro_log_printf_t log ) { struct retro_vfs_interface_info info; - info.required_interface_version = 4; + info.required_interface_version = 5; info.iface = NULL; if ( !env_cb( RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &info ) || !info.iface ) { + info.required_interface_version = 4; + info.iface = NULL; + } + if ( !info.iface && ( !env_cb( RETRO_ENVIRONMENT_GET_VFS_INTERFACE, &info ) || !info.iface ) ) { info.required_interface_version = 3; info.iface = NULL; } @@ -397,7 +503,7 @@ void vfs_hybrid_init( retro_environment_t env_cb, retro_log_printf_t log ) { { struct retro_vfs_interface_info ours; - ours.required_interface_version = 4; + ours.required_interface_version = 5; ours.iface = &hyb_iface; filestream_vfs_init( &ours ); path_vfs_init( &ours ); diff --git a/libretro-common/vfs/vfs_implementation.c b/libretro-common/vfs/vfs_implementation.c index d2b9d7ba43c3..6039ad6de5d3 100644 --- a/libretro-common/vfs/vfs_implementation.c +++ b/libretro-common/vfs/vfs_implementation.c @@ -71,6 +71,7 @@ # include # include # include +# include #elif !defined(_WIN32) # if defined(PSP) # include @@ -205,6 +206,97 @@ #include #include #include +#include + +/* 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 +#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 +#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 +#if defined(__has_include) +#if __has_include() \ + && defined(MAC_OS_X_VERSION_MIN_REQUIRED) \ + && MAC_OS_X_VERSION_MIN_REQUIRED >= 101200 +#include +#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 +#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 @@ -2166,22 +2258,30 @@ const uint8_t *retro_vfs_file_get_mapped_ptr_impl( * 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) + 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; - *file_info = GetFileAttributes(path_local); + 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 ( *file_info == INVALID_FILE_ATTRIBUTES - || _stati64(path_local, (struct _stati64*)stat_buf) != 0) + if (_stati64(path_local, (struct _stati64*)stat_buf) != 0) #else - if ( *file_info == INVALID_FILE_ATTRIBUTES - || _stat64(path_local, stat_buf) != 0) + if (_stat64(path_local, stat_buf) != 0) #endif { free(path_local); @@ -2195,17 +2295,23 @@ static int vfs_stat_win32_ansi(const char *path, #if !defined(LEGACY_WIN32) || defined(LEGACY_WIN32_RUNTIME) static int vfs_stat_win32_wide(const char *path, - struct _stat64 *stat_buf, DWORD *file_info) + 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; - *file_info = GetFileAttributesW(path_wide); + if (!GetFileAttributesExW(path_wide, GetFileExInfoStandard, &ad)) + { + free(path_wide); + return 0; + } + *file_info = ad.dwFileAttributes; + *mtime_ft = ad.ftLastWriteTime; - if ( *file_info == INVALID_FILE_ATTRIBUTES - || _wstat64(path_wide, stat_buf) != 0) + if (_wstat64(path_wide, stat_buf) != 0) { free(path_wide); return 0; @@ -2217,7 +2323,12 @@ static int vfs_stat_win32_wide(const char *path, #endif #endif -int retro_vfs_stat_64_impl(const char *path, int64_t *size) +/* 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; @@ -2258,9 +2369,17 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) 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; @@ -2270,14 +2389,19 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) 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; @@ -2303,24 +2427,28 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) #if defined(LEGACY_WIN32_RUNTIME) if (win32_needs_local_encoding()) { - if (!vfs_stat_win32_ansi(stat_path, &stat_buf, &file_info)) + 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)) + 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)) + 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)) + 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 @@ -2338,11 +2466,15 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) 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 @@ -2364,16 +2496,46 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) 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; @@ -2581,6 +2743,667 @@ int retro_vfs_restrict_permissions_impl(const char *path) #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 +#include +#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; + char dst_buf[PATH_MAX_LENGTH]; + + 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); + if (n >= sizeof(dst_buf)) + return NULL; + memcpy(dst_buf, dst, n); + dst_buf[n] = '\0'; + /* mkdir reports "exists" for a file of that name too, so + * confirm the parent really is a directory. */ + if ( vfs_copy_mkdir_parents(dst_buf) != 0 + || !(retro_vfs_stat_full(dst_buf, NULL, NULL) & RETRO_VFS_STAT_IS_DIRECTORY)) + 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(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) { @@ -2925,6 +3748,96 @@ bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir *rdir) } } +/* The join-and-stat fallback: one full-path stat per entry, which is + * exactly what a caller without dirent_stat would do itself, so it is + * never worse than today. Split out so the PATH_MAX_LENGTH local + * stays off the fast paths' stack. Only compiled where some branch + * of dirent_stat reaches it. */ +#if defined(HAVE_SMBCLIENT) || (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[PATH_MAX_LENGTH]; + const char *name = retro_vfs_dirent_get_name_impl(rdir); + if (!name || !rdir->orig_path) + return 0; + fill_pathname_join_special(path, rdir->orig_path, name, sizeof(path)); + return retro_vfs_stat_full(path, size, mtime); +} +#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 +#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; diff --git a/libretro-common/vfs/vfs_implementation_uwp.cpp b/libretro-common/vfs/vfs_implementation_uwp.cpp index 9896d29db918..d46aaf6727f0 100644 --- a/libretro-common/vfs/vfs_implementation_uwp.cpp +++ b/libretro-common/vfs/vfs_implementation_uwp.cpp @@ -762,15 +762,307 @@ int retro_vfs_stat_64_impl(const char *path, int64_t *size) } } free(path_wide); - return (attribdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - ? RETRO_VFS_STAT_IS_VALID | RETRO_VFS_STAT_IS_DIRECTORY - : RETRO_VFS_STAT_IS_VALID; + { + int ret = RETRO_VFS_STAT_IS_VALID; + if (attribdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + ret |= RETRO_VFS_STAT_IS_DIRECTORY; + if (attribdata.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + ret |= RETRO_VFS_STAT_IS_READONLY; + return ret; + } } } free(path_wide); return 0; } +/* VFS API v5 ------------------------------------------------------- */ + +/* FILETIME is 100 ns ticks since 1601-01-01; Unix time is seconds + * since 1970-01-01. Kept in uint64_t so the offset is never a + * signed-overflow site. Same helpers as vfs_implementation.c. */ +static const uint64_t UWP_FILETIME_EPOCH_DIFF = 116444736000000000ULL; +static const uint64_t UWP_FILETIME_TICKS_PER_S = 10000000ULL; + +static int64_t uwp_filetime_to_unix(const FILETIME &ft) +{ + uint64_t t = ((uint64_t)ft.dwHighDateTime << 32) | (uint64_t)ft.dwLowDateTime; + if (t < UWP_FILETIME_EPOCH_DIFF) + return -(int64_t)((UWP_FILETIME_EPOCH_DIFF - t) / UWP_FILETIME_TICKS_PER_S); + return (int64_t)((t - UWP_FILETIME_EPOCH_DIFF) / UWP_FILETIME_TICKS_PER_S); +} + +/* FILETIME covers 1601-01-01 .. ~30828 AD; out-of-range values clamp + * to the nearest end rather than wrapping. -unix_s is never formed. */ +static void uwp_unix_to_filetime(int64_t unix_s, FILETIME &ft) +{ + const int64_t min_unix = -(int64_t)(UWP_FILETIME_EPOCH_DIFF / UWP_FILETIME_TICKS_PER_S); + const int64_t max_unix = (int64_t)((0x7fffffffffffffffULL - UWP_FILETIME_EPOCH_DIFF) / UWP_FILETIME_TICKS_PER_S); + uint64_t t; + if (unix_s <= min_unix) + t = 0; + else if (unix_s >= max_unix) + t = 0x7fffffffffffffffULL; + else if (unix_s < 0) + t = UWP_FILETIME_EPOCH_DIFF - ((uint64_t)0 - (uint64_t)unix_s) * UWP_FILETIME_TICKS_PER_S; + else + t = UWP_FILETIME_EPOCH_DIFF + (uint64_t)unix_s * UWP_FILETIME_TICKS_PER_S; + ft.dwLowDateTime = (DWORD)(t & 0xffffffffULL); + ft.dwHighDateTime = (DWORD)(t >> 32); +} + +int retro_vfs_set_readonly_impl(const char *path, int readonly) +{ + wchar_t *path_wide; + _WIN32_FILE_ATTRIBUTE_DATA attribdata; + DWORD attrs; + BOOL ok = FALSE; + + if (!path || !*path) + return -1; + + path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + + if (GetFileAttributesExFromAppW(path_wide, GetFileExInfoStandard, &attribdata) + && attribdata.dwFileAttributes != INVALID_FILE_ATTRIBUTES) + { + attrs = attribdata.dwFileAttributes; + if (readonly) + attrs |= FILE_ATTRIBUTE_READONLY; + else + attrs &= ~FILE_ATTRIBUTE_READONLY; + /* No FromApp variant exists; the plain call is in the UWP API + * set and works on any path the app already has access to. */ + ok = SetFileAttributesW(path_wide, attrs); + } + free(path_wide); + return ok ? 0 : -1; +} + +int retro_vfs_get_mtime_impl(const char *path, int64_t *mtime) +{ + wchar_t *path_wide; + _WIN32_FILE_ATTRIBUTE_DATA attribdata; + BOOL ok; + + if (!path || !*path || !mtime) + return -1; + + path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + ok = GetFileAttributesExFromAppW(path_wide, GetFileExInfoStandard, &attribdata); + free(path_wide); + if (!ok || attribdata.dwFileAttributes == INVALID_FILE_ATTRIBUTES) + return -1; + *mtime = uwp_filetime_to_unix(attribdata.ftLastWriteTime); + return 0; +} + +int retro_vfs_set_mtime_impl(const char *path, int64_t mtime) +{ + wchar_t *path_wide; + HANDLE h; + FILETIME ft; + BOOL ok; + + if (!path || !*path) + return -1; + + uwp_unix_to_filetime(mtime, ft); + path_wide = utf8_to_utf16_string_alloc(path); + windowsize_path(path_wide); + h = CreateFile2FromAppW(path_wide, FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, NULL); + free(path_wide); + if (h == INVALID_HANDLE_VALUE) + return -1; + ok = SetFileTime(h, NULL, NULL, &ft); + CloseHandle(h); + return ok ? 0 : -1; +} + +/* copy: begin / step / close. A resumable state machine the caller + * advances; no thread, no lock in here. Both ends go through this + * backend's own file I/O (CreateFile2FromAppW underneath) with one + * transfer buffer; a step moves at most the requested bytes. */ +/* Same sizing as the C backend: 128 KiB is as fast as 1 MiB and small + * files get only what they need. */ +#define UWP_COPY_BUF_MAX (128 * 1024) +#define UWP_COPY_BUF_MIN (16 * 1024) +#define UWP_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; + libretro_vfs_implementation_file *in; + libretro_vfs_implementation_file *out; + char *buf; + size_t buf_len; +}; + +static void uwp_copy_release_ends(struct retro_vfs_copy_handle *h) +{ + 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; } +} + +static void uwp_copy_handle_free(struct retro_vfs_copy_handle *h) +{ + if (!h) + return; + uwp_copy_release_ends(h); + free(h->buf); + free(h->src); + free(h->dst); + free(h); +} + +struct retro_vfs_copy_handle *retro_vfs_copy_begin_impl( + const char *src, const char *dst, unsigned flags) +{ + struct retro_vfs_copy_handle *h; + int64_t src_size = 0; + int sflags, dflags; + + if (!src || !*src || !dst || !*dst) + return NULL; + if (_stricmp(src, dst) == 0) + return NULL; + + sflags = retro_vfs_stat_64_impl(src, &src_size); + if (!(sflags & RETRO_VFS_STAT_IS_VALID) || (sflags & RETRO_VFS_STAT_IS_DIRECTORY)) + return NULL; + + dflags = retro_vfs_stat_64_impl(dst, 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: a read-only stale dst must not defeat an explicit + * overwrite; 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 + { + std::filesystem::path parent = std::filesystem::path(dst).parent_path(); + if (!parent.empty()) + { + uwp_mkdir_impl(parent); + if (!(retro_vfs_stat_64_impl(parent.string().c_str(), NULL) & RETRO_VFS_STAT_IS_DIRECTORY)) + return NULL; + } + } + + h = (struct retro_vfs_copy_handle*)calloc(1, sizeof(*h)); + if (!h) + return NULL; + 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; + h->buf_len = UWP_COPY_BUF_MAX; + if (src_size > 0 && src_size < (int64_t)h->buf_len) + h->buf_len = (size_t)src_size; + if (h->buf_len < UWP_COPY_BUF_MIN) + h->buf_len = UWP_COPY_BUF_MIN; + if (!(h->buf = (char*)malloc(h->buf_len))) + { + h->buf_len = UWP_COPY_BUF_MIN; + if (!(h->buf = (char*)malloc(h->buf_len))) + goto fail; + } + h->in = retro_vfs_file_open_impl(src, RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK); + if (!h->in) + goto fail; + h->out = retro_vfs_file_open_impl(dst, RETRO_VFS_FILE_ACCESS_WRITE, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!h->out) + goto fail; + return h; + +fail: + uwp_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 : UWP_COPY_DEFAULT_STEP; + while (budget > 0 && h->status == RETRO_VFS_COPY_RUNNING) + { + 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); + if (n < 0) + h->status = RETRO_VFS_COPY_FAILED; + else if (n == 0) + { + libretro_vfs_implementation_file *out = h->out; + h->out = NULL; + h->status = (retro_vfs_file_close_impl(out) == 0) + ? RETRO_VFS_COPY_DONE : RETRO_VFS_COPY_FAILED; + } + else if (retro_vfs_file_write_impl(h->out, h->buf, (uint64_t)n) != n) + h->status = RETRO_VFS_COPY_FAILED; + else + { + h->done += n; + budget -= n; + } + } + if (h->status == RETRO_VFS_COPY_FAILED) + { + uwp_copy_release_ends(h); + 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) + { + uwp_copy_release_ends(h); + retro_vfs_file_remove_impl(h->dst); + } + uwp_copy_handle_free(h); + return status == RETRO_VFS_COPY_DONE ? 0 : -1; +} + int retro_vfs_stat_impl(const char *path, int32_t *size) { int64_t size64 = 0; @@ -929,6 +1221,38 @@ bool retro_vfs_dirent_is_dir_impl(libretro_vfs_implementation_dir* rdir) return entry->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; } +int retro_vfs_dirent_stat_impl(libretro_vfs_implementation_dir* rdir, + int64_t *size, int64_t *mtime) +{ + int ret = RETRO_VFS_STAT_IS_VALID; + + if (!rdir) + return 0; +#ifdef HAVE_SMBCLIENT + if (rdir->smb_handle && rdir->smb_handle->dir != 0) + { + char full[PATH_MAX_LENGTH]; + const char *name = retro_vfs_dirent_get_name_impl(rdir); + if (!name) + return 0; + fill_pathname_join_special(full, rdir->orig_path, name, sizeof(full)); + /* The SMB stat helper carries no mtime. */ + return retro_vfs_stat_smb(full, size); + } +#endif + /* All of it is already in the find data: no I/O. */ + if (size) + *size = (int64_t)(((uint64_t)rdir->entry.nFileSizeHigh << 32) + | (uint64_t)rdir->entry.nFileSizeLow); + if (mtime) + *mtime = uwp_filetime_to_unix(rdir->entry.ftLastWriteTime); + if (rdir->entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + ret |= RETRO_VFS_STAT_IS_DIRECTORY; + if (rdir->entry.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + ret |= RETRO_VFS_STAT_IS_READONLY; + return ret; +} + int retro_vfs_closedir_impl(libretro_vfs_implementation_dir* rdir) { if (!rdir) diff --git a/runloop.c b/runloop.c index 5eba719aaa9c..95d8a9136e25 100644 --- a/runloop.c +++ b/runloop.c @@ -3176,7 +3176,7 @@ bool runloop_environment_cb(unsigned cmd, void *data) case RETRO_ENVIRONMENT_GET_VFS_INTERFACE: { - const uint32_t supported_vfs_version = 4; + const uint32_t supported_vfs_version = 5; static struct retro_vfs_interface vfs_iface = { /* VFS API v1 */ @@ -3203,6 +3203,14 @@ bool runloop_environment_cb(unsigned cmd, void *data) retro_vfs_closedir_impl, /* VFS API v4 */ retro_vfs_stat_64_impl, + /* VFS API v5 */ + retro_vfs_set_readonly_impl, + retro_vfs_get_mtime_impl, + retro_vfs_set_mtime_impl, + retro_vfs_copy_begin_impl, + retro_vfs_copy_step_impl, + retro_vfs_copy_close_impl, + retro_vfs_dirent_stat_impl }; struct retro_vfs_interface_info *vfs_iface_info = (struct retro_vfs_interface_info *) data; diff --git a/samples/gfx/gfx_thumbnail_preview/preview_audio_decode_test.c b/samples/gfx/gfx_thumbnail_preview/preview_audio_decode_test.c index 0a93a587daa0..746002ed5db9 100644 --- a/samples/gfx/gfx_thumbnail_preview/preview_audio_decode_test.c +++ b/samples/gfx/gfx_thumbnail_preview/preview_audio_decode_test.c @@ -270,3 +270,7 @@ int config_userdata_get_string(void *u, const char *k, char **v, bool path_is_directory(const char *p) { struct stat st; return p && stat(p, &st) == 0 && S_ISDIR(st.st_mode); } bool path_mkdir(const char *d) { (void)d; return false; } +/* filestream_copy()'s v1-frontend fallback checks the destination + * first; this sample never copies, but the symbol must resolve. */ +bool path_is_valid(const char *p) +{ struct stat st; return p && stat(p, &st) == 0; }