Files
ppsspp/libretro/libretro-common/streams/file_stream.c
T
libretroadmin cf9d102864 libretro: vendor libretro-common from RetroArch in-tree
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.
2026-10-08 14:14:37 +02:00

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