SDL_Libretro 0.1.0
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_vfs.h
Go to the documentation of this file.
1
7#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_VFS_IMPL_ONCE)
8#define SDL_LIBRETRO_VFS_IMPL_ONCE
9
10
11#ifdef __DOXYGEN
19#define SDL_LIBRETRO_DISABLE_VFS
20#endif
21
22#ifndef SDL_LIBRETRO_DISABLE_VFS
23
24/*
25 * SDL_libretro - libretro VFS interface backed by SDL_IOStream
26 *
27 * Provides GET_VFS_INTERFACE v1–v4.
28 */
29
33struct retro_vfs_file_handle {
34 SDL_IOStream* io;
35 char* path;
36 unsigned mode;
37};
38
42struct retro_vfs_dir_handle {
43 char* dir;
44 char** names;
45 bool* isDir;
46 size_t count;
47 size_t index;
48};
49
53static const char* SDL_Libretro_VFS_GetPath(struct retro_vfs_file_handle* stream) {
54 return stream ? stream->path : NULL;
55}
56
63static struct retro_vfs_file_handle* SDL_Libretro_VFS_WrapIO(SDL_IOStream* io, const char* path, unsigned mode) {
64 struct retro_vfs_file_handle* h = (struct retro_vfs_file_handle*)SDL_malloc(sizeof(*h));
65 if (!h) {
66 SDL_CloseIO(io);
67 return NULL;
68 }
69 h->path = SDL_strdup(path);
70 if (!h->path) {
71 SDL_free(h);
72 SDL_CloseIO(io);
73 return NULL;
74 }
75 h->io = io;
76 h->mode = mode;
77 return h;
78}
79
83static struct retro_vfs_file_handle* SDL_Libretro_VFS_Open(const char* path, unsigned mode, unsigned hints) {
84 (void)hints;
85 if (!path) return NULL;
86
87 // VFS requires open() to fail on a directory.
88 SDL_PathInfo pathInfo;
89 if (SDL_GetPathInfo(path, &pathInfo) && pathInfo.type == SDL_PATHTYPE_DIRECTORY) return NULL;
90
91 const char* sdlMode;
92 bool update = (mode & RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING) != 0;
93 bool read = (mode & RETRO_VFS_FILE_ACCESS_READ) != 0;
94 bool write = (mode & RETRO_VFS_FILE_ACCESS_WRITE) != 0;
95
96 if (read && write) {
97 sdlMode = update ? "r+b" : "w+b";
98 }
99 else if (write) {
100 // UPDATE_EXISTING must preserve existing contents rather than truncate.
101 sdlMode = update ? "r+b" : "wb";
102 }
103 else {
104 sdlMode = "rb";
105 }
106
107 SDL_IOStream* io = SDL_IOFromFile(path, sdlMode);
108 // UPDATE_EXISTING still permits creating a missing file, but "r+b" cannot.
109 if (!io && write && !read && update) {
110 io = SDL_IOFromFile(path, "wb");
111 }
112 if (!io) return NULL;
113
114 return SDL_Libretro_VFS_WrapIO(io, path, mode);
115}
116
120static int SDL_Libretro_VFS_Close(struct retro_vfs_file_handle* stream) {
121 if (!stream) return -1;
122 int ret = SDL_CloseIO(stream->io) ? 0 : -1;
123 SDL_free(stream->path);
124 SDL_free(stream);
125 return ret;
126}
127
131static int64_t SDL_Libretro_VFS_Size(struct retro_vfs_file_handle* stream) {
132 if (!stream) return -1;
133 int64_t sz = SDL_GetIOSize(stream->io);
134 return sz < 0 ? -1 : sz;
135}
136
140static int64_t SDL_Libretro_VFS_Tell(struct retro_vfs_file_handle* stream) {
141 if (!stream) return -1;
142 return SDL_TellIO(stream->io);
143}
144
151static int64_t SDL_Libretro_VFS_Seek(struct retro_vfs_file_handle* stream, int64_t offset, int seek_position) {
152 if (!stream) return -1;
153 SDL_IOWhence whence;
154 switch (seek_position) {
155 case RETRO_VFS_SEEK_POSITION_START: whence = SDL_IO_SEEK_SET; break;
156 case RETRO_VFS_SEEK_POSITION_CURRENT: whence = SDL_IO_SEEK_CUR; break;
157 case RETRO_VFS_SEEK_POSITION_END: whence = SDL_IO_SEEK_END; break;
158 default: return -1;
159 }
160 return SDL_SeekIO(stream->io, offset, whence) < 0 ? -1 : 0;
161}
162
166static int64_t SDL_Libretro_VFS_Read(struct retro_vfs_file_handle* stream, void* s, uint64_t len) {
167 if (!stream || !s) return -1;
168 size_t n = SDL_ReadIO(stream->io, s, (size_t)len);
169 if (n == 0 && SDL_GetIOStatus(stream->io) == SDL_IO_STATUS_ERROR) return -1;
170 return (int64_t)n;
171}
172
176static int64_t SDL_Libretro_VFS_Write(struct retro_vfs_file_handle* stream, const void* s, uint64_t len) {
177 if (!stream || !s) return -1;
178 size_t n = SDL_WriteIO(stream->io, s, (size_t)len);
179 if (n == 0 && SDL_GetIOStatus(stream->io) == SDL_IO_STATUS_ERROR) return -1;
180 return (int64_t)n;
181}
182
186static int SDL_Libretro_VFS_Flush(struct retro_vfs_file_handle* stream) {
187 if (!stream) return -1;
188 return SDL_FlushIO(stream->io) ? 0 : -1;
189}
190
194static int SDL_Libretro_VFS_Remove(const char* path) {
195 return (path && SDL_RemovePath(path)) ? 0 : -1;
196}
197
201static int SDL_Libretro_VFS_Rename(const char* old_path, const char* new_path) {
202 return (old_path && new_path && SDL_RenamePath(old_path, new_path)) ? 0 : -1;
203}
204
214static int64_t SDL_Libretro_VFS_Truncate(struct retro_vfs_file_handle* stream, int64_t length) {
215 if (!stream || length < 0) return -1;
216
217 // Read-only handles are skipped because the below updates
218 // the stream->path. This shouldn't happen for read-only content.
219 if ((stream->mode & RETRO_VFS_FILE_ACCESS_WRITE) == 0) return -1;
220
221 int64_t cur_size = SDL_GetIOSize(stream->io);
222 if (cur_size < 0) return -1;
223
224 // Preserve the caller's read/write position across the operation.
225 int64_t orig_pos = SDL_TellIO(stream->io);
226
227 if (length >= cur_size) {
228 // Grow: seek to end and pad with zeros in chunks.
229 if (SDL_SeekIO(stream->io, 0, SDL_IO_SEEK_END) < 0) return -1;
230 int64_t to_pad = length - cur_size;
231 if (to_pad > 0) {
232 Uint8 zeros[4096];
233 SDL_memset(zeros, 0, sizeof(zeros));
234 while (to_pad > 0) {
235 size_t chunk = (to_pad < (int64_t)sizeof(zeros)) ? (size_t)to_pad : sizeof(zeros);
236 if (SDL_WriteIO(stream->io, zeros, chunk) != chunk) return -1;
237 to_pad -= (int64_t)chunk;
238 }
239 }
240 if (orig_pos >= 0) SDL_SeekIO(stream->io, orig_pos, SDL_IO_SEEK_SET);
241 return 0;
242 }
243
244 // Shrink: read the bytes we're keeping into memory, write them to a sibling
245 // temp file, then rename it over the original. Portable (no OS-specific
246 // truncate call) at the cost of buffering the kept prefix, and the original
247 // is only replaced once the new contents are fully on disk.
248 void* kept = SDL_malloc(length > 0 ? (size_t)length : 1);
249 if (!kept) return -1;
250 if (length > 0) {
251 if (SDL_SeekIO(stream->io, 0, SDL_IO_SEEK_SET) < 0 ||
252 SDL_ReadIO(stream->io, kept, (size_t)length) != (size_t)length) {
253 SDL_free(kept);
254 if (orig_pos >= 0) SDL_SeekIO(stream->io, orig_pos, SDL_IO_SEEK_SET);
255 return -1;
256 }
257 }
258
259 char* tmpPath = NULL;
260 if (SDL_asprintf(&tmpPath, "%s.tmp", stream->path) < 0 || !tmpPath) {
261 SDL_free(kept);
262 if (orig_pos >= 0) SDL_SeekIO(stream->io, orig_pos, SDL_IO_SEEK_SET);
263 return -1;
264 }
265 bool saved = SDL_SaveFile(tmpPath, kept, (size_t)length);
266 SDL_free(kept);
267 if (!saved) {
268 // The original file and handle are untouched.
269 SDL_RemovePath(tmpPath);
270 SDL_free(tmpPath);
271 if (orig_pos >= 0) SDL_SeekIO(stream->io, orig_pos, SDL_IO_SEEK_SET);
272 return -1;
273 }
274
275 // POSIX can rename over a file that is still open, which keeps the old
276 // handle alive as a fallback until the new one opens. Windows refuses to
277 // replace an open file, so close the handle and retry.
278 bool renamed = SDL_RenamePath(tmpPath, stream->path);
279 if (!renamed) {
280 SDL_CloseIO(stream->io);
281 stream->io = NULL;
282 renamed = SDL_RenamePath(tmpPath, stream->path);
283 if (!renamed) SDL_RemovePath(tmpPath);
284 }
285 SDL_free(tmpPath);
286
287 // Reopen for continued use, falling back to read-only rather than leaving
288 // the handle without a stream.
289 SDL_IOStream* io = SDL_IOFromFile(stream->path, "r+b");
290 if (!io) io = SDL_IOFromFile(stream->path, "rb");
291 if (io) {
292 if (stream->io) SDL_CloseIO(stream->io);
293 stream->io = io;
294 }
295 if (!stream->io) {
296 SDL_SetError("[SDL_Libretro] VFS truncate could not reopen '%s'", stream->path);
297 return -1;
298 }
299
300 if (!renamed) {
301 // The original contents survived; put the position back as it was.
302 if (orig_pos >= 0) SDL_SeekIO(stream->io, orig_pos, SDL_IO_SEEK_SET);
303 return -1;
304 }
305
306 // Restore the position, clamped to the new (smaller) length.
307 if (orig_pos >= 0) {
308 SDL_SeekIO(stream->io, orig_pos < length ? orig_pos : length, SDL_IO_SEEK_SET);
309 }
310 return io ? 0 : -1;
311}
312
316static int SDL_Libretro_VFS_Stat64(const char* path, int64_t* size) {
317 if (!path) return 0;
318 SDL_PathInfo info;
319 if (!SDL_GetPathInfo(path, &info)) {
320 return 0;
321 }
322
323 int flags = RETRO_VFS_STAT_IS_VALID;
324 if (info.type == SDL_PATHTYPE_DIRECTORY) {
325 flags |= RETRO_VFS_STAT_IS_DIRECTORY;
326 }
327
328 if (size != NULL) {
329 *size = info.size;
330 }
331 return flags;
332}
333
339static int32_t SDL_Libretro_VFS_ClampStatSize(const char* path, int64_t size) {
340 if (size > SDL_MAX_SINT32) {
341 SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[SDL_Libretro] VFS stat size %" SDL_PRIs64 " for '%s' exceeds int32. Clamping to SDL_MAX_SINT32.", size, path);
342 return SDL_MAX_SINT32;
343 }
344 return (int32_t)size;
345}
346
350static int SDL_Libretro_VFS_Stat(const char* path, int32_t* size) {
351 if (!path) return 0;
352 int64_t outSize = 0;
353 int out = SDL_Libretro_VFS_Stat64(path, &outSize);
354 if (size != NULL) {
355 *size = SDL_Libretro_VFS_ClampStatSize(path, outSize);
356 }
357 return out;
358}
359
363static int SDL_Libretro_VFS_Mkdir(const char* dir) {
364 if (!dir) return -1;
365 SDL_PathInfo info;
366 if (SDL_GetPathInfo(dir, &info)) return -2; /* already exists */
367 return SDL_CreateDirectory(dir) ? 0 : -1;
368}
369
373typedef struct {
374 char** names;
375 bool* isDir;
376 size_t count;
377 size_t capacity;
378 const char* dirPath;
379 bool includeHidden;
380} SDL_Libretro_DirCollect;
381
385static SDL_EnumerationResult SDL_Libretro_DirCallback(void* userdata, const char* dirname, const char* fname) {
386 (void)dirname;
387 SDL_Libretro_DirCollect* ctx = (SDL_Libretro_DirCollect*)userdata;
388 if (!ctx->includeHidden && fname[0] == '.') return SDL_ENUM_CONTINUE;
389
390 if (ctx->count >= ctx->capacity) {
391 size_t newCap = ctx->capacity ? ctx->capacity * 2 : 32;
392 // Store each successful realloc back immediately: realloc frees the old block, so on partial failure the cleanup path must not be left holding a dangling pointer (use-after-free / double-free).
393 char** newNames = (char**)SDL_realloc(ctx->names, newCap * sizeof(char*));
394 if (newNames) ctx->names = newNames;
395 bool* newIsDir = (bool*)SDL_realloc(ctx->isDir, newCap * sizeof(bool));
396 if (newIsDir) ctx->isDir = newIsDir;
397 if (!newNames || !newIsDir) return SDL_ENUM_FAILURE;
398 ctx->capacity = newCap;
399 }
400
401 ctx->names[ctx->count] = SDL_strdup(fname);
402 if (!ctx->names[ctx->count]) return SDL_ENUM_FAILURE;
403
404 /* Determine if entry is a directory. */
405 char fullpath[SDL_LIBRETRO_MAX_PATH];
406 SDL_snprintf(fullpath, sizeof(fullpath), "%s/%s", ctx->dirPath, fname);
407 SDL_PathInfo info;
408 ctx->isDir[ctx->count] = SDL_GetPathInfo(fullpath, &info) && info.type == SDL_PATHTYPE_DIRECTORY;
409 ctx->count++;
410 return SDL_ENUM_CONTINUE;
411}
412
416static struct retro_vfs_dir_handle* SDL_Libretro_VFS_Opendir(const char* dir, bool include_hidden) {
417 if (!dir) return NULL;
418
419 SDL_Libretro_DirCollect ctx;
420 SDL_memset(&ctx, 0, sizeof(ctx));
421 ctx.dirPath = dir;
422 ctx.includeHidden = include_hidden;
423
424 if (!SDL_EnumerateDirectory(dir, SDL_Libretro_DirCallback, &ctx)) {
425 // Free partial results on failure.
426 for (size_t i = 0; i < ctx.count; i++) SDL_free(ctx.names[i]);
427 SDL_free(ctx.names);
428 SDL_free(ctx.isDir);
429 return NULL;
430 }
431
432 struct retro_vfs_dir_handle* h = (struct retro_vfs_dir_handle*)SDL_malloc(sizeof(*h));
433 if (!h) {
434 for (size_t i = 0; i < ctx.count; i++) SDL_free(ctx.names[i]);
435 SDL_free(ctx.names);
436 SDL_free(ctx.isDir);
437 return NULL;
438 }
439 h->dir = SDL_strdup(dir);
440 if (!h->dir) {
441 for (size_t i = 0; i < ctx.count; i++) SDL_free(ctx.names[i]);
442 SDL_free(ctx.names);
443 SDL_free(ctx.isDir);
444 SDL_free(h);
445 return NULL;
446 }
447 h->names = ctx.names;
448 h->isDir = ctx.isDir;
449 h->count = ctx.count;
450 h->index = (size_t)-1; // readdir advances before returning
451 return h;
452}
453
457static bool SDL_Libretro_VFS_Readdir(struct retro_vfs_dir_handle* dirstream) {
458 if (!dirstream) return false;
459 size_t next = (dirstream->index == (size_t)-1) ? 0 : dirstream->index + 1;
460 if (next >= dirstream->count) return false;
461 dirstream->index = next;
462 return true;
463}
464
468static const char* SDL_Libretro_VFS_DirentGetName(struct retro_vfs_dir_handle* dirstream) {
469 if (!dirstream || dirstream->index >= dirstream->count) return NULL;
470 return dirstream->names[dirstream->index];
471}
472
476static bool SDL_Libretro_VFS_DirentIsDir(struct retro_vfs_dir_handle* dirstream) {
477 if (!dirstream || dirstream->index >= dirstream->count) return false;
478 return dirstream->isDir[dirstream->index];
479}
480
484static int SDL_Libretro_VFS_Closedir(struct retro_vfs_dir_handle* dirstream) {
485 if (!dirstream) return -1;
486 for (size_t i = 0; i < dirstream->count; i++) SDL_free(dirstream->names[i]);
487 SDL_free(dirstream->names);
488 SDL_free(dirstream->isDir);
489 SDL_free(dirstream->dir);
490 SDL_free(dirstream);
491 return 0;
492}
493
511void SDL_Libretro_SetVFS(SDL_Libretro* lr, void* vfs_interface) {
512 if (!lr) return;
513 struct retro_vfs_interface temp = {0};
514 struct retro_vfs_interface* vfs = (struct retro_vfs_interface*)vfs_interface;
515 if (vfs == NULL) {
516 vfs = &temp;
517 }
518
519 lr->vfs_interface.get_path = vfs->get_path != NULL ? vfs->get_path : &SDL_Libretro_VFS_GetPath;
520 lr->vfs_interface.open = vfs->open != NULL ? vfs->open : &SDL_Libretro_VFS_Open;
521 lr->vfs_interface.close = vfs->close != NULL ? vfs->close : &SDL_Libretro_VFS_Close;
522 lr->vfs_interface.size = vfs->size != NULL ? vfs->size : &SDL_Libretro_VFS_Size;
523 lr->vfs_interface.tell = vfs->tell != NULL ? vfs->tell : &SDL_Libretro_VFS_Tell;
524 lr->vfs_interface.seek = vfs->seek != NULL ? vfs->seek : &SDL_Libretro_VFS_Seek;
525 lr->vfs_interface.read = vfs->read != NULL ? vfs->read : &SDL_Libretro_VFS_Read;
526 lr->vfs_interface.write = vfs->write != NULL ? vfs->write : &SDL_Libretro_VFS_Write;
527 lr->vfs_interface.flush = vfs->flush != NULL ? vfs->flush : &SDL_Libretro_VFS_Flush;
528 lr->vfs_interface.remove = vfs->remove != NULL ? vfs->remove : &SDL_Libretro_VFS_Remove;
529 lr->vfs_interface.rename = vfs->rename != NULL ? vfs->rename : &SDL_Libretro_VFS_Rename;
530 lr->vfs_interface.truncate = vfs->truncate != NULL ? vfs->truncate : &SDL_Libretro_VFS_Truncate;
531 lr->vfs_interface.stat = vfs->stat != NULL ? vfs->stat : &SDL_Libretro_VFS_Stat;
532 lr->vfs_interface.mkdir = vfs->mkdir != NULL ? vfs->mkdir : &SDL_Libretro_VFS_Mkdir;
533 lr->vfs_interface.opendir = vfs->opendir != NULL ? vfs->opendir : &SDL_Libretro_VFS_Opendir;
534 lr->vfs_interface.readdir = vfs->readdir != NULL ? vfs->readdir : &SDL_Libretro_VFS_Readdir;
535 lr->vfs_interface.dirent_get_name = vfs->dirent_get_name != NULL ? vfs->dirent_get_name : &SDL_Libretro_VFS_DirentGetName;
536 lr->vfs_interface.dirent_is_dir = vfs->dirent_is_dir != NULL ? vfs->dirent_is_dir : &SDL_Libretro_VFS_DirentIsDir;
537 lr->vfs_interface.closedir = vfs->closedir != NULL ? vfs->closedir : &SDL_Libretro_VFS_Closedir;
538 lr->vfs_interface.stat_64 = vfs->stat_64 != NULL ? vfs->stat_64 : &SDL_Libretro_VFS_Stat64;
539}
540
541#else /* SDL_LIBRETRO_DISABLE_VFS */
542
543// Without the SDL VFS there are no defaults to fall back on: the given
544// interface is used as-is, and NULL leaves the core without a VFS.
562void SDL_Libretro_SetVFS(SDL_Libretro* lr, void* vfs_interface) {
563 if (!lr) return;
564 if (vfs_interface) {
565 lr->vfs_interface = *(struct retro_vfs_interface*)vfs_interface;
566 }
567 else {
568 SDL_zero(lr->vfs_interface);
569 }
570}
571
572#endif /* !SDL_LIBRETRO_DISABLE_VFS */
573
574#endif /* SDL_LIBRETRO_VFS_IMPL_ONCE */
void SDL_Libretro_SetVFS(SDL_Libretro *lr, void *vfs_interface)
Set the Virtual File System the core uses for file access.