/* * fat_dev.c * * Loosely based on ff_dev.c from libusbhsfs. * * Copyright (c) 2020-2024, DarkMatterCore . * * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). * * nxdumptool 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 3 of the License, or * (at your option) any later version. * * nxdumptool is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include /* Helper macros. */ #define FAT_DEV_INIT_FILE_VARS DEVOPTAB_INIT_FILE_VARS(FIL) #define FAT_DEV_INIT_DIR_VARS DEVOPTAB_INIT_DIR_VARS(FDIR) #define FAT_DEV_INIT_FS_ACCESS DEVOPTAB_DECL_FS_CTX(FATFS) /* Function prototypes. */ static int fatdev_open(struct _reent *r, void *fd, const char *path, int flags, int mode); static int fatdev_close(struct _reent *r, void *fd); static ssize_t fatdev_read(struct _reent *r, void *fd, char *ptr, size_t len); static off_t fatdev_seek(struct _reent *r, void *fd, off_t pos, int dir); static int fatdev_stat(struct _reent *r, const char *file, struct stat *st); static DIR_ITER* fatdev_diropen(struct _reent *r, DIR_ITER *dirState, const char *path); static int fatdev_dirreset(struct _reent *r, DIR_ITER *dirState); static int fatdev_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat); static int fatdev_dirclose(struct _reent *r, DIR_ITER *dirState); static int fatdev_statvfs(struct _reent *r, const char *path, struct statvfs *buf); static const char *fatdev_get_fixed_path(struct _reent *r, const char *path, FATFS *fatfs); static void fatdev_fill_stat(struct stat *st, const FILINFO *info); static int fatdev_translate_error(FRESULT res); /* Global variables. */ __thread char g_fatDevicePathBuffer[FS_MAX_PATH] = {0}; static const devoptab_t fatdev_devoptab = { .name = NULL, .structSize = sizeof(FIL), .open_r = fatdev_open, .close_r = fatdev_close, .write_r = rodev_write, ///< Supported by FatFs, but disabled on purpose. .read_r = fatdev_read, .seek_r = fatdev_seek, .fstat_r = rodev_fstat, ///< Not supported by FatFs. .stat_r = fatdev_stat, .link_r = rodev_link, ///< Supported by FatFs, but disabled on purpose. .unlink_r = rodev_unlink, ///< Supported by FatFs, but disabled on purpose. .chdir_r = rodev_chdir, ///< No need to deal with cwd shenanigans, so we won't support it. .rename_r = rodev_rename, ///< Supported by FatFs, but disabled on purpose. .mkdir_r = rodev_mkdir, ///< Supported by FatFs, but disabled on purpose. .dirStateSize = sizeof(FDIR), .diropen_r = fatdev_diropen, .dirreset_r = fatdev_dirreset, .dirnext_r = fatdev_dirnext, .dirclose_r = fatdev_dirclose, .statvfs_r = fatdev_statvfs, .ftruncate_r = rodev_ftruncate, ///< Supported by FatFs, but disabled on purpose. .fsync_r = rodev_fsync, ///< Supported by FatFs, but disabled on purpose. .deviceData = NULL, .chmod_r = rodev_chmod, ///< Supported by FatFs, but disabled on purpose. .fchmod_r = rodev_fchmod, ///< Supported by FatFs, but disabled on purpose. .rmdir_r = rodev_rmdir, ///< Supported by FatFs, but disabled on purpose. .lstat_r = fatdev_stat, ///< Symlinks aren't supported, so we'll just alias lstat() to stat(). .utimes_r = rodev_utimes, ///< Supported by FatFs, but disabled on purpose. .fpathconf_r = rodev_fpathconf, ///< Not supported by FatFs. .pathconf_r = rodev_pathconf, ///< Not supported by FatFs. .symlink_r = rodev_symlink, ///< Not supported by FatFs. .readlink_r = rodev_readlink ///< Not supported by FatFs. }; const devoptab_t *fatdev_get_devoptab() { return &fatdev_devoptab; } static int fatdev_open(struct _reent *r, void *fd, const char *path, int flags, int mode) { NX_IGNORE_ARG(mode); BYTE fatdev_flags = (FA_READ | FA_OPEN_EXISTING); FRESULT res = FR_OK; FAT_DEV_INIT_FILE_VARS; FAT_DEV_INIT_FS_ACCESS; /* Validate input. */ if (!file || (flags & (O_WRONLY | O_RDWR | O_APPEND | O_CREAT | O_TRUNC | O_EXCL))) DEVOPTAB_SET_ERROR_AND_EXIT(EROFS); /* Get fixed path. */ if (!(path = fatdev_get_fixed_path(r, path, fs_ctx))) DEVOPTAB_EXIT; //LOG_MSG_DEBUG("Opening \"%s\" with flags 0x%X (volume \"%s:\").", path, fatdev_flags, dev_ctx->name); /* Reset file descriptor. */ memset(file, 0, sizeof(FIL)); /* Open file. */ res = f_open(file, path, fatdev_flags); if (res != FR_OK) DEVOPTAB_SET_ERROR(fatdev_translate_error(res)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static int fatdev_close(struct _reent *r, void *fd) { FRESULT res = FR_OK; FAT_DEV_INIT_FILE_VARS; /* Sanity check. */ if (!file) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); //LOG_MSG_DEBUG("Closing file from \"%u:\" (volume \"%s:\").", file->obj.fs->pdrv, dev_ctx->name); /* Close file. */ res = f_close(file); if (res != FR_OK) DEVOPTAB_SET_ERROR_AND_EXIT(fatdev_translate_error(res)); /* Reset file descriptor. */ memset(file, 0, sizeof(FIL)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static ssize_t fatdev_read(struct _reent *r, void *fd, char *ptr, size_t len) { UINT br = 0; FRESULT res = FR_OK; FAT_DEV_INIT_FILE_VARS; /* Sanity check. */ if (!file || !ptr || !len) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); /* Check if the file was opened with read access. */ if (!(file->flag & FA_READ)) DEVOPTAB_SET_ERROR_AND_EXIT(EBADF); //LOG_MSG_DEBUG("Reading 0x%lX byte(s) at offset 0x%lX from file in \"%u:\" (volume \"%s:\").", len, f_tell(file), ((FATFS*)dev_ctx->fs_ctx)->pdrv, dev_ctx->name); /* Read file data. */ res = f_read(file, ptr, (UINT)len, &br); if (res != FR_OK) DEVOPTAB_SET_ERROR(fatdev_translate_error(res)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT((ssize_t)br); } static off_t fatdev_seek(struct _reent *r, void *fd, off_t pos, int dir) { off_t offset = 0; FRESULT res = FR_OK; FAT_DEV_INIT_FILE_VARS; /* Sanity check. */ if (!file) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); /* Find the offset to seek from. */ switch(dir) { case SEEK_SET: /* Set absolute position relative to zero (start offset). */ break; case SEEK_CUR: /* Set position relative to the current position. */ offset = (off_t)f_tell(file); break; case SEEK_END: /* Set position relative to EOF. */ offset = (off_t)f_size(file); break; default: /* Invalid option. */ DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); } /* Don't allow negative seeks beyond the beginning of file. */ if (pos < 0 && offset < -pos) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); /* Calculate actual offset. */ offset += pos; /* Don't allow positive seeks beyond the end of file. */ if (offset > (off_t)f_size(file)) DEVOPTAB_SET_ERROR_AND_EXIT(EOVERFLOW); //LOG_MSG_DEBUG("Seeking to offset 0x%lX from file in \"%u:\" (volume \"%s:\").", offset, file->obj.fs->pdrv, dev_ctx->name); /* Perform file seek. */ res = f_lseek(file, (FSIZE_t)offset); if (res != FR_OK) DEVOPTAB_SET_ERROR(fatdev_translate_error(res)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(offset); } static int fatdev_stat(struct _reent *r, const char *file, struct stat *st) { FILINFO info = {0}; FRESULT res = FR_OK; DEVOPTAB_INIT_VARS; FAT_DEV_INIT_FS_ACCESS; /* Sanity check. */ if (!file || !st) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); /* Get fixed path. */ if (!(file = fatdev_get_fixed_path(r, file, fs_ctx))) DEVOPTAB_EXIT; //LOG_MSG_DEBUG("Getting file stats for \"%s\" (volume \"%s:\").", file, dev_ctx->name); /* Get stats. */ res = f_stat(file, &info); if (res != FR_OK) DEVOPTAB_SET_ERROR_AND_EXIT(fatdev_translate_error(res)); /* Fill stat info. */ fatdev_fill_stat(st, &info); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static DIR_ITER *fatdev_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) { FRESULT res = FR_OK; DIR_ITER *ret = NULL; FAT_DEV_INIT_DIR_VARS; FAT_DEV_INIT_FS_ACCESS; /* Get fixed path. */ if (!(path = fatdev_get_fixed_path(r, path, fs_ctx))) DEVOPTAB_EXIT; //LOG_MSG_DEBUG("Opening directory \"%s\" (volume \"%s:\").", path, dev_ctx->name); /* Reset directory state. */ memset(dir, 0, sizeof(FDIR)); /* Open directory. */ res = f_opendir(dir, path); if (res != FR_OK) DEVOPTAB_SET_ERROR_AND_EXIT(fatdev_translate_error(res)); /* Update return value. */ ret = dirState; end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_PTR(ret); } static int fatdev_dirreset(struct _reent *r, DIR_ITER *dirState) { FRESULT res = FR_OK; FAT_DEV_INIT_DIR_VARS; //LOG_MSG_DEBUG("Resetting state for directory in \"%u:\" (volume \"%s:\").", dir->obj.fs->pdrv, dev_ctx->name); /* Reset directory state. */ res = f_rewinddir(dir); if (res != FR_OK) DEVOPTAB_SET_ERROR(fatdev_translate_error(res)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static int fatdev_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { FILINFO info = {0}; FRESULT res = FR_OK; FAT_DEV_INIT_DIR_VARS; /* Sanity check. */ if (!filename || !filestat) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); //LOG_MSG_DEBUG("Getting info from next directory entry in \"%u:\" (volume \"%s:\").", dir->obj.fs->pdrv, dev_ctx->name); /* Read directory. */ res = f_readdir(dir, &info); if (res != FR_OK) DEVOPTAB_SET_ERROR_AND_EXIT(fatdev_translate_error(res)); /* Check if we haven't reached EOD. */ /* FatFs returns an empty string if so. */ if (info.fname[0]) { /* Copy filename. */ strcpy(filename, info.fname); /* Fill stat info. */ fatdev_fill_stat(filestat, &info); } else { /* ENOENT signals EOD. */ DEVOPTAB_SET_ERROR(ENOENT); } end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static int fatdev_dirclose(struct _reent *r, DIR_ITER *dirState) { FRESULT res = FR_OK; FAT_DEV_INIT_DIR_VARS; //LOG_MSG_DEBUG("Closing directory from \"%s:\" (volume \"%u:\").", dir->obj.fs->pdrv, dev_ctx->name); /* Close directory. */ res = f_closedir(dir); if (res != FR_OK) DEVOPTAB_SET_ERROR_AND_EXIT(fatdev_translate_error(res)); /* Reset directory state. */ memset(dir, 0, sizeof(FDIR)); end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static int fatdev_statvfs(struct _reent *r, const char *path, struct statvfs *buf) { NX_IGNORE_ARG(path); DEVOPTAB_INIT_VARS; FAT_DEV_INIT_FS_ACCESS; /* Sanity check. */ if (!buf) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); //LOG_MSG_DEBUG("Getting filesystem stats for \"%u:\" (volume \"%s:\").", fs_ctx->pdrv, dev_ctx->name); /* Fill filesystem stats. */ memset(buf, 0, sizeof(struct statvfs)); buf->f_bsize = FF_MIN_SS; /* Sector size. */ buf->f_frsize = FF_MIN_SS; /* Sector size. */ buf->f_blocks = ((fs_ctx->n_fatent - 2) * (DWORD)fs_ctx->csize); /* Total cluster count * cluster size in sectors. */ buf->f_bfree = 0; buf->f_bavail = 0; buf->f_files = 0; buf->f_ffree = 0; buf->f_favail = 0; buf->f_fsid = 0; buf->f_flag = ST_NOSUID; buf->f_namemax = FF_LFN_BUF; end: DEVOPTAB_DEINIT_VARS; DEVOPTAB_RETURN_INT(0); } static const char *fatdev_get_fixed_path(struct _reent *r, const char *path, FATFS *fatfs) { const u8 *p = (const u8*)path; ssize_t units = 0; u32 code = 0; size_t len = 0; char name[DEVOPTAB_MOUNT_NAME_LENGTH] = {0}; if (!r || !path || !*path || !fatfs) DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); //LOG_MSG_DEBUG("Input path: \"%s\".", path); /* Generate FatFs mount name ID. */ snprintf(name, sizeof(name), "%u:", fatfs->pdrv); /* Move the path pointer to the start of the actual path. */ do { units = decode_utf8(&code, p); if (units < 0) DEVOPTAB_SET_ERROR_AND_EXIT(EILSEQ); p += units; } while(code >= ' ' && code != ':'); /* We found a colon; p points to the actual path. */ if (code == ':') path = (const char*)p; /* Make sure the provided path starts with a slash. */ if (path[0] != '/') DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); /* Make sure there are no more colons and that the remainder of the string is valid UTF-8. */ p = (const u8*)path; do { units = decode_utf8(&code, p); if (units < 0) DEVOPTAB_SET_ERROR_AND_EXIT(EILSEQ); if (code == ':') DEVOPTAB_SET_ERROR_AND_EXIT(EINVAL); p += units; } while(code >= ' '); /* Verify fixed path length. */ len = (strlen(name) + strlen(path)); if (len >= sizeof(g_fatDevicePathBuffer)) DEVOPTAB_SET_ERROR_AND_EXIT(ENAMETOOLONG); /* Generate fixed path. */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-truncation" snprintf(g_fatDevicePathBuffer, sizeof(g_fatDevicePathBuffer), "%s%s", name, path); #pragma GCC diagnostic pop //LOG_MSG_DEBUG("Fixed path: \"%s\".", g_fatDevicePathBuffer); end: DEVOPTAB_RETURN_PTR(g_fatDevicePathBuffer); } static void fatdev_fill_stat(struct stat *st, const FILINFO *info) { struct tm timeinfo = {0}; /* Clear stat struct. */ memset(st, 0, sizeof(struct stat)); /* Fill stat struct. */ st->st_nlink = 1; if (info->fattrib & AM_DIR) { /* We're dealing with a directory entry. */ st->st_mode = (S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH); } else { /* We're dealing with a file entry. */ st->st_size = (off_t)info->fsize; st->st_mode = (S_IFREG | S_IRUSR | S_IRGRP | S_IROTH); } /* Convert date/time into an actual UTC POSIX timestamp using the system local time. */ timeinfo.tm_year = (((info->fdate >> 9) & 0x7F) + 80); /* DOS time: offset since 1980. POSIX time: offset since 1900. */ timeinfo.tm_mon = (((info->fdate >> 5) & 0xF) - 1); /* DOS time: 1-12 range (inclusive). POSIX time: 0-11 range (inclusive). */ timeinfo.tm_mday = (info->fdate & 0x1F); timeinfo.tm_hour = ((info->ftime >> 11) & 0x1F); timeinfo.tm_min = ((info->ftime >> 5) & 0x3F); timeinfo.tm_sec = ((info->ftime & 0x1F) << 1); /* DOS time: 2-second intervals with a 0-29 range (inclusive, 58 seconds max). POSIX time: 0-59 range (inclusive). */ st->st_atime = 0; /* Not returned by FatFs + only available under exFAT. */ st->st_mtime = mktime(&timeinfo); st->st_ctime = 0; /* Not returned by FatFs + only available under exFAT. */ //LOG_MSG_DEBUG("DOS timestamp: 0x%04X%04X. Generated POSIX timestamp: %lu.", info->fdate, info->ftime, st->st_mtime); } static int fatdev_translate_error(FRESULT res) { int ret; switch(res) { case FR_OK: ret = 0; break; case FR_DISK_ERR: case FR_NOT_READY: ret = EIO; break; case FR_INT_ERR: case FR_INVALID_NAME: case FR_INVALID_PARAMETER: ret = EINVAL; break; case FR_NO_FILE: case FR_NO_PATH: ret = ENOENT; break; case FR_DENIED: ret = EACCES; break; case FR_EXIST: ret = EEXIST; break; case FR_INVALID_OBJECT: ret = EFAULT; break; case FR_WRITE_PROTECTED: ret = EROFS; break; case FR_INVALID_DRIVE: ret = ENODEV; break; case FR_NOT_ENABLED: ret = ENOEXEC; break; case FR_NO_FILESYSTEM: ret = ENFILE; break; case FR_TIMEOUT: ret = EAGAIN; break; case FR_LOCKED: ret = EBUSY; break; case FR_NOT_ENOUGH_CORE: ret = ENOMEM; break; case FR_TOO_MANY_OPEN_FILES: ret = EMFILE; break; default: ret = EPERM; break; } //LOG_MSG_DEBUG("FRESULT: %u. Translated errno: %d.", res, ret); return ret; }