mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-19 09:19:05 +02:00
The libretro-common submodule pinned the standalone repository at 76a3d54. It is replaced by an in-tree copy of RetroArch master (b655a20) holding only what the core uses: the VFS, file, string, encoding, compat and time sources it already built, the headers they and the core include, the threading set - rthreads, retro_atomic, retro_spsc and retro_waitable_spsc, mpsc_stack, retro_triple_buffer, retro_eventcount, retro_asym_eventcount, retro_procbarrier and tpool, with features_cpu and rstrtod, which they need - and the codecs that replace the bundled zlib, zstd, libchdr, libzip and libpng: rinflate and rdeflate, rzstd, rchd with its LZMA and FLAC decoders, rzip_archive, rpng and crc32. stdstring.c, which nothing calls, is no longer built. The threading sources are always built, by both the static Makefile and CMake; the VFS set stays behind HAVE_LIBRETRO_VFS as before. The codecs are built by the change that switches the core over to them.
2390 lines
75 KiB
C
2390 lines
75 KiB
C
/* Copyright (C) 2010-2020 The RetroArch team
|
|
*
|
|
* ---------------------------------------------------------------------------------------
|
|
* The following license statement only applies to this file (file_stream.c).
|
|
* ---------------------------------------------------------------------------------------
|
|
*
|
|
* Permission is hereby granted, free of charge,
|
|
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
|
* to deal in the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
#include <ctype.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <encodings/utf.h>
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#ifdef _MSC_VER
|
|
#include <compat/msvc.h>
|
|
#endif
|
|
#include <compat/strl.h>
|
|
#include <string/stdstring.h>
|
|
|
|
#include <retro_miscellaneous.h>
|
|
#include <file/file_path.h>
|
|
#include <streams/file_stream.h>
|
|
#define VFS_FRONTEND
|
|
#include <vfs/vfs_implementation.h>
|
|
#include <string/rstrtod.h>
|
|
|
|
#define VFS_ERROR_RETURN_VALUE -1
|
|
|
|
/* Size of the per-handle sequential lookahead buffer filled by
|
|
* filestream_gets()/filestream_getc(). Large enough that the
|
|
* per-call cost of crossing the VFS interface (which for a core
|
|
* running under a frontend-supplied VFS is an indirect call across
|
|
* the libretro ABI) amortizes to nothing, small enough to be an
|
|
* acceptable lazy per-handle heap cost on memory-constrained
|
|
* platforms. Handles that never touch gets/getc never allocate it.
|
|
* Overridable for platforms that want a different trade-off. */
|
|
#ifndef FILESTREAM_RBUF_LEN
|
|
#define FILESTREAM_RBUF_LEN 16384
|
|
#endif
|
|
|
|
/* Allocation seam for that buffer: a refused allocation is one of the
|
|
* two ways filestream_rbuf_fill() gives up and nothing else can reach
|
|
* it, so samples/file/vfs/filestream_rbuf_fault_test overrides this
|
|
* from its Makefile. Undefined everywhere else, RetroArch included,
|
|
* it is malloc() and no symbol or branch is added. #undef'd after its
|
|
* only use below so it cannot leak through a unity build. */
|
|
#ifndef FILESTREAM_RBUF_MALLOC
|
|
#define FILESTREAM_RBUF_MALLOC(len) malloc(len)
|
|
#endif
|
|
|
|
/* Largest single read filestream_read_file() will ask for. Every
|
|
* backend it can reach expresses a count in something at least this
|
|
* wide - the narrowest is Windows' _read(), an unsigned int returning
|
|
* int - so a request of this size can be satisfied in full and a
|
|
* short return means what it says rather than "too big to ask for".
|
|
* Whole-file reads above it just take another turn of the loop. */
|
|
#ifndef FILESTREAM_BULK_READ_MAX
|
|
#define FILESTREAM_BULK_READ_MAX ((int64_t)64 * 1024 * 1024)
|
|
#endif
|
|
|
|
/* Largest whole-file read filestream_read_file() will attempt.
|
|
*
|
|
* Relying on malloc() to refuse an absurd request is not enough: a
|
|
* directory opens like a file on Linux through both fopen() and
|
|
* open(), and seeking to its end reports INT64_MAX, so a caller that
|
|
* hands this function a path that turns out to be a directory asks
|
|
* the allocator for 8 EiB. Ordinarily that just fails and the call
|
|
* returns an error, but under AddressSanitizer an allocation past its
|
|
* ceiling is fatal - the process aborts rather than returning NULL,
|
|
* so every sanitizer build inherits a crash from a path that merely
|
|
* needed to fail.
|
|
*
|
|
* A ceiling refuses it without a syscall, and refuses any other
|
|
* backend that reports a size which cannot be one. The value is far
|
|
* above anything read whole - the largest are ROMs and disc images,
|
|
* and a machine able to hold one of these in a single heap buffer is
|
|
* not what this function is sized for - and far below the sanitizer
|
|
* threshold, so the failure is a clean one. On 32-bit hosts the
|
|
* address space binds first and this never applies. */
|
|
#ifndef FILESTREAM_READ_FILE_MAX
|
|
#define FILESTREAM_READ_FILE_MAX ((uint64_t)64 * 1024 * 1024 * 1024)
|
|
#endif
|
|
|
|
struct RFILE
|
|
{
|
|
struct retro_vfs_file_handle *hfile;
|
|
bool err_flag;
|
|
/* Set once filestream_rbuf_fill() has seen this handle's tell()
|
|
* fail, so it stops asking: unlatched, the fallback pays a failed
|
|
* tell() per byte on top of the one-byte read, which is more work
|
|
* than the unbuffered code the lookahead replaced.
|
|
*
|
|
* Not set for the other way fill() gives up, a failed allocation.
|
|
* That one is usually a moment rather than a property, and it is
|
|
* likeliest on the memory-constrained targets the lookahead exists
|
|
* for; fill() steps the request down instead.
|
|
*
|
|
* Cleared by a successful seek because tell() fails for two
|
|
* unrelated reasons. A backend with no tell callback - pipe,
|
|
* FIFO, socket - cannot be seeked either, so its latch never
|
|
* clears. But retro_vfs_fp_tell64() falls through to ftell()
|
|
* where none of RETRO_VFS_HAVE_FSEEKI64 / RETRO_VFS_HAVE_FPOS64 /
|
|
* HAVE_64BIT_OFFSETS is set, and that fails past LONG_MAX; there
|
|
* the failure is a property of where the handle is, and seeking
|
|
* back fixes it. Clearing wrongly costs one failed tell() per
|
|
* seek, since the latch re-arms on the first failure and not per
|
|
* byte; never clearing costs per-byte reads without bound. */
|
|
bool rbuf_no_tell;
|
|
/* Sequential read lookahead. Only filestream_rbuf_fill() ever
|
|
* puts bytes here, and only filestream_gets()/filestream_getc()
|
|
* trigger a fill; every other operation on the handle either
|
|
* serves from it (read, tell) or discards it (seek, write,
|
|
* truncate, flush) so the logical stream position stays exact.
|
|
*
|
|
* Invariants while rbuf_len != 0:
|
|
* - rbuf_pos <= rbuf_len
|
|
* - the underlying handle sits at rbuf_file_off + rbuf_len
|
|
* - the logical position is rbuf_file_off + rbuf_pos
|
|
* rbuf_len == 0 means inactive: the underlying position is the
|
|
* logical position, as before this buffer existed.
|
|
*
|
|
* Note that code holding the raw handle from
|
|
* filestream_get_vfs_handle() bypasses this layer; the in-tree
|
|
* callers of that function (cdrom, disc tasks) never mix it with
|
|
* gets/getc on the same RFILE, and out-of-tree code that does
|
|
* was already bypassing RFILE state (err_flag) anyway. */
|
|
uint8_t *rbuf;
|
|
size_t rbuf_cap;
|
|
size_t rbuf_len;
|
|
size_t rbuf_pos;
|
|
int64_t rbuf_file_off;
|
|
/* C89 7.9.5.3: on an update stream, output followed by input (or
|
|
* input followed by output, short of EOF) requires an intervening
|
|
* file positioning or flush call. Backends over stdio really do
|
|
* misbehave without it - reads after writes return stale bytes.
|
|
* Track the last data direction and interpose a no-op seek on
|
|
* transitions, at this layer so every VFS backend is covered. */
|
|
signed char last_io;
|
|
};
|
|
|
|
#define FILESTREAM_LAST_IO_NONE 0
|
|
#define FILESTREAM_LAST_IO_READ 1
|
|
#define FILESTREAM_LAST_IO_WRITE 2
|
|
|
|
static int64_t filestream_raw_read(RFILE *stream, void *s, int64_t len);
|
|
static int64_t filestream_raw_seek(RFILE *stream,
|
|
int64_t offset, int seek_position);
|
|
static int64_t filestream_raw_tell(RFILE *stream);
|
|
static void filestream_rbuf_discard(RFILE *stream);
|
|
static int filestream_rbuf_fill(RFILE *stream);
|
|
|
|
static retro_vfs_get_path_t filestream_get_path_cb = NULL;
|
|
static retro_vfs_open_t filestream_open_cb = NULL;
|
|
static retro_vfs_close_t filestream_close_cb = NULL;
|
|
static retro_vfs_size_t filestream_size_cb = NULL;
|
|
static retro_vfs_truncate_t filestream_truncate_cb = NULL;
|
|
static retro_vfs_tell_t filestream_tell_cb = NULL;
|
|
static retro_vfs_seek_t filestream_seek_cb = NULL;
|
|
static retro_vfs_read_t filestream_read_cb = NULL;
|
|
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 */
|
|
|
|
void filestream_vfs_init(const struct retro_vfs_interface_info* vfs_info)
|
|
{
|
|
const struct retro_vfs_interface *
|
|
vfs_iface = vfs_info->iface;
|
|
|
|
filestream_get_path_cb = NULL;
|
|
filestream_open_cb = NULL;
|
|
filestream_close_cb = NULL;
|
|
filestream_tell_cb = NULL;
|
|
filestream_size_cb = NULL;
|
|
filestream_truncate_cb = NULL;
|
|
filestream_seek_cb = NULL;
|
|
filestream_read_cb = NULL;
|
|
filestream_write_cb = NULL;
|
|
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 <
|
|
FILESTREAM_REQUIRED_VFS_VERSION)
|
|
|| !vfs_iface)
|
|
return;
|
|
|
|
filestream_get_path_cb = vfs_iface->get_path;
|
|
filestream_open_cb = vfs_iface->open;
|
|
filestream_close_cb = vfs_iface->close;
|
|
filestream_size_cb = vfs_iface->size;
|
|
filestream_truncate_cb = vfs_iface->truncate;
|
|
filestream_tell_cb = vfs_iface->tell;
|
|
filestream_seek_cb = vfs_iface->seek;
|
|
filestream_read_cb = vfs_iface->read;
|
|
filestream_write_cb = vfs_iface->write;
|
|
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 */
|
|
bool filestream_exists(const char *path)
|
|
{
|
|
RFILE *dummy = NULL;
|
|
|
|
if (!path || !*path)
|
|
return false;
|
|
if (!(dummy = filestream_open(path,
|
|
RETRO_VFS_FILE_ACCESS_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE)))
|
|
return false;
|
|
|
|
if (filestream_close(dummy) != 0)
|
|
free(dummy);
|
|
|
|
dummy = NULL;
|
|
return true;
|
|
}
|
|
|
|
int64_t filestream_get_size(RFILE *stream)
|
|
{
|
|
int64_t output;
|
|
|
|
if (filestream_size_cb)
|
|
output = filestream_size_cb(stream->hfile);
|
|
else
|
|
output = retro_vfs_file_size_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
int64_t filestream_get_sparse_granularity(RFILE *stream)
|
|
{
|
|
if (!stream)
|
|
return 0;
|
|
|
|
/* Same reasoning as filestream_punch_hole: only the built-in
|
|
* implementation has a descriptor to ask about. */
|
|
if (filestream_open_cb != NULL)
|
|
return 0;
|
|
|
|
return retro_vfs_file_get_sparse_granularity_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile);
|
|
}
|
|
|
|
int filestream_punch_hole(RFILE *stream, int64_t offset, int64_t len)
|
|
{
|
|
if (!stream)
|
|
return -1;
|
|
|
|
/* Only the built-in implementation can do this: a frontend-supplied
|
|
* VFS handle need not be a file, and the interface exposes no
|
|
* descriptor to punch through. Reporting -1 lets the caller fall back
|
|
* to writing zeroes. */
|
|
if (filestream_open_cb != NULL)
|
|
return -1;
|
|
|
|
return retro_vfs_file_punch_hole_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, offset, len);
|
|
}
|
|
|
|
int64_t filestream_truncate(RFILE *stream, int64_t length)
|
|
{
|
|
int64_t output;
|
|
|
|
/* The lookahead may hold bytes past the new length; drop it and
|
|
* restore the underlying position first. */
|
|
if (stream)
|
|
filestream_rbuf_discard(stream);
|
|
|
|
if (filestream_truncate_cb)
|
|
output = filestream_truncate_cb(stream->hfile, length);
|
|
else
|
|
output = retro_vfs_file_truncate_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, length);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
/* Drop the lookahead, rewinding the underlying handle over any bytes
|
|
* read ahead but not yet consumed, so the underlying position becomes
|
|
* the logical position again. Must run before anything that hands the
|
|
* underlying position to the VFS directly (write, truncate, flush).
|
|
*
|
|
* If the rewind itself fails the handle is one that cannot seek, in
|
|
* which case interleaving buffered reads with writes was never going
|
|
* to be coherent on it; err_flag is set by the failed raw seek. */
|
|
static void filestream_rbuf_discard(RFILE *stream)
|
|
{
|
|
if (stream->rbuf_len != 0)
|
|
{
|
|
size_t unconsumed = stream->rbuf_len - stream->rbuf_pos;
|
|
if (unconsumed != 0)
|
|
filestream_raw_seek(stream, -(int64_t)unconsumed,
|
|
RETRO_VFS_SEEK_POSITION_CURRENT);
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
}
|
|
|
|
/* Refill the lookahead from the current underlying position.
|
|
* Returns 1 with at least one byte buffered, 0 at end of file or on a
|
|
* read error (the raw read has set err_flag for the latter, matching
|
|
* what the old one-byte filestream_read() did), or -1 when no buffer
|
|
* could be allocated or the handle cannot report a position - in both
|
|
* of those cases the caller falls back to the unbuffered byte path,
|
|
* which behaves exactly as this function did before the lookahead
|
|
* existed.
|
|
*
|
|
* The two -1 arms are not interchangeable; see rbuf_no_tell in struct
|
|
* RFILE for why one of them is remembered and the other is not. */
|
|
static int filestream_rbuf_fill(RFILE *stream)
|
|
{
|
|
int64_t off;
|
|
int64_t got;
|
|
|
|
/* Asked once, answered for the life of the handle. */
|
|
if (stream->rbuf_no_tell)
|
|
return -1;
|
|
|
|
if (!stream->rbuf)
|
|
{
|
|
size_t cap;
|
|
|
|
/* Step down rather than give up: 16 KiB, then 4, then 1. A
|
|
* handle that gets the smallest is still a buffered handle.
|
|
* Runs only after the allocator has refused the nominal size,
|
|
* which is unchanged. Whatever is granted is kept for the life
|
|
* of the handle - there is no path back up to 16 KiB once a
|
|
* smaller buffer exists, and none is worth the bookkeeping when
|
|
* the worst case is still a thousandth of the VFS traffic the
|
|
* byte path would cost. The cap != 0 term keeps the loop off
|
|
* malloc(0) for an override small enough that the floor divides
|
|
* to zero. */
|
|
for (cap = FILESTREAM_RBUF_LEN;
|
|
cap != 0 && cap >= FILESTREAM_RBUF_LEN / 16; cap >>= 2)
|
|
{
|
|
if ((stream->rbuf = (uint8_t*)FILESTREAM_RBUF_MALLOC(cap)))
|
|
{
|
|
stream->rbuf_cap = cap;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!stream->rbuf)
|
|
return -1;
|
|
}
|
|
|
|
if ((off = filestream_raw_tell(stream)) < 0)
|
|
{
|
|
/* Nothing will pass through the buffer until a seek clears the
|
|
* latch, so hand it back rather than hold 16 KiB per pipe or
|
|
* socket for the life of the handle. rbuf_len is necessarily 0
|
|
* here - a span only exists after a fill that succeeded - so
|
|
* there is nothing to discard. */
|
|
free(stream->rbuf);
|
|
stream->rbuf = NULL;
|
|
stream->rbuf_cap = 0;
|
|
stream->rbuf_no_tell = true;
|
|
return -1;
|
|
}
|
|
|
|
if ((got = filestream_raw_read(stream, stream->rbuf,
|
|
(int64_t)stream->rbuf_cap)) <= 0)
|
|
return 0;
|
|
|
|
stream->rbuf_file_off = off;
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = (size_t)got;
|
|
return 1;
|
|
}
|
|
|
|
#undef FILESTREAM_RBUF_MALLOC
|
|
|
|
RFILE* filestream_open(const char *path, unsigned mode, unsigned hints)
|
|
{
|
|
struct retro_vfs_file_handle *fp = NULL;
|
|
RFILE* output = (RFILE*)malloc(sizeof(RFILE));
|
|
|
|
if (!output)
|
|
return NULL;
|
|
|
|
if (filestream_open_cb)
|
|
fp = (struct retro_vfs_file_handle*)
|
|
filestream_open_cb(path, mode, hints);
|
|
else
|
|
fp = (struct retro_vfs_file_handle*)
|
|
retro_vfs_file_open_impl(path, mode, hints);
|
|
|
|
if (!fp)
|
|
{
|
|
free(output);
|
|
return NULL;
|
|
}
|
|
|
|
output->err_flag = false;
|
|
output->rbuf_no_tell = false;
|
|
output->hfile = fp;
|
|
output->rbuf = NULL;
|
|
output->rbuf_cap = 0;
|
|
output->rbuf_len = 0;
|
|
output->rbuf_pos = 0;
|
|
output->rbuf_file_off = 0;
|
|
output->last_io = FILESTREAM_LAST_IO_NONE;
|
|
return output;
|
|
}
|
|
|
|
char* filestream_gets(RFILE *stream, char *s, size_t len)
|
|
{
|
|
size_t done = 0;
|
|
size_t want;
|
|
bool hit_end = false;
|
|
|
|
/* len == 0 leaves no room even for the terminator; the previous
|
|
* implementation decremented the size_t straight past zero and
|
|
* wrote unbounded output. fgets() with n <= 0 returns NULL on
|
|
* every libc worth matching, so do that. */
|
|
if (!stream || len == 0)
|
|
return NULL;
|
|
|
|
/* get max bytes or up to a newline */
|
|
want = len - 1;
|
|
|
|
/* Zero-copy path: when the handle carries a file mapping
|
|
* (RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS, or a frontend
|
|
* mapped-ptr callback), find the line bound with memchr() over the
|
|
* mapping and copy exactly the bytes returned. Only taken with an
|
|
* empty lookahead so the two never both hold stream position. */
|
|
if (want != 0 && stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
int64_t map_len = 0;
|
|
const uint8_t *map = filestream_get_mapped_ptr(stream, &map_len);
|
|
if (map)
|
|
{
|
|
int64_t pos = filestream_tell(stream);
|
|
if (pos >= 0)
|
|
{
|
|
size_t n = 0;
|
|
if (pos < map_len)
|
|
{
|
|
const uint8_t *nl;
|
|
uint64_t avail = (uint64_t)(map_len - pos);
|
|
n = want;
|
|
if ((uint64_t)n > avail)
|
|
n = (size_t)avail;
|
|
if ((nl = (const uint8_t*)memchr(map + pos, '\n', n)))
|
|
n = (size_t)(nl - (map + pos)) + 1;
|
|
memcpy(s, map + pos, n);
|
|
if (n != 0)
|
|
filestream_seek(stream, pos + (int64_t)n,
|
|
RETRO_VFS_SEEK_POSITION_START);
|
|
}
|
|
s[n] = '\0';
|
|
if (n == 0)
|
|
return NULL;
|
|
return s;
|
|
}
|
|
/* Position unknown; the buffered path below reads
|
|
* sequentially and does not need it. */
|
|
}
|
|
}
|
|
|
|
while (done < want)
|
|
{
|
|
const uint8_t *span;
|
|
const uint8_t *nl;
|
|
size_t take;
|
|
bool newline = false;
|
|
|
|
if (stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
int filled = filestream_rbuf_fill(stream);
|
|
if (filled == 0)
|
|
{
|
|
hit_end = true;
|
|
break;
|
|
}
|
|
if (filled < 0)
|
|
{
|
|
/* No lookahead available on this handle; degrade to the
|
|
* historical byte loop for the remainder. */
|
|
int c = 0;
|
|
while (done < want)
|
|
{
|
|
if ((c = filestream_getc(stream)) == EOF)
|
|
{
|
|
hit_end = true;
|
|
break;
|
|
}
|
|
s[done++] = (char)c;
|
|
if (c == '\n')
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
span = stream->rbuf + stream->rbuf_pos;
|
|
take = stream->rbuf_len - stream->rbuf_pos;
|
|
if (take > want - done)
|
|
take = want - done;
|
|
if ((nl = (const uint8_t*)memchr(span, '\n', take)))
|
|
{
|
|
take = (size_t)(nl - span) + 1;
|
|
newline = true;
|
|
}
|
|
memcpy(s + done, span, take);
|
|
done += take;
|
|
stream->rbuf_pos += take;
|
|
if (stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
if (newline)
|
|
break;
|
|
}
|
|
|
|
s[done] = '\0';
|
|
|
|
if (done == 0 && hit_end)
|
|
return NULL;
|
|
return (s);
|
|
}
|
|
|
|
int filestream_getc(RFILE *stream)
|
|
{
|
|
char c = 0;
|
|
int filled;
|
|
|
|
if (!stream)
|
|
return EOF;
|
|
|
|
if (stream->rbuf_pos < stream->rbuf_len)
|
|
{
|
|
int out = (int)stream->rbuf[stream->rbuf_pos++];
|
|
if (stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if ((filled = filestream_rbuf_fill(stream)) > 0)
|
|
{
|
|
int out = (int)stream->rbuf[stream->rbuf_pos++];
|
|
if (stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
return out;
|
|
}
|
|
if (filled == 0)
|
|
return EOF;
|
|
|
|
/* No lookahead on this handle; historical one-byte read. */
|
|
if (filestream_raw_read(stream, &c, 1) == 1)
|
|
return (int)(unsigned char)c;
|
|
return EOF;
|
|
}
|
|
|
|
/* ---- internal scanf engine ----------------------------------------
|
|
*
|
|
* The previous implementation walked the format string and for each
|
|
* %-spec built a per-conversion sub-format string (e.g. "%d%n") and
|
|
* invoked sscanf() for every specifier. sscanf on most libcs runs
|
|
* strlen() over the entire remaining input on every call to compute
|
|
* the internal FILE-stream length, and glibc additionally allocates
|
|
* transient state per call. For input parsed one conversion at a
|
|
* time this is the dominant cost.
|
|
*
|
|
* The native engine below scans every supported conversion in place
|
|
* with no sscanf call anywhere in the path:
|
|
*
|
|
* %d %i %u %o %x %X - via fs_scan_int (custom digit loop)
|
|
* %f %e %g %a (+caps) - via fs_scan_float (uses strtod, which
|
|
* unlike sscanf does not strlen the buffer
|
|
* or allocate per-call state)
|
|
* %p - via fs_scan_pointer (hex parse)
|
|
* %s %c %[..] - via fs_scan_str/char/set (byte loop)
|
|
* %ls %lc %l[..] - via fs_scan_wide (UTF-8 decoding via
|
|
* libretro-common's utf8_walk; no
|
|
* mbrtowc dependency)
|
|
* %n - direct write of chars-consumed-so-far
|
|
* %% - literal match
|
|
*
|
|
* with the full grammar: '*' assignment suppression, decimal width,
|
|
* and length modifiers hh / h / l / ll / z / j / t / L (plus the
|
|
* MSVC-only I / I32 / I64 size prefixes that some legacy core code
|
|
* still uses). */
|
|
|
|
#define FS_LM_NONE 0
|
|
#define FS_LM_HH 1
|
|
#define FS_LM_H 2
|
|
#define FS_LM_L 3
|
|
#define FS_LM_LL 4
|
|
#define FS_LM_J 5
|
|
#define FS_LM_Z 6
|
|
#define FS_LM_T 7
|
|
#define FS_LM_BIG_L 8
|
|
|
|
typedef struct
|
|
{
|
|
int width; /* 0 means unbounded */
|
|
int length_mod; /* FS_LM_* */
|
|
char specifier; /* d i u o x X c s [ n % f e g a F E G A p */
|
|
bool suppress; /* %* */
|
|
/* For '[' specifiers: bounds of the scanset (after '[' or '[^',
|
|
* up to but not including ']'). */
|
|
const char *set_start;
|
|
const char *set_end;
|
|
bool set_negate;
|
|
} fs_scan_spec_t;
|
|
|
|
/* Parse one conversion specifier starting at `fmt` (which points
|
|
* just past '%'). Returns updated fmt position, or NULL if the
|
|
* specifier is malformed. */
|
|
static const char *fs_parse_spec(const char *fmt, fs_scan_spec_t *sp)
|
|
{
|
|
sp->width = 0;
|
|
sp->length_mod = FS_LM_NONE;
|
|
sp->specifier = '\0';
|
|
sp->suppress = false;
|
|
sp->set_start = NULL;
|
|
sp->set_end = NULL;
|
|
sp->set_negate = false;
|
|
|
|
if (*fmt == '*')
|
|
{
|
|
sp->suppress = true;
|
|
fmt++;
|
|
}
|
|
while (*fmt >= '0' && *fmt <= '9')
|
|
{
|
|
sp->width = sp->width * 10 + (*fmt - '0');
|
|
fmt++;
|
|
}
|
|
switch (*fmt)
|
|
{
|
|
case 'h':
|
|
if (fmt[1] == 'h') { sp->length_mod = FS_LM_HH; fmt += 2; }
|
|
else { sp->length_mod = FS_LM_H; fmt += 1; }
|
|
break;
|
|
case 'l':
|
|
if (fmt[1] == 'l') { sp->length_mod = FS_LM_LL; fmt += 2; }
|
|
else { sp->length_mod = FS_LM_L; fmt += 1; }
|
|
break;
|
|
case 'j': sp->length_mod = FS_LM_J; fmt++; break;
|
|
case 'z': sp->length_mod = FS_LM_Z; fmt++; break;
|
|
case 't': sp->length_mod = FS_LM_T; fmt++; break;
|
|
case 'L': sp->length_mod = FS_LM_BIG_L; fmt++; break;
|
|
/* MSVC-style "I" size prefix: I64 -> __int64 (==int64_t), I32
|
|
* -> 32-bit (==int), I alone -> ptrdiff_t / size_t. This is a
|
|
* Microsoft extension some libretro core authors use on
|
|
* older MSVC toolchains that predate full C99 %lld support. */
|
|
case 'I':
|
|
if (fmt[1] == '6' && fmt[2] == '4')
|
|
{ sp->length_mod = FS_LM_LL; fmt += 3; }
|
|
else if (fmt[1] == '3' && fmt[2] == '2')
|
|
{ sp->length_mod = FS_LM_NONE; fmt += 3; }
|
|
else
|
|
{ sp->length_mod = FS_LM_Z; fmt += 1; }
|
|
break;
|
|
default: break;
|
|
}
|
|
if (!*fmt)
|
|
return NULL;
|
|
sp->specifier = *fmt++;
|
|
if (sp->specifier == '[')
|
|
{
|
|
if (*fmt == '^')
|
|
{
|
|
sp->set_negate = true;
|
|
fmt++;
|
|
}
|
|
sp->set_start = fmt;
|
|
/* literal ']' as first scanset char is part of the set */
|
|
if (*fmt == ']')
|
|
fmt++;
|
|
while (*fmt && *fmt != ']')
|
|
fmt++;
|
|
if (*fmt != ']')
|
|
return NULL;
|
|
sp->set_end = fmt;
|
|
fmt++;
|
|
}
|
|
return fmt;
|
|
}
|
|
|
|
static void fs_store_int(const fs_scan_spec_t *sp, va_list *args,
|
|
int64_t v, bool is_signed)
|
|
{
|
|
if (is_signed)
|
|
{
|
|
switch (sp->length_mod)
|
|
{
|
|
case FS_LM_HH: *va_arg(*args, signed char*) = (signed char)v; break;
|
|
case FS_LM_H: *va_arg(*args, short*) = (short)v; break;
|
|
case FS_LM_L: *va_arg(*args, long*) = (long)v; break;
|
|
case FS_LM_LL: *va_arg(*args, int64_t*) = (int64_t)v; break;
|
|
case FS_LM_Z: *va_arg(*args, size_t*) = (size_t)v; break;
|
|
case FS_LM_J: *va_arg(*args, intmax_t*) = (intmax_t)v; break;
|
|
case FS_LM_T: *va_arg(*args, ptrdiff_t*) = (ptrdiff_t)v; break;
|
|
default: *va_arg(*args, int*) = (int)v; break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint64_t uv = (uint64_t)v;
|
|
switch (sp->length_mod)
|
|
{
|
|
case FS_LM_HH: *va_arg(*args, unsigned char*) = (unsigned char)uv; break;
|
|
case FS_LM_H: *va_arg(*args, unsigned short*) = (unsigned short)uv; break;
|
|
case FS_LM_L: *va_arg(*args, unsigned long*) = (unsigned long)uv; break;
|
|
case FS_LM_LL: *va_arg(*args, uint64_t*) = (uint64_t)uv; break;
|
|
case FS_LM_Z: *va_arg(*args, size_t*) = (size_t)uv; break;
|
|
case FS_LM_J: *va_arg(*args, uintmax_t*) = (uintmax_t)uv; break;
|
|
case FS_LM_T: *va_arg(*args, size_t*) = (size_t)uv; break;
|
|
default: *va_arg(*args, unsigned int*) = (unsigned int)uv; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Scan one integer conversion. `base` is 0 for %i (auto-detect),
|
|
* else the explicit base (8/10/16). Returns chars consumed, or
|
|
* -1 on parse failure. On success, advances *pp.
|
|
*
|
|
* Width handling: a width of 0 from sp->width means "unlimited" — we
|
|
* normalize that to INT_MAX up front so every subsequent comparison
|
|
* is a single check (no special-casing of "<= 0 means unbounded"
|
|
* which broke when width was legitimately consumed down to 0 by
|
|
* the sign/0x prefix). */
|
|
static int fs_scan_int(const char **pp, const fs_scan_spec_t *sp,
|
|
int base, bool is_signed, va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
const char *digit_start;
|
|
uint64_t acc = 0;
|
|
int neg = 0;
|
|
int width = sp->width > 0 ? sp->width : INT_MAX;
|
|
int digits = 0;
|
|
|
|
/* skip leading whitespace */
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
|
|
/* optional sign */
|
|
if ((*p == '+' || *p == '-') && width > 0)
|
|
{
|
|
if (*p == '-')
|
|
neg = 1;
|
|
p++;
|
|
width--;
|
|
}
|
|
|
|
/* base autodetect / optional 0x prefix.
|
|
* Note: sscanf's "%x" / "%i" treat "0x" with no following hex
|
|
* digit as a successful match of the value 0, consuming the
|
|
* "0x" (2 chars). We mirror that by counting the leading "0"
|
|
* as our first digit when we commit to base 16 via "0x". */
|
|
if (base == 0)
|
|
{
|
|
if (*p == '0')
|
|
{
|
|
if ((p[1] == 'x' || p[1] == 'X') && width >= 2)
|
|
{
|
|
base = 16;
|
|
acc = 0;
|
|
digits = 1; /* the "0" counts as a digit */
|
|
p += 2;
|
|
width -= 2;
|
|
}
|
|
else
|
|
base = 8;
|
|
}
|
|
else
|
|
base = 10;
|
|
}
|
|
else if (base == 16)
|
|
{
|
|
if (*p == '0' && (p[1] == 'x' || p[1] == 'X') && width >= 2)
|
|
{
|
|
acc = 0;
|
|
digits = 1; /* the "0" counts as a digit */
|
|
p += 2;
|
|
width -= 2;
|
|
}
|
|
}
|
|
|
|
digit_start = p;
|
|
while (*p && (p - digit_start) < width)
|
|
{
|
|
unsigned char c = (unsigned char)*p;
|
|
int d;
|
|
if (c >= '0' && c <= '9') d = c - '0';
|
|
else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
|
|
else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
|
|
else break;
|
|
if (d >= base)
|
|
break;
|
|
acc = acc * (uint64_t)base + (uint64_t)d;
|
|
p++;
|
|
digits++;
|
|
}
|
|
|
|
if (digits == 0)
|
|
return -1;
|
|
|
|
if (!sp->suppress)
|
|
{
|
|
int64_t sv = neg
|
|
? -(int64_t)acc
|
|
: (int64_t)acc;
|
|
fs_store_int(sp, args, sv, is_signed);
|
|
}
|
|
*pp = p;
|
|
return (int)(p - start);
|
|
}
|
|
|
|
/* %s — skip leading ws, copy non-ws up to width (default unbounded).
|
|
* Always NUL-terminates output. Fails only on no chars matched. */
|
|
static int fs_scan_str(const char **pp, const fs_scan_spec_t *sp,
|
|
va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
char *out = NULL;
|
|
int n = 0;
|
|
int width = sp->width;
|
|
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
|
|
if (!sp->suppress)
|
|
out = va_arg(*args, char*);
|
|
|
|
while (*p && !isspace((unsigned char)*p) && (width <= 0 || n < width))
|
|
{
|
|
if (out) out[n] = *p;
|
|
n++;
|
|
p++;
|
|
}
|
|
if (n == 0)
|
|
return -1;
|
|
if (out) out[n] = '\0';
|
|
*pp = p;
|
|
return (int)(p - start);
|
|
}
|
|
|
|
/* %c — read exactly width chars (default 1). Does NOT skip
|
|
* whitespace. Does NOT NUL-terminate. Fails if fewer than width
|
|
* chars available. */
|
|
static int fs_scan_char(const char **pp, const fs_scan_spec_t *sp,
|
|
va_list *args)
|
|
{
|
|
const char *p = *pp;
|
|
int width = sp->width > 0 ? sp->width : 1;
|
|
int i;
|
|
char *out = sp->suppress ? NULL : va_arg(*args, char*);
|
|
|
|
for (i = 0; i < width; i++)
|
|
{
|
|
if (!p[i])
|
|
return -1;
|
|
if (out)
|
|
out[i] = p[i];
|
|
}
|
|
*pp = p + width;
|
|
return width;
|
|
}
|
|
|
|
/* Test scanset membership. `set` points just after '[' (or after
|
|
* '[^' if negated); `end` points at the closing ']'. */
|
|
static bool fs_scanset_match(const char *set, const char *end,
|
|
bool negate, unsigned char c)
|
|
{
|
|
const char *p = set;
|
|
bool in_set = false;
|
|
/* literal ']' as first char is part of the set */
|
|
if (p < end && *p == ']')
|
|
{
|
|
if (c == ']') in_set = true;
|
|
p++;
|
|
}
|
|
while (p < end)
|
|
{
|
|
if (p + 2 < end && p[1] == '-' && p[2] != ']')
|
|
{
|
|
unsigned char lo = (unsigned char)p[0];
|
|
unsigned char hi = (unsigned char)p[2];
|
|
if (lo > hi) { unsigned char t = lo; lo = hi; hi = t; }
|
|
if (c >= lo && c <= hi) in_set = true;
|
|
p += 3;
|
|
}
|
|
else
|
|
{
|
|
if ((unsigned char)*p == c) in_set = true;
|
|
p++;
|
|
}
|
|
}
|
|
return negate ? !in_set : in_set;
|
|
}
|
|
|
|
/* %[..] — does NOT skip leading whitespace. */
|
|
static int fs_scan_set(const char **pp, const fs_scan_spec_t *sp,
|
|
va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
char *out = sp->suppress ? NULL : va_arg(*args, char*);
|
|
int n = 0;
|
|
int width = sp->width;
|
|
|
|
while (*p && (width <= 0 || n < width)
|
|
&& fs_scanset_match(sp->set_start, sp->set_end,
|
|
sp->set_negate, (unsigned char)*p))
|
|
{
|
|
if (out) out[n] = *p;
|
|
n++;
|
|
p++;
|
|
}
|
|
if (n == 0)
|
|
return -1;
|
|
if (out) out[n] = '\0';
|
|
*pp = p;
|
|
return n;
|
|
}
|
|
|
|
/* %n — store chars-consumed-so-far into the destination of the
|
|
* appropriate width. Suppressed %n is a no-op. */
|
|
static void fs_store_n(const fs_scan_spec_t *sp, va_list *args, int n)
|
|
{
|
|
if (sp->suppress)
|
|
return;
|
|
switch (sp->length_mod)
|
|
{
|
|
case FS_LM_HH: *va_arg(*args, signed char*) = (signed char)n; break;
|
|
case FS_LM_H: *va_arg(*args, short*) = (short)n; break;
|
|
case FS_LM_L: *va_arg(*args, long*) = (long)n; break;
|
|
case FS_LM_LL: *va_arg(*args, int64_t*) = (int64_t)n; break;
|
|
case FS_LM_Z: *va_arg(*args, size_t*) = (size_t)n; break;
|
|
case FS_LM_J: *va_arg(*args, intmax_t*) = (intmax_t)n; break;
|
|
case FS_LM_T: *va_arg(*args, ptrdiff_t*) = (ptrdiff_t)n; break;
|
|
default: *va_arg(*args, int*) = n; break;
|
|
}
|
|
}
|
|
|
|
/* Decides whether the engine can serve a (specifier, length_mod)
|
|
* combination at all. All standard combinations are now handled
|
|
* natively (no sscanf calls remain). Returns false only for an
|
|
* unrecognized specifier or an out-of-spec length modifier
|
|
* (e.g. %hf, %jc), in which case the dispatch breaks the parse
|
|
* loop the same way the prior engine did on malformed input. */
|
|
static bool fs_native_handles(char specifier, int length_mod)
|
|
{
|
|
switch (specifier)
|
|
{
|
|
case 'd': case 'i': case 'u': case 'o': case 'x': case 'X':
|
|
/* Integers: every standard length modifier is supported.
|
|
* The MSVC "I" prefix maps to LL/NONE/Z in fs_parse_spec. */
|
|
return length_mod == FS_LM_NONE
|
|
|| length_mod == FS_LM_HH
|
|
|| length_mod == FS_LM_H
|
|
|| length_mod == FS_LM_L
|
|
|| length_mod == FS_LM_LL
|
|
|| length_mod == FS_LM_Z
|
|
|| length_mod == FS_LM_J
|
|
|| length_mod == FS_LM_T;
|
|
case 'f': case 'e': case 'g': case 'a':
|
|
case 'F': case 'E': case 'G': case 'A':
|
|
/* Floats: NONE -> float, L -> double, BIG_L -> long double. */
|
|
return length_mod == FS_LM_NONE
|
|
|| length_mod == FS_LM_L
|
|
|| length_mod == FS_LM_BIG_L;
|
|
case 's': case 'c': case '[':
|
|
/* Strings/chars/scansets: NONE writes char, L writes wchar_t. */
|
|
return length_mod == FS_LM_NONE
|
|
|| length_mod == FS_LM_L;
|
|
case 'p':
|
|
return length_mod == FS_LM_NONE;
|
|
case 'n':
|
|
/* All standard int length mods are handled by fs_store_n. */
|
|
return length_mod == FS_LM_NONE
|
|
|| length_mod == FS_LM_HH
|
|
|| length_mod == FS_LM_H
|
|
|| length_mod == FS_LM_L
|
|
|| length_mod == FS_LM_LL
|
|
|| length_mod == FS_LM_Z
|
|
|| length_mod == FS_LM_J
|
|
|| length_mod == FS_LM_T;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Scan a floating-point conversion. Handles %f / %e / %g / %a and
|
|
* their capitalized siblings. Uses rstrtod() — which is C89 and,
|
|
* unlike sscanf, does not run strlen() across the rest of the
|
|
* buffer or allocate any per-call FILE-stream state. Width is
|
|
* honored by copying at most `width` non-whitespace bytes into a
|
|
* stack scratch buffer, NUL-terminating, and letting strtod parse
|
|
* that. For %Lf, strtod's double result is cast to long double:
|
|
* on platforms where long double is wider than double (notably
|
|
* x86 with 80-bit extended), this loses precision relative to a
|
|
* hypothetical strtold call — but strtold is C99 and not available
|
|
* in strict MSVC C89, and %Lf via filestream_scanf is essentially
|
|
* unused, so we accept the trade. */
|
|
static int fs_scan_float(const char **pp, const fs_scan_spec_t *sp,
|
|
va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
char *endp;
|
|
double val;
|
|
ptrdiff_t advance;
|
|
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
if (!*p)
|
|
return -1;
|
|
|
|
if (sp->width > 0)
|
|
{
|
|
char tmp[64];
|
|
int w = sp->width;
|
|
int i;
|
|
if (w >= (int)sizeof(tmp))
|
|
w = (int)sizeof(tmp) - 1;
|
|
for (i = 0; i < w && p[i] && !isspace((unsigned char)p[i]); i++)
|
|
tmp[i] = p[i];
|
|
tmp[i] = '\0';
|
|
val = rstrtod(tmp, &endp);
|
|
if (endp == tmp)
|
|
return -1;
|
|
advance = endp - tmp;
|
|
}
|
|
else
|
|
{
|
|
val = rstrtod(p, &endp);
|
|
if (endp == p)
|
|
return -1;
|
|
advance = endp - p;
|
|
}
|
|
p += advance;
|
|
|
|
if (!sp->suppress)
|
|
{
|
|
switch (sp->length_mod)
|
|
{
|
|
case FS_LM_NONE: *va_arg(*args, float*) = (float)val; break;
|
|
case FS_LM_L: *va_arg(*args, double*) = val; break;
|
|
case FS_LM_BIG_L: *va_arg(*args, long double*) = (long double)val; break;
|
|
default: return -1;
|
|
}
|
|
}
|
|
*pp = p;
|
|
return (int)(p - start);
|
|
}
|
|
|
|
/* %p — implementation-defined per C99, but the de-facto format
|
|
* everywhere (glibc, musl, BSD, MSVC's CRT for the common case)
|
|
* is hex digits with an optional "0x"/"0X" prefix, optionally
|
|
* preceded by whitespace. Parse that, store as void*. */
|
|
static int fs_scan_pointer(const char **pp, const fs_scan_spec_t *sp,
|
|
va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
const char *digit_start;
|
|
uint64_t acc = 0;
|
|
int width = sp->width > 0 ? sp->width : INT_MAX;
|
|
int digits = 0;
|
|
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
|
|
if (*p == '0' && (p[1] == 'x' || p[1] == 'X') && width >= 2)
|
|
{
|
|
p += 2;
|
|
width -= 2;
|
|
}
|
|
digit_start = p;
|
|
while (*p && (p - digit_start) < width)
|
|
{
|
|
unsigned char c = (unsigned char)*p;
|
|
int d;
|
|
if (c >= '0' && c <= '9') d = c - '0';
|
|
else if (c >= 'a' && c <= 'f') d = c - 'a' + 10;
|
|
else if (c >= 'A' && c <= 'F') d = c - 'A' + 10;
|
|
else break;
|
|
acc = (acc << 4) | (uint64_t)d;
|
|
p++;
|
|
digits++;
|
|
}
|
|
if (digits == 0)
|
|
return -1;
|
|
if (!sp->suppress)
|
|
*va_arg(*args, void**) = (void*)(uintptr_t)acc;
|
|
*pp = p;
|
|
return (int)(p - start);
|
|
}
|
|
|
|
/* Wide-character scanset membership. Set elements in the format
|
|
* string are encoded as bytes; we promote each to wchar_t by zero
|
|
* extension via unsigned char. This is exact for ASCII set chars
|
|
* (the typical case) and is the same convention glibc uses for
|
|
* wide scansets with ASCII format strings. */
|
|
static bool fs_wscanset_match(const char *set, const char *end,
|
|
bool negate, wchar_t c)
|
|
{
|
|
const char *p = set;
|
|
bool in_set = false;
|
|
if (p < end && *p == ']')
|
|
{
|
|
if (c == (wchar_t)']') in_set = true;
|
|
p++;
|
|
}
|
|
while (p < end)
|
|
{
|
|
if (p + 2 < end && p[1] == '-' && p[2] != ']')
|
|
{
|
|
wchar_t lo = (wchar_t)(unsigned char)p[0];
|
|
wchar_t hi = (wchar_t)(unsigned char)p[2];
|
|
if (lo > hi) { wchar_t t = lo; lo = hi; hi = t; }
|
|
if (c >= lo && c <= hi) in_set = true;
|
|
p += 3;
|
|
}
|
|
else
|
|
{
|
|
if ((wchar_t)(unsigned char)*p == c) in_set = true;
|
|
p++;
|
|
}
|
|
}
|
|
return negate ? !in_set : in_set;
|
|
}
|
|
|
|
/* Wide-char scanf primitive. `mode` selects the variant:
|
|
* 's' — skip ws, read non-ws wide chars until ws/width/end; NUL-terminate
|
|
* 'c' — read exactly width wide chars (default 1); no ws skip; no NUL
|
|
* '[' — read wide chars while scanset matches; NUL-terminate
|
|
*
|
|
* Multibyte input is decoded as UTF-8 via libretro-common's stateless
|
|
* utf8_walk(); this avoids any dependency on the host libc's
|
|
* mbrtowc/mbstate_t (absent in DJGPP and other older toolchains) and
|
|
* gives the same behavior across platforms. The lead byte's UTF-8
|
|
* width is checked against the buffer's NUL boundary before walking
|
|
* so truncated input never reads past the terminator. */
|
|
static int fs_scan_wide(const char **pp, const fs_scan_spec_t *sp,
|
|
char mode, va_list *args)
|
|
{
|
|
const char *start = *pp;
|
|
const char *p = start;
|
|
wchar_t *out;
|
|
int n = 0;
|
|
int width = sp->width;
|
|
int max_n;
|
|
|
|
if (mode == 's')
|
|
{
|
|
while (isspace((unsigned char)*p))
|
|
p++;
|
|
max_n = (width > 0) ? width : INT_MAX;
|
|
}
|
|
else if (mode == 'c')
|
|
max_n = (width > 0) ? width : 1;
|
|
else /* '[' */
|
|
max_n = (width > 0) ? width : INT_MAX;
|
|
|
|
out = sp->suppress ? NULL : va_arg(*args, wchar_t*);
|
|
|
|
while (n < max_n)
|
|
{
|
|
const char *before;
|
|
unsigned char lead;
|
|
int needed;
|
|
int k;
|
|
uint32_t cp;
|
|
|
|
if (!*p)
|
|
break;
|
|
if (mode == 's' && isspace((unsigned char)*p))
|
|
break;
|
|
|
|
/* Bounds-check the UTF-8 sequence width against the NUL
|
|
* boundary before letting utf8_walk advance past it. */
|
|
lead = (unsigned char)*p;
|
|
if (lead < 0x80) needed = 1;
|
|
else if (lead < 0xC0) return -1; /* stray continuation byte */
|
|
else if (lead < 0xE0) needed = 2;
|
|
else if (lead < 0xF0) needed = 3;
|
|
else needed = 4;
|
|
for (k = 1; k < needed; k++)
|
|
if (!p[k])
|
|
return -1;
|
|
|
|
before = p;
|
|
cp = utf8_walk(&p);
|
|
|
|
if (mode == '[' && !fs_wscanset_match(sp->set_start, sp->set_end,
|
|
sp->set_negate, (wchar_t)cp))
|
|
{
|
|
p = before; /* codepoint rejected — do not consume */
|
|
break;
|
|
}
|
|
if (out)
|
|
out[n] = (wchar_t)cp;
|
|
n++;
|
|
}
|
|
|
|
/* %lc must read exactly max_n wide chars; falling short is failure. */
|
|
if (mode == 'c' && n < max_n)
|
|
return -1;
|
|
/* %ls / %l[ must match at least one wide char. */
|
|
if (mode != 'c' && n == 0)
|
|
return -1;
|
|
/* %ls and %l[ NUL-terminate; %lc does not. */
|
|
if (mode != 'c' && out)
|
|
out[n] = 0;
|
|
|
|
*pp = p;
|
|
return (int)(p - start);
|
|
}
|
|
|
|
int filestream_vscanf(RFILE *stream, const char *format, va_list *args)
|
|
{
|
|
/* The scan window is heap rather than a local: at char buf[4096]
|
|
* this was a 4368-byte frame, over half of the 8 KiB a GEKKO
|
|
* thread gets (the GEKKO STACKSIZE in rthreads.c). Shrinking it
|
|
* instead would have been the cheaper change and the wrong one -
|
|
* the window is how far a single conversion may reach, so a
|
|
* smaller one fails differently on long input rather than merely
|
|
* more slowly. One allocation per call is nothing beside the read
|
|
* this function already performs. */
|
|
char *buf = (char*)malloc(FILESTREAM_SCANF_WINDOW);
|
|
va_list args_copy;
|
|
const char *bufiter;
|
|
const char *fmt = format;
|
|
int ret = 0;
|
|
int64_t startpos;
|
|
int64_t maxlen;
|
|
|
|
if (!buf)
|
|
return EOF;
|
|
|
|
startpos = filestream_tell(stream);
|
|
maxlen = filestream_read(stream, buf, FILESTREAM_SCANF_WINDOW - 1);
|
|
|
|
if (maxlen <= 0)
|
|
{
|
|
free(buf);
|
|
return EOF;
|
|
}
|
|
|
|
buf[maxlen] = '\0';
|
|
|
|
#ifdef __va_copy
|
|
__va_copy(args_copy, *args);
|
|
#else
|
|
va_copy(args_copy, *args);
|
|
#endif
|
|
|
|
bufiter = buf;
|
|
|
|
while (*fmt && *bufiter)
|
|
{
|
|
if (*fmt == '%')
|
|
{
|
|
fs_scan_spec_t sp;
|
|
const char *fmt_next;
|
|
int consumed = -1;
|
|
|
|
fmt++; /* past '%' */
|
|
|
|
/* literal %% */
|
|
if (*fmt == '%')
|
|
{
|
|
if (*bufiter != '%')
|
|
break;
|
|
bufiter++;
|
|
fmt++;
|
|
continue;
|
|
}
|
|
|
|
fmt_next = fs_parse_spec(fmt, &sp);
|
|
if (!fmt_next)
|
|
break; /* malformed spec — stop, matching prior behavior */
|
|
|
|
if (!fs_native_handles(sp.specifier, sp.length_mod))
|
|
break; /* unsupported combination — stop, matching prior behavior */
|
|
|
|
switch (sp.specifier)
|
|
{
|
|
case 'd':
|
|
consumed = fs_scan_int(&bufiter, &sp, 10, true, &args_copy); break;
|
|
case 'i':
|
|
consumed = fs_scan_int(&bufiter, &sp, 0, true, &args_copy); break;
|
|
case 'u':
|
|
consumed = fs_scan_int(&bufiter, &sp, 10, false, &args_copy); break;
|
|
case 'o':
|
|
consumed = fs_scan_int(&bufiter, &sp, 8, false, &args_copy); break;
|
|
case 'x':
|
|
case 'X':
|
|
consumed = fs_scan_int(&bufiter, &sp, 16, false, &args_copy); break;
|
|
case 'f': case 'e': case 'g': case 'a':
|
|
case 'F': case 'E': case 'G': case 'A':
|
|
consumed = fs_scan_float(&bufiter, &sp, &args_copy); break;
|
|
case 'p':
|
|
consumed = fs_scan_pointer(&bufiter, &sp, &args_copy); break;
|
|
case 's':
|
|
if (sp.length_mod == FS_LM_L)
|
|
consumed = fs_scan_wide(&bufiter, &sp, 's', &args_copy);
|
|
else
|
|
consumed = fs_scan_str(&bufiter, &sp, &args_copy);
|
|
break;
|
|
case 'c':
|
|
if (sp.length_mod == FS_LM_L)
|
|
consumed = fs_scan_wide(&bufiter, &sp, 'c', &args_copy);
|
|
else
|
|
consumed = fs_scan_char(&bufiter, &sp, &args_copy);
|
|
break;
|
|
case '[':
|
|
if (sp.length_mod == FS_LM_L)
|
|
consumed = fs_scan_wide(&bufiter, &sp, '[', &args_copy);
|
|
else
|
|
consumed = fs_scan_set(&bufiter, &sp, &args_copy);
|
|
break;
|
|
case 'n':
|
|
fs_store_n(&sp, &args_copy, (int)(bufiter - buf));
|
|
consumed = 0; /* succeeds, consumes no input */
|
|
break;
|
|
default:
|
|
/* Unreachable: fs_native_handles returned false for
|
|
* anything not in this switch. */
|
|
consumed = -1;
|
|
break;
|
|
}
|
|
|
|
if (consumed < 0)
|
|
break;
|
|
|
|
/* %n doesn't count toward the return value; nor do
|
|
* assignment-suppressed conversions. */
|
|
if (!sp.suppress && sp.specifier != 'n')
|
|
ret++;
|
|
|
|
/* Suppressed conversion that consumed nothing — bail,
|
|
* matching the prior "sublen == 0" guard. */
|
|
if (sp.suppress && sp.specifier != 'n' && consumed == 0)
|
|
break;
|
|
|
|
fmt = fmt_next;
|
|
}
|
|
else if (isspace((unsigned char)*fmt))
|
|
{
|
|
while (isspace((unsigned char)*bufiter))
|
|
bufiter++;
|
|
while (isspace((unsigned char)*fmt))
|
|
fmt++;
|
|
}
|
|
else
|
|
{
|
|
if (*bufiter != *fmt)
|
|
break;
|
|
bufiter++;
|
|
fmt++;
|
|
}
|
|
}
|
|
|
|
va_end(args_copy);
|
|
|
|
/* Seek before the free: the new position is derived from how far
|
|
* bufiter walked into buf. */
|
|
filestream_seek(stream, startpos + (bufiter - buf),
|
|
RETRO_VFS_SEEK_POSITION_START);
|
|
|
|
free(buf);
|
|
return ret;
|
|
}
|
|
|
|
int filestream_scanf(RFILE *stream, const char* format, ...)
|
|
{
|
|
int ret;
|
|
va_list vl;
|
|
va_start(vl, format);
|
|
ret = filestream_vscanf(stream, format, &vl);
|
|
va_end(vl);
|
|
return ret;
|
|
}
|
|
|
|
static int64_t filestream_raw_seek(RFILE *stream,
|
|
int64_t offset, int seek_position)
|
|
{
|
|
int64_t output;
|
|
|
|
if (filestream_seek_cb)
|
|
output = filestream_seek_cb(stream->hfile, offset, seek_position);
|
|
else
|
|
output = retro_vfs_file_seek_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile,
|
|
offset, seek_position);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
else
|
|
/* A successful positioning call satisfies the update-stream
|
|
* transition rule in both directions. */
|
|
stream->last_io = FILESTREAM_LAST_IO_NONE;
|
|
|
|
return output;
|
|
}
|
|
|
|
int64_t filestream_seek(RFILE *stream, int64_t offset, int seek_position)
|
|
{
|
|
int64_t plain;
|
|
|
|
if (stream && stream->rbuf_len != 0)
|
|
{
|
|
int64_t output;
|
|
int64_t adjusted = offset;
|
|
|
|
/* The underlying handle sits past the logical position by the
|
|
* unconsumed lookahead; a relative seek must be taken from the
|
|
* logical position. */
|
|
if (seek_position == RETRO_VFS_SEEK_POSITION_CURRENT)
|
|
adjusted -= (int64_t)(stream->rbuf_len - stream->rbuf_pos);
|
|
|
|
output = filestream_raw_seek(stream, adjusted, seek_position);
|
|
|
|
/* Every successful seek drops the lookahead rather than trying
|
|
* to serve targets inside it: matching stdio, where fseek()
|
|
* discards the read buffer, keeps rewind-and-reread of
|
|
* synthetic files (procfs/sysfs pollers) returning fresh
|
|
* bytes instead of cached ones. On failure the underlying
|
|
* position is unchanged, so the lookahead is still valid and
|
|
* the logical position observably does not move. */
|
|
if (output != VFS_ERROR_RETURN_VALUE)
|
|
{
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/* The latch clears here and only here: it cannot be set while a
|
|
* span exists, so a latched handle always has rbuf_len == 0 and
|
|
* always takes this path. A clear in the branch above would be
|
|
* unreachable. */
|
|
plain = filestream_raw_seek(stream, offset, seek_position);
|
|
if (plain != VFS_ERROR_RETURN_VALUE)
|
|
stream->rbuf_no_tell = false;
|
|
return plain;
|
|
}
|
|
|
|
int filestream_eof(RFILE *stream)
|
|
{
|
|
return filestream_tell(stream) == filestream_get_size(stream) ? EOF : 0;
|
|
}
|
|
|
|
static int64_t filestream_raw_tell(RFILE *stream)
|
|
{
|
|
int64_t output;
|
|
|
|
if (filestream_tell_cb)
|
|
output = filestream_tell_cb(stream->hfile);
|
|
else
|
|
output = retro_vfs_file_tell_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
int64_t filestream_tell(RFILE *stream)
|
|
{
|
|
/* While the lookahead is active the logical position is tracked
|
|
* here, a fill-time snapshot plus consumed bytes; no VFS call. */
|
|
if (stream && stream->rbuf_len != 0)
|
|
return stream->rbuf_file_off + (int64_t)stream->rbuf_pos;
|
|
return filestream_raw_tell(stream);
|
|
}
|
|
|
|
void filestream_rewind(RFILE *stream)
|
|
{
|
|
if (!stream)
|
|
return;
|
|
filestream_seek(stream, 0L, RETRO_VFS_SEEK_POSITION_START);
|
|
stream->err_flag = false;
|
|
}
|
|
|
|
static int64_t filestream_raw_read(RFILE *stream, void *s, int64_t len)
|
|
{
|
|
int64_t output;
|
|
|
|
/* Update-stream rule: input directly after output needs a
|
|
* positioning call between them, or stdio-backed VFS paths hand
|
|
* back stale bytes. A zero-byte relative seek is the cheapest
|
|
* legal one. */
|
|
if (stream->last_io == FILESTREAM_LAST_IO_WRITE)
|
|
filestream_raw_seek(stream, 0, RETRO_VFS_SEEK_POSITION_CURRENT);
|
|
stream->last_io = FILESTREAM_LAST_IO_READ;
|
|
|
|
if (filestream_read_cb)
|
|
output = filestream_read_cb(stream->hfile, s, len);
|
|
else
|
|
output = retro_vfs_file_read_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, s, len);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
int64_t filestream_read(RFILE *stream, void *s, int64_t len)
|
|
{
|
|
if (stream && stream->rbuf_pos < stream->rbuf_len && len > 0)
|
|
{
|
|
int64_t rest;
|
|
size_t drained = stream->rbuf_len - stream->rbuf_pos;
|
|
|
|
if ((uint64_t)len < (uint64_t)drained)
|
|
drained = (size_t)len;
|
|
memcpy(s, stream->rbuf + stream->rbuf_pos, drained);
|
|
stream->rbuf_pos += drained;
|
|
if (stream->rbuf_pos == stream->rbuf_len)
|
|
{
|
|
stream->rbuf_pos = 0;
|
|
stream->rbuf_len = 0;
|
|
}
|
|
if ((int64_t)drained == len)
|
|
return len;
|
|
|
|
/* err_flag is set by the raw read on failure; having already
|
|
* moved bytes to the caller, report the short count the way a
|
|
* partially satisfied stdio read would. */
|
|
if ((rest = filestream_raw_read(stream,
|
|
(uint8_t*)s + drained, len - (int64_t)drained)) < 0)
|
|
return (int64_t)drained;
|
|
return (int64_t)drained + rest;
|
|
}
|
|
return filestream_raw_read(stream, s, len);
|
|
}
|
|
|
|
int filestream_flush(RFILE *stream)
|
|
{
|
|
int output;
|
|
|
|
/* Match stdio fflush() on an update stream: synchronize the
|
|
* underlying position with the logical one. */
|
|
if (stream)
|
|
filestream_rbuf_discard(stream);
|
|
|
|
if (filestream_flush_cb)
|
|
output = filestream_flush_cb(stream->hfile);
|
|
else
|
|
output = retro_vfs_file_flush_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
else if (stream)
|
|
/* A flush also satisfies the update-stream transition rule. */
|
|
stream->last_io = FILESTREAM_LAST_IO_NONE;
|
|
|
|
return output;
|
|
}
|
|
|
|
int filestream_delete(const char *path)
|
|
{
|
|
if (filestream_remove_cb)
|
|
return filestream_remove_cb(path);
|
|
|
|
return retro_vfs_file_remove_impl(path);
|
|
}
|
|
|
|
int filestream_rename(const char *old_path, const char *new_path)
|
|
{
|
|
if (filestream_rename_cb)
|
|
return filestream_rename_cb(old_path, new_path);
|
|
|
|
return retro_vfs_file_rename_impl(old_path, new_path);
|
|
}
|
|
|
|
/* 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 = 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;
|
|
|
|
strlcpy(path_dst, dst, sizeof(path_dst));
|
|
path_basedir(path_dst);
|
|
if (!path_is_directory(path_dst))
|
|
path_mkdir(path_dst);
|
|
|
|
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)
|
|
goto end;
|
|
}
|
|
ret = (n < 0) ? -1 : 0;
|
|
|
|
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;
|
|
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_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
|
|
if (!fp_src || !fp_dst || filestream_get_size(fp_src) != filestream_get_size(fp_dst))
|
|
ret = -1;
|
|
|
|
if (ret >= 0)
|
|
{
|
|
char buf_src[256] = {0};
|
|
char buf_dst[256] = {0};
|
|
while ((filestream_read(fp_src, buf_src, sizeof(buf_src))) > 0 && ret == 0)
|
|
{
|
|
filestream_read(fp_dst, buf_dst, sizeof(buf_dst));
|
|
ret = memcmp(buf_src, buf_dst, sizeof(buf_src));
|
|
}
|
|
}
|
|
|
|
if (fp_src)
|
|
{
|
|
filestream_close(fp_src);
|
|
fp_src = NULL;
|
|
}
|
|
if (fp_dst)
|
|
{
|
|
filestream_close(fp_dst);
|
|
fp_dst = NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
const char* filestream_get_path(RFILE *stream)
|
|
{
|
|
if (filestream_get_path_cb)
|
|
return filestream_get_path_cb(stream->hfile);
|
|
|
|
return retro_vfs_file_get_path_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile);
|
|
}
|
|
|
|
bool filestream_matches_buf(const char *path, const void *data, size_t len)
|
|
{
|
|
const uint8_t *mem = (const uint8_t*)data;
|
|
const uint8_t *map = NULL;
|
|
int64_t map_len = 0;
|
|
bool match = false;
|
|
RFILE *file;
|
|
|
|
if (!path || !*path || (len && !data))
|
|
return false;
|
|
|
|
if (!(file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS)))
|
|
return false;
|
|
|
|
/* The cheap answer, before a byte moves. Callers that slurped the
|
|
* file to memcmp it were paying a full-size allocation and a
|
|
* full-file read to learn this, and learning it last. */
|
|
if (filestream_get_size(file) != (int64_t)len)
|
|
goto done;
|
|
|
|
if ((map = filestream_get_mapped_ptr(file, &map_len)))
|
|
{
|
|
/* A view that does not span the file would compare the wrong
|
|
* bytes; fall through to the copying path rather than guess. */
|
|
if (map_len == (int64_t)len)
|
|
{
|
|
match = (len == 0) || (memcmp(map, mem, len) == 0);
|
|
goto done;
|
|
}
|
|
if (filestream_seek(file, 0, RETRO_VFS_SEEK_POSITION_START) != 0)
|
|
goto done;
|
|
}
|
|
|
|
{
|
|
/* FILESTREAM_MATCHES_BUF_WINDOW, which is sized by the smallest
|
|
* thread stack in the tree rather than by throughput. GEKKO
|
|
* threads get 8 KiB (STACKSIZE in rthreads.c), 3DS and PSP
|
|
* 32 KiB and Vita 64 KiB - and this is libretro-common API, so
|
|
* a caller on a spawned thread is not hypothetical. The 8 KiB
|
|
* floor is GEKKO's.
|
|
*
|
|
* Bigger reads are faster, and at or above the VFS's own 64 KiB
|
|
* stdio buffer they skip it entirely: measured on the unchanged
|
|
* case, which reads every byte, 1 KiB against 4 KiB against
|
|
* 64 KiB is 1753 / 1432 / 1153 us on an 8 MiB file. The
|
|
* platforms paying that are the ones with no memory mapping -
|
|
* the mapped branch above copies nothing - which are the same
|
|
* ones with the 8 KiB stacks, and this runs when a save is
|
|
* written rather than in a frame. */
|
|
uint8_t window[FILESTREAM_MATCHES_BUF_WINDOW];
|
|
size_t off = 0;
|
|
|
|
match = true;
|
|
while (off < len)
|
|
{
|
|
int64_t got;
|
|
size_t want = len - off;
|
|
|
|
if (want > sizeof(window))
|
|
want = sizeof(window);
|
|
|
|
if ((got = filestream_read(file, window, (int64_t)want))
|
|
!= (int64_t)want)
|
|
{
|
|
match = false;
|
|
break;
|
|
}
|
|
/* Stop at the first difference: the caller asked a yes/no
|
|
* question and a 'no' is final. */
|
|
if (memcmp(window, mem + off, (size_t)got) != 0)
|
|
{
|
|
match = false;
|
|
break;
|
|
}
|
|
off += (size_t)got;
|
|
}
|
|
}
|
|
|
|
done:
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
return match;
|
|
}
|
|
|
|
/* Optional extension: a VFS provider that CAN expose a mapping for
|
|
* some of its handles (a local-first hybrid, say) registers this and
|
|
* answers per handle; NULL from the callback means unmapped, same as
|
|
* everywhere else. Without a registration, an installed VFS keeps the
|
|
* conservative answer below. */
|
|
static filestream_mapped_ptr_cb_t filestream_mapped_ptr_cb;
|
|
|
|
void filestream_set_mapped_ptr_cb(filestream_mapped_ptr_cb_t cb)
|
|
{
|
|
filestream_mapped_ptr_cb = cb;
|
|
}
|
|
|
|
void filestream_prefetch(RFILE *stream, uint64_t offset, uint64_t len)
|
|
{
|
|
if (!stream || !stream->hfile)
|
|
return;
|
|
if (filestream_read_cb)
|
|
return; /* no such request in the libretro VFS interface */
|
|
retro_vfs_file_prefetch_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, offset, len);
|
|
}
|
|
|
|
const uint8_t *filestream_get_mapped_ptr(RFILE *stream, int64_t *len)
|
|
{
|
|
if (len)
|
|
*len = 0;
|
|
if (!stream)
|
|
return NULL;
|
|
if (filestream_read_cb)
|
|
{
|
|
if (filestream_mapped_ptr_cb)
|
|
return filestream_mapped_ptr_cb(stream->hfile, len);
|
|
/* A frontend- or core-supplied VFS is driven entirely through
|
|
* the callbacks and has no mapping this side of them. */
|
|
return NULL;
|
|
}
|
|
return retro_vfs_file_get_mapped_ptr_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, len);
|
|
}
|
|
|
|
int64_t filestream_write(RFILE *stream, const void *s, int64_t len)
|
|
{
|
|
int64_t output;
|
|
|
|
/* Reads may have run ahead of the logical position; put the
|
|
* underlying handle back on it before writing through it. The
|
|
* discard's rewind doubles as the read-to-write transition seek;
|
|
* when there is nothing to discard, interpose one (update-stream
|
|
* rule, mirror of the one in filestream_raw_read). */
|
|
if (stream)
|
|
{
|
|
filestream_rbuf_discard(stream);
|
|
if (stream->last_io == FILESTREAM_LAST_IO_READ)
|
|
filestream_raw_seek(stream, 0, RETRO_VFS_SEEK_POSITION_CURRENT);
|
|
stream->last_io = FILESTREAM_LAST_IO_WRITE;
|
|
}
|
|
|
|
if (filestream_write_cb)
|
|
output = filestream_write_cb(stream->hfile, s, len);
|
|
else
|
|
output = retro_vfs_file_write_impl(
|
|
(libretro_vfs_implementation_file*)stream->hfile, s, len);
|
|
|
|
if (output == VFS_ERROR_RETURN_VALUE)
|
|
stream->err_flag = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
int filestream_putc(RFILE *stream, int c)
|
|
{
|
|
char c_char = (char)c;
|
|
if (!stream)
|
|
return EOF;
|
|
return filestream_write(stream, &c_char, 1) == 1
|
|
? (int)(unsigned char)c
|
|
: EOF;
|
|
}
|
|
|
|
int filestream_vprintf(RFILE *stream, const char* format, va_list args)
|
|
{
|
|
static char buffer[8 * 1024];
|
|
int _len = vsnprintf(buffer, sizeof(buffer),
|
|
format, args);
|
|
if (_len < 0)
|
|
return -1;
|
|
else if (_len == 0)
|
|
return 0;
|
|
return (int)filestream_write(stream, buffer, _len);
|
|
}
|
|
|
|
int filestream_printf(RFILE *stream, const char* format, ...)
|
|
{
|
|
va_list vl;
|
|
int ret;
|
|
va_start(vl, format);
|
|
ret = filestream_vprintf(stream, format, vl);
|
|
va_end(vl);
|
|
return ret;
|
|
}
|
|
|
|
int filestream_error(RFILE *stream)
|
|
{
|
|
return (stream && stream->err_flag);
|
|
}
|
|
|
|
int filestream_close(RFILE *stream)
|
|
{
|
|
int output;
|
|
struct retro_vfs_file_handle* fp = stream->hfile;
|
|
|
|
if (filestream_close_cb)
|
|
output = filestream_close_cb(fp);
|
|
else
|
|
output = retro_vfs_file_close_impl(
|
|
(libretro_vfs_implementation_file*)fp);
|
|
|
|
if (output == 0)
|
|
{
|
|
free(stream->rbuf);
|
|
free(stream);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/* Read an already-open stream to EOF, growing the buffer as it goes.
|
|
*
|
|
* For the path below that sizes its allocation from
|
|
* filestream_get_size(), a file reporting zero bytes yields a zero-
|
|
* byte read - so procfs, whose entries all stat as size 0, came back
|
|
* as an empty string *and a success return*. Callers cannot tell
|
|
* that from a genuinely empty file, and did not: the /proc/apm
|
|
* battery probe, the /proc/modules sg check in cdrom.c and the
|
|
* dingux /proc/jz/battery reader all silently saw nothing. (Which
|
|
* is also why mem_stats.c reads /proc/meminfo through a raw open()
|
|
* and read() rather than this helper.)
|
|
*
|
|
* A stat size is a hint, not a contract - synthetic filesystems have
|
|
* no length until they are read, and a growing file's is stale the
|
|
* moment it is taken - so when there is no usable hint, read until
|
|
* the stream says stop. Returns bytes read, or -1.
|
|
*/
|
|
static int64_t filestream_read_file_to_eof(RFILE *file, void **buf)
|
|
{
|
|
/* One page holds essentially every procfs entry this is used for,
|
|
* so the doubling below rarely runs at all. */
|
|
size_t cap = 4096;
|
|
size_t used = 0;
|
|
char *data = (char*)malloc(cap);
|
|
|
|
if (!data)
|
|
return -1;
|
|
|
|
for (;;)
|
|
{
|
|
int64_t got;
|
|
|
|
/* Keep a spare byte for the NUL, and grow before reading so a
|
|
* read is never issued with zero space. */
|
|
if (used + 1 >= cap)
|
|
{
|
|
char *tmp;
|
|
size_t want = cap * 2;
|
|
|
|
/* Refuse a doubling that would wrap, rather than allocating
|
|
* a smaller buffer than the offsets below assume. */
|
|
if (want < cap)
|
|
{
|
|
free(data);
|
|
return -1;
|
|
}
|
|
if (!(tmp = (char*)realloc(data, want)))
|
|
{
|
|
free(data);
|
|
return -1;
|
|
}
|
|
data = tmp;
|
|
cap = want;
|
|
}
|
|
|
|
if ((got = filestream_read(file, data + used,
|
|
(int64_t)(cap - used - 1))) < 0)
|
|
{
|
|
free(data);
|
|
return -1;
|
|
}
|
|
if (got == 0)
|
|
break;
|
|
|
|
used += (size_t)got;
|
|
}
|
|
|
|
data[used] = '\0';
|
|
*buf = data;
|
|
return (int64_t)used;
|
|
}
|
|
|
|
int64_t filestream_read_file(const char *path, void **buf, int64_t *len)
|
|
{
|
|
int64_t ret = 0;
|
|
int64_t content_buf_size = 0;
|
|
void *content_buf = NULL;
|
|
/* This function is the definition of a bulk sequential read: open,
|
|
* take the whole file in one go, close. Saying so lets the VFS
|
|
* skip a read buffer that cannot be reused - the bytes are already
|
|
* going straight into storage the caller keeps. */
|
|
RFILE *file = filestream_open(path,
|
|
RETRO_VFS_FILE_ACCESS_READ,
|
|
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
|
|
|
|
if (!file)
|
|
{
|
|
*buf = NULL;
|
|
return 0;
|
|
}
|
|
|
|
if ((content_buf_size = filestream_get_size(file)) < 0)
|
|
goto error;
|
|
|
|
/* No usable size hint: read to EOF instead of trusting the zero.
|
|
* See filestream_read_file_to_eof() above.
|
|
*
|
|
* A zero here is one of three things: a genuinely empty file, a
|
|
* stream whose length is not known in advance (procfs and the
|
|
* like), or a directory - which opens and seeks like a file on
|
|
* every platform checked and then yields nothing. Only the last
|
|
* is an error, and nothing about the bytes distinguishes it from
|
|
* the first, so it costs one stat. Paid only on this branch: a
|
|
* file with a size never reaches it, which is every read on the
|
|
* paths that matter for throughput. */
|
|
if (content_buf_size == 0)
|
|
{
|
|
if (path_is_directory(path))
|
|
goto error;
|
|
|
|
if ((ret = filestream_read_file_to_eof(file, &content_buf)) < 0)
|
|
goto error;
|
|
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
|
|
*buf = content_buf;
|
|
if (len)
|
|
*len = ret;
|
|
return 1;
|
|
}
|
|
|
|
/* Reject sizes that would not survive the cast to size_t for
|
|
* the malloc below. Pre-patch the only check here was a
|
|
* tautological '(int64_t)(uint64_t)X != X' (which is false
|
|
* for any positive int64_t), and on 32-bit hosts any file
|
|
* larger than ~4 GiB silently truncated through (size_t),
|
|
* the malloc was undersized, and the filestream_read below
|
|
* overran it.
|
|
*
|
|
* The '+ 1' for the terminator is done in the unsigned domain and
|
|
* only after the bound is known to hold. It used to sit inside
|
|
* the malloc argument as '(size_t)(content_buf_size + 1)', where
|
|
* the addition happens in int64_t first: a size of INT64_MAX
|
|
* overflows there, which is undefined behaviour rather than a
|
|
* large number. That is not hypothetical - opening a directory
|
|
* succeeds on Linux through both fopen() and open(), and seeking
|
|
* to its end reports exactly INT64_MAX, so any caller that hands
|
|
* this function a path that turns out to be a directory reaches
|
|
* it. See FILESTREAM_READ_FILE_MAX above for why such a size is
|
|
* refused here rather than left for malloc() to decline. */
|
|
if ( content_buf_size < 0
|
|
|| (uint64_t)content_buf_size >= (uint64_t)((size_t)-1)
|
|
|| (uint64_t)content_buf_size > FILESTREAM_READ_FILE_MAX)
|
|
goto error;
|
|
|
|
if (!(content_buf = malloc((size_t)content_buf_size + 1)))
|
|
goto error;
|
|
|
|
/* Read in a loop rather than trusting a single call to satisfy the
|
|
* whole request. A read is permitted to come up short and try
|
|
* again: fread() loops internally so the buffered path always did
|
|
* return the full count, but a descriptor-backed one - which is
|
|
* what the hint above now selects - hands back whatever read(2)
|
|
* gave it, and a frontend-supplied VFS may do anything at all.
|
|
* The single call this replaces then returned a buffer holding
|
|
* fewer bytes than the file, with nothing to distinguish it from a
|
|
* complete read of a shorter file.
|
|
*
|
|
* Requests are capped so no single read exceeds what a platform's
|
|
* count argument can express: read(2) takes a size_t on POSIX but
|
|
* _read() takes an unsigned int on Windows and returns int, so a
|
|
* >2 GiB request there fails outright instead of coming up short.
|
|
*
|
|
* A genuinely short *file* - one truncated between the size query
|
|
* and the read - still succeeds with the byte count it had, which
|
|
* is what this function has always done. */
|
|
while (ret < content_buf_size)
|
|
{
|
|
int64_t want = content_buf_size - ret;
|
|
int64_t got;
|
|
|
|
if (want > FILESTREAM_BULK_READ_MAX)
|
|
want = FILESTREAM_BULK_READ_MAX;
|
|
|
|
if ((got = filestream_read(file, (uint8_t*)content_buf + ret, want)) < 0)
|
|
goto error;
|
|
if (got == 0)
|
|
break;
|
|
|
|
ret += got;
|
|
}
|
|
|
|
/* The size said there were bytes and not one arrived. That is not
|
|
* a file that shrank - a shrunk file still hands over what it has,
|
|
* which the loop above accepts - it is a stream that cannot be
|
|
* read at all. A directory is exactly that shape wherever the
|
|
* platform lets one through: Linux reports INT64_MAX from lseek
|
|
* and is turned away by the ceiling above, but Darwin's buffered
|
|
* path reports a small plausible length and then reads zero
|
|
* bytes, and treating that as success handed back an empty buffer
|
|
* indistinguishable from an empty file. Same call, same
|
|
* arguments, a different answer per platform. */
|
|
if (ret == 0)
|
|
goto error;
|
|
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
|
|
*buf = content_buf;
|
|
|
|
/* Allow for easy reading of strings to be safe.
|
|
* Will only work with sane character formatting (Unix). */
|
|
((char*)content_buf)[ret] = '\0';
|
|
|
|
if (len)
|
|
*len = ret;
|
|
|
|
return 1;
|
|
|
|
error:
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
if (content_buf)
|
|
free(content_buf);
|
|
if (len)
|
|
*len = -1;
|
|
*buf = NULL;
|
|
return 0;
|
|
}
|
|
|
|
bool filestream_write_file(const char *path, const void *data, int64_t size)
|
|
{
|
|
int64_t ret = 0;
|
|
/* Everything this stream will ever write is in 'data' already, so
|
|
* ask for the descriptor path where one exists: one write for the
|
|
* whole buffer instead of a copy through a stdio buffer and a
|
|
* flush at close. */
|
|
RFILE *file = filestream_open(path,
|
|
RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
|
|
if (!file)
|
|
return false;
|
|
ret = filestream_write(file, data, size);
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
return (ret == size);
|
|
}
|
|
|
|
/* <path>.tmp, the sibling an atomic write goes through; NULL on OOM */
|
|
static char *filestream_atomic_temp_path(const char *path)
|
|
{
|
|
size_t path_len = strlen(path);
|
|
char *temp_path = (char*)malloc(path_len + sizeof(".tmp"));
|
|
if (!temp_path)
|
|
return NULL;
|
|
memcpy(temp_path, path, path_len);
|
|
memcpy(temp_path + path_len, ".tmp", sizeof(".tmp"));
|
|
return temp_path;
|
|
}
|
|
|
|
RFILE *filestream_open_atomic(const char *path)
|
|
{
|
|
RFILE *file;
|
|
char *temp_path;
|
|
|
|
if (!path || !*path)
|
|
return NULL;
|
|
if (!(temp_path = filestream_atomic_temp_path(path)))
|
|
return NULL;
|
|
|
|
file = filestream_open(temp_path,
|
|
RETRO_VFS_FILE_ACCESS_WRITE,
|
|
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK);
|
|
free(temp_path);
|
|
return file;
|
|
}
|
|
|
|
int filestream_commit_atomic(RFILE *file, const char *path, bool ok)
|
|
{
|
|
char *temp_path;
|
|
|
|
if (!file)
|
|
return -1;
|
|
if (!path || !*path || !(temp_path = filestream_atomic_temp_path(path)))
|
|
{
|
|
if (filestream_close(file) != 0)
|
|
free(file);
|
|
return -1;
|
|
}
|
|
|
|
/* A buffered write reports a full disk at close, not at write,
|
|
* so both have to agree before the rename goes ahead. */
|
|
if (filestream_close(file) != 0)
|
|
{
|
|
free(file);
|
|
ok = false;
|
|
}
|
|
|
|
if (!ok)
|
|
{
|
|
filestream_delete(temp_path);
|
|
free(temp_path);
|
|
return -1;
|
|
}
|
|
|
|
if (filestream_rename(temp_path, path) == 0)
|
|
{
|
|
free(temp_path);
|
|
return 0;
|
|
}
|
|
|
|
/* The built-in rename replaces the destination on every platform,
|
|
* and puts it back when its replacement cannot be moved in, so a
|
|
* failure there leaves the old file as it was. A frontend's
|
|
* rename may refuse an existing destination, as the libretro VFS
|
|
* API allows, so that one needs the target gone first. */
|
|
if (filestream_rename_cb)
|
|
{
|
|
filestream_delete(path);
|
|
if (filestream_rename(temp_path, path) == 0)
|
|
{
|
|
free(temp_path);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
free(temp_path);
|
|
return 1;
|
|
}
|
|
|
|
bool filestream_write_file_atomic(const char *path,
|
|
const void *data, int64_t size)
|
|
{
|
|
int ret;
|
|
RFILE *file = filestream_open_atomic(path);
|
|
|
|
if (!file)
|
|
return false;
|
|
|
|
ret = filestream_commit_atomic(file, path,
|
|
filestream_write(file, data, size) == size);
|
|
|
|
/* Written but not renamed into place */
|
|
if (ret == 1)
|
|
{
|
|
char *temp_path = filestream_atomic_temp_path(path);
|
|
if (temp_path)
|
|
{
|
|
filestream_delete(temp_path);
|
|
free(temp_path);
|
|
}
|
|
}
|
|
|
|
return ret == 0;
|
|
}
|
|
|
|
char *filestream_getline(RFILE *stream)
|
|
{
|
|
char *newline = NULL;
|
|
char *newline_tmp = NULL;
|
|
size_t cur_size = 256;
|
|
size_t idx = 0;
|
|
int in;
|
|
|
|
if (!stream)
|
|
return NULL;
|
|
|
|
newline = (char*)malloc(cur_size + 1);
|
|
if (!newline)
|
|
return NULL;
|
|
|
|
in = filestream_getc(stream);
|
|
if (in == EOF)
|
|
{
|
|
free(newline);
|
|
return NULL;
|
|
}
|
|
|
|
while (in != EOF && in != '\n')
|
|
{
|
|
newline[idx++] = (char)in;
|
|
if (idx == cur_size)
|
|
{
|
|
cur_size *= 2;
|
|
newline_tmp = (char*)realloc(newline, cur_size + 1);
|
|
if (!newline_tmp)
|
|
{
|
|
free(newline);
|
|
return NULL;
|
|
}
|
|
newline = newline_tmp;
|
|
}
|
|
in = filestream_getc(stream);
|
|
}
|
|
|
|
/* Shrink to fit if we overallocated significantly */
|
|
if (cur_size > idx + 64)
|
|
{
|
|
newline_tmp = (char*)realloc(newline, idx + 1);
|
|
if (newline_tmp)
|
|
newline = newline_tmp;
|
|
}
|
|
|
|
newline[idx] = '\0';
|
|
return newline;
|
|
}
|
|
|
|
libretro_vfs_implementation_file* filestream_get_vfs_handle(RFILE *stream)
|
|
{
|
|
return (libretro_vfs_implementation_file*)stream->hfile;
|
|
}
|