SDL_Libretro 0.1.0
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_physfs.h
Go to the documentation of this file.
1
11#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_PHYSFS_IMPL_ONCE) && defined(SDL_LIBRETRO_ENABLE_PHYSFS) && !defined(SDL_LIBRETRO_DISABLE_PHYSFS)
12#define SDL_LIBRETRO_PHYSFS_IMPL_ONCE
13
14#ifdef SDL_LIBRETRO_DISABLE_VFS
15#error "SDL_libretro_physfs requires the SDL VFS; do not define SDL_LIBRETRO_DISABLE_VFS"
16#endif
17
18#ifndef SDL_LIBRETRO_PHYSFS_NO_SDL_PHYSFS_IMPLEMENTATION
19#define SDL_PHYSFS_IMPLEMENTATION
20#endif
21#include "SDL_PhysFS.h"
22
23#ifndef SDL_PHYSFS_PHYSFS_H
24#define SDL_PHYSFS_PHYSFS_H "physfs.h"
25#endif
26#include SDL_PHYSFS_PHYSFS_H
27
28#ifndef SDL_LIBRETRO_PHYSFS_MOUNT_POINT
34#define SDL_LIBRETRO_PHYSFS_MOUNT_POINT "game"
35#endif
36
45static struct retro_vfs_file_handle* SDL_Libretro_PhysFS_VFS_Open(const char* path, unsigned mode, unsigned hints) {
46 PHYSFS_Stat st;
47 if (path && (mode & RETRO_VFS_FILE_ACCESS_WRITE) == 0 &&
48 PHYSFS_stat(path, &st) && st.filetype == PHYSFS_FILETYPE_REGULAR) {
49 SDL_IOStream* io = SDL_PhysFS_IOFromFile(path);
50 if (io) {
51 return SDL_Libretro_VFS_WrapIO(io, path, mode);
52 }
53 }
54 return SDL_Libretro_VFS_Open(path, mode, hints);
55}
56
63static int SDL_Libretro_PhysFS_VFS_Stat64(const char* path, int64_t* size) {
64 PHYSFS_Stat st;
65 if (path && PHYSFS_stat(path, &st)) {
66 int flags = RETRO_VFS_STAT_IS_VALID;
67 if (st.filetype == PHYSFS_FILETYPE_DIRECTORY) {
68 flags |= RETRO_VFS_STAT_IS_DIRECTORY;
69 }
70 if (size != NULL) {
71 *size = (int64_t)st.filesize;
72 }
73 return flags;
74 }
75 return SDL_Libretro_VFS_Stat64(path, size);
76}
77
83static int SDL_Libretro_PhysFS_VFS_Stat(const char* path, int32_t* size) {
84 int64_t outSize = 0;
85 int out = SDL_Libretro_PhysFS_VFS_Stat64(path, &outSize);
86 if (size != NULL) {
87 *size = SDL_Libretro_VFS_ClampStatSize(path, outSize);
88 }
89 return out;
90}
91
98static struct retro_vfs_dir_handle* SDL_Libretro_PhysFS_VFS_Opendir(const char* dir, bool include_hidden) {
99 PHYSFS_Stat st;
100 if (!dir || !PHYSFS_stat(dir, &st) || st.filetype != PHYSFS_FILETYPE_DIRECTORY) {
101 return SDL_Libretro_VFS_Opendir(dir, include_hidden);
102 }
103
104 char** entries = PHYSFS_enumerateFiles(dir);
105 if (!entries) return NULL;
106
107 size_t count = 0;
108 while (entries[count]) count++;
109
110 struct retro_vfs_dir_handle* h = (struct retro_vfs_dir_handle*)SDL_calloc(1, sizeof(*h));
111 char** names = count > 0 ? (char**)SDL_calloc(count, sizeof(char*)) : NULL;
112 bool* isDir = count > 0 ? (bool*)SDL_calloc(count, sizeof(bool)) : NULL;
113 if (!h || (count > 0 && (!names || !isDir))) {
114 SDL_free(h);
115 SDL_free(names);
116 SDL_free(isDir);
117 PHYSFS_freeList(entries);
118 return NULL;
119 }
120
121 size_t stored = 0;
122 for (size_t i = 0; i < count; i++) {
123 if (!include_hidden && entries[i][0] == '.') continue;
124 names[stored] = SDL_strdup(entries[i]);
125 if (!names[stored]) continue;
126 char full[SDL_LIBRETRO_MAX_PATH];
127 SDL_snprintf(full, sizeof(full), "%s/%s", dir, entries[i]);
128 PHYSFS_Stat entryStat;
129 isDir[stored] = PHYSFS_stat(full, &entryStat) && entryStat.filetype == PHYSFS_FILETYPE_DIRECTORY;
130 stored++;
131 }
132 PHYSFS_freeList(entries);
133
134 h->dir = SDL_strdup(dir);
135 if (!h->dir) {
136 for (size_t i = 0; i < stored; i++) SDL_free(names[i]);
137 SDL_free(names);
138 SDL_free(isDir);
139 SDL_free(h);
140 return NULL;
141 }
142 h->names = names;
143 h->isDir = isDir;
144 h->count = stored;
145 h->index = (size_t)-1; // readdir advances before returning
146 return h;
147}
148
153static void SDL_Libretro_PhysFS_ClearMount(SDL_Libretro* lr) {
154 if (!lr) return;
155 if (lr->physfsMountSource[0] != '\0') {
156 SDL_PhysFS_Unmount(lr->physfsMountSource);
157 lr->physfsMountSource[0] = '\0';
158 }
159}
160
178bool SDL_Libretro_PhysFS_Init(SDL_Libretro* lr) {
179 if (!lr) return false;
180
181 if (!lr->physfsReady) {
182 if (!SDL_PhysFS_Init(NULL)) {
183 return false;
184 }
185 lr->physfsReady = true;
186 }
187
188 struct retro_vfs_interface vfs = {0};
189 vfs.open = SDL_Libretro_PhysFS_VFS_Open;
190 vfs.stat = SDL_Libretro_PhysFS_VFS_Stat;
191 vfs.stat_64 = SDL_Libretro_PhysFS_VFS_Stat64;
192 vfs.opendir = SDL_Libretro_PhysFS_VFS_Opendir;
193 SDL_Libretro_SetVFS(lr, &vfs);
194 return true;
195}
196
208void SDL_Libretro_PhysFS_Quit(SDL_Libretro* lr) {
209 if (!lr) return;
210 SDL_Libretro_PhysFS_ClearMount(lr);
211 SDL_Libretro_SetVFS(lr, NULL);
212 if (lr->physfsReady) {
213 SDL_PhysFS_Quit();
214 lr->physfsReady = false;
215 }
216}
217
223typedef struct SDL_Libretro_PhysFS_FileList {
224 char** paths;
225 int count;
226 int capacity;
227} SDL_Libretro_PhysFS_FileList;
228
233static void SDL_Libretro_PhysFS_CollectFiles(const char* dir, SDL_Libretro_PhysFS_FileList* list) {
234 char** entries = PHYSFS_enumerateFiles(dir);
235 if (!entries) return;
236
237 for (char** entry = entries; *entry; entry++) {
238 char full[SDL_LIBRETRO_MAX_PATH];
239 SDL_snprintf(full, sizeof(full), "%s/%s", dir, *entry);
240 PHYSFS_Stat st;
241 if (!PHYSFS_stat(full, &st)) continue;
242 if (st.filetype == PHYSFS_FILETYPE_DIRECTORY) {
243 SDL_Libretro_PhysFS_CollectFiles(full, list);
244 }
245 else if (st.filetype == PHYSFS_FILETYPE_REGULAR) {
246 if (list->count >= list->capacity) {
247 int newCapacity = list->capacity > 0 ? list->capacity * 2 : 16;
248 char** grown = (char**)SDL_realloc(list->paths, (size_t)newCapacity * sizeof(char*));
249 if (!grown) continue;
250 list->paths = grown;
251 list->capacity = newCapacity;
252 }
253 list->paths[list->count] = SDL_strdup(full);
254 if (list->paths[list->count]) list->count++;
255 }
256 }
257 PHYSFS_freeList(entries);
258}
259
266static bool SDL_Libretro_PhysFS_IsLoadableExtension(const SDL_Libretro* lr, const char* ext, const char* validExts) {
267 if (validExts[0] != '\0') {
268 return SDL_Libretro_ExtensionInList(ext, validExts);
269 }
270 for (unsigned c = 0; c < lr->coreLibraryCount; c++) {
271 if (SDL_Libretro_ExtensionInList(ext, lr->coreLibrary[c].supported_extensions)) {
272 return true;
273 }
274 }
275 return lr->coreLibraryCount == 0;
276}
277
301static bool SDL_Libretro_PhysFS_PickContent(SDL_Libretro* lr, const char* archivePath, char* dst, size_t dstSize) {
302 SDL_Libretro_PhysFS_FileList list = {0};
303 SDL_Libretro_PhysFS_CollectFiles(SDL_LIBRETRO_PHYSFS_MOUNT_POINT, &list);
304
305 // Get the valid extensions for the core.
306 const char* validExts = SDL_Libretro_IsCoreReady(lr) ? lr->core.validExtensions : "";
307 bool hasExts = validExts[0] != '\0';
308
309 // Get the archive's filename.
310 char archiveName[SDL_LIBRETRO_MAX_PATH];
311 SDL_Libretro_GetFileName(archiveName, sizeof(archiveName), archivePath, false);
312
313 // Search for the file to load within the .zip.
314 const char* found = NULL;
315
316 // Pass 1: Disc metadata, gated on the core actually supporting it.
317 static const char* discExts[] = {"m3u", "cue"};
318 for (size_t d = 0; !found && d < SDL_arraysize(discExts); d++) {
319 if (hasExts && !SDL_Libretro_ExtensionInList(discExts[d], validExts)) continue;
320 for (int i = 0; !found && i < list.count; i++) {
321 if (SDL_strcasecmp(SDL_Libretro_GetExtension(list.paths[i]), discExts[d]) == 0) {
322 found = list.paths[i];
323 }
324 }
325 }
326
327 // Pass 2: Base name matching the archive's, among the loadable entries,
328 // so a "mario.nfo" next to "mario.nes" doesn't win just by sorting first.
329 for (int i = 0; !found && i < list.count; i++) {
330 char entryName[SDL_LIBRETRO_MAX_PATH];
331 SDL_Libretro_GetFileName(entryName, sizeof(entryName), list.paths[i], false);
332 if (SDL_strcasecmp(entryName, archiveName) == 0 &&
333 SDL_Libretro_PhysFS_IsLoadableExtension(lr, SDL_Libretro_GetExtension(list.paths[i]), validExts)) {
334 found = list.paths[i];
335 }
336 }
337
338 // Pass 3: extension known to the loaded core, or to any core in the library.
339 for (int i = 0; !found && i < list.count; i++) {
340 if (SDL_Libretro_PhysFS_IsLoadableExtension(lr, SDL_Libretro_GetExtension(list.paths[i]), validExts)) {
341 found = list.paths[i];
342 }
343 }
344
345 // Pass 4: No file found, so assume it's the first file.
346 if (!found && !hasExts && list.count > 0) {
347 found = list.paths[0];
348 }
349
350 if (found) {
351 SDL_strlcpy(dst, found, dstSize);
352 }
353
354 // Clear the file list.
355 for (int i = 0; i < list.count; i++) {
356 SDL_free(list.paths[i]);
357 }
358 SDL_free(list.paths);
359
360 return found != NULL;
361}
362
379static bool SDL_Libretro_PhysFS_ExtractArchive(SDL_Libretro* lr, const char* archivePath, char* dir, size_t dirSize) {
380 char tempDir[SDL_LIBRETRO_MAX_PATH];
381 if (!SDL_Libretro_GetTempDirectory(lr, tempDir, sizeof(tempDir))) {
382 return false;
383 }
384 char name[SDL_LIBRETRO_MAX_PATH];
385 if (SDL_Libretro_GetFileName(name, sizeof(name), archivePath, false) == 0) {
386 SDL_strlcpy(name, "content", sizeof(name));
387 }
388
389 // Find a directory name that doesn't exist yet.
390 SDL_snprintf(dir, dirSize, "%s%s/", tempDir, name);
391 for (int attempt = 1; SDL_GetPathInfo(dir, NULL); attempt++) {
392 if (attempt > 99) {
393 return SDL_SetError("[SDL_Libretro] Could not find a free directory name for '%s' in '%s'", name, tempDir);
394 }
395 SDL_snprintf(dir, dirSize, "%s%s-%d/", tempDir, name, attempt);
396 }
397
398 SDL_Libretro_PhysFS_FileList list = {0};
399 SDL_Libretro_PhysFS_CollectFiles(SDL_LIBRETRO_PHYSFS_MOUNT_POINT, &list);
400 size_t prefixLen = SDL_strlen(SDL_LIBRETRO_PHYSFS_MOUNT_POINT "/");
401 bool ok = SDL_CreateDirectory(dir);
402 for (int i = 0; ok && i < list.count; i++) {
403 // PhysFS rejects ".." and absolute paths, so entries stay inside `dir`.
404 char path[SDL_LIBRETRO_MAX_PATH];
405 SDL_snprintf(path, sizeof(path), "%s%s", dir, list.paths[i] + prefixLen);
406
407 // Make sure the entry's subdirectory exists.
408 char* slash = SDL_strrchr(path, '/');
409 if (slash && slash > path + SDL_strlen(dir) - 1) {
410 *slash = '\0';
411 ok = SDL_CreateDirectory(path);
412 *slash = '/';
413 }
414
415 size_t size = 0;
416 void* data = ok ? SDL_LoadFile_IO(SDL_PhysFS_IOFromFile(list.paths[i]), &size, true) : NULL;
417 ok = data != NULL && SDL_SaveFile(path, data, size);
418 SDL_free(data);
419 if (!ok) {
420 SDL_SetError("[SDL_Libretro] Failed to extract '%s' to '%s': %s", list.paths[i], path, SDL_GetError());
421 }
422 }
423
424 for (int i = 0; i < list.count; i++) {
425 SDL_free(list.paths[i]);
426 }
427 SDL_free(list.paths);
428
429 if (!ok) {
430 SDL_Libretro_RemoveTree(dir);
431 }
432 return ok;
433}
434
470bool SDL_Libretro_PhysFS_LoadGame(SDL_Libretro* lr, const char* gamePath) {
471 if (!lr) return false;
472
473 // Unload any running game before touching the mount. Otherwise the old
474 // archive is pulled out from under it before retro_unload_game(), and the
475 // unload inside SDL_Libretro_LoadGameFile() would unmount the new one.
477 SDL_Libretro_PhysFS_ClearMount(lr);
478
479 // Load the core without content.
480 if (!gamePath) {
481 return SDL_Libretro_LoadGameFile(lr, NULL);
482 }
483
484 // If it's not a .zip file, load it as normal.
485 if (SDL_strcasecmp(SDL_Libretro_GetExtension(gamePath), "zip") != 0) {
486 return SDL_Libretro_LoadGameFile(lr, gamePath);
487 }
488
489 // Initialize PhysFS on demand. Fall back to the plain loader on failure.
490 if (!SDL_Libretro_PhysFS_Init(lr)) {
491 return SDL_Libretro_LoadGameFile(lr, gamePath);
492 }
493
494 // Cores that parse archives themselves get the raw .zip.
495 if (SDL_Libretro_IsCoreReady(lr)) {
496 if (lr->core.blockExtract || SDL_Libretro_ExtensionInList("zip", lr->core.validExtensions)) {
497 return SDL_Libretro_LoadGameFile(lr, gamePath);
498 }
499 }
500 else {
501 // With no core loaded, look at the core that would be picked for a
502 // .zip (the first in sorted library order). Only when its .info sets
503 // block_extract does it get the raw archive; a core that merely lists
504 // "zip" would otherwise claim every archive, whatever is inside.
505 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
506 const SDL_Libretro_CoreInfo* info = &lr->coreLibrary[i];
507 if (!SDL_Libretro_ExtensionInList("zip", info->supported_extensions)) continue;
508 if (info->block_extract && SDL_Libretro_LoadCore(lr, info->path)) {
509 return SDL_Libretro_LoadGameFile(lr, gamePath);
510 }
511 break;
512 }
513 }
514
515 // Mount the .zip file.
516 if (!SDL_PhysFS_Mount(gamePath, SDL_LIBRETRO_PHYSFS_MOUNT_POINT)) {
517 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] Failed to mount '%s': %s", gamePath, SDL_GetError());
518 return SDL_Libretro_LoadGameFile(lr, gamePath);
519 }
520
521 // Remember which .zip file was mounted.
522 SDL_strlcpy(lr->physfsMountSource, gamePath, sizeof(lr->physfsMountSource));
523
524 // Find the content to load within the mount.
525 char virtualPath[SDL_LIBRETRO_MAX_PATH];
526 if (!SDL_Libretro_PhysFS_PickContent(lr, gamePath, virtualPath, sizeof(virtualPath))) {
527 SDL_Libretro_PhysFS_ClearMount(lr);
528 SDL_SetError("[SDL_Libretro] No suitable content found inside '%s'", gamePath);
529 return false;
530 }
531
532 // The core must be loaded to resolve need_fullpath for the picked file.
533 if (!SDL_Libretro_IsCoreReady(lr) && !SDL_Libretro_LoadCoreForGame(lr, virtualPath)) {
534 SDL_Libretro_PhysFS_ClearMount(lr);
535 return false;
536 }
537
538 // Attempt to load the content.
539 bool result;
540 if (SDL_Libretro_ContentNeedsFullpath(lr, SDL_Libretro_GetExtension(virtualPath)) && lr->core.usedVFS) {
541 // VFS-aware full-path core: pass the virtual path; the PhysFS-backed
542 // VFS serves it straight out of the mount.
543 result = SDL_Libretro_LoadGameFile(lr, virtualPath);
544 }
545 else if (SDL_Libretro_ContentNeedsFullpath(lr, SDL_Libretro_GetExtension(virtualPath))) {
546 // Full-path core that reads from the OS: extract the archive, so the
547 // content finds any files it refers to, and load from there.
548 char dir[SDL_LIBRETRO_MAX_PATH];
549 result = SDL_Libretro_PhysFS_ExtractArchive(lr, gamePath, dir, sizeof(dir));
550 if (result) {
551 char path[SDL_LIBRETRO_MAX_PATH];
552 SDL_snprintf(path, sizeof(path), "%s%s", dir, virtualPath + SDL_strlen(SDL_LIBRETRO_PHYSFS_MOUNT_POINT "/"));
553 result = SDL_Libretro_LoadGameFile(lr, path);
554 if (result) {
555 SDL_strlcpy(lr->core.contentTempDir, dir, sizeof(lr->core.contentTempDir));
556 }
557 else {
558 SDL_Libretro_RemoveTree(dir);
559 }
560 }
561 }
562 else {
563 // Byte-oriented cores get the content bytes.
564 SDL_IOStream* io = SDL_PhysFS_IOFromFile(virtualPath);
565 result = io != NULL && SDL_Libretro_LoadGame_IO(lr, io, virtualPath, true);
566 }
567
568 if (!result) {
569 SDL_Libretro_PhysFS_ClearMount(lr);
570 }
571 return result;
572}
573
574#else
575
576bool SDL_Libretro_PhysFS_Init(SDL_Libretro* lr) {
577 (void)lr;
578 return SDL_SetError("SDL_Libretro_PhysFS not enabled");
579}
580
581void SDL_Libretro_PhysFS_Quit(SDL_Libretro* lr) {
582 (void)lr;
583}
584
585bool SDL_Libretro_PhysFS_LoadGame(SDL_Libretro* lr, const char* gamePath) {
586 (void)lr;
587 (void)gamePath;
588 return SDL_SetError("SDL_Libretro_PhysFS not enabled");
589}
590
591static void SDL_Libretro_PhysFS_ClearMount(SDL_Libretro* lr) {
592 (void)lr;
593}
594
595#endif
#define SDL_LIBRETRO_PHYSFS_MOUNT_POINT
PhysFS mount point for archives.
bool SDL_Libretro_IsCoreReady(const SDL_Libretro *lr)
Indicates whether a core is loaded.
size_t SDL_Libretro_GetFileName(char *dst, size_t dstSize, const char *path, bool withExtension)
Copy the file name portion of a path into a caller-provided buffer.
bool SDL_Libretro_PhysFS_LoadGame(SDL_Libretro *lr, const char *gamePath)
Archive-aware replacement for SDL_Libretro_LoadGame().
bool SDL_Libretro_PhysFS_Init(SDL_Libretro *lr)
Initialize PhysFS and route the libretro VFS through it.
bool SDL_Libretro_LoadGame_IO(SDL_Libretro *lr, SDL_IOStream *src, const char *path, bool closeio)
Loads a game from an SDL_IOStream.
void SDL_Libretro_SetVFS(SDL_Libretro *lr, void *vfs)
Set the Virtual File System the core uses for file access.
void SDL_Libretro_PhysFS_Quit(SDL_Libretro *lr)
Tear down PhysFS, unmounting any active archive and restoring the plain SDL VFS on the given context.
void SDL_Libretro_UnloadGame(SDL_Libretro *lr)
Unloads the actively loaded game.
bool SDL_Libretro_LoadCore(SDL_Libretro *lr, const char *core)
Loads a libretro core.