/*
 * ext_dev.c
 *
 * Copyright (c) 2020-2021, DarkMatterCore <pabloacurielz@gmail.com>.
 *
 * This file is part of libusbhsfs (https://github.com/DarkMatterCore/libusbhsfs).
 *
 * Loosely based on fs_dev.c from libnx, et al.
 */

#include <sys/param.h>
#include <fcntl.h>

#include "../usbhsfs_manager.h"
#include "../usbhsfs_mount.h"

/* Helper macros. */

#define ext_end                     goto end
#define ext_ended_with_error        (_errno != 0)
#define ext_set_error(x)            r->_errno = _errno = (x)
#define ext_set_error_and_exit(x)   \
do { \
    ext_set_error((x)); \
    ext_end; \
} while(0)

#define ext_declare_error_state     int _errno = 0
#define ext_declare_file_state      ext4_file *file = (ext4_file*)fd
#define ext_declare_dir_state       ext4_dir *dir = (ext4_dir*)dirState->dirStruct
#define ext_declare_fs_ctx          UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx = (UsbHsFsDriveLogicalUnitFileSystemContext*)r->deviceData
#define ext_declare_lun_ctx         UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)fs_ctx->lun_ctx
#define ext_declare_drive_ctx       UsbHsFsDriveContext *drive_ctx = (UsbHsFsDriveContext*)lun_ctx->drive_ctx
#define ext_declare_vol_state       ext_vd *vd = fs_ctx->ext

#define ext_lock_drive_ctx          ext_declare_fs_ctx; \
                                    ext_declare_lun_ctx; \
                                    ext_declare_drive_ctx; \
                                    bool drive_ctx_valid = usbHsFsManagerIsDriveContextPointerValid(drive_ctx); \
                                    if (!drive_ctx_valid) ext_set_error_and_exit(ENODEV)

#define ext_unlock_drive_ctx        if (drive_ctx_valid) mutexUnlock(&(drive_ctx->mutex))

#define ext_return(x)               return (ext_ended_with_error ? -1 : (x))
#define ext_return_ptr(x)           return (ext_ended_with_error ? NULL : (x))
#define ext_return_bool             return (ext_ended_with_error ? false : true)

/* Function prototypes. */

static int       extdev_open(struct _reent *r, void *fd, const char *path, int flags, int mode);
static int       extdev_close(struct _reent *r, void *fd);
static ssize_t   extdev_write(struct _reent *r, void *fd, const char *ptr, size_t len);
static ssize_t   extdev_read(struct _reent *r, void *fd, char *ptr, size_t len);
static off_t     extdev_seek(struct _reent *r, void *fd, off_t pos, int dir);
static int       extdev_fstat(struct _reent *r, void *fd, struct stat *st);
static int       extdev_stat(struct _reent *r, const char *file, struct stat *st);
static int       extdev_link(struct _reent *r, const char *existing, const char *newLink);
static int       extdev_unlink(struct _reent *r, const char *name);
static int       extdev_chdir(struct _reent *r, const char *name);
static int       extdev_rename(struct _reent *r, const char *oldName, const char *newName);
static int       extdev_mkdir(struct _reent *r, const char *path, int mode);
static DIR_ITER* extdev_diropen(struct _reent *r, DIR_ITER *dirState, const char *path);
static int       extdev_dirreset(struct _reent *r, DIR_ITER *dirState);
static int       extdev_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat);
static int       extdev_dirclose(struct _reent *r, DIR_ITER *dirState);
static int       extdev_statvfs(struct _reent *r, const char *path, struct statvfs *buf);
static int       extdev_ftruncate(struct _reent *r, void *fd, off_t len);
static int       extdev_fsync(struct _reent *r, void *fd);
static int       extdev_chmod(struct _reent *r, const char *path, mode_t mode);
static int       extdev_fchmod(struct _reent *r, void *fd, mode_t mode);
static int       extdev_rmdir(struct _reent *r, const char *name);
static int       extdev_utimes(struct _reent *r, const char *filename, const struct timeval times[2]);

static bool extdev_fixpath(struct _reent *r, const char *path, UsbHsFsDriveLogicalUnitFileSystemContext **fs_ctx, char *outpath);

static void extdev_fill_stat(const struct ext4_inode *inode, u32 st_dev, u32 st_ino, u32 st_blksize, struct stat *st);

static int ext_trans_start(struct ext4_fs *ext_fs);
static int ext_trans_stop(struct ext4_fs *ext_fs);
static void ext_trans_abort(struct ext4_fs *ext_fs);

/* Global variables. */

static const devoptab_t extdev_devoptab = {
    .name         = NULL,
    .structSize   = sizeof(ext4_file),
    .open_r       = extdev_open,
    .close_r      = extdev_close,
    .write_r      = extdev_write,
    .read_r       = extdev_read,
    .seek_r       = extdev_seek,
    .fstat_r      = extdev_fstat,
    .stat_r       = extdev_stat,
    .link_r       = extdev_link,
    .unlink_r     = extdev_unlink,
    .chdir_r      = extdev_chdir,
    .rename_r     = extdev_rename,
    .mkdir_r      = extdev_mkdir,
    .dirStateSize = sizeof(ext4_dir),
    .diropen_r    = extdev_diropen,
    .dirreset_r   = extdev_dirreset,
    .dirnext_r    = extdev_dirnext,
    .dirclose_r   = extdev_dirclose,
    .statvfs_r    = extdev_statvfs,
    .ftruncate_r  = extdev_ftruncate,
    .fsync_r      = extdev_fsync,       ///< Not supported by lwext4.
    .deviceData   = NULL,
    .chmod_r      = extdev_chmod,
    .fchmod_r     = extdev_fchmod,
    .rmdir_r      = extdev_rmdir,
    .lstat_r      = extdev_stat,        ///< We'll just alias lstat() to stat().
    .utimes_r     = extdev_utimes
};

const devoptab_t *extdev_get_devoptab()
{
    return &extdev_devoptab;
}

static int extdev_open(struct _reent *r, void *fd, const char *path, int flags, int mode)
{
    (void)mode;
    
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file) ext_set_error_and_exit(EINVAL);
    
    /* Fix input path. */
    if (!extdev_fixpath(r, path, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Opening file \"%s\" (\"%s\") with flags 0x%X.", path, __usbhsfs_dev_path_buf, flags);
    
    /* Reset file descriptor. */
    memset(file, 0, sizeof(ext4_file));
    
    /* Open file. */
    ret = ext4_fopen2(file, __usbhsfs_dev_path_buf, flags);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_close(struct _reent *r, void *fd)
{
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file) ext_set_error_and_exit(EINVAL);
    
    USBHSFS_LOG_MSG("Closing file %u.", file->inode);
    
    /* Close file. */
    ret = ext4_fclose(file);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Reset file descriptor. */
    memset(file, 0, sizeof(ext4_file));
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static ssize_t extdev_write(struct _reent *r, void *fd, const char *ptr, size_t len)
{
    size_t bw = 0;
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file || !ptr || !len) ext_set_error_and_exit(EINVAL);
    
    /* Check if the append flag is enabled. */
    if ((file->flags & O_APPEND) && ext4_ftell(file) != ext4_fsize(file))
    {
        /* Seek to EOF. */
        ret = ext4_fseek(file, 0, SEEK_END);
        if (ret) ext_set_error_and_exit(ret);
    }
    
    USBHSFS_LOG_MSG("Writing 0x%lX byte(s) to file %u at offset 0x%lX.", len, file->inode, ext4_ftell(file));
    
    /* Write file data. */
    ret = ext4_fwrite(file, ptr, len, &bw);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return((ssize_t)bw);
}

static ssize_t extdev_read(struct _reent *r, void *fd, char *ptr, size_t len)
{
    size_t br = 0;
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file || !ptr || !len) ext_set_error_and_exit(EINVAL);
    
    USBHSFS_LOG_MSG("Reading 0x%lX byte(s) from file %u at offset 0x%lX.", len, file->inode, ext4_ftell(file));
    
    /* Read file data. */
    ret = ext4_fread(file, ptr, len, &br);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return((ssize_t)br);
}

static off_t extdev_seek(struct _reent *r, void *fd, off_t pos, int dir)
{
    off_t offset = 0;
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file) ext_set_error_and_exit(EINVAL);
    
    USBHSFS_LOG_MSG("Seeking 0x%lX byte(s) from current position in file %u.", pos, file->inode);
    
    /* Perform file seek. */
    ret = ext4_fseek(file, (s64)pos, (u32)dir);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Update current offset. */
    offset = (off_t)ext4_ftell(file);
    
end:
    ext_unlock_drive_ctx;
    ext_return(offset);
}

static int extdev_fstat(struct _reent *r, void *fd, struct stat *st)
{
    struct ext4_inode_ref inode_ref = {0};
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    ext_declare_vol_state;
    
    /* Sanity check. */
    if (!file || !st) ext_set_error_and_exit(EINVAL);
    
    /* Get inode reference. */
    ret = ext4_fs_get_inode_ref(vd->bdev->fs, file->inode, &inode_ref);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Fill stat info. */
    extdev_fill_stat(inode_ref.inode, fs_ctx->device_id, file->inode, vd->bdev->lg_bsize, st);
    
    /* Put back inode reference. */
    ret = ext4_fs_put_inode_ref(&inode_ref);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_stat(struct _reent *r, const char *file, struct stat *st)
{
    u32 inode_num = 0;
    struct ext4_inode inode = {0};
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    ext_declare_vol_state;
    
    /* Sanity check. */
    if (!st) ext_set_error_and_exit(EINVAL);
    
    /* Fix input path. */
    if (!extdev_fixpath(r, file, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Getting stats for \"%s\" (\"%s\").", file, __usbhsfs_dev_path_buf);
    
    /* Get inode. */
    ret = ext4_raw_inode_fill(__usbhsfs_dev_path_buf, &inode_num, &inode);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Fill stat info. */
    extdev_fill_stat(&inode, fs_ctx->device_id, inode_num, vd->bdev->lg_bsize, st);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_link(struct _reent *r, const char *existing, const char *newLink)
{
    char existing_path[MAX_PATH_LENGTH] = {0};
    char *new_path = __usbhsfs_dev_path_buf;
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input paths. */
    if (!extdev_fixpath(r, existing, &fs_ctx, existing_path) || !extdev_fixpath(r, newLink, &fs_ctx, new_path)) ext_end;
    
    USBHSFS_LOG_MSG("Linking \"%s\" (\"%s\") to \"%s\" (\"%s\").", existing, existing_path, newLink, new_path);
    
    /* Create hard link. */
    ret = ext4_flink(existing_path, new_path);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_unlink(struct _reent *r, const char *name)
{
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, name, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Deleting \"%s\" (\"%s\").", name, __usbhsfs_dev_path_buf);
    
    /* Delete file. */
    ret = ext4_fremove(__usbhsfs_dev_path_buf);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_chdir(struct _reent *r, const char *name)
{
    ext4_dir dir = {0};
    size_t cwd_len = 0;
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, name, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Changing current directory to \"%s\" (\"%s\").", name, __usbhsfs_dev_path_buf);
    
    /* Open directory. */
    ret = ext4_dir_open(&dir, __usbhsfs_dev_path_buf);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Close directory. */
    ext4_dir_close(&dir);
    
    /* Update current working directory. */
    sprintf(fs_ctx->cwd, "%s", strchr(__usbhsfs_dev_path_buf + 1, '/'));
    
    cwd_len = strlen(fs_ctx->cwd);
    if (fs_ctx->cwd[cwd_len - 1] != '/')
    {
        fs_ctx->cwd[cwd_len] = '/';
        fs_ctx->cwd[cwd_len + 1] = '\0';
    }
    
    /* Set default devoptab device. */
    usbHsFsMountSetDefaultDevoptabDevice(fs_ctx);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_rename(struct _reent *r, const char *oldName, const char *newName)
{
    char old_path[MAX_PATH_LENGTH] = {0};
    char *new_path = __usbhsfs_dev_path_buf;
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input paths. */
    if (!extdev_fixpath(r, oldName, &fs_ctx, old_path) || !extdev_fixpath(r, newName, &fs_ctx, new_path)) ext_end;
    
    USBHSFS_LOG_MSG("Renaming \"%s\" (\"%s\") to \"%s\" (\"%s\").", oldName, old_path, newName, new_path);
    
    /* Rename entry. */
    ret = ext4_frename(old_path, new_path);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_mkdir(struct _reent *r, const char *path, int mode)
{
    (void)mode;
    
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, path, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Creating directory \"%s\" (\"%s\").", path, __usbhsfs_dev_path_buf);
    
    /* Create directory. */
    ret = ext4_dir_mk(__usbhsfs_dev_path_buf);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static DIR_ITER *extdev_diropen(struct _reent *r, DIR_ITER *dirState, const char *path)
{
    int res = -1;
    DIR_ITER *ret = NULL;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!dirState) ext_set_error_and_exit(EINVAL);
    
    ext_declare_dir_state;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, path, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Opening directory \"%s\" (\"%s\").", path, __usbhsfs_dev_path_buf);
    
    /* Reset directory state. */
    memset(dir, 0, sizeof(ext4_dir));
    
    /* Open directory. */
    res = ext4_dir_open(dir, __usbhsfs_dev_path_buf);
    if (res) ext_set_error_and_exit(res);
    
    /* Update return value. */
    ret = dirState;
    
end:
    ext_unlock_drive_ctx;
    ext_return_ptr(ret);
}

static int extdev_dirreset(struct _reent *r, DIR_ITER *dirState)
{
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!dirState) ext_set_error_and_exit(EINVAL);
    
    ext_declare_dir_state;
    
    USBHSFS_LOG_MSG("Resetting state from directory %u.", dir->f.inode);
    
    /* Reset directory state. */
    ext4_dir_entry_rewind(dir);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat)
{
    const ext4_direntry *entry = NULL;
    struct ext4_inode_ref inode_ref = {0};
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    ext_declare_vol_state;
    
    /* Sanity check. */
    if (!dirState || !filename || !filestat) ext_set_error_and_exit(EINVAL);
    
    ext_declare_dir_state;
    
    USBHSFS_LOG_MSG("Getting info from next entry in directory %u.", dir->f.inode);
    
    while(true)
    {
        /* Read directory. */
        entry = ext4_dir_entry_next(dir);
        if (!entry) break;
        
        /* Filter entry types. */
        if (entry->inode_type != EXT4_DE_REG_FILE && entry->inode_type != EXT4_DE_DIR && entry->inode_type != EXT4_DE_SYMLINK) continue;
        
        /* Filter dot directory entries. */
        if (entry->inode_type == EXT4_DE_DIR && (!strcmp((char*)entry->name, ".") || !strcmp((char*)entry->name, ".."))) continue;
        
        /* Jackpot. */
        break;
    }
    
    if (!entry) ext_set_error_and_exit(ENOENT); /* ENOENT signals EOD. */
    
    /* Get inode reference. */
    ret = ext4_fs_get_inode_ref(vd->bdev->fs, entry->inode, &inode_ref);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Fill stat info. */
    extdev_fill_stat(inode_ref.inode, fs_ctx->device_id, entry->inode, vd->bdev->lg_bsize, filestat);
    
    /* Put back inode reference. */
    ret = ext4_fs_put_inode_ref(&inode_ref);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Copy filename. */
    strcpy(filename, (char*)entry->name);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_dirclose(struct _reent *r, DIR_ITER *dirState)
{
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!dirState) ext_set_error_and_exit(EINVAL);
    
    ext_declare_dir_state;
    
    USBHSFS_LOG_MSG("Closing directory %u.", dir->f.inode);
    
    /* Close directory. */
    ret = ext4_dir_close(dir);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Reset directory state. */
    memset(dir, 0, sizeof(ext4_dir));
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_statvfs(struct _reent *r, const char *path, struct statvfs *buf)
{
    (void)path;
    
    char mount_point[CONFIG_EXT4_MAX_MP_NAME + 3] = {0};
    struct ext4_mount_stats mount_stats = {0};
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    ext_declare_vol_state;
    
    /* Sanity check. */
    if (!buf) ext_set_error_and_exit(EINVAL);
    
    /* Generate lwext4 mount point. */
    sprintf(mount_point, "/%s/", vd->dev_name);
    
    USBHSFS_LOG_MSG("Getting filesystem stats for \"%s\" (\"%s\").", path, mount_point);
    
    /* Get volume information. */
    ret = ext4_mount_point_stats(mount_point, &mount_stats);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Fill filesystem stats. */
    memset(buf, 0, sizeof(struct statvfs));
    
    buf->f_bsize = mount_stats.block_size;
    buf->f_frsize = mount_stats.block_size;
    buf->f_blocks = mount_stats.blocks_count;
    buf->f_bfree = mount_stats.free_blocks_count;
    buf->f_bavail = mount_stats.free_blocks_count;
    buf->f_files = mount_stats.inodes_count;
    buf->f_ffree = mount_stats.free_inodes_count;
    buf->f_favail = mount_stats.free_inodes_count;
    buf->f_fsid = fs_ctx->device_id;
    buf->f_flag = ST_NOSUID;
    buf->f_namemax = EXT4_DIRECTORY_FILENAME_LEN;
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_ftruncate(struct _reent *r, void *fd, off_t len)
{
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    
    /* Sanity check. */
    if (!file || len < 0) ext_set_error_and_exit(EINVAL);
    
    USBHSFS_LOG_MSG("Truncating file %u to 0x%lX bytes.", file->inode, len);
    
    /* Truncate file. */
    ret = ext4_ftruncate(file, (u64)len);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_fsync(struct _reent *r, void *fd)
{
    (void)fd;
    
    /* Not supported by lwext4. */
    r->_errno = ENOSYS;
    return -1;
}

static int extdev_chmod(struct _reent *r, const char *path, mode_t mode)
{
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, path, &fs_ctx, NULL)) ext_end;
    
    USBHSFS_LOG_MSG("Changing permissions for \"%s\" (\"%s\") to %o.", path, __usbhsfs_dev_path_buf, mode);
    
    /* Change permissions. */
    ret = ext4_mode_set(__usbhsfs_dev_path_buf, (u32)mode);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_fchmod(struct _reent *r, void *fd, mode_t mode)
{
    struct ext4_fs *ext_fs = NULL;
    struct ext4_sblock *sblock = NULL;
    struct ext4_inode_ref inode_ref = {0};
    u32 orig_mode = 0;
    int ret = -1;
    
    ext_declare_error_state;
    ext_declare_file_state;
    ext_lock_drive_ctx;
    ext_declare_vol_state;
    
    ext_fs = vd->bdev->fs;
    sblock = &(vd->bdev->fs->sb);
    
    /* Sanity check. */
    if (!file) ext_set_error_and_exit(EINVAL);
    
    /* Start journal transfer. */
    ret = ext_trans_start(ext_fs);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Get inode reference. */
    ret = ext4_fs_get_inode_ref(ext_fs, file->inode, &inode_ref);
    if (ret)
    {
        ext_trans_abort(ext_fs);
        ext_set_error_and_exit(ret);
    }
    
    USBHSFS_LOG_MSG("Changing permissions for file %u to %o.", file->inode, mode);
    
    /* Change permissions. */
	orig_mode = ext4_inode_get_mode(sblock, inode_ref.inode);
	orig_mode &= ~0xFFF;
	orig_mode |= ((u32)mode & 0xFFF);
	ext4_inode_set_mode(sblock, inode_ref.inode, orig_mode);
    inode_ref.dirty = true;
    
    /* Put back inode reference. */
    ret = ext4_fs_put_inode_ref(&inode_ref);
    if (ret)
    {
        ext_trans_abort(ext_fs);
        ext_set_error_and_exit(ret);
    }
    
    /* Stop journal transfer. */
    ret = ext_trans_stop(ext_fs);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static int extdev_rmdir(struct _reent *r, const char *name)
{
    /* Exactly the same as extdev_unlink(). */
    return extdev_unlink(r, name);
}

static int extdev_utimes(struct _reent *r, const char *filename, const struct timeval times[2])
{
    struct timespec ts_times[2] = {0};
    int ret = -1;
    
    ext_declare_error_state;
    ext_lock_drive_ctx;
    
    /* Fix input path. */
    if (!extdev_fixpath(r, filename, &fs_ctx, NULL)) ext_end;
    
    /* Check if we should use the current time. */
    if (!times)
    {
        /* Get current time. */
        clock_gettime(CLOCK_REALTIME, &(ts_times[0]));
        memcpy(&(ts_times[1]), &(ts_times[0]), sizeof(struct timespec));
    } else {
        /* Convert provided timeval values to timespec values. */
        TIMEVAL_TO_TIMESPEC(&(times[0]), &(ts_times[0]));
        TIMEVAL_TO_TIMESPEC(&(times[1]), &(ts_times[1]));
    }
    
    USBHSFS_LOG_MSG("Setting last access and modification times for \"%s\" (\"%s\") to 0x%lX and 0x%lX, respectively.", filename, __usbhsfs_dev_path_buf, ts_times[0].tv_sec, ts_times[1].tv_sec);
    
    /* Set access time. */
    ret = ext4_atime_set(__usbhsfs_dev_path_buf, (u32)ts_times[0].tv_sec);
    if (ret) ext_set_error_and_exit(ret);
    
    /* Set modification time. */
    ret = ext4_mtime_set(__usbhsfs_dev_path_buf, (u32)ts_times[1].tv_sec);
    if (ret) ext_set_error(ret);
    
end:
    ext_unlock_drive_ctx;
    ext_return(0);
}

static bool extdev_fixpath(struct _reent *r, const char *path, UsbHsFsDriveLogicalUnitFileSystemContext **fs_ctx, char *outpath)
{
    ext_vd *vd = NULL;
    const u8 *p = (const u8*)path;
    ssize_t units = 0;
    u32 code = 0;
    size_t len = 0;
    char mount_point[CONFIG_EXT4_MAX_MP_NAME + 3] = {0}, *outptr = (outpath ? outpath : __usbhsfs_dev_path_buf), *cwd = NULL;
    
    ext_declare_error_state;
    
    if (!r || !path || !*path || !fs_ctx || !*fs_ctx || !(vd = (*fs_ctx)->ext) || !(cwd = (*fs_ctx)->cwd)) ext_set_error_and_exit(EINVAL);
    
    USBHSFS_LOG_MSG("Input path: \"%s\".", path);
    
    /* Generate lwext4 mount point. */
    sprintf(mount_point, "/%s", vd->dev_name);
    
    /* Move the path pointer to the start of the actual path. */
    do {
        units = decode_utf8(&code, p);
        if (units < 0) ext_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 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) ext_set_error_and_exit(EILSEQ);
        if (code == ':') ext_set_error_and_exit(EINVAL);
        p += units;
    } while(code >= ' ');
    
    /* Verify fixed path length. */
    len = (strlen(mount_point) + strlen(path));
    if (path[0] != '/') len += strlen(cwd);
    
    if (len >= MAX_PATH_LENGTH) ext_set_error_and_exit(ENAMETOOLONG);
    
    /* Generate fixed path. */
    if (path[0] == '/')
    {
        sprintf(outptr, "%s%s", mount_point, path);
    } else {
        sprintf(outptr, "%s%s%s", mount_point, cwd, path);
    }
    
    USBHSFS_LOG_MSG("Fixed path: \"%s\".", outptr);
    
end:
    ext_return_bool;
}

static void extdev_fill_stat(const struct ext4_inode *inode, u32 st_dev, u32 st_ino, u32 st_blksize, struct stat *st)
{
    /* Clear stat struct. */
    memset(st, 0, sizeof(struct stat));
    
    /* Fill stat struct. */
    st->st_dev = st_dev;
    st->st_ino = st_ino;
    st->st_mode = inode->mode;
    st->st_nlink = inode->links_count;
    st->st_uid = inode->uid;
    st->st_gid = inode->gid;
    if (inode->mode & (EXT4_INODE_MODE_FILE | EXT4_INODE_MODE_SOFTLINK)) st->st_size = ((((u64)inode->size_hi << 0x20) & 0xFFFFFFFF00000000ULL) | (u64)inode->size_lo);
    st->st_blksize = st_blksize;
    st->st_blocks = inode->blocks_count_lo;
    
    st->st_atim.tv_sec = inode->access_time;
    st->st_atim.tv_nsec = inode->atime_extra;
    
    st->st_mtim.tv_sec = inode->modification_time;
    st->st_mtim.tv_nsec = inode->mtime_extra;
    
    st->st_ctim.tv_sec = inode->crtime;
    st->st_ctim.tv_nsec = inode->crtime_extra;
}

static int ext_trans_start(struct ext4_fs *ext_fs)
{
    struct jbd_journal *journal = NULL;
    struct jbd_trans *trans = NULL;
    int ret = 0;
    
    if (ext_fs->jbd_journal && !ext_fs->curr_trans)
    {
        journal = ext_fs->jbd_journal;
        
        trans = jbd_journal_new_trans(journal);
        if (!trans)
        {
            ret = ENOMEM;
            goto end;
        }
        
        ext_fs->curr_trans = trans;
    }
    
end:
    return ret;
}

static int ext_trans_stop(struct ext4_fs *ext_fs)
{
    struct jbd_journal *journal = NULL;
    struct jbd_trans *trans = NULL;
    int ret = 0;
    
    if (ext_fs->jbd_journal && ext_fs->curr_trans)
    {
        journal = ext_fs->jbd_journal;
        trans = ext_fs->curr_trans;
        ret = jbd_journal_commit_trans(journal, trans);
        ext_fs->curr_trans = NULL;
    }
    
    return ret;
}

static void ext_trans_abort(struct ext4_fs *ext_fs)
{
    struct jbd_journal *journal = NULL;
    struct jbd_trans *trans = NULL;
    
    if (ext_fs->jbd_journal && ext_fs->curr_trans)
    {
        journal = ext_fs->jbd_journal;
        trans = ext_fs->curr_trans;
        jbd_journal_free_trans(journal, trans, true);
        ext_fs->curr_trans = NULL;
    }
}
