mirror of
https://github.com/kuraeow/ultratears.git
synced 2026-11-07 10:16:00 -10:00
An unofficial native PC port of The Legend of Zelda: Tears of the Kingdom (Switch 1.4.3) by static recompilation, built on the player's PC from their own dump. See README.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
731 lines
33 KiB
C
731 lines
33 KiB
C
/* nn::fs for the recompiled game: the read-only RomFS and writable save, temporary and cache storage.
|
|
* Ported from MacNativeGames games/totk/runtime/fs_host.c and romfs.c @b75be8c (MIT). Changes:
|
|
* - guest-register binding through HLE_NATIVE (every nn::fs entry point takes integers and pointers only);
|
|
* - Windows file calls (mkdir, directory types via stat, rename that refuses to replace, _commit);
|
|
* - RomFS: the runtime index (tools/make_romfs_index.py, MacNativeGames' TOTKRFS1 layout) and the extracted
|
|
* romfs.bin, both already verified at extraction, or (the player's dump) a folder of plain files, indexed by
|
|
* walking it at start (tools/setup.py checks it against the expected version); overlays (mods, data patches), split images and the startup
|
|
* re-hash are not ported yet. RomFS directory listing stays unsupported, as in MacNativeGames.
|
|
* Save, temporary and cache storage are host directories under the profile directory (TOTK_PROFILE_DIR, default
|
|
* userdata); application save data is created on first start, as the system launcher does. */
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#include "host.h"
|
|
#include "../runtime/hle.h"
|
|
|
|
#define R_PATH_NOT_FOUND 0x202u
|
|
#define R_ALREADY_EXISTS 0x402u
|
|
#define R_NOT_EMPTY 0x1002u
|
|
#define R_TARGET_NOT_FOUND 0x7d402u
|
|
#define R_INVALID_ARGUMENT 0x2ee202u
|
|
#define R_INVALID_PATH 0x2ee602u
|
|
#define R_OPEN_MODE 0x307402u
|
|
#define R_APPEND_REQUIRED 0x2f5e02u
|
|
#define R_ROM_UNSUPPORTED 0x6402u
|
|
#define R_ROM_INVALID 0xe402u
|
|
#define TOTK_APPLICATION_ID 0x0100f2c0115b6000ULL
|
|
enum { M_FREE, M_SAVE, M_TEMP, M_CACHE };
|
|
enum { OPEN_READ = 1, OPEN_WRITE = 2, OPEN_APPEND = 4 };
|
|
#define ROM_BASE 0x100000000ULL
|
|
#define NROM 65536
|
|
#define FILE_BASE 0x200000000ULL
|
|
#define DIR_BASE 0x300000000ULL
|
|
#define NFILES 1024
|
|
#define NDIRS 256
|
|
|
|
void runtime_account_uid(uint64_t out[2]);
|
|
const char *runtime_profile_dir(void);
|
|
|
|
static void need(int c, const char *m) { if (!c) runtime_fail(m); }
|
|
static int make_dir_p(const char *p) { return mkdir(p, 0700); }
|
|
|
|
/* ======================================== RomFS ======================================== */
|
|
static uint8_t *index_bytes;
|
|
static uint64_t file_count, strings_offset;
|
|
static int image_fd = -1;
|
|
static char mount_name[32];
|
|
static uint64_t rom_handles[NROM]; /* file index + 1; 0 is free */
|
|
static pthread_mutex_t rom_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
typedef struct { uint64_t offset, size; uint32_t name, len; } Entry;
|
|
_Static_assert(sizeof(Entry) == 24, "index entry layout");
|
|
static Entry entry(uint64_t i) { Entry e; memcpy(&e, index_bytes + 24 + i * 24, 24); return e; }
|
|
static const char *entry_name(Entry e) { return (const char *)index_bytes + strings_offset + e.name; }
|
|
|
|
/* A RomFS folder (the player's extraction: plain files): the index is built by walking it, in the same layout as the
|
|
runtime index, with each entry's offset unused; a file is opened by its path when the game opens it. */
|
|
static char rom_dir[4096];
|
|
static int rom_folder;
|
|
static int rom_fds[NROM];
|
|
typedef struct { char *path; uint64_t size; } Walked;
|
|
static Walked *walked;
|
|
static size_t nwalked, walked_cap;
|
|
static void walk_add(const char *path, uint64_t size) {
|
|
if (nwalked == walked_cap) {
|
|
walked_cap = walked_cap ? walked_cap * 2 : 65536;
|
|
walked = realloc(walked, walked_cap * sizeof *walked);
|
|
need(walked != NULL, "RomFS folder index memory");
|
|
}
|
|
walked[nwalked].path = strdup(path);
|
|
walked[nwalked++].size = size;
|
|
}
|
|
static void walk(const char *root, const char *rel) {
|
|
char pattern[4096];
|
|
snprintf(pattern, sizeof pattern, "%s/%s%s*", root, rel, *rel ? "/" : "");
|
|
#ifdef _WIN32
|
|
WIN32_FIND_DATAA d;
|
|
HANDLE h = FindFirstFileExA(pattern, FindExInfoBasic, &d, FindExSearchNameMatch, NULL, FIND_FIRST_EX_LARGE_FETCH);
|
|
if (h == INVALID_HANDLE_VALUE) return;
|
|
do {
|
|
if (!strcmp(d.cFileName, ".") || !strcmp(d.cFileName, "..")) continue;
|
|
char sub[4096];
|
|
snprintf(sub, sizeof sub, "%s%s%s", rel, *rel ? "/" : "", d.cFileName);
|
|
if (d.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) walk(root, sub);
|
|
else walk_add(sub, (uint64_t)d.nFileSizeHigh << 32 | d.nFileSizeLow);
|
|
} while (FindNextFileA(h, &d));
|
|
FindClose(h);
|
|
#else
|
|
pattern[strlen(pattern) - 1] = 0;
|
|
DIR *dir = opendir(pattern);
|
|
if (!dir) return;
|
|
for (struct dirent *e; (e = readdir(dir));) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
|
char sub[4096], full[8192];
|
|
snprintf(sub, sizeof sub, "%s%s%s", rel, *rel ? "/" : "", e->d_name);
|
|
snprintf(full, sizeof full, "%s/%s", root, sub);
|
|
struct stat st;
|
|
if (stat(full, &st)) continue;
|
|
if (S_ISDIR(st.st_mode)) walk(root, sub);
|
|
else walk_add(sub, (uint64_t)st.st_size);
|
|
}
|
|
closedir(dir);
|
|
#endif
|
|
}
|
|
static int walked_cmp(const void *a, const void *b) { return strcmp(((const Walked *)a)->path, ((const Walked *)b)->path); }
|
|
static void index_folder(const char *dir) {
|
|
uint64_t t0 = runtime_now_ns();
|
|
walk(dir, "");
|
|
need(nwalked > 0, "RomFS folder is empty");
|
|
qsort(walked, nwalked, sizeof *walked, walked_cmp);
|
|
size_t names = 0;
|
|
for (size_t i = 0; i < nwalked; i++) names += strlen(walked[i].path) + 1;
|
|
file_count = nwalked;
|
|
strings_offset = 24 + file_count * 24;
|
|
index_bytes = malloc(strings_offset + names);
|
|
need(index_bytes != NULL, "RomFS folder index memory");
|
|
memcpy(index_bytes, "TOTKRFS1", 8);
|
|
memcpy(index_bytes + 8, &file_count, 8);
|
|
memcpy(index_bytes + 16, &strings_offset, 8);
|
|
uint32_t at = 0;
|
|
for (size_t i = 0; i < nwalked; i++) {
|
|
uint32_t len = (uint32_t)strlen(walked[i].path) + 1;
|
|
Entry e = {0, walked[i].size, at, len};
|
|
memcpy(index_bytes + 24 + i * 24, &e, 24);
|
|
memcpy(index_bytes + strings_offset + at, walked[i].path, len);
|
|
at += len;
|
|
free(walked[i].path);
|
|
}
|
|
free(walked);
|
|
walked = NULL;
|
|
fprintf(stderr, "[totk] RomFS: %llu files in the folder %s (indexed in %.0f ms)\n", (unsigned long long)file_count, dir,
|
|
(runtime_now_ns() - t0) / 1e6);
|
|
}
|
|
|
|
void runtime_fs_init(void) {
|
|
if (image_fd >= 0 || rom_folder) return;
|
|
const char *dir = getenv("TOTK_ROMFS");
|
|
if (!dir) dir = "data/romfs";
|
|
char path[4096];
|
|
snprintf(path, sizeof path, "%s/romfs.bin", dir);
|
|
struct stat st;
|
|
if (stat(path, &st)) { // no image: a folder of plain files
|
|
need(snprintf(rom_dir, sizeof rom_dir, "%s", dir) < (int)sizeof rom_dir, "RomFS folder path");
|
|
rom_folder = 1;
|
|
index_folder(rom_dir);
|
|
return;
|
|
}
|
|
snprintf(path, sizeof path, "%s/runtime-index.bin", dir);
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) { fprintf(stderr, "[totk] %s missing: run tools/make_romfs_index.py\n", path); runtime_fail("RomFS index missing"); }
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
need(size >= 24 && size < 128L * 1024 * 1024, "RomFS index size");
|
|
index_bytes = malloc((size_t)size);
|
|
need(index_bytes && fread(index_bytes, 1, (size_t)size, f) == (size_t)size, "read RomFS index");
|
|
fclose(f);
|
|
need(!memcmp(index_bytes, "TOTKRFS1", 8), "RomFS index format");
|
|
memcpy(&file_count, index_bytes + 8, 8);
|
|
memcpy(&strings_offset, index_bytes + 16, 8);
|
|
need(file_count < 1000000 && strings_offset == 24 + file_count * 24 && strings_offset < (uint64_t)size, "RomFS index layout");
|
|
snprintf(path, sizeof path, "%s/romfs.bin", dir);
|
|
image_fd = open(path, O_RDONLY | O_BINARY);
|
|
need(image_fd >= 0, "RomFS image missing");
|
|
fprintf(stderr, "[totk] RomFS: %llu files from %s\n", (unsigned long long)file_count, dir);
|
|
}
|
|
/* First entry whose path is >= p */
|
|
static uint64_t lower(const char *p) {
|
|
uint64_t lo = 0, hi = file_count;
|
|
while (lo < hi) { uint64_t m = lo + (hi - lo) / 2; if (strcmp(entry_name(entry(m)), p) < 0) lo = m + 1; else hi = m; }
|
|
return lo;
|
|
}
|
|
static uint64_t file_index(const char *p) {
|
|
uint64_t i = lower(p);
|
|
return i < file_count && !strcmp(p, entry_name(entry(i))) ? i : UINT64_MAX;
|
|
}
|
|
static int folder_exists(const char *folder) {
|
|
size_t n = strlen(folder);
|
|
uint64_t i = lower(folder);
|
|
return i < file_count && !strncmp(entry_name(entry(i)), folder, n);
|
|
}
|
|
/* "mount:/a/b" (or "mount://a" from audio stream paths) -> "a/b"; NULL on traversal or a foreign mount */
|
|
static const char *rom_path(const char *path) {
|
|
size_t n = strlen(mount_name);
|
|
if (!n || !path || strncmp(path, mount_name, n) || path[n] != ':' || path[n + 1] != '/') return NULL;
|
|
path += n + 2;
|
|
while (*path == '/') ++path;
|
|
if (!*path || strchr(path, '\\')) return NULL;
|
|
for (const char *part = path; *part;) {
|
|
const char *end = strchr(part, '/');
|
|
size_t size = end ? (size_t)(end - part) : strlen(part);
|
|
if (!size || (size == 1 && part[0] == '.') || (size == 2 && part[0] == '.' && part[1] == '.')) return NULL;
|
|
if (!end) break;
|
|
part = end + 1;
|
|
}
|
|
return path;
|
|
}
|
|
static int fs_log_on = -1;
|
|
static uint64_t fs_log_t0;
|
|
void runtime_fs_log(const char *what, const char *path) {
|
|
if (fs_log_on < 0) { fs_log_on = getenv("TOTK_FS_LOG") != NULL; fs_log_t0 = runtime_now_ns(); }
|
|
if (fs_log_on) fprintf(stderr, "[totk-fs] %8.3f %s %s\n", (runtime_now_ns() - fs_log_t0) / 1e9, what, path);
|
|
}
|
|
static int romfs_owns(const char *path) {
|
|
size_t n = strlen(mount_name);
|
|
return n && path && !strncmp(path, mount_name, n) && path[n] == ':';
|
|
}
|
|
static int romfs_handle(uint64_t h) { return h > ROM_BASE && h < ROM_BASE + NROM; }
|
|
static unsigned romfs_open(uint64_t *out, const char *path, int mode) {
|
|
runtime_fs_log("rom", path ? path : "(null)");
|
|
if (!out || mode != OPEN_READ) return R_ROM_UNSUPPORTED;
|
|
const char *p = rom_path(path);
|
|
if (!p) return R_PATH_NOT_FOUND;
|
|
uint64_t i = file_index(p);
|
|
if (i == UINT64_MAX) return R_PATH_NOT_FOUND;
|
|
int fd = -1;
|
|
if (rom_folder) {
|
|
char full[8192];
|
|
snprintf(full, sizeof full, "%s/%s", rom_dir, p);
|
|
if ((fd = open(full, O_RDONLY | O_BINARY)) < 0) return R_PATH_NOT_FOUND;
|
|
}
|
|
pthread_mutex_lock(&rom_lock);
|
|
for (uint64_t h = 1; h < NROM; h++)
|
|
if (!rom_handles[h]) { rom_handles[h] = i + 1; rom_fds[h] = fd; *out = ROM_BASE + h; pthread_mutex_unlock(&rom_lock); return 0; }
|
|
pthread_mutex_unlock(&rom_lock);
|
|
runtime_fail("RomFS handle table full");
|
|
}
|
|
static unsigned romfs_read(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uint64_t size) {
|
|
if (!out || offset < 0 || (!buffer && size)) return R_ROM_INVALID;
|
|
uint64_t i = rom_handles[h - ROM_BASE];
|
|
if (!i) return R_ROM_INVALID;
|
|
Entry e = entry(i - 1);
|
|
if ((uint64_t)offset > e.size) return R_ROM_INVALID;
|
|
uint64_t n = size < e.size - (uint64_t)offset ? size : e.size - (uint64_t)offset, done = 0;
|
|
guest_prepare_write(buffer, n);
|
|
while (done < n) {
|
|
ssize_t r = pread(rom_folder ? rom_fds[h - ROM_BASE] : image_fd, (uint8_t *)buffer + done, (size_t)(n - done), (off_t)(e.offset + (uint64_t)offset + done));
|
|
need(r > 0, "RomFS image read");
|
|
done += (uint64_t)r;
|
|
}
|
|
*out = n;
|
|
return 0;
|
|
}
|
|
static unsigned romfs_size(int64_t *out, uint64_t h) {
|
|
uint64_t i = rom_handles[h - ROM_BASE];
|
|
if (!out || !i) return R_ROM_INVALID;
|
|
*out = (int64_t)entry(i - 1).size;
|
|
return 0;
|
|
}
|
|
static void romfs_close(uint64_t h) {
|
|
pthread_mutex_lock(&rom_lock);
|
|
need(rom_handles[h - ROM_BASE] != 0, "close of a closed RomFS handle");
|
|
rom_handles[h - ROM_BASE] = 0;
|
|
if (rom_folder) close(rom_fds[h - ROM_BASE]);
|
|
pthread_mutex_unlock(&rom_lock);
|
|
}
|
|
static unsigned romfs_entry_type(int *out, const char *path) {
|
|
if (!out) return R_ROM_INVALID;
|
|
const char *p = rom_path(path);
|
|
if (!p) return R_PATH_NOT_FOUND;
|
|
if (file_index(p) != UINT64_MAX) { *out = 1; return 0; }
|
|
char dir[4096];
|
|
if (snprintf(dir, sizeof dir, "%s/", p) >= (int)sizeof dir) return R_ROM_INVALID;
|
|
if (folder_exists(dir)) { *out = 0; return 0; }
|
|
return R_PATH_NOT_FOUND;
|
|
}
|
|
static unsigned mount_rom(const char *name, void *cache, uint64_t size) {
|
|
(void)cache; (void)size;
|
|
runtime_fs_init();
|
|
if (!name || !*name || strlen(name) >= sizeof mount_name || strchr(name, '/') || strchr(name, ':')) return R_ROM_INVALID;
|
|
if (*mount_name && strcmp(name, mount_name)) runtime_fail("second RomFS mount not implemented");
|
|
strcpy(mount_name, name);
|
|
fprintf(stderr, "[totk] fs mount %s: -> RomFS\n", name);
|
|
return 0;
|
|
}
|
|
static unsigned rom_cache_size(uint64_t *out) { if (!out) return R_ROM_INVALID; *out = 0; return 0; }
|
|
|
|
/* =============================== save, temporary and cache storage =============================== */
|
|
typedef struct { int kind; char name[32]; char root[1024]; } Mount;
|
|
typedef struct { int fd, mode, save; } File; /* save: on a save-data mount (counts for runtime_fs_save_busy) */
|
|
typedef struct { DIR *dir; char path[1024]; int mode; } Dir;
|
|
static Mount mounts[16];
|
|
static File files[NFILES];
|
|
static Dir dirs[NDIRS];
|
|
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static char profile[1024], save_root[1024];
|
|
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
|
|
|
/* Save in progress: a save-data file open for writing, or save data changed in the last 3 s and not committed yet. */
|
|
static _Atomic int save_writers, save_dirty;
|
|
static _Atomic uint64_t save_changed_ns;
|
|
static void save_touched(void) { atomic_store(&save_changed_ns, runtime_now_ns()); atomic_store(&save_dirty, 1); }
|
|
int runtime_fs_save_busy(void) {
|
|
if (atomic_load(&save_writers) > 0) return 1;
|
|
return atomic_load(&save_dirty) && runtime_now_ns() - atomic_load(&save_changed_ns) < 3000000000ull;
|
|
}
|
|
static void make_dir(const char *p) { if (make_dir_p(p) && errno != EEXIST) runtime_fail("create profile storage directory"); }
|
|
static int is_dir(const char *p) { struct stat st; return !stat(p, &st) && S_ISDIR(st.st_mode); }
|
|
static int remove_tree(const char *path) {
|
|
DIR *d = opendir(path);
|
|
if (!d) return errno == ENOENT ? 0 : -1;
|
|
struct dirent *e;
|
|
int fail = 0;
|
|
char child[2048];
|
|
while ((e = readdir(d))) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
|
snprintf(child, sizeof child, "%s/%s", path, e->d_name);
|
|
fail |= is_dir(child) ? remove_tree(child) : unlink(child);
|
|
}
|
|
closedir(d);
|
|
return fail | rmdir(path);
|
|
}
|
|
static void init(void) {
|
|
need(snprintf(profile, sizeof profile, "%s", runtime_profile_dir()) < (int)sizeof profile, "profile path");
|
|
uint64_t uid[2];
|
|
runtime_account_uid(uid);
|
|
char p[1100];
|
|
make_dir(profile);
|
|
snprintf(p, sizeof p, "%s/save", profile); make_dir(p);
|
|
need(snprintf(save_root, sizeof save_root, "%s/save/%016llx%016llx", profile, (unsigned long long)uid[1], (unsigned long long)uid[0]) < (int)sizeof save_root, "save path");
|
|
make_dir(save_root); /* launcher-style EnsureSaveData */
|
|
snprintf(p, sizeof p, "%s/temp", profile); remove_tree(p); make_dir(p);
|
|
snprintf(p, sizeof p, "%s/cache", profile); make_dir(p);
|
|
fprintf(stderr, "[totk] host save storage: %s\n", save_root);
|
|
}
|
|
static void ready(void) { pthread_once(&once, init); }
|
|
|
|
static unsigned mount_add(const char *name, int kind, const char *root) {
|
|
if (!name || !*name || strlen(name) >= 32 || strchr(name, ':') || strchr(name, '/')) return R_INVALID_ARGUMENT;
|
|
pthread_mutex_lock(&lock);
|
|
int slot = -1;
|
|
for (int i = 0; i < 16; i++) {
|
|
if (mounts[i].kind && !strcmp(mounts[i].name, name)) { pthread_mutex_unlock(&lock); return R_ALREADY_EXISTS; }
|
|
if (!mounts[i].kind && slot < 0) slot = i;
|
|
}
|
|
if (slot < 0) runtime_fail("fs mount table full");
|
|
mounts[slot].kind = kind;
|
|
strcpy(mounts[slot].name, name);
|
|
snprintf(mounts[slot].root, sizeof mounts[slot].root, "%s", root);
|
|
pthread_mutex_unlock(&lock);
|
|
fprintf(stderr, "[totk] fs mount %s: -> %s\n", name, root);
|
|
return 0;
|
|
}
|
|
static int component_ok(const char *c, size_t n) { return n && !(n == 1 && c[0] == '.') && !(n == 2 && c[0] == '.' && c[1] == '.'); }
|
|
/* "name:/a/b" -> host path; 0 on success; *kind = the mount's kind (M_SAVE ...) when kind is given */
|
|
static unsigned host_path_kind(const char *path, char *out, size_t cap, int *kind) {
|
|
if (!path) return R_INVALID_PATH;
|
|
const char *colon = strchr(path, ':');
|
|
if (!colon || colon[1] != '/') return R_INVALID_PATH;
|
|
size_t n = (size_t)(colon - path);
|
|
pthread_mutex_lock(&lock);
|
|
const Mount *m = NULL;
|
|
for (int i = 0; i < 16; i++) if (mounts[i].kind && strlen(mounts[i].name) == n && !strncmp(mounts[i].name, path, n)) m = &mounts[i];
|
|
char root[1024];
|
|
if (m) strcpy(root, m->root);
|
|
int k = m ? m->kind : M_FREE;
|
|
pthread_mutex_unlock(&lock);
|
|
if (!m) return R_PATH_NOT_FOUND;
|
|
if (kind) *kind = k;
|
|
const char *rest = colon + 1;
|
|
for (const char *q = rest + 1; *q;) {
|
|
const char *e = strchr(q, '/');
|
|
size_t len = e ? (size_t)(e - q) : strlen(q);
|
|
if (!e && !len) break;
|
|
if (!component_ok(q, len) || memchr(q, '\\', len)) return R_INVALID_PATH;
|
|
q = e ? e + 1 : q + len;
|
|
}
|
|
if (snprintf(out, cap, "%s%s", root, rest) >= (int)cap) return R_INVALID_PATH;
|
|
size_t L = strlen(out);
|
|
while (L > strlen(root) && out[L - 1] == '/') out[--L] = 0;
|
|
return 0;
|
|
}
|
|
static unsigned host_path(const char *path, char *out, size_t cap) { return host_path_kind(path, out, cap, NULL); }
|
|
static unsigned save_change_path(const char *path, char *out, size_t cap) {
|
|
int kind = M_FREE;
|
|
unsigned r = host_path_kind(path, out, cap, &kind);
|
|
if (!r && kind == M_SAVE) save_touched();
|
|
return r;
|
|
}
|
|
static unsigned errno_result(void) {
|
|
return errno == ENOENT || errno == ENOTDIR ? R_PATH_NOT_FOUND : errno == EEXIST ? R_ALREADY_EXISTS
|
|
: errno == ENOTEMPTY ? R_NOT_EMPTY : R_INVALID_ARGUMENT;
|
|
}
|
|
|
|
static unsigned mount_save(const char *name, const uint64_t *uid) {
|
|
ready();
|
|
uint64_t me[2];
|
|
runtime_account_uid(me);
|
|
if (!uid || uid[0] != me[0] || uid[1] != me[1]) return R_TARGET_NOT_FOUND;
|
|
return mount_add(name, M_SAVE, save_root);
|
|
}
|
|
static unsigned char save_exists(uint64_t application, const uint64_t *uid) {
|
|
ready();
|
|
uint64_t me[2];
|
|
runtime_account_uid(me);
|
|
return application == TOTK_APPLICATION_ID && uid && uid[0] == me[0] && uid[1] == me[1];
|
|
}
|
|
/* Other titles' save data (e.g. data-link bonuses) is not present on this host. */
|
|
static unsigned mount_save_ro(const char *name, uint64_t application, const uint64_t *uid) {
|
|
if (!save_exists(application, uid)) return R_TARGET_NOT_FOUND;
|
|
return mount_add(name, M_SAVE, save_root);
|
|
}
|
|
static unsigned mount_temp(const char *name) { ready(); char p[1100]; snprintf(p, sizeof p, "%s/temp", profile); return mount_add(name, M_TEMP, p); }
|
|
static void cache_dir(int index, char *out, size_t cap) { snprintf(out, cap, "%s/cache/%d", profile, index); }
|
|
static unsigned create_cache(int index, int64_t size, int64_t journal) {
|
|
ready();
|
|
if (index < 0 || size < 0 || journal < 0) return R_INVALID_ARGUMENT;
|
|
char p[1100];
|
|
cache_dir(index, p, sizeof p);
|
|
if (make_dir_p(p)) return errno == EEXIST ? R_ALREADY_EXISTS : errno_result();
|
|
return 0;
|
|
}
|
|
static unsigned delete_cache(int index) { ready(); char p[1100]; cache_dir(index, p, sizeof p); return remove_tree(p) ? R_TARGET_NOT_FOUND : 0; }
|
|
static unsigned mount_cache(const char *name, int index) {
|
|
ready();
|
|
char p[1100];
|
|
cache_dir(index, p, sizeof p);
|
|
if (!is_dir(p)) return R_TARGET_NOT_FOUND;
|
|
return mount_add(name, M_CACHE, p);
|
|
}
|
|
typedef struct { int32_t index; uint8_t reserved[0x1c]; } CacheStorageInfo;
|
|
typedef struct { int list[64]; int count, next; } CacheList;
|
|
static unsigned open_cache_list(uint64_t *out) {
|
|
ready();
|
|
if (!out) return R_INVALID_ARGUMENT;
|
|
CacheList *l = calloc(1, sizeof *l);
|
|
need(l != NULL, "cache list allocation");
|
|
char p[1100];
|
|
snprintf(p, sizeof p, "%s/cache", profile);
|
|
DIR *d = opendir(p);
|
|
struct dirent *e;
|
|
while (d && (e = readdir(d)) && l->count < 64) {
|
|
char *end;
|
|
long v = strtol(e->d_name, &end, 10);
|
|
if (*e->d_name && !*end && v >= 0) l->list[l->count++] = (int)v;
|
|
}
|
|
if (d) closedir(d);
|
|
*out = (uint64_t)(uintptr_t)l;
|
|
return 0;
|
|
}
|
|
static unsigned read_cache_list(int *count, CacheStorageInfo *out, uint64_t h, int max) {
|
|
CacheList *l = (CacheList *)(uintptr_t)h;
|
|
if (!l || !count || max < 0 || (!out && max)) return R_INVALID_ARGUMENT;
|
|
int n = 0;
|
|
while (n < max && l->next < l->count) { memset(&out[n], 0, sizeof *out); out[n++].index = l->list[l->next++]; }
|
|
*count = n;
|
|
return 0;
|
|
}
|
|
static void close_cache_list(uint64_t h) { free((void *)(uintptr_t)h); }
|
|
static void unmount(const char *name) {
|
|
pthread_mutex_lock(&lock);
|
|
for (int i = 0; i < 16; i++)
|
|
if (mounts[i].kind && !strcmp(mounts[i].name, name)) { mounts[i].kind = M_FREE; pthread_mutex_unlock(&lock); return; }
|
|
pthread_mutex_unlock(&lock);
|
|
if (!romfs_owns(name) && strcmp(name, mount_name)) runtime_fail("unmount of unknown fs mount");
|
|
}
|
|
static unsigned commit(const char *name) {
|
|
pthread_mutex_lock(&lock);
|
|
int found = 0, save = 0;
|
|
for (int i = 0; i < 16; i++) if (mounts[i].kind && !strcmp(mounts[i].name, name)) { found = 1; save = mounts[i].kind == M_SAVE; }
|
|
pthread_mutex_unlock(&lock);
|
|
if (!found) return R_PATH_NOT_FOUND;
|
|
for (int i = 0; i < NFILES; i++) {
|
|
pthread_mutex_lock(&lock);
|
|
int fd = files[i].fd;
|
|
pthread_mutex_unlock(&lock);
|
|
if (fd > 0) fsync(fd);
|
|
}
|
|
if (save) atomic_store(&save_dirty, 0);
|
|
return 0;
|
|
}
|
|
|
|
static File *file_of(uint64_t h, int *fd, int *mode) {
|
|
if (h < FILE_BASE || h >= FILE_BASE + NFILES) return NULL;
|
|
pthread_mutex_lock(&lock);
|
|
File *f = &files[h - FILE_BASE];
|
|
int v = f->fd;
|
|
if (fd) *fd = f->fd;
|
|
if (mode) *mode = f->mode;
|
|
pthread_mutex_unlock(&lock);
|
|
return v > 0 ? f : NULL;
|
|
}
|
|
static unsigned open_file(uint64_t *out, const char *path, int mode) {
|
|
if (romfs_owns(path)) return romfs_open(out, path, mode);
|
|
if (path) runtime_fs_log(mode & 2 ? "write" : "read", path);
|
|
if (!out || !(mode & (OPEN_READ | OPEN_WRITE)) || (mode & ~7)) return R_INVALID_ARGUMENT;
|
|
char p[1100];
|
|
int kind = M_FREE;
|
|
unsigned r = host_path_kind(path, p, sizeof p, &kind);
|
|
if (r) return r;
|
|
struct stat st;
|
|
if (stat(p, &st) || !S_ISREG(st.st_mode)) return R_PATH_NOT_FOUND;
|
|
int save_writer = kind == M_SAVE && (mode & OPEN_WRITE);
|
|
int fd = open(p, (mode & OPEN_WRITE ? (mode & OPEN_READ ? O_RDWR : O_WRONLY) : O_RDONLY) | O_BINARY);
|
|
if (fd < 0) return errno_result();
|
|
pthread_mutex_lock(&lock);
|
|
for (int i = 0; i < NFILES; i++)
|
|
if (files[i].fd <= 0) {
|
|
files[i] = (File){fd, mode, save_writer};
|
|
*out = FILE_BASE + (uint64_t)i;
|
|
pthread_mutex_unlock(&lock);
|
|
if (save_writer) { atomic_fetch_add(&save_writers, 1); save_touched(); }
|
|
return 0;
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
runtime_fail("host file handle table full");
|
|
}
|
|
static unsigned read_file(uint64_t *out, uint64_t h, int64_t offset, void *buffer, uint64_t size) {
|
|
if (romfs_handle(h)) return romfs_read(out, h, offset, buffer, size);
|
|
int fd, mode;
|
|
if (!out || offset < 0 || (!buffer && size) || !file_of(h, &fd, &mode)) return R_INVALID_ARGUMENT;
|
|
if (!(mode & OPEN_READ)) return R_OPEN_MODE;
|
|
guest_prepare_write(buffer, size);
|
|
uint64_t done = 0;
|
|
while (done < size) {
|
|
ssize_t n = pread(fd, (uint8_t *)buffer + done, (size_t)(size - done), offset + (off_t)done);
|
|
if (n < 0) runtime_fail("host save read failure");
|
|
if (!n) break;
|
|
done += (uint64_t)n;
|
|
}
|
|
*out = done;
|
|
return 0;
|
|
}
|
|
static unsigned read_options(uint64_t *out, uint64_t h, int64_t off, void *buf, uint64_t size, const void *options) {
|
|
(void)options;
|
|
return read_file(out, h, off, buf, size);
|
|
}
|
|
static unsigned read_exact(uint64_t h, int64_t off, void *buf, uint64_t size) {
|
|
uint64_t n = 0;
|
|
unsigned r = read_file(&n, h, off, buf, size);
|
|
return r ? r : n == size ? 0 : R_INVALID_ARGUMENT;
|
|
}
|
|
static unsigned write_file(uint64_t h, int64_t offset, const void *buffer, uint64_t size, const int32_t *option) {
|
|
int fd, mode;
|
|
File *f = offset < 0 || (!buffer && size) ? NULL : file_of(h, &fd, &mode);
|
|
if (!f) return R_INVALID_ARGUMENT;
|
|
if (!(mode & OPEN_WRITE)) return R_OPEN_MODE;
|
|
struct stat st;
|
|
if (fstat(fd, &st)) runtime_fail("host save stat");
|
|
if ((uint64_t)offset + size > (uint64_t)st.st_size && !(mode & OPEN_APPEND)) return R_APPEND_REQUIRED;
|
|
if (f->save) save_touched();
|
|
uint64_t done = 0;
|
|
while (done < size) {
|
|
ssize_t n = pwrite(fd, (const uint8_t *)buffer + done, (size_t)(size - done), offset + (off_t)done);
|
|
if (n <= 0) {
|
|
fprintf(stderr, "[totk] save write at %#llx (%#llx bytes from %p) failed: errno %d\n",
|
|
(unsigned long long)(offset + (off_t)done), (unsigned long long)(size - done),
|
|
(const void *)((const uint8_t *)buffer + done), errno);
|
|
runtime_fail("host save write failure");
|
|
}
|
|
done += (uint64_t)n;
|
|
}
|
|
if (option && (*option & 1)) fsync(fd);
|
|
return 0;
|
|
}
|
|
static unsigned file_size(int64_t *out, uint64_t h) {
|
|
if (romfs_handle(h)) return romfs_size(out, h);
|
|
int fd;
|
|
struct stat st;
|
|
if (!out || !file_of(h, &fd, NULL) || fstat(fd, &st)) return R_INVALID_ARGUMENT;
|
|
*out = st.st_size;
|
|
return 0;
|
|
}
|
|
static unsigned set_file_size(uint64_t h, int64_t size) {
|
|
int fd, mode;
|
|
if (size < 0 || !file_of(h, &fd, &mode)) return R_INVALID_ARGUMENT;
|
|
if (!(mode & OPEN_WRITE)) return R_OPEN_MODE;
|
|
if (files[h - FILE_BASE].save) save_touched();
|
|
return _chsize_s(fd, size) ? errno_result() : 0;
|
|
}
|
|
static unsigned flush_file(uint64_t h) { int fd; if (!file_of(h, &fd, NULL)) return R_INVALID_ARGUMENT; fsync(fd); return 0; }
|
|
static void close_file(uint64_t h) {
|
|
if (romfs_handle(h)) { romfs_close(h); return; }
|
|
if (!file_of(h, NULL, NULL)) runtime_fail("close invalid file handle");
|
|
pthread_mutex_lock(&lock);
|
|
int fd = files[h - FILE_BASE].fd, save = files[h - FILE_BASE].save;
|
|
files[h - FILE_BASE].fd = 0;
|
|
pthread_mutex_unlock(&lock);
|
|
close(fd);
|
|
if (save) { save_touched(); atomic_fetch_sub(&save_writers, 1); }
|
|
}
|
|
static unsigned create_file(const char *path, int64_t size) {
|
|
char p[1100];
|
|
unsigned r = save_change_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
if (size < 0) return R_INVALID_ARGUMENT;
|
|
int fd = open(p, O_WRONLY | O_CREAT | O_EXCL | O_BINARY, 0600);
|
|
if (fd < 0) return errno_result();
|
|
int bad = _chsize_s(fd, size);
|
|
close(fd);
|
|
return bad ? errno_result() : 0;
|
|
}
|
|
static unsigned delete_file(const char *path) {
|
|
char p[1100];
|
|
unsigned r = save_change_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
return unlink(p) ? errno_result() : 0;
|
|
}
|
|
static unsigned rename_file(const char *from, const char *to) {
|
|
char a[1100], b[1100];
|
|
unsigned r = save_change_path(from, a, sizeof a);
|
|
if (!r) r = host_path(to, b, sizeof b);
|
|
if (r) return r;
|
|
struct stat st;
|
|
if (!stat(b, &st)) return R_ALREADY_EXISTS;
|
|
return rename(a, b) ? errno_result() : 0;
|
|
}
|
|
static unsigned entry_type(int *out, const char *path) {
|
|
if (romfs_owns(path)) return romfs_entry_type(out, path);
|
|
char p[1100];
|
|
unsigned r = host_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
struct stat st;
|
|
if (!out) return R_INVALID_ARGUMENT;
|
|
if (stat(p, &st)) return R_PATH_NOT_FOUND;
|
|
*out = S_ISDIR(st.st_mode) ? 0 : 1;
|
|
return 0;
|
|
}
|
|
|
|
typedef struct { char name[769]; uint8_t pad0[3]; int8_t type; uint8_t pad1[3]; int64_t size; } DirectoryEntry;
|
|
_Static_assert(sizeof(DirectoryEntry) == 0x310, "nn::fs::DirectoryEntry");
|
|
static unsigned create_directory(const char *path) {
|
|
char p[1100];
|
|
unsigned r = save_change_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
return make_dir_p(p) ? errno_result() : 0;
|
|
}
|
|
static unsigned delete_recursive(const char *path) {
|
|
char p[1100];
|
|
unsigned r = save_change_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
struct stat st;
|
|
if (stat(p, &st)) return R_PATH_NOT_FOUND;
|
|
return remove_tree(p) ? R_INVALID_ARGUMENT : 0;
|
|
}
|
|
static unsigned open_directory(uint64_t *out, const char *path, int mode) {
|
|
if (romfs_owns(path)) { fprintf(stderr, "[totk] OpenDirectory on RomFS %s\n", path); runtime_fail("RomFS directory listing not implemented"); }
|
|
char p[1100];
|
|
unsigned r = host_path(path, p, sizeof p);
|
|
if (r) return r;
|
|
if (!out || mode < 1 || mode > 3) return R_INVALID_ARGUMENT;
|
|
DIR *d = opendir(p);
|
|
if (!d) return errno_result();
|
|
pthread_mutex_lock(&lock);
|
|
for (int i = 0; i < NDIRS; i++)
|
|
if (!dirs[i].dir) {
|
|
dirs[i].dir = d; dirs[i].mode = mode; strcpy(dirs[i].path, p);
|
|
*out = DIR_BASE + (uint64_t)i;
|
|
pthread_mutex_unlock(&lock);
|
|
return 0;
|
|
}
|
|
pthread_mutex_unlock(&lock);
|
|
runtime_fail("host directory handle table full");
|
|
}
|
|
static unsigned read_directory(int64_t *count, DirectoryEntry *out, uint64_t h, int64_t max) {
|
|
if (h < DIR_BASE || h >= DIR_BASE + NDIRS || !count || max < 0 || (!out && max)) return R_INVALID_ARGUMENT;
|
|
Dir *d = &dirs[h - DIR_BASE];
|
|
if (!d->dir) return R_INVALID_ARGUMENT;
|
|
int64_t n = 0;
|
|
struct dirent *e;
|
|
while (n < max && (e = readdir(d->dir))) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..") || strlen(e->d_name) > 768) continue;
|
|
char child[2048];
|
|
snprintf(child, sizeof child, "%s/%s", d->path, e->d_name);
|
|
struct stat st;
|
|
if (stat(child, &st)) continue;
|
|
int dir = S_ISDIR(st.st_mode);
|
|
if (!(d->mode & (dir ? 1 : 2))) continue;
|
|
memset(&out[n], 0, sizeof *out);
|
|
strcpy(out[n].name, e->d_name);
|
|
out[n].type = dir ? 0 : 1;
|
|
out[n].size = dir ? 0 : st.st_size;
|
|
n++;
|
|
}
|
|
*count = n;
|
|
return 0;
|
|
}
|
|
static void close_directory(uint64_t h) {
|
|
if (h < DIR_BASE || h >= DIR_BASE + NDIRS || !dirs[h - DIR_BASE].dir) runtime_fail("close invalid directory handle");
|
|
pthread_mutex_lock(&lock);
|
|
closedir(dirs[h - DIR_BASE].dir);
|
|
dirs[h - DIR_BASE].dir = NULL;
|
|
pthread_mutex_unlock(&lock);
|
|
}
|
|
static void set_priority(int priority) { (void)priority; } /* fs priority orders I/O inside the system fs process */
|
|
static void fs_initialize(void) { runtime_fs_init(); }
|
|
|
|
HLE_NATIVE("_ZN2nn2fs24InitializeForApplicationEv", fs_initialize, 'v')
|
|
HLE_NATIVE("_ZN2nn2fs8MountRomEPKcPvm", mount_rom, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs22QueryMountRomCacheSizeEPm", rom_cache_size, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci", open_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs11GetFileSizeEPlNS0_10FileHandleE", file_size, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs9CloseFileENS0_10FileHandleE", close_file, 'v')
|
|
HLE_NATIVE("_ZN2nn2fs12GetEntryTypeEPNS0_18DirectoryEntryTypeEPKc", entry_type, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs8ReadFileEPmNS0_10FileHandleElPvm", read_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs8ReadFileEPmNS0_10FileHandleElPvmRKNS0_10ReadOptionE", read_options, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs8ReadFileENS0_10FileHandleElPvm", read_exact, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs9WriteFileENS0_10FileHandleElPKvmRKNS0_11WriteOptionE", write_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs11SetFileSizeENS0_10FileHandleEl", set_file_size, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs9FlushFileENS0_10FileHandleE", flush_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs10CreateFileEPKcl", create_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs10DeleteFileEPKc", delete_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs10RenameFileEPKcS2_", rename_file, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs15CreateDirectoryEPKc", create_directory, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs26DeleteDirectoryRecursivelyEPKc", delete_recursive, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs13OpenDirectoryEPNS0_15DirectoryHandleEPKci", open_directory, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs13ReadDirectoryEPlPNS0_14DirectoryEntryENS0_15DirectoryHandleEl", read_directory, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs14CloseDirectoryENS0_15DirectoryHandleE", close_directory, 'v')
|
|
HLE_NATIVE("_ZN2nn2fs13MountSaveDataEPKcRKNS_7account3UidE", mount_save, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs21MountSaveDataReadOnlyEPKcNS_13ApplicationIdERKNS_7account3UidE", mount_save_ro, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs18IsSaveDataExistingENS_13ApplicationIdERKNS_7account3UidE", save_exists, 'b')
|
|
HLE_NATIVE("_ZN2nn2fs21MountTemporaryStorageEPKc", mount_temp, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs7UnmountEPKc", unmount, 'v')
|
|
HLE_NATIVE("_ZN2nn2fs6CommitEPKc", commit, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs18CreateCacheStorageEill", create_cache, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs18DeleteCacheStorageEi", delete_cache, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs17MountCacheStorageEPKci", mount_cache, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs20OpenCacheStorageListEPNS0_22CacheStorageListHandleE", open_cache_list, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs20ReadCacheStorageListEPiPNS0_16CacheStorageInfoENS0_22CacheStorageListHandleEi", read_cache_list, 'w')
|
|
HLE_NATIVE("_ZN2nn2fs21CloseCacheStorageListENS0_22CacheStorageListHandleE", close_cache_list, 'v')
|
|
HLE_NATIVE("_ZN2nn2fs26SetPriorityOnCurrentThreadENS0_8PriorityE", set_priority, 'v')
|