rockbox/firmware/target/hosted/ctru/lib/sys_file.c
Mauricio Garrido 752467dee4 3ds: Various fixes, optimizations.
This commit does the following changes:

- Fix mkdir implementation not reporting EEXIST error.
- Fix database build feature.
- Small speed-up when parsing directories and files.
- Fix buffering thread hogging cpu and preventing other threads to run.
- Fix sdl plugins not compiling by re-adding ctru specific cflags in sdl.make.

Change-Id: I507c0dcc85cdbcc607ab9c9c6d0b42e6a80caa5a
2026-01-09 20:54:44 -05:00

772 lines
20 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 Mauricio G.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#define RB_FILESYSTEM_OS
#include "config.h"
#include "system.h"
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <utime.h>
#include "file.h"
#include "debug.h"
#include "string-extra.h"
#include "fs_defines.h"
#include "sys_file.h"
/* This file is based on firmware/common/file.c */
/* Define LOGF_ENABLE to enable logf output in this file */
// #define LOGF_ENABLE
#include "logf.h"
/**
* These functions provide a roughly POSIX-compatible file I/O API.
* Important: the bufferio I/O library (bfile) used in the 3ds does not work
* with write-only files due to an internal limitation.
* So all files will be opened with the read flag by default.
*/
/* structure used for open file descriptors */
static struct filestr_desc
{
struct filestr_base stream; /* basic stream info (first!) */
file_size_t offset; /* current offset for stream */
u64 *sizep; /* shortcut to file size in fileobj */
} open_streams[MAX_OPEN_FILES] =
{
[0 ... MAX_OPEN_FILES-1] = { .stream = { .cache = NULL, .flags = 0 } }
};
extern FS_Archive sdmcArchive;
/* check and return a struct filestr_desc* from a file descriptor number */
static struct filestr_desc * get_filestr(int fildes)
{
struct filestr_desc *file = &open_streams[fildes];
if ((unsigned int)fildes >= MAX_OPEN_FILES)
file = NULL;
else if (file->stream.cache != NULL)
return file;
logf("fildes %d: bad file number\n", fildes);
errno = (file && (file->stream.cache == NULL)) ? ENXIO : EBADF;
return NULL;
}
#define GET_FILESTR(type, fildes) \
({ \
file_internal_lock_##type(); \
struct filestr_desc * _file = get_filestr(fildes); \
if (_file) \
FILESTR_LOCK(type, &_file->stream); \
else { \
file_internal_unlock_##type(); \
}\
_file; \
})
/* release the lock on the filestr_desc* */
#define RELEASE_FILESTR(type, file) \
({ \
FILESTR_UNLOCK(type, &(file)->stream); \
file_internal_unlock_##type(); \
})
/* find a free file descriptor */
static int alloc_filestr(struct filestr_desc **filep)
{
for (int fildes = 0; fildes < MAX_OPEN_FILES; fildes++)
{
struct filestr_desc *file = &open_streams[fildes];
if (file->stream.cache == NULL)
{
*filep = file;
return fildes;
}
}
logf("Too many files open\n");
return -1;
}
/* check for file existence */
int test_stream_exists_internal(const char *path)
{
int rc;
bool is_dir = false;
Handle handle;
Result res = FSUSER_OpenFile(&handle,
sdmcArchive,
fsMakePath(PATH_ASCII, path),
FS_OPEN_READ,
0);
if (R_FAILED(res)) {
/* not a file, try to open a directory */
res = FSUSER_OpenDirectory(&handle,
sdmcArchive,
fsMakePath(PATH_ASCII, path));
if (R_FAILED(res)) {
logf("File does not exist\n");
FILE_ERROR(ERRNO, -1);
}
is_dir = true;
}
rc = 1;
file_error:
if (handle > 0) {
if (is_dir)
FSDIR_Close(handle);
else
FSFILE_Close(handle);
}
return rc;
}
/* set the file pointer */
static off_t lseek_internal(struct filestr_desc *file, off_t offset,
int whence)
{
off_t rc;
off_t pos;
off_t size = MIN(*file->sizep, FILE_SIZE_MAX);
off_t file_offset = AtomicLoad(&file->offset);
switch (whence)
{
case SEEK_SET:
if (offset < 0 || (off_t)offset > size)
FILE_ERROR(EINVAL, -1);
pos = offset;
break;
case SEEK_CUR:
if ((offset < 0 && (off_t)-offset > file_offset) ||
(offset > 0 && (off_t)offset > size - file_offset))
FILE_ERROR(EINVAL, -1);
pos = file_offset + offset;
break;
case SEEK_END:
if (offset > 0 || (off_t)-offset > size)
FILE_ERROR(EINVAL, -1);
pos = size + offset;
break;
default:
FILE_ERROR(EINVAL, -1);
}
AtomicSwap(&file->offset, pos);
return pos;
file_error:
return rc;
}
/* read from or write to the file; back end to read() and write() */
static ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte,
bool write)
{
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
/* DEBUGF("readwrite(%p,%lx,%lu,%s)\n",
file, (long)buf, (unsigned long)nbyte, write ? "write" : "read"); */
#endif
const file_size_t size = *file->sizep;
size_t filerem;
if (write)
{
/* if opened in append mode, move pointer to end */
if (file->stream.flags & O_APPEND)
AtomicSwap(&file->offset, MIN(size, FILE_SIZE_MAX));
filerem = FILE_SIZE_MAX - AtomicLoad(&file->offset);
}
else
{
/* limit to maximum possible offset (EOF or FILE_SIZE_MAX) */
filerem = MIN(size, FILE_SIZE_MAX) - AtomicLoad(&file->offset);
}
if (nbyte > filerem)
{
nbyte = filerem;
if (nbyte > 0)
{}
else if (write)
FILE_ERROR_RETURN(EFBIG, -1); /* would get too large */
else if (AtomicLoad(&file->offset) >= FILE_SIZE_MAX)
FILE_ERROR_RETURN(EOVERFLOW, -2); /* can't read here */
}
if (nbyte == 0)
return 0;
int rc = 0;
int_error_t n_err;
if (write) {
n_err = PageReader_WriteAt(file->stream.cache,
buf,
nbyte,
AtomicLoad(&file->offset),
file->stream.flags & O_RDWR ? true : false);
}
else {
n_err = PageReader_ReadAt(file->stream.cache,
buf,
nbyte,
AtomicLoad(&file->offset));
}
if ((n_err.err != NULL) && strcmp(n_err.err, "io.EOF")) {
FILE_ERROR(ERRNO, -3);
}
file_error:;
#ifdef DEBUG
if (errno == ENOSPC)
logf("No space left on device\n");
#endif
size_t done = n_err.n;
if (done)
{
/* error or not, update the file offset and size if anything was
transferred */
AtomicAdd(&file->offset, done);
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
/* DEBUGF("file offset: %lld\n", file->offset); */
#endif
/* adjust file size to length written */
if (write && AtomicLoad(&file->offset) > size)
*file->sizep = AtomicLoad(&file->offset);
return done;
}
return rc;
}
/* initialize the base descriptor */
static void filestr_base_init(struct filestr_base *stream)
{
stream->cache = NULL;
stream->handle = 0;
stream->size = 0;
LightLock_Init(&stream->mtx);
}
int open_internal_inner2(Handle *handle, const char *path, u32 openFlags, u32 attributes)
{
int rc;
Result res = FSUSER_OpenFile(handle,
sdmcArchive,
fsMakePath(PATH_ASCII, path),
openFlags,
attributes);
if (R_FAILED(res)) {
FILE_ERROR(ERRNO, -1);
}
rc = 1;
file_error:
return rc;
}
static int open_internal_inner1(const char *path, int oflag)
{
int rc;
struct filestr_desc *file;
int fildes = alloc_filestr(&file);
if (fildes < 0)
FILE_ERROR_RETURN(EMFILE, -1);
u32 openFlags = 0, attributes = 0;
/* open for reading by default */
openFlags = FS_OPEN_READ;
if (oflag & O_ACCMODE)
{
if ((oflag & O_ACCMODE) == O_RDONLY) {
attributes |= FS_ATTRIBUTE_READ_ONLY;
}
if ((oflag & O_ACCMODE) == O_WRONLY) {
openFlags |= FS_OPEN_WRITE;
}
if ((oflag & O_ACCMODE) == O_RDWR) {
openFlags |= FS_OPEN_WRITE;
}
}
else if (oflag & O_TRUNC)
{
/* O_TRUNC requires write mode */
logf("No write mode but have O_TRUNC\n");
FILE_ERROR(EINVAL, -2);
}
/* O_CREAT and O_APPEND are fine without write mode
* for the former, an empty file is created but no data may be written
* for the latter, no append will be allowed anyway */
if (!(oflag & O_CREAT))
oflag &= ~O_EXCL; /* result is undefined: we choose "ignore" */
filestr_base_init(&file->stream);
rc = open_internal_inner2(&file->stream.handle, path, openFlags, attributes);
if (rc > 0) {
if (oflag & O_EXCL)
{
logf("File exists\n");
FILE_ERROR(EEXIST, -4);
}
}
else if (oflag & O_CREAT)
{
/* not found; try to create it */
openFlags |= FS_OPEN_CREATE;
rc = open_internal_inner2(&file->stream.handle, path, openFlags, attributes);
if (rc < 0)
FILE_ERROR(ERRNO, rc * 10 - 6);
}
else
{
logf("File not found\n");
FILE_ERROR(ENOENT, -5);
}
/* truncate file if requested */
if (oflag & O_TRUNC) {
Result res = FSFILE_SetSize(file->stream.handle, 0);
if (R_FAILED(res)) {
FILE_ERROR(ERRNO, -6);
}
}
/* we need to set file size here, or else lseek
will fail if no read or write has been done */
u64 size = 0;
Result res = FSFILE_GetSize(file->stream.handle, &size);
if (R_FAILED(res)) {
FILE_ERROR(ERRNO, -8);
}
file->stream.cache = NewPageReader(file->stream.handle, defaultPageSize);
if (file->stream.cache == NULL) {
FILE_ERROR(ERRNO, -7);
}
file->stream.flags = oflag;
file->stream.size = size;
file->sizep = &file->stream.size;
AtomicSwap(&file->offset, 0);
/* we will use file path to implement ctru_fsamefile function */
strcpy(file->stream.path, path);
return fildes;
file_error:
if (fildes >= 0) {
if (file->stream.cache != NULL) {
/* FSFILE_Flush(file->stream.handle); */
PageReader_Free(file->stream.cache);
file->stream.cache = NULL;
}
FSFILE_Close(file->stream.handle);
file->stream.handle = 0;
}
return rc;
}
static int open_internal_locked(const char *path, int oflag)
{
file_internal_lock_WRITER();
int rc = open_internal_inner1(path, oflag);
file_internal_unlock_WRITER();
return rc;
}
int ctru_open(const char *path, int oflag, ...)
{
logf("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag);
return open_internal_locked(path, oflag);
}
int ctru_creat(const char *path, mode_t mode)
{
logf("creat(path=\"%s\")\n", path);
return ctru_open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
}
int ctru_close(int fildes)
{
logf("close(fd=%d)\n", fildes);
int rc;
file_internal_lock_WRITER();
/* needs to work even if marked "nonexistant" */
struct filestr_desc *file = &open_streams[fildes];
if ((unsigned int)fildes >= MAX_OPEN_FILES || (file->stream.cache == NULL))
{
logf("filedes %d not open\n", fildes);
FILE_ERROR(EBADF, -2);
}
if (file->stream.cache != NULL) {
/* FSFILE_Flush(file->stream.handle); */
PageReader_Free(file->stream.cache);
file->stream.cache = NULL;
}
FSFILE_Close(file->stream.handle);
file->stream.handle = 0;
file->stream.path[0] = '\0';
rc = 0;
file_error:
file_internal_unlock_WRITER();
return rc;
}
/* truncate a file to a specified length */
int ctru_ftruncate(int fildes, off_t length)
{
logf("ftruncate(fd=%d,len=%ld)\n", fildes, (long)length);
struct filestr_desc * const file = GET_FILESTR(READER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
int rc;
if (file->stream.flags & O_RDONLY)
{
logf("Descriptor is read-only mode\n");
FILE_ERROR(EBADF, -2);
}
if (length < 0)
{
logf("Length %ld is invalid\n", (long)length);
FILE_ERROR(EINVAL, -3);
}
Result res = FSFILE_SetSize(file->stream.handle, length);
if (R_FAILED(res)) {
FILE_ERROR(ERRNO, -11);
}
*file->sizep = length;
rc = 0;
file_error:
RELEASE_FILESTR(READER, file);
return rc;
}
/* synchronize changes to a file */
int ctru_fsync(int fildes)
{
logf("fsync(fd=%d)\n", fildes);
struct filestr_desc * const file = GET_FILESTR(WRITER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
int rc;
if (file->stream.flags & O_RDONLY)
{
logf("Descriptor is read-only mode\n");
FILE_ERROR(EINVAL, -2);
}
/* flush all pending changes to disk */
Result res = FSFILE_Flush(file->stream.handle);
if (R_FAILED(res)) {
FILE_ERROR(ERRNO, -3);
}
rc = 0;
file_error:
RELEASE_FILESTR(WRITER, file);
return rc;
}
/* move the read/write file offset */
off_t ctru_lseek(int fildes, off_t offset, int whence)
{
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
/* DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence); */
#endif
struct filestr_desc * const file = GET_FILESTR(READER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
off_t rc = lseek_internal(file, offset, whence);
if (rc < 0)
FILE_ERROR(ERRNO, rc * 10 - 2);
file_error:
RELEASE_FILESTR(READER, file);
return rc;
}
/* read from a file */
ssize_t ctru_read(int fildes, void *buf, size_t nbyte)
{
struct filestr_desc * const file = GET_FILESTR(READER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
ssize_t rc;
if (file->stream.flags & O_WRONLY)
{
logf("read(fd=%d,buf=%p,nb=%lu) - "
"descriptor is write-only mode\n",
fildes, buf, (unsigned long)nbyte);
FILE_ERROR(EBADF, -2);
}
rc = readwrite(file, buf, nbyte, false);
if (rc < 0)
FILE_ERROR(ERRNO, rc * 10 - 3);
file_error:
RELEASE_FILESTR(READER, file);
return rc;
}
/* write on a file */
ssize_t ctru_write(int fildes, const void *buf, size_t nbyte)
{
struct filestr_desc * const file = GET_FILESTR(READER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
ssize_t rc;
if (file->stream.flags & O_RDONLY)
{
logf("write(fd=%d,buf=%p,nb=%lu) - "
"descriptor is read-only mode\n",
fildes, buf, (unsigned long)nbyte);
FILE_ERROR(EBADF, -2);
}
rc = readwrite(file, (void *)buf, nbyte, true);
if (rc < 0)
FILE_ERROR(ERRNO, rc * 10 - 3);
file_error:
RELEASE_FILESTR(READER, file);
return rc;
}
/* remove a file */
int ctru_remove(const char *path)
{
logf("remove(path=\"%s\")\n", path);
int rc;
file_internal_lock_WRITER();
Result res = FSUSER_DeleteFile(sdmcArchive,
fsMakePath(PATH_ASCII, path));
if (R_FAILED(res))
FILE_ERROR(ERRNO, -1);
rc = 0;
file_error:
file_internal_unlock_WRITER();
return rc;
}
/* rename a file */
int ctru_rename(const char *old, const char *new)
{
/* note: move by rename does not work in devkitARM toolchain */
logf("rename(old=\"%s\",new=\"%s\")\n", old, new);
int rc;
/* if 'old' is a directory then 'new' is also required to be one if 'new'
is to be overwritten */
bool are_dirs = false;
file_internal_lock_WRITER();
if (!strcmp(new, old)) /* case-only is ok */
{
logf("No name change (success)\n");
rc = 0;
FILE_ERROR(ERRNO, _RC);
}
/* open 'old'; it must exist */
Handle open1rc;
Result res = FSUSER_OpenFile(&open1rc,
sdmcArchive,
fsMakePath(PATH_ASCII, old),
FS_OPEN_READ,
0);
if (R_FAILED(res)) {
/* not a file, try to open a directory */
res = FSUSER_OpenDirectory(&open1rc,
sdmcArchive,
fsMakePath(PATH_ASCII, old));
if (R_FAILED(res)) {
logf("Failed opening old\n");
FILE_ERROR(ERRNO, -1);
}
are_dirs = true;
}
if (are_dirs) {
/* rename directory */
FSUSER_RenameDirectory(sdmcArchive,
fsMakePath(PATH_ASCII, old),
sdmcArchive,
fsMakePath(PATH_ASCII, new));
}
else {
/* rename file */
FSUSER_RenameFile(sdmcArchive,
fsMakePath(PATH_ASCII, old),
sdmcArchive,
fsMakePath(PATH_ASCII, new));
}
if (R_FAILED(res)) {
logf("Rename failed\n");
FILE_ERROR(ERRNO, -2);
}
rc = 0;
file_error:
/* for now, there is nothing to fail upon closing the old stream */
if (open1rc > 0) {
if (are_dirs)
FSDIR_Close(open1rc);
else
FSFILE_Close(open1rc);
}
file_internal_unlock_WRITER();
return rc;
}
/** Extensions **/
/* todo: utime does not work in devkitARM toolchain */
int ctru_modtime(const char *path, time_t modtime)
{
struct utimbuf times =
{
.actime = modtime,
.modtime = modtime,
};
return utime(path, &times);
}
/* get the binary size of a file (in bytes) */
off_t ctru_filesize(int fildes)
{
struct filestr_desc * const file = GET_FILESTR(READER, fildes);
if (!file)
FILE_ERROR_RETURN(ERRNO, -1);
off_t rc;
file_size_t size = *file->sizep;
if (size > FILE_SIZE_MAX)
FILE_ERROR(EOVERFLOW, -2);
rc = (off_t)size;
file_error:
RELEASE_FILESTR(READER, file);
return rc;
}
/* test if two file descriptors refer to the same file */
int ctru_fsamefile(int fildes1, int fildes2)
{
struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1);
if (!file1)
FILE_ERROR_RETURN(ERRNO, -1);
int rc = -2;
struct filestr_desc * const file2 = get_filestr(fildes2);
if (file2)
rc = strcmp(file1->stream.path, file2->stream.path) == 0 ? 1 : 0;
RELEASE_FILESTR(WRITER, file1);
return rc;
}
/* tell the relationship of path1 to path2 */
int ctru_relate(const char *path1, const char *path2)
{
/* FAT32 file system does not support symbolic links,
therefore, comparing the two full paths should be enough
to tell relationship */
logf("relate(path1=\"%s\",path2=\"%s\")\n", path1, path2);
int rc = RELATE_DIFFERENT;
if (strcmp(path1, path2) == 0)
rc = RELATE_SAME;
return rc;
}
/* test file or directory existence */
bool ctru_file_exists(const char *path)
{
file_internal_lock_WRITER();
bool rc = test_stream_exists_internal(path) > 0;
file_internal_unlock_WRITER();
return rc;
}
/* note: no symbolic links support in devkitARM */
ssize_t ctru_readlink(const char *path, char *buf, size_t bufsiz)
{
return readlink(path, buf, bufsiz);
}