Files
LibretroAdmin 3702e90cbd Route all core file I/O through the libretro VFS
Pull the libretro VFS stack into libretro/libretro-common (from the
snes9x2010 tree's curated copy): vfs_implementation, file_stream,
file_stream_transforms, file_path/file_path_io, the compat family
(strl/posix_string/strcasestr/snprintf/fopen_utf8), encoding_utf,
stdstring and rtime, plus the include tree. libretro.h is upgraded to
the current version -- required because file_path_io uses the
retro_vfs_stat_64_t extension the old 2020-era header predates. A
file_stream_transforms.h header is reconstructed from the .c's
signatures (the source tree ships the implementation without it).
Makefile.common builds these outside STATIC_LINKING, same split as
snes9x2010: statically linked frontends export all of these symbols
themselves. retro_set_environment now requests
RETRO_ENVIRONMENT_GET_VFS_INTERFACE and initializes filestream and
path with it; without a frontend VFS, filestream falls back to its
local implementation.

Conversion:
- The FSTREAM macro layer in snes9x.h moves wholesale from the
  ZLIB/stdio dual implementation to RFILE (rfopen family), which
  converts every STREAM/fStream consumer at once: stream.cpp,
  snapshot.cpp, msu1's fStream path, conffile parsing, memmap's
  soft-patch loader and snes9x.cpp. Compressed (.gz) stream support
  is gone by design -- the frontend handles compressed content.
  REOPEN_FSTREAM / reopenStreamFromFd (fdopen-based, no VFS
  equivalent, zero callers) are deleted.
- Direct stdio converted to RFILE: memmap (BIOS + SRTC RTC data),
  crosshairs (text format), cheats2 (load/save), apu (SPC dump),
  conffile SaveTo, bsx (Satellaview stream files; SBSX struct fields
  retyped), libretro.cpp's BIOS loader, and the DEBUGGER trace
  handles in debug.h / snes9x.cpp / cpuops.cpp.
- bml switches from std::ifstream to filestream_read_file plus an
  std::istringstream; the parser itself now takes std::istream.
- minizip's ioapi.c backend is rewritten on RFILE, so unzOpen (zipped
  ROM loading, MSU-1 .msu1 packs) goes through the VFS; the stdio
  fopen64/fseeko64 platform selection in ioapi.h is removed. The
  unused minizip tools (zip.c, miniunz.c, minizip.c, mztools,
  iowin32) are deleted. zlib remains linked for DEFLATE
  decompression only.
- crosshairs' HAVE_LIBPNG block is removed: never defined in this
  build, and png_init_io requires stdio FILE.

stderr/stdout console logging is intentionally untouched -- it is not
file I/O. After conversion, a tree-wide audit finds no fopen family,
gzopen, fstream, or fdopen outside libretro-common.

Validation:
- Functional VFS smoke: loading a ROM by path (data == NULL) through
  memmap -> fStream -> filestream produces the byte-identical m7 test
  ROM hash, and loading the same ROM from a .zip exercises
  loadzip -> unzip -> the RFILE ioapi backend with the identical
  hash again.
- Full gauntlet byte-exact: Contra III savestate vs pre-port oracle,
  Mega Man X2, Mode 5 bright/dim, m7 option matrix, probe
  (serialize size 823407).
- ASan+UBSan+LSan over the probe, the path-load and the zip-load
  scenarios: zero findings in the new I/O paths; the only report is
  a pre-existing 288-byte S9xGetCommandT leak (controls.cpp input
  bindings, unrelated).

Untested but converted by the same mechanical mapping: SRTC RTC
file, BS-X Satellaview streams, cheat file save, SPC dump, DEBUGGER
traces.
2026-08-04 02:08:38 +02:00

300 lines
6.2 KiB
C++

#include <sstream>
#include <streams/file_stream.h>
#include <vector>
#include <iostream>
#include <stack>
#include <stdio.h>
#include "port.h"
#include "bml.h"
bml_node::bml_node()
{
type = CHILD;
depth = -1;
}
static inline int islf(char c)
{
return (c == '\r' || c == '\n');
}
static inline int isblank(char c)
{
return (c == ' ' || c == '\t');
}
static inline int isblankorlf(char c)
{
return (islf(c) || isblank(c));
}
static inline int isalnum(char c)
{
return ((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9'));
}
static inline int bml_valid(char c)
{
return (isalnum(c) || c == '-');
}
static std::string trim(std::string str)
{
int start;
int end;
for (start = 0; str[start] && start != (int)str.length() && isblank(str[start]); start++) {}
if (start >= (int)str.length())
return std::string("");
for (end = str.length() - 1; isblankorlf(str[end]); end--) {}
return str.substr(start, end - start + 1);
}
static std::string trimcomments(std::string str)
{
int end = str.length();
size_t comment = str.find("//");
if (comment != std::string::npos)
end = comment;
for (int i = end - 1; i >= 0; i--)
{
if (!isblankorlf(str[i]))
{
end = i + 1;
break;
}
}
return str.substr(0, end);
}
static inline int bml_read_depth(std::string &data)
{
size_t depth;
for (depth = 0; isblank(data[depth]) && depth < data.length(); depth++) {}
return depth == data.length() ? -1 : depth;
}
static void bml_parse_depth(bml_node &node, std::string &line)
{
unsigned int depth = bml_read_depth(line);
line.erase(0, depth);
node.depth = depth;
}
static void bml_parse_name(bml_node &node, std::string &line)
{
int len;
for (len = 0; bml_valid(line[len]); len++) {};
node.name = trim(line.substr(0, len));
line.erase(0, len);
}
static void bml_parse_data(bml_node &node, std::string &line)
{
int len;
if (line[0] == '=' && line[1] == '\"')
{
len = 2;
while (line[len] && line[len] != '\"' && !islf(line[len]))
len++;
if (line[len] != '\"')
return;
node.data = line.substr(2, len - 2);
line.erase(0, len + 1);
}
else if (line[0] == '=')
{
len = 1;
while (line[len] && !islf(line[len]) && line[len] != '"' && line[len] != ' ')
len++;
if (line[len] == '\"')
return;
node.data = line.substr(1, len - 1);
line.erase(0, len);
}
else if (line[0] == ':')
{
len = 1;
while (line[len] && !islf(line[len]))
len++;
node.data = trim(line.substr(1, len - 1));
line.erase(0, len);
}
return;
}
static std::string bml_read_line(std::istream &fd)
{
std::string line;
while (fd)
{
std::getline(fd, line);
line = trimcomments(line);
if (!line.empty())
{
return line;
}
}
return std::string("");
}
static void bml_parse_attr(bml_node &node, std::string &line)
{
int len;
while (line.length() > 0)
{
if (!isblank(line[0]))
return;
while (isblank(line[0]))
line.erase(0, 1);
bml_node n;
len = 0;
while (bml_valid(line[len]))
len++;
if (len == 0)
return;
n.name = trim(line.substr(0, len));
line.erase(0, len);
bml_parse_data(n, line);
n.depth = node.depth + 1;
n.type = bml_node::ATTRIBUTE;
node.child.push_back(n);
}
}
static int contains_space(const char *str)
{
for (int i = 0; str[i]; i++)
{
if (isblank(str[i]))
return 1;
}
return 0;
}
static void bml_print_node(bml_node &node, int depth)
{
int i;
for (i = 0; i < depth * 2; i++)
{
printf(" ");
}
if (!node.name.empty())
printf("%s", node.name.c_str());
if (!node.data.empty())
{
if (contains_space(node.data.c_str()))
printf("=\"%s\"", node.data.c_str());
else
printf(": %s", node.data.c_str());
}
for (i = 0; i < (int)node.child.size() && node.child[i].type == bml_node::ATTRIBUTE; i++)
{
if (!node.child[i].name.empty())
{
printf(" %s", node.child[i].name.c_str());
if (!node.child[i].data.empty())
{
if (contains_space(node.child[i].data.c_str()))
printf("=\"%s\"", node.child[i].data.c_str());
else
printf("=%s", node.child[i].data.c_str());
}
}
}
if (depth >= 0)
printf("\n");
for (; i < (int)node.child.size(); i++)
{
bml_print_node(node.child[i], depth + 1);
}
if (depth == 0)
printf("\n");
}
void bml_node::print()
{
bml_print_node(*this, -1);
}
void bml_node::parse(std::istream &fd)
{
std::stack<bml_node *> nodestack;
nodestack.push(this);
while (fd)
{
bml_node newnode;
std::string line = bml_read_line(fd);
if (line.empty())
return;
int line_depth = bml_read_depth(line);
while (line_depth <= nodestack.top()->depth && nodestack.size() > 1)
nodestack.pop();
bml_parse_depth(newnode, line);
bml_parse_name(newnode, line);
bml_parse_data(newnode, line);
bml_parse_attr(newnode, line);
nodestack.top()->child.push_back(newnode);
nodestack.push(&nodestack.top()->child.back());
}
return;
}
bml_node *bml_node::find_subnode(std::string name)
{
unsigned int i;
for (i = 0; i < child.size(); i++)
{
if (name.compare(child[i].name) == 0)
return &child[i];
}
return NULL;
}
bool bml_node::parse_file(std::string filename)
{
/* Read through the libretro VFS, then parse from memory; the
parser itself only needs an std::istream. */
void *buf = NULL;
int64_t len = 0;
if (filestream_read_file(filename.c_str(), &buf, &len) <= 0 || !buf)
return false;
std::string contents((const char *) buf, (size_t) len);
free(buf);
std::istringstream stream(contents);
parse(stream);
return true;
}