/*
 * usbhsfs_mount.c
 *
 * Copyright (c) 2020-2021, DarkMatterCore <pabloacurielz@gmail.com>.
 * Copyright (c) 2020-2021, XorTroll.
 * Copyright (c) 2020-2021, Rhys Koedijk.
 *
 * This file is part of libusbhsfs (https://github.com/DarkMatterCore/libusbhsfs).
 */

#include "usbhsfs_utils.h"
#include "usbhsfs_manager.h"
#include "usbhsfs_mount.h"
#include "usbhsfs_scsi.h"
#include "fatfs/ff_dev.h"

#ifdef GPL_BUILD
#include "ntfs-3g/ntfs_dev.h"
#include "lwext4/ext_dev.h"
#endif

#define MOUNT_NAME_PREFIX      "ums"

#define BOOT_SIGNATURE          0xAA55

#define MBR_PARTITION_COUNT     4

#define DEVOPTAB_INVALID_ID     UINT32_MAX

#ifdef DEBUG
#define FS_TYPE_STR(x)          ((x) == UsbHsFsDriveLogicalUnitFileSystemType_FAT ? "FAT" : ((x) == UsbHsFsDriveLogicalUnitFileSystemType_NTFS ? "NTFS" : "EXT"))
#endif

/* Type definitions. */

/// DOS 2.0 BIOS Parameter Block. Used for FAT12 (13 bytes).
#pragma pack(push, 1)
typedef struct {
    u16 sector_size;        ///< Logical sector size in bytes. Belongs to the DOS 2.0 BPB area.
    u8 sectors_per_cluster; ///< Logical sectors per cluster. Belongs to the DOS 2.0 BPB area.
    u16 reserved_sectors;   ///< Reserved sectors. Belongs to the DOS 2.0 BPB area.
    u8 num_fats;            ///< Number of FATs. Belongs to the DOS 2.0 BPB area.
    u16 root_dir_entries;   ///< Root directory entries. Belongs to the DOS 2.0 BPB area.
    u16 total_sectors;      ///< Total logical sectors. Belongs to the DOS 2.0 BPB area.
    u8 media_desc;          ///< Media descriptor. Belongs to the DOS 2.0 BPB area.
    u16 sectors_per_fat;    ///< Logical sectors per FAT. Belongs to the DOS 2.0 BPB area.
} DOS_2_0_BPB;
#pragma pack(pop)

/// DOS 3.31 BIOS Parameter Block. Used for FAT12, FAT16 and FAT16B (25 bytes).
#pragma pack(push, 1)
typedef struct {
    DOS_2_0_BPB dos_2_0_bpb;    ///< DOS 2.0 BIOS Parameter Block.
    u16 sectors_per_track;      ///< Physical sectors per track.
    u16 num_heads;              ///< Number of heads.
    u32 hidden_sectors;         ///< Hidden sectors.
    u32 total_sectors;          ///< Large total logical sectors.
} DOS_3_31_BPB;
#pragma pack(pop)

/// DOS 7.1 Extended BIOS Parameter Block (full variant). Used for FAT32 (79 bytes).
#pragma pack(push, 1)
typedef struct {
    DOS_3_31_BPB dos_3_31_bpb;  ///< DOS 3.31 BIOS Parameter Block.
    u32 sectors_per_fat;        ///< Logical sectors per FAT.
    u16 mirroring_flags;        ///< Mirroring flags.
    u16 version;                ///< Version.
    u32 root_dir_cluster;       ///< Root directory cluster.
    u16 fsinfo_sector;          ///< Location of FS Information Sector.
    u16 backup_sector;          ///< Location of Backup Sector.
    u8 boot_filename[0xC];      ///< Boot filename.
    u8 pdrv;                    ///< Physical drive number.
    u8 flags;                   ///< Flags.
    u8 ext_boot_sig;            ///< Extended boot signature (0x29).
    u32 vol_serial_num;         ///< Volume serial number.
    u8 vol_label[0xB];          ///< Volume label.
    u8 fs_type[0x8];            ///< Filesystem type. Padded with spaces (0x20). Set to "FAT32   " if this is an FAT32 VBR.
} DOS_7_1_EBPB;
#pragma pack(pop)

/// Volume Boot Record (VBR). Represents the first sector from every FAT and NTFS filesystem. If a drive is formatted using Super Floppy Drive (SFD) configuration, this is located at LBA 0.
typedef struct {
    u8 jmp_boot[0x3];           ///< Jump boot code. First byte must match 0xEB (short jump), 0xE9 (near jump) or 0xE8 (near call). Set to "\xEB\x76\x90" is this is an exFAT VBR.
    char oem_name[0x8];         ///< OEM name. Padded with spaces (0x20). Set to "EXFAT   " if this is an exFAT VBR. Set to "NTFS    " if this is an NTFS VBR.
    DOS_7_1_EBPB dos_7_1_ebpb;  ///< DOS 7.1 Extended BIOS Parameter Block (full variant).
    u8 boot_code[0x1A3];        ///< File system and operating system specific boot code.
    u8 pdrv;                    ///< Physical drive number.
    u16 boot_sig;               ///< Matches BOOT_SIGNATURE for FAT32, exFAT and NTFS. Serves a different purpose under other FAT filesystems.
} VolumeBootRecord;

/// Master Boot Record (MBR) partition types. All these types support logical block addresses. CHS addressing only and hidden types have been excluded.
typedef enum {
    MasterBootRecordPartitionType_Empty                                 = 0x00,
    MasterBootRecordPartitionType_FAT12                                 = 0x01,
    MasterBootRecordPartitionType_FAT16                                 = 0x04,
    MasterBootRecordPartitionType_ExtendedBootRecord_CHS                = 0x05,
    MasterBootRecordPartitionType_FAT16B                                = 0x06,
    MasterBootRecordPartitionType_NTFS_exFAT                            = 0x07,
    MasterBootRecordPartitionType_FAT32_CHS                             = 0x0B,
    MasterBootRecordPartitionType_FAT32_LBA                             = 0x0C,
    MasterBootRecordPartitionType_FAT16B_LBA                            = 0x0E,
    MasterBootRecordPartitionType_ExtendedBootRecord_LBA                = 0x0F,
    MasterBootRecordPartitionType_LinuxFileSystem                       = 0x83,
    MasterBootRecordPartitionType_ExtendedBootRecord_Linux              = 0x85, ///< Corresponds to MasterBootRecordPartitionType_ExtendedBootRecord_CHS.
    MasterBootRecordPartitionType_GPT_Protective_MBR                    = 0xEE
} MasterBootRecordPartitionType;

/// Master Boot Record (MBR) partition entry.
typedef struct {
    u8 status;          ///< Partition status. We won't use this.
    u8 chs_start[0x3];  ///< Cylinder-head-sector address to the first block in the partition. Unused nowadays.
    u8 type;            ///< MasterBootRecordPartitionType.
    u8 chs_end[0x3];    ///< Cylinder-head-sector address to the last block in the partition. Unused nowadays.
    u32 lba;            ///< Logical block address to the first block in the partition.
    u32 block_count;    ///< Logical block count in the partition.
} MasterBootRecordPartitionEntry;

/// Master Boot Record (MBR). Always located at LBA 0, as long as SFD configuration isn't used (VBR at LBA 0).
#pragma pack(push, 1)
typedef struct {
    u8 code_area[0x1BE];                                            ///< Bootstrap code area. We won't use this.
    MasterBootRecordPartitionEntry partitions[MBR_PARTITION_COUNT]; ///< Primary partition entries.
    u16 boot_sig;                                                   ///< Boot signature. Must match BOOT_SIGNATURE.
} MasterBootRecord;
#pragma pack(pop)

/// Extended Boot Record (EBR). Represents a way to store more than 4 partitions in a MBR-formatted logical unit using linked lists.
typedef struct {
    u8 code_area[0x1BE];                        ///< Bootstrap code area. Normally empty.
    MasterBootRecordPartitionEntry partition;   ///< Primary partition entry.
    MasterBootRecordPartitionEntry next_ebr;    ///< Next EBR in the chain.
    u8 reserved[0x20];                          ///< Normally empty.
    u16 boot_sig;                               ///< Boot signature. Must match BOOT_SIGNATURE.
} ExtendedBootRecord;

/// Globally Unique ID Partition Table (GPT) entry. These usually start at LBA 2.
typedef struct {
    u8 type_guid[0x10];     ///< Partition type GUID.
    u8 unique_guid[0x10];   ///< Unique partition GUID.
    u64 lba_start;          ///< First LBA.
    u64 lba_end;            ///< Last LBA (inclusive).
    u64 flags;              ///< Attribute flags.
    u16 name[0x24];         ///< Partition name (36 UTF-16LE code units).
} GuidPartitionTableEntry;

/// Globally Unique ID Partition Table (GPT) header. If available, it's always located at LBA 1.
typedef struct {
    u64 signature;                  ///< Must match "EFI PART".
    u32 revision;                   ///< GUID Partition Table revision.
    u32 header_size;                ///< Header size. Must match 0x5C.
    u32 header_crc32;               ///< Little-endian CRC32 checksum calculated over this header, with this field zeroed during calculation.
    u8 reserved_1[0x4];             ///< Reserved.
    u64 cur_header_lba;             ///< LBA from this GPT header.
    u64 backup_header_lba;          ///< LBA from the backup GPT header.
    u64 partition_lba_start;        ///< First usable LBA for partitions (primary partition table last LBA + 1).
    u64 partition_lba_end;          ///< Last usable LBA (secondary partition table first LBA - 1).
    u8 disk_guid[0x10];             ///< Disk GUID.
    u64 partition_array_lba;        ///< Starting LBA of array of partition entries (always 2 in primary copy).
    u32 partition_array_count;      ///< Number of partition entries in array.
    u32 partition_array_entry_size; ///< Size of a single partition entry (usually 0x80).
    u32 partition_array_crc32;      ///< Little-endian CRC32 checksum calculated over the partition array.
    u8 reserved_2[0x1A4];           ///< Reserved; must be zeroes for the rest of the block.
} GuidPartitionTableHeader;

static_assert(sizeof(DOS_2_0_BPB) == 0xD, "Bad DOS_2_0_BPB size! Expected 0xD.");
static_assert(sizeof(DOS_3_31_BPB) == 0x19, "Bad DOS_3_31_BPB size! Expected 0x19.");
static_assert(sizeof(DOS_7_1_EBPB) == 0x4F, "Bad DOS_7_1_EBPB size! Expected 0x4F.");
static_assert(sizeof(VolumeBootRecord) == 0x200, "Bad VolumeBootRecord size! Expected 0x200.");
static_assert(sizeof(MasterBootRecord) == 0x200, "Bad MasterBootRecord size! Expected 0x200.");
static_assert(sizeof(MasterBootRecordPartitionEntry) == 0x10, "Bad MasterBootRecordPartitionEntry size! Expected 0x10.");
static_assert(sizeof(GuidPartitionTableEntry) == 0x80, "Bad GuidPartitionTableEntry size! Expected 0x80.");
static_assert(sizeof(GuidPartitionTableHeader) == 0x200, "Bad GuidPartitionTableHeader size! Expected 0x200.");

/* Global variables. */

static u32 g_devoptabDeviceCount = 0;
static u32 *g_devoptabDeviceIds = NULL;

static u32 g_devoptabDefaultDeviceId = DEVOPTAB_INVALID_ID;
static Mutex g_devoptabDefaultDeviceMutex = 0;

static bool g_fatFsVolumeTable[FF_VOLUMES] = { false };

static const u8 g_microsoftBasicDataPartitionGuid[0x10] = { 0xA2, 0xA0, 0xD0, 0xEB, 0xE5, 0xB9, 0x33, 0x44, 0x87, 0xC0, 0x68, 0xB6, 0xB7, 0x26, 0x99, 0xC7 };   /* EBD0A0A2-B9E5-4433-87C0-68B6B72699C7. */
static const u8 g_linuxFilesystemDataGuid[0x10] = { 0xAF, 0x3D, 0xC6, 0x0F, 0x83, 0x84, 0x72, 0x47, 0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4 };           /* 0FC63DAF-8483-4772-8E79-3D69D8477DE4. */

static u32 g_fileSystemMountFlags = (UsbHsFsMountFlags_UpdateAccessTimes | UsbHsFsMountFlags_ShowHiddenFiles | UsbHsFsMountFlags_ReplayJournal);

__thread char __usbhsfs_dev_path_buf[MAX_PATH_LENGTH] = {0};

/* Function prototypes. */

static bool usbHsFsMountParseMasterBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block);
static void usbHsFsMountParseMasterBootRecordPartitionEntry(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u8 type, u64 lba, u64 size, bool parse_ebr_gpt);

static u8 usbHsFsMountInspectVolumeBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr);
#ifdef GPL_BUILD
static u8 usbHsFsMountInspectExtSuperBlock(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr);
#endif

static void usbHsFsMountParseExtendedBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 ebr_lba);
static void usbHsFsMountParseGuidPartitionTable(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 gpt_lba);

static bool usbHsFsMountRegisterVolume(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr, u64 block_count, u8 fs_type);

static bool usbHsFsMountRegisterFatVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u8 *block, u64 block_addr);
static void usbHsFsMountUnregisterFatVolume(char *name, UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx);

#ifdef GPL_BUILD
static bool usbHsFsMountRegisterNtfsVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u8 *block, u64 block_addr);
static void usbHsFsMountUnregisterNtfsVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx);

static bool usbHsFsMountRegisterExtVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u64 block_addr, u64 block_count);
static void usbHsFsMountUnregisterExtVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx);
#endif

static bool usbHsFsMountRegisterDevoptabDevice(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx);
static u32 usbHsFsMountGetAvailableDevoptabDeviceId(void);

static void usbHsFsMountUnsetDefaultDevoptabDevice(u32 device_id);

bool usbHsFsMountInitializeLogicalUnitFileSystemContexts(UsbHsFsDriveLogicalUnitContext *lun_ctx)
{
    if (!usbHsFsDriveIsValidLogicalUnitContext(lun_ctx))
    {
        USBHSFS_LOG_MSG("Invalid parameters!");
        return false;
    }
    
    u8 *block = NULL;
    u8 fs_type = 0;
    bool ret = false;
    
    /* Allocate memory to hold data from a single logical block. */
    block = malloc(lun_ctx->block_length);
    if (!block)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory to hold logical block data! (interface %d, LUN %u).", lun_ctx->usb_if_id, lun_ctx->lun);
        goto end;
    }
    
    /* Check if we're dealing with a SFD-formatted logical unit with a Microsoft VBR at LBA 0. */
    fs_type = usbHsFsMountInspectVolumeBootRecord(lun_ctx, block, 0);
    if (fs_type > UsbHsFsDriveLogicalUnitFileSystemType_Unsupported)
    {
        /* Mount volume at LBA 0 right away. */
        ret = usbHsFsMountRegisterVolume(lun_ctx, block, 0, lun_ctx->block_count, fs_type);
    } else
    if (fs_type == UsbHsFsDriveLogicalUnitFileSystemType_Unsupported)
    {
        /* Parse MBR. */
        ret = usbHsFsMountParseMasterBootRecord(lun_ctx, block);
    } else {
#ifdef GPL_BUILD
        /* We may be dealing with an EXT volume at LBA 0. */
        fs_type = usbHsFsMountInspectExtSuperBlock(lun_ctx, block, 0);
        if (fs_type == UsbHsFsDriveLogicalUnitFileSystemType_EXT)
        {
            /* Mount EXT volume at LBA 0. */
            ret = usbHsFsMountRegisterVolume(lun_ctx, block, 0, lun_ctx->block_count, fs_type);
        } else {
            USBHSFS_LOG_MSG("Unable to locate a valid boot sector! (interface %d, LUN %u).", lun_ctx->usb_if_id, lun_ctx->lun);
        }
#else
        USBHSFS_LOG_MSG("Unable to locate a valid boot sector! (interface %d, LUN %u).", lun_ctx->usb_if_id, lun_ctx->lun);
#endif
    }
    
end:
    if (block) free(block);
    
    return ret;
}

void usbHsFsMountDestroyLogicalUnitFileSystemContext(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
    if (!usbHsFsDriveIsValidLogicalUnitFileSystemContext(fs_ctx)) return;
    
    char name[MOUNT_NAME_LENGTH] = {0};
    u32 *tmp_device_ids = NULL;
    
    /* Unset default devoptab device. */
    usbHsFsMountUnsetDefaultDevoptabDevice(fs_ctx->device_id);
    
    /* Unregister devoptab interface. */
    sprintf(name, "%s:", fs_ctx->name);
    RemoveDevice(name);
    
    /* Free devoptab virtual device interface. */
    free(fs_ctx->device);
    fs_ctx->device = NULL;
    
    /* Free current working directory. */
    free(fs_ctx->cwd);
    fs_ctx->cwd = NULL;
    
    /* Free mount name. */
    free(fs_ctx->name);
    fs_ctx->name = NULL;
    
    /* Locate device ID in devoptab device ID buffer and remove it. */
    for(u32 i = 0; i < g_devoptabDeviceCount; i++)
    {
        if (g_devoptabDeviceIds[i] != fs_ctx->device_id) continue;
        
        USBHSFS_LOG_MSG("Found device ID %u at index %u.", fs_ctx->device_id, i);
        
        if (g_devoptabDeviceCount > 1)
        {
            /* Move data within the device ID buffer, if needed. */
            if (i < (g_devoptabDeviceCount - 1)) memmove(&(g_devoptabDeviceIds[i]), &(g_devoptabDeviceIds[i + 1]), (g_devoptabDeviceCount - (i + 1)) * sizeof(u32));
            
            /* Reallocate devoptab device IDs buffer. */
            tmp_device_ids = realloc(g_devoptabDeviceIds, (g_devoptabDeviceCount - 1) * sizeof(u32));
            if (tmp_device_ids)
            {
                g_devoptabDeviceIds = tmp_device_ids;
                tmp_device_ids = NULL;
            }
        } else {
            /* Free devoptab device ID buffer. */
            free(g_devoptabDeviceIds);
            g_devoptabDeviceIds = NULL;
        }
        
        /* Decrease devoptab virtual device count. */
        g_devoptabDeviceCount--;
        
        break;
    }
    
    /* Unmount filesystem. */
    switch(fs_ctx->fs_type)
    {
        case UsbHsFsDriveLogicalUnitFileSystemType_FAT:     /* FAT12/FAT16/FAT32/exFAT. */
            usbHsFsMountUnregisterFatVolume(name, fs_ctx);
            break;
#ifdef GPL_BUILD
        case UsbHsFsDriveLogicalUnitFileSystemType_NTFS:    /* NTFS. */
            usbHsFsMountUnregisterNtfsVolume(fs_ctx);        
            break;
        case UsbHsFsDriveLogicalUnitFileSystemType_EXT:     /* EXT2/3/4. */
            usbHsFsMountUnregisterExtVolume(fs_ctx);        
            break;
#endif
        
        /* TO DO: populate this after adding support for additional filesystems. */
        
        default:
            break;
    }
}

u32 usbHsFsMountGetDevoptabDeviceCount(void)
{
    return g_devoptabDeviceCount;
}

bool usbHsFsMountSetDefaultDevoptabDevice(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
    bool ret = false;
    
    SCOPED_LOCK(&g_devoptabDefaultDeviceMutex)
    {
        if (!g_devoptabDeviceCount || !g_devoptabDeviceIds || !usbHsFsDriveIsValidLogicalUnitFileSystemContext(fs_ctx))
        {
            USBHSFS_LOG_MSG("Invalid parameters!");
            break;
        }
        
        const devoptab_t *cur_default_devoptab = NULL;
        int new_default_device = -1;
        char name[MOUNT_NAME_LENGTH] = {0};
        
        /* Get current default devoptab device index. */
        cur_default_devoptab = GetDeviceOpTab("");
        if (cur_default_devoptab && cur_default_devoptab->deviceData == fs_ctx)
        {
            /* Device already set as default. */
            USBHSFS_LOG_MSG("Device \"%s\" already set as default.", fs_ctx->name);
            ret = true;
            break;
        }
        
        /* Get devoptab device index for our filesystem. */
        sprintf(name, "%s:", fs_ctx->name);
        new_default_device = FindDevice(name);
        if (new_default_device < 0)
        {
            USBHSFS_LOG_MSG("Failed to retrieve devoptab device index for \"%s\"!", fs_ctx->name);
            break;
        }
        
        /* Set default devoptab device. */
        setDefaultDevice(new_default_device);
        cur_default_devoptab = GetDeviceOpTab("");
        if (!cur_default_devoptab || cur_default_devoptab->deviceData != fs_ctx)
        {
            USBHSFS_LOG_MSG("Failed to set default devoptab device to index %d! (device \"%s\").", new_default_device, fs_ctx->name);
            break;
        }
        
        USBHSFS_LOG_MSG("Successfully set default devoptab device to index %d! (device \"%s\").", new_default_device, fs_ctx->name);
        
        /* Update default device ID. */
        g_devoptabDefaultDeviceId = fs_ctx->device_id;
        
        /* Update return value. */
        ret = true;
    }
    
    return ret;
}

u32 usbHsFsMountGetFileSystemMountFlags(void)
{
    return g_fileSystemMountFlags;
}

void usbHsFsMountSetFileSystemMountFlags(u32 flags)
{
    g_fileSystemMountFlags = flags;
}

static bool usbHsFsMountParseMasterBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block)
{
    MasterBootRecord mbr = {0};
    bool ret = false;
    
    memcpy(&mbr, block, sizeof(MasterBootRecord));
    
    /* Parse MBR partition entries. */
    for(u8 i = 0; i < MBR_PARTITION_COUNT; i++)
    {
        MasterBootRecordPartitionEntry *partition = &(mbr.partitions[i]);
        usbHsFsMountParseMasterBootRecordPartitionEntry(lun_ctx, block, partition->type, partition->lba, partition->block_count, true);
    }
    
    /* Update return value. */
    ret = (lun_ctx->fs_count > 0);
    
    return ret;
}

static void usbHsFsMountParseMasterBootRecordPartitionEntry(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u8 type, u64 lba, u64 size, bool parse_ebr_gpt)
{
    u8 fs_type = UsbHsFsDriveLogicalUnitFileSystemType_Invalid;
    
    switch(type)
    {
        case MasterBootRecordPartitionType_Empty:
            USBHSFS_LOG_MSG("Found empty partition entry (interface %d, LUN %u). Skipping.", lun_ctx->usb_if_id, lun_ctx->lun);
            break;
        case MasterBootRecordPartitionType_FAT12:
        case MasterBootRecordPartitionType_FAT16:
        case MasterBootRecordPartitionType_FAT16B:
        case MasterBootRecordPartitionType_NTFS_exFAT:
        case MasterBootRecordPartitionType_FAT32_CHS:
        case MasterBootRecordPartitionType_FAT32_LBA:
        case MasterBootRecordPartitionType_FAT16B_LBA:
            USBHSFS_LOG_MSG("Found FAT/NTFS partition entry with type 0x%02X at LBA 0x%lX (interface %d, LUN %u).", type, lba, lun_ctx->usb_if_id, lun_ctx->lun);
            
            /* Inspect VBR. */
            fs_type = usbHsFsMountInspectVolumeBootRecord(lun_ctx, block, lba);
            
            break;
        case MasterBootRecordPartitionType_LinuxFileSystem:
            USBHSFS_LOG_MSG("Found Linux partition entry with type 0x%02X at LBA 0x%lX (interface %d, LUN %u).", type, lba, lun_ctx->usb_if_id, lun_ctx->lun);
            
#ifdef GPL_BUILD
            /* Inspect EXT superblock. */
            fs_type = usbHsFsMountInspectExtSuperBlock(lun_ctx, block, lba);
#endif
            
            break;
        case MasterBootRecordPartitionType_ExtendedBootRecord_CHS:
        case MasterBootRecordPartitionType_ExtendedBootRecord_LBA:
        case MasterBootRecordPartitionType_ExtendedBootRecord_Linux:
            USBHSFS_LOG_MSG("Found EBR partition entry with type 0x%02X at LBA 0x%lX (interface %d, LUN %u).", type, lba, lun_ctx->usb_if_id, lun_ctx->lun);
            
            /* Parse EBR. */
            if (parse_ebr_gpt) usbHsFsMountParseExtendedBootRecord(lun_ctx, block, lba);
            
            break;
        case MasterBootRecordPartitionType_GPT_Protective_MBR:
            USBHSFS_LOG_MSG("Found GPT partition entry at LBA 0x%lX (interface %d, LUN %u).", lba, lun_ctx->usb_if_id, lun_ctx->lun);
            
            /* Parse GPT. */
            if (parse_ebr_gpt) usbHsFsMountParseGuidPartitionTable(lun_ctx, block, lba);
            
            break;
        default:
            USBHSFS_LOG_MSG("Found unsupported partition entry with type 0x%02X (interface %d, LUN %u). Skipping.", type, lun_ctx->usb_if_id, lun_ctx->lun);
            break;
    }
    
    /* Register detected volume. */
    if (fs_type > UsbHsFsDriveLogicalUnitFileSystemType_Unsupported && usbHsFsMountRegisterVolume(lun_ctx, block, lba, size, fs_type))
    {
        USBHSFS_LOG_MSG("Successfully registered %s volume at LBA 0x%lX (interface %d, LUN %u).", FS_TYPE_STR(fs_type), lba, lun_ctx->usb_if_id, lun_ctx->lun);
    }
}

static u8 usbHsFsMountInspectVolumeBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr)
{
    u32 block_length = lun_ctx->block_length;
    u8 ret = UsbHsFsDriveLogicalUnitFileSystemType_Invalid;
    
    /* Read block at the provided address from this LUN. */
    if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, block_addr, 1))
    {
        USBHSFS_LOG_MSG("Failed to read block at LBA 0x%lX! (interface %d, LUN %u).", block_addr, lun_ctx->usb_if_id, lun_ctx->lun);
        goto end;
    }
    
    VolumeBootRecord *vbr = (VolumeBootRecord*)block;
    u8 jmp_code = vbr->jmp_boot[0];
    u16 boot_sig = vbr->boot_sig;
    
    DOS_3_31_BPB *dos_3_31_bpb = &(vbr->dos_7_1_ebpb.dos_3_31_bpb);
    DOS_2_0_BPB *dos_2_0_bpb = &(dos_3_31_bpb->dos_2_0_bpb);
    
    u8 sectors_per_cluster = dos_2_0_bpb->sectors_per_cluster, num_fats = dos_2_0_bpb->num_fats;
    u16 sector_size = dos_2_0_bpb->sector_size, reserved_sectors = dos_2_0_bpb->reserved_sectors, root_dir_entries = dos_2_0_bpb->root_dir_entries, total_sectors_16 = dos_2_0_bpb->total_sectors;
    u16 sectors_per_fat = dos_2_0_bpb->sectors_per_fat;
    u32 total_sectors_32 = dos_3_31_bpb->total_sectors;
    
    /* Check if we have a valid boot sector signature. */
    if (boot_sig == BOOT_SIGNATURE)
    {
        /* Check if this is an exFAT VBR. */
        if (!memcmp(vbr->jmp_boot, "\xEB\x76\x90" "EXFAT   ", 11))
        {
            ret = UsbHsFsDriveLogicalUnitFileSystemType_FAT;
            goto end;
        }
        
        /* Check if this is an NTFS VBR. */
        if (!memcmp(vbr->oem_name, "NTFS    ", 8))
        {
            ret = UsbHsFsDriveLogicalUnitFileSystemType_NTFS;
            goto end;
        }
    }
    
    /* Check if we have a valid jump boot code. */
    if (jmp_code == 0xEB || jmp_code == 0xE9 || jmp_code == 0xE8)
    {
        /* Check if this is a FAT32 VBR. */
        if (boot_sig == BOOT_SIGNATURE && !memcmp(vbr->dos_7_1_ebpb.fs_type, "FAT32   ", 8))
        {
            ret = UsbHsFsDriveLogicalUnitFileSystemType_FAT;
            goto end;
        }
        
        /* FAT volumes formatted with old tools lack a boot sector signature and a filesystem type string, so we'll try to identify the FAT VBR without them. */
        if ((sector_size & (sector_size - 1)) == 0 && sector_size <= (u16)block_length && sectors_per_cluster != 0 && (sectors_per_cluster & (sectors_per_cluster - 1)) == 0 && \
            reserved_sectors != 0 && (num_fats - 1) <= 1 && root_dir_entries != 0 && (total_sectors_16 >= 128 || total_sectors_32 >= 0x10000) && sectors_per_fat != 0) \
                ret = UsbHsFsDriveLogicalUnitFileSystemType_FAT;
    }
    
    /* Change return value if we couldn't identify a potential VBR but there's valid boot signature. */
    /* We may be dealing with a MBR/EBR. */
    if (ret == UsbHsFsDriveLogicalUnitFileSystemType_Invalid && boot_sig == BOOT_SIGNATURE) ret = UsbHsFsDriveLogicalUnitFileSystemType_Unsupported;
    
end:
    if (ret > UsbHsFsDriveLogicalUnitFileSystemType_Unsupported) USBHSFS_LOG_MSG("Found %s VBR at LBA 0x%lX (interface %d, LUN %u).", FS_TYPE_STR(ret), block_addr, lun_ctx->usb_if_id, lun_ctx->lun);
    
    return ret;
}

#ifdef GPL_BUILD

static u8 usbHsFsMountInspectExtSuperBlock(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr)
{
    u32 block_length = lun_ctx->block_length;
    u32 block_read_addr = (block_addr + (EXT4_SUPERBLOCK_OFFSET / block_length));
    u32 block_read_count = (block_length >= EXT4_SUPERBLOCK_SIZE ? 1 : (EXT4_SUPERBLOCK_SIZE / block_length));
    struct ext4_sblock superblock = {0};
    u8 ret = UsbHsFsDriveLogicalUnitFileSystemType_Invalid;
    
    if (block_read_count == 1)
    {
        /* Read entire EXT superblock. */
        if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, block_read_addr, 1))
        {
            USBHSFS_LOG_MSG("Failed to read block at LBA 0x%X! (interface %d, LUN %u).", block_read_addr, lun_ctx->usb_if_id, lun_ctx->lun);
            goto end;
        }
        
        /* Copy EXT superblock data. */
        memcpy(&superblock, block + (block_read_addr == block_addr ? EXT4_SUPERBLOCK_OFFSET : 0), sizeof(struct ext4_sblock));
    } else {
        /* Read entire EXT superblock. */
        if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, (u8*)&superblock, block_read_addr, block_read_count))
        {
            USBHSFS_LOG_MSG("Failed to read %u blocks at LBA 0x%X! (interface %d, LUN %u).", block_read_count, block_read_addr, lun_ctx->usb_if_id, lun_ctx->lun);
            goto end;
        }
    }
    
    /* Check if this is a valid EXT superblock. */
    if (ext4_sb_check(&superblock)) ret = UsbHsFsDriveLogicalUnitFileSystemType_EXT;
    
end:
    if (ret == UsbHsFsDriveLogicalUnitFileSystemType_EXT) USBHSFS_LOG_MSG("Found EXT superblock at LBA 0x%X (interface %d, LUN %u).", block_read_addr, lun_ctx->usb_if_id, lun_ctx->lun);
    
    return ret;
}

#endif  /* GPL_BUILD */

static void usbHsFsMountParseExtendedBootRecord(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 ebr_lba)
{
    ExtendedBootRecord ebr = {0};
    u64 next_ebr_lba = 0, part_lba = 0;
    
    do {
        /* Read current EBR sector. */
        if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, ebr_lba + next_ebr_lba, 1))
        {
            USBHSFS_LOG_MSG("Failed to read EBR at LBA 0x%lX! (interface %d, LUN %u).", ebr_lba, lun_ctx->usb_if_id, lun_ctx->lun);
            break;
        }
        
        /* Copy EBR data to struct. */
        memcpy(&ebr, block, sizeof(ExtendedBootRecord));
        
        /* Check boot signature. */
        if (ebr.boot_sig == BOOT_SIGNATURE)
        {
            /* Calculate LBAs for the current partition and the next EBR in the chain. */
            part_lba = (ebr_lba + next_ebr_lba + ebr.partition.lba);
            next_ebr_lba = ebr.next_ebr.lba;
            
            /* Parse partition entry. */
            usbHsFsMountParseMasterBootRecordPartitionEntry(lun_ctx, block, ebr.partition.type, part_lba, ebr.partition.block_count, false);
        } else {
            /* Reset LBA from next EBR. */
            next_ebr_lba = 0;
        }
    } while(next_ebr_lba);
}

static void usbHsFsMountParseGuidPartitionTable(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 gpt_lba)
{
    GuidPartitionTableHeader gpt_header = {0};
    u32 header_crc32 = 0, header_crc32_calc = 0, part_count = 0, part_per_block = 0, part_array_block_count = 0;
    u64 part_lba = 0;
    
    /* Read block where the GPT header is located. */
    if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, gpt_lba, 1))
    {
        USBHSFS_LOG_MSG("Failed to read GPT header from LBA 0x%lX! (interface %d, LUN %u).", gpt_lba, lun_ctx->usb_if_id, lun_ctx->lun);
        return;
    }
    
    /* Copy GPT header data. */
    memcpy(&gpt_header, block, sizeof(GuidPartitionTableHeader));
    
    /* Verify GPT header signature, revision and header size fields. */
    if (memcmp(&(gpt_header.signature), "EFI PART" "\x00\x00\x01\x00" "\x5C\x00\x00\x00", 16) != 0)
    {
        USBHSFS_LOG_MSG("Invalid GPT header at LBA 0x%lX! (interface %d, LUN %u).", gpt_lba, lun_ctx->usb_if_id, lun_ctx->lun);
        return;
    }
    
    /* Verify GPT header CRC32 checksum. */
    header_crc32 = gpt_header.header_crc32;
    gpt_header.header_crc32 = 0;
    header_crc32_calc = crc32Calculate(&gpt_header, gpt_header.header_size);
    gpt_header.header_crc32 = header_crc32;
    
    if (header_crc32_calc != header_crc32)
    {
        USBHSFS_LOG_MSG("Invalid CRC32 checksum for GPT header at LBA 0x%lX! (%08X != %08X) (interface %d, LUN %u).", gpt_lba, header_crc32_calc, header_crc32, lun_ctx->usb_if_id, lun_ctx->lun);
        
        /* Check if the LBA for the backup GPT header points to a valid location. */
        gpt_lba = gpt_header.backup_header_lba;
        if (!gpt_lba || gpt_lba == gpt_header.cur_header_lba || gpt_lba >= lun_ctx->block_count) return;
        
        /* Read block where the backup GPT header is located. */
        if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, gpt_lba, 1))
        {
            USBHSFS_LOG_MSG("Failed to read backup GPT header from LBA 0x%lX! (interface %d, LUN %u).", gpt_lba, lun_ctx->usb_if_id, lun_ctx->lun);
            return;
        }
        
        /* Copy backup GPT header data. */
        memcpy(&gpt_header, block, sizeof(GuidPartitionTableHeader));
        
        /* Verify backup GPT header CRC32 checksum. */
        header_crc32 = gpt_header.header_crc32;
        gpt_header.header_crc32 = 0;
        header_crc32_calc = crc32Calculate(&gpt_header, gpt_header.header_size);
        gpt_header.header_crc32 = header_crc32;
        
        if (header_crc32_calc != header_crc32)
        {
            USBHSFS_LOG_MSG("Invalid CRC32 checksum for backup GPT header at LBA 0x%lX! (%08X != %08X) (interface %d, LUN %u).", gpt_lba, header_crc32_calc, header_crc32, lun_ctx->usb_if_id, \
                        lun_ctx->lun);
            return;
        }
        
        USBHSFS_LOG_MSG("Backup GPT header located at LBA 0x%lX (interface %d, LUN %u).", gpt_lba, lun_ctx->usb_if_id, lun_ctx->lun);
    }
    
    /* Verify GPT partition entry size. */
    if (gpt_header.partition_array_entry_size != sizeof(GuidPartitionTableEntry))
    {
        USBHSFS_LOG_MSG("Invalid GPT partition entry size in GPT header at LBA 0x%lX! (0x%X != 0x%lX) (interface %d, LUN %u).", gpt_lba, gpt_header.partition_array_entry_size, \
                    sizeof(GuidPartitionTableEntry), lun_ctx->usb_if_id, lun_ctx->lun);
        return;
    }
    
    /* Get GPT partition entry count. Only process the first 128 entries if there's more than that. */
    part_count = gpt_header.partition_array_count;
    if (part_count > 128) part_count = 128;
    
    /* Calculate number of partition entries per block and the total block count for the whole partition array. */
    part_lba = gpt_header.partition_array_lba;
    part_per_block = (lun_ctx->block_length / (u32)sizeof(GuidPartitionTableEntry));
    part_array_block_count = (part_count / part_per_block);
    
    /* Parse GPT partition entries. */
    for(u32 i = 0; i < part_array_block_count; i++)
    {
        /* Read current partition array block. */
        if (!usbHsFsScsiReadLogicalUnitBlocks(lun_ctx, block, part_lba + i, 1))
        {
            USBHSFS_LOG_MSG("Failed to read GPT partition array block #%u from LBA 0x%lX! (interface %d, LUN %u).", i, part_lba + i, lun_ctx->usb_if_id, lun_ctx->lun);
            break;
        }
        
        for(u32 j = 0; j < part_per_block; j++)
        {
            GuidPartitionTableEntry *gpt_entry = (GuidPartitionTableEntry*)(block + (j * sizeof(GuidPartitionTableEntry)));
            u64 entry_lba = gpt_entry->lba_start;
            u64 entry_size = ((gpt_entry->lba_end + 1) - gpt_entry->lba_start);
            u8 fs_type = UsbHsFsDriveLogicalUnitFileSystemType_Invalid;
            
            if (!memcmp(gpt_entry->type_guid, g_microsoftBasicDataPartitionGuid, sizeof(g_microsoftBasicDataPartitionGuid)))
            {
                /* We're dealing with a Microsoft Basic Data Partition entry. */
                USBHSFS_LOG_MSG("Found Microsoft Basic Data Partition entry at LBA 0x%lX (interface %d, LUN %u).", entry_lba, lun_ctx->usb_if_id, lun_ctx->lun);
                
                /* Inspect Microsoft VBR. Register the volume if we detect a supported VBR. */
                fs_type = usbHsFsMountInspectVolumeBootRecord(lun_ctx, block, entry_lba);
#ifdef GPL_BUILD
                if (fs_type == UsbHsFsDriveLogicalUnitFileSystemType_Invalid)
                {
                    /* We may be dealing with a EXT volume. Check if we can find a valid EXT superblock. */
                    /* Certain tools set the type GUID from EXT volumes to the one from Microsoft. */
                    fs_type = usbHsFsMountInspectExtSuperBlock(lun_ctx, block, entry_lba);
                }
#endif
            } else
            if (!memcmp(gpt_entry->type_guid, g_linuxFilesystemDataGuid, sizeof(g_linuxFilesystemDataGuid)))
            {
                /* We're dealing with a Linux Filesystem Data entry. */
                USBHSFS_LOG_MSG("Found Linux Filesystem Data entry at LBA 0x%lX (interface %d, LUN %u).", entry_lba, lun_ctx->usb_if_id, lun_ctx->lun);
                
#ifdef GPL_BUILD
                /* Check if this LBA points to a valid EXT superblock. Register the EXT volume if so. */
                fs_type = usbHsFsMountInspectExtSuperBlock(lun_ctx, block, entry_lba);
#endif
            }
            
            /* Register volume. */
            if (fs_type > UsbHsFsDriveLogicalUnitFileSystemType_Unsupported && usbHsFsMountRegisterVolume(lun_ctx, block, entry_lba, entry_size, fs_type))
            {
                USBHSFS_LOG_MSG("Successfully registered %s volume at LBA 0x%lX (interface %d, LUN %u).", FS_TYPE_STR(fs_type), entry_lba, lun_ctx->usb_if_id, lun_ctx->lun);
            }
        }
    }
}

static bool usbHsFsMountRegisterVolume(UsbHsFsDriveLogicalUnitContext *lun_ctx, u8 *block, u64 block_addr, u64 block_count, u8 fs_type)
{
#ifndef GPL_BUILD
    (void)block_count;
#endif
    
    UsbHsFsDriveLogicalUnitFileSystemContext **tmp_fs_ctx = NULL, *fs_ctx = NULL;
    bool ret = false, free_entry = false;
    
    /* Reallocate filesystem context pointer array. */
    tmp_fs_ctx = realloc(lun_ctx->fs_ctx, (lun_ctx->fs_count + 1) * sizeof(UsbHsFsDriveLogicalUnitFileSystemContext*));
    if (!tmp_fs_ctx)
    {
        USBHSFS_LOG_MSG("Failed to reallocate filesystem context pointer array! (interface %d, LUN %u).", lun_ctx->usb_if_id, lun_ctx->lun);
        goto end;
    }
    
    lun_ctx->fs_ctx = tmp_fs_ctx;
    tmp_fs_ctx = NULL;
    free_entry = true;
    
    /* Allocate memory for a new filesystem context. */
    fs_ctx = calloc(1, sizeof(UsbHsFsDriveLogicalUnitFileSystemContext));
    if (!fs_ctx)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for filesystem context entry #%u! (interface %d, LUN %u).", lun_ctx->fs_count, lun_ctx->usb_if_id, lun_ctx->lun);
        goto end;
    }
    
    /* Set filesystem context properties. */
    fs_ctx->lun_ctx = lun_ctx;
    fs_ctx->fs_idx = lun_ctx->fs_count;
    fs_ctx->fs_type = fs_type;
    fs_ctx->flags = g_fileSystemMountFlags;
    
    /* Set filesystem context entry pointer and update filesystem context count. */
    lun_ctx->fs_ctx[(lun_ctx->fs_count)++] = fs_ctx;
    
    /* Mount and register filesystem. */
    switch(fs_type)
    {
        case UsbHsFsDriveLogicalUnitFileSystemType_FAT:     /* FAT12/FAT16/FAT32/exFAT. */
            ret = usbHsFsMountRegisterFatVolume(fs_ctx, block, block_addr);
            break;
#ifdef GPL_BUILD
        case UsbHsFsDriveLogicalUnitFileSystemType_NTFS:    /* NTFS. */
            ret = usbHsFsMountRegisterNtfsVolume(fs_ctx, block, block_addr);
            break;
        case UsbHsFsDriveLogicalUnitFileSystemType_EXT:     /* EXT2/3/4. */
            ret = usbHsFsMountRegisterExtVolume(fs_ctx, block_addr, block_count);
            break;
#endif
        
        /* TO DO: populate this after adding support for additional filesystems. */
        
        default:
            USBHSFS_LOG_MSG("Invalid FS type provided! (0x%02X) (interface %d, LUN %u, FS %u).", fs_type, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
            break;
    }
    
end:
    if (!ret && free_entry)
    {
        /* Free filesystem context. */
        if (fs_ctx)
        {
            free(fs_ctx);
            
            /* Update filesystem context count and clear filesystem context entry pointer. */
            lun_ctx->fs_ctx[--(lun_ctx->fs_count)] = fs_ctx = NULL;
        }
        
        if (lun_ctx->fs_count)
        {
            /* Reallocate filesystem context buffer. */
            tmp_fs_ctx = realloc(lun_ctx->fs_ctx, lun_ctx->fs_count * sizeof(UsbHsFsDriveLogicalUnitFileSystemContext*));
            if (tmp_fs_ctx)
            {
                lun_ctx->fs_ctx = tmp_fs_ctx;
                tmp_fs_ctx = NULL;
            }
        } else {
            /* Free filesystem context buffer. */
            free(lun_ctx->fs_ctx);
            lun_ctx->fs_ctx = NULL;
        }
    }
    
    return ret;
}

static bool usbHsFsMountRegisterFatVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u8 *block, u64 block_addr)
{
#ifdef DEBUG
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)fs_ctx->lun_ctx;
#endif
    
    u8 pdrv = 0;
    char name[MOUNT_NAME_LENGTH] = {0};
    FRESULT ff_res = FR_DISK_ERR;
    bool ret = false;
    
    /* Check if there's a free FatFs volume slot. */
    for(pdrv = 0; pdrv < FF_VOLUMES; pdrv++)
    {
        if (!g_fatFsVolumeTable[pdrv])
        {
            /* Jackpot. Prepare mount name. */
            sprintf(name, "%u:", pdrv);
            break;
        }
    }
    
    if (pdrv == FF_VOLUMES)
    {
        USBHSFS_LOG_MSG("Failed to locate a free FatFs volume slot! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    USBHSFS_LOG_MSG("Located free FatFs volume slot: %u (interface %d, LUN %u, FS %u).", pdrv, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
    
    /* Allocate memory for the FatFs object. */
    fs_ctx->fatfs = calloc(1, sizeof(FATFS));
    if (!fs_ctx->fatfs)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for FATFS object! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Copy VBR data. */
    fs_ctx->fatfs->winsect = (LBA_t)block_addr;
    memcpy(fs_ctx->fatfs->win, block, sizeof(VolumeBootRecord));
    
    /* Try to mount FAT volume. */
    ff_res = ff_mount(fs_ctx->fatfs, name, 1);
    if (ff_res != FR_OK)
    {
        USBHSFS_LOG_MSG("Failed to mount FAT volume! (%u) (interface %d, LUN %u, FS %u).", ff_res, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Register devoptab device. */
    if (!usbHsFsMountRegisterDevoptabDevice(fs_ctx)) goto end;
    
    /* Update FatFs volume slot. */
    g_fatFsVolumeTable[pdrv] = true;
    
    /* Update return value. */
    ret = true;
    
end:
    /* Free stuff if something went wrong. */
    if (!ret && fs_ctx->fatfs)
    {
        if (ff_res == FR_OK) ff_unmount(name);
        free(fs_ctx->fatfs);
        fs_ctx->fatfs = NULL;
    }
    
    return ret;
}

static void usbHsFsMountUnregisterFatVolume(char *name, UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
    /* Update FatFs volume slot. */
    g_fatFsVolumeTable[fs_ctx->fatfs->pdrv] = false;
    
    /* Prepare mount name. */
    sprintf(name, "%u:", fs_ctx->fatfs->pdrv);
    
    /* Unmount FAT volume. */
    ff_unmount(name);
    
    /* Free FATFS object. */
    free(fs_ctx->fatfs);
    fs_ctx->fatfs = NULL;
}

#ifdef GPL_BUILD

static bool usbHsFsMountRegisterNtfsVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u8 *block, u64 block_addr)
{
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)fs_ctx->lun_ctx;
    char name[MOUNT_NAME_LENGTH] = {0};
    u32 flags = fs_ctx->flags;
    bool ret = false;
    
    /* Allocate memory for the NTFS volume descriptor. */
    fs_ctx->ntfs = calloc(1, sizeof(ntfs_vd));
    if (!fs_ctx->ntfs)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for NTFS volume descriptor! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Allocate memory for the NTFS device descriptor. */
    fs_ctx->ntfs->dd = calloc(1, sizeof(ntfs_dd));
    if (!fs_ctx->ntfs->dd)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for NTFS device descriptor! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Get available devoptab device ID. */
    fs_ctx->device_id = usbHsFsMountGetAvailableDevoptabDeviceId();
    sprintf(name, MOUNT_NAME_PREFIX "%u", fs_ctx->device_id);
    
    /* Allocate memory for the NTFS device handle. */
    fs_ctx->ntfs->dev = ntfs_device_alloc(name, 0, ntfs_disk_io_get_dops(), fs_ctx->ntfs->dd);
    if (!fs_ctx->ntfs->dev)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for NTFS device object! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Copy VBR data. */
    memcpy(&(fs_ctx->ntfs->dd->vbr), block, sizeof(NTFS_BOOT_SECTOR));
    
    /* Setup NTFS device descriptor. */
    fs_ctx->ntfs->dd->lun_ctx = lun_ctx;
    fs_ctx->ntfs->dd->sector_start = block_addr;
    
    /* Setup NTFS volume descriptor. */
    fs_ctx->ntfs->id = fs_ctx->device_id;
    fs_ctx->ntfs->update_access_times = (flags & UsbHsFsMountFlags_UpdateAccessTimes);
    fs_ctx->ntfs->ignore_read_only_attr = (flags & UsbHsFsMountFlags_IgnoreFileReadOnlyAttribute);
    fs_ctx->ntfs->show_hidden_files = (flags & UsbHsFsMountFlags_ShowHiddenFiles);
    fs_ctx->ntfs->show_system_files = (flags & UsbHsFsMountFlags_ShowSystemFiles);
    
    if ((flags & UsbHsFsMountFlags_ReadOnly) || lun_ctx->write_protect) fs_ctx->ntfs->flags |= NTFS_MNT_RDONLY;
    if (flags & UsbHsFsMountFlags_ReplayJournal) fs_ctx->ntfs->flags |= NTFS_MNT_RECOVER;
    if (flags & UsbHsFsMountFlags_IgnoreHibernation) fs_ctx->ntfs->flags |= NTFS_MNT_IGNORE_HIBERFILE;
    
    /* Try to mount NTFS volume. */
    fs_ctx->ntfs->vol = ntfs_device_mount(fs_ctx->ntfs->dev, fs_ctx->ntfs->flags);
    if (!fs_ctx->ntfs->vol)
    {    
        USBHSFS_LOG_MSG("Failed to mount NTFS volume! (%d) (interface %d, LUN %u, FS %u).", ntfs_volume_error(errno), lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Setup volume case sensitivity. */
	if (flags & UsbHsFsMountFlags_IgnoreCaseSensitivity) ntfs_set_ignore_case(fs_ctx->ntfs->vol);
    
    /* Register devoptab device. */
    if (!usbHsFsMountRegisterDevoptabDevice(fs_ctx)) goto end;
    
    /* Update return value. */
    ret = true;
    
end:
    /* Free stuff if something went wrong. */
    if (!ret && fs_ctx->ntfs)
    {
        if (fs_ctx->ntfs->vol)
        {
            ntfs_umount(fs_ctx->ntfs->vol, true);
            fs_ctx->ntfs->vol = NULL;
            fs_ctx->ntfs->dev = NULL;   /* ntfs_umount() calls ntfs_device_free() for us. */
        }
        
        if (fs_ctx->ntfs->dev)
        {
            ntfs_device_free(fs_ctx->ntfs->dev);
            fs_ctx->ntfs->dev = NULL;
        }
        
        if (fs_ctx->ntfs->dd)
        {
            free(fs_ctx->ntfs->dd);
            fs_ctx->ntfs->dd = NULL;
        }
        
        free(fs_ctx->ntfs);
        fs_ctx->ntfs = NULL;
    }
    
    return ret;
}

static void usbHsFsMountUnregisterNtfsVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
    /* Unmount NTFS volume. */
    /* We don't need to manually free the NTFS device handle - ntfs_umount() does it for us. */
    ntfs_umount(fs_ctx->ntfs->vol, true);
    fs_ctx->ntfs->vol = NULL;
    fs_ctx->ntfs->dev = NULL;
    
    /* Free NTFS device descriptor. */
    free(fs_ctx->ntfs->dd);
    fs_ctx->ntfs->dd = NULL;
    
    /* Free NTFS volume descriptor. */
    free(fs_ctx->ntfs);
    fs_ctx->ntfs = NULL;
}

static bool usbHsFsMountRegisterExtVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx, u64 block_addr, u64 block_count)
{
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)fs_ctx->lun_ctx;
    bool ret = false, vol_mounted = false;
    
    /* Allocate memory for the EXT volume descriptor. */
    fs_ctx->ext = calloc(1, sizeof(ext_vd));
    if (!fs_ctx->ext)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for EXT volume descriptor! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Setup EXT block device handle. */
    fs_ctx->ext->bdev = ext_disk_io_alloc_blockdev(lun_ctx, block_addr, block_count);
    if (!fs_ctx->ext->bdev)
    {
        USBHSFS_LOG_MSG("Failed to setup EXT block device handle! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Get available devoptab device ID. */
    fs_ctx->device_id = usbHsFsMountGetAvailableDevoptabDeviceId();
    
    /* Setup EXT volume descriptor. */
    sprintf(fs_ctx->ext->dev_name, MOUNT_NAME_PREFIX "%u", fs_ctx->device_id);
    fs_ctx->ext->flags = fs_ctx->flags;
    fs_ctx->ext->id = fs_ctx->device_id;
    
    /* Try to mount EXT volume. */
    if (!ext_mount(fs_ctx->ext))
    {    
        USBHSFS_LOG_MSG("Failed to mount EXT volume! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    vol_mounted = true;
    
    /* Register devoptab device. */
    if (!usbHsFsMountRegisterDevoptabDevice(fs_ctx)) goto end;
    
    /* Update return value. */
    ret = true;
    
end:
    /* Free stuff if something went wrong. */
    if (!ret && fs_ctx->ext)
    {
        if (vol_mounted) ext_umount(fs_ctx->ext);
        
        if (fs_ctx->ext->bdev)
        {
            ext_disk_io_free_blockdev(fs_ctx->ext->bdev);
            fs_ctx->ext->bdev = NULL;
        }
        
        free(fs_ctx->ext);
        fs_ctx->ext = NULL;
    }
    
    return ret;
}

static void usbHsFsMountUnregisterExtVolume(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
    /* Unmount EXT volume. */
    ext_umount(fs_ctx->ext);
    
    /* Free EXT block device handle. */
    ext_disk_io_free_blockdev(fs_ctx->ext->bdev);
    fs_ctx->ext->bdev = NULL;
    
    /* Free EXT volume descriptor. */
    free(fs_ctx->ext);
    fs_ctx->ext = NULL;
}

#endif  /* GPL_BUILD */

static bool usbHsFsMountRegisterDevoptabDevice(UsbHsFsDriveLogicalUnitFileSystemContext *fs_ctx)
{
#ifdef DEBUG
    UsbHsFsDriveLogicalUnitContext *lun_ctx = (UsbHsFsDriveLogicalUnitContext*)fs_ctx->lun_ctx;
#endif
    
    char name[MOUNT_NAME_LENGTH] = {0};
    const devoptab_t *fs_device = NULL;
    int ad_res = -1;
    u32 *tmp_device_ids = NULL;
    bool ret = false;
    
    /* Generate devoptab mount name. */
    fs_ctx->name = calloc(MOUNT_NAME_LENGTH, sizeof(char));
    if (!fs_ctx->name)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for the mount name! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    fs_ctx->device_id = usbHsFsMountGetAvailableDevoptabDeviceId();
    USBHSFS_LOG_MSG("Available device ID: %u (interface %d, LUN %u, FS %u).", fs_ctx->device_id, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
    
    sprintf(fs_ctx->name, MOUNT_NAME_PREFIX "%u", fs_ctx->device_id);
    sprintf(name, "%s:", fs_ctx->name); /* Will be used if something goes wrong and we end up having to remove the devoptab device. */
    
    /* Allocate memory for the current working directory. */
    fs_ctx->cwd = calloc(MAX_PATH_LENGTH, sizeof(char));
    if (!fs_ctx->cwd)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for the current working directory! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    fs_ctx->cwd[0] = '/';   /* Always start at the root directory. */
    
    /* Allocate memory for our devoptab virtual device interface. */
    fs_ctx->device = calloc(1, sizeof(devoptab_t));
    if (!fs_ctx->device)
    {
        USBHSFS_LOG_MSG("Failed to allocate memory for devoptab virtual device interface! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Retrieve pointer to the devoptab interface from our filesystem type. */
    switch(fs_ctx->fs_type)
    {
        case UsbHsFsDriveLogicalUnitFileSystemType_FAT:     /* FAT12/FAT16/FAT32/exFAT. */
            fs_device = ffdev_get_devoptab();
            break;
#ifdef GPL_BUILD
        case UsbHsFsDriveLogicalUnitFileSystemType_NTFS:    /* NTFS. */
            fs_device = ntfsdev_get_devoptab();
            break;
        case UsbHsFsDriveLogicalUnitFileSystemType_EXT:     /* EXT2/3/4. */
            fs_device = extdev_get_devoptab();
            break;
#endif
        default:
            USBHSFS_LOG_MSG("Invalid FS type provided! (0x%02X) (interface %d, LUN %u, FS %u).", fs_ctx->fs_type, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
            break;
    }
    
    if (!fs_device)
    {
        USBHSFS_LOG_MSG("Failed to get pointer to devoptab interface! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Copy devoptab interface data and set mount name and device data. */
    memcpy(fs_ctx->device, fs_device, sizeof(devoptab_t));
    fs_ctx->device->name = fs_ctx->name;
    fs_ctx->device->deviceData = fs_ctx;
    
    /* Add devoptab device. */
    ad_res = AddDevice(fs_ctx->device);
    if (ad_res < 0)
    {
        USBHSFS_LOG_MSG("AddDevice failed! (%d) (interface %d, LUN %u, FS %u).", ad_res, lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    /* Reallocate devoptab device IDs buffer. */
    tmp_device_ids = realloc(g_devoptabDeviceIds, (g_devoptabDeviceCount + 1) * sizeof(u32));
    if (!tmp_device_ids)
    {
        USBHSFS_LOG_MSG("Failed to reallocate devoptab device IDs buffer! (interface %d, LUN %u, FS %u).", lun_ctx->usb_if_id, lun_ctx->lun, fs_ctx->fs_idx);
        goto end;
    }
    
    g_devoptabDeviceIds = tmp_device_ids;
    tmp_device_ids = NULL;
    
    /* Store devoptab device ID and increase devoptab virtual device count. */
    g_devoptabDeviceIds[g_devoptabDeviceCount++] = fs_ctx->device_id;
    
    /* Update return value. */
    ret = true;
    
end:
    /* Free stuff if something went wrong. */
    if (!ret)
    {
        if (ad_res >= 0) RemoveDevice(name);
        
        if (fs_ctx->device)
        {
            free(fs_ctx->device);
            fs_ctx->device = NULL;
        }
        
        if (fs_ctx->cwd)
        {
            free(fs_ctx->cwd);
            fs_ctx->cwd = NULL;
        }
        
        if (fs_ctx->name)
        {
            free(fs_ctx->name);
            fs_ctx->name = NULL;
        }
    }
    
    return ret;
}

static u32 usbHsFsMountGetAvailableDevoptabDeviceId(void)
{
    if (!g_devoptabDeviceCount || !g_devoptabDeviceIds) return 0;
    
    u32 i = 0, ret = 0;
    
    while(true)
    {
        if (i >= g_devoptabDeviceCount) break;
        
        if (ret == g_devoptabDeviceIds[i])
        {
            ret++;
            i = 0;
        } else {
            i++;
        }
    }
    
    return ret;
}

static void usbHsFsMountUnsetDefaultDevoptabDevice(u32 device_id)
{
    SCOPED_LOCK(&g_devoptabDefaultDeviceMutex)
    {
        /* Check if the provided device ID matches the current default devoptab device ID. */
        if (g_devoptabDefaultDeviceId == DEVOPTAB_INVALID_ID || g_devoptabDefaultDeviceId != device_id) break;
        
        USBHSFS_LOG_MSG("Current default devoptab device matches provided device ID! (%u).", device_id);
        
        u32 cur_device_id = 0;
        const devoptab_t *cur_default_devoptab = GetDeviceOpTab("");
        
        /* Check if the current default devoptab device is the one we previously set. */
        /* If so, set the SD card as the new default devoptab device. */
        if (cur_default_devoptab && cur_default_devoptab->name && strlen(cur_default_devoptab->name) >= 4 && sscanf(cur_default_devoptab->name, MOUNT_NAME_PREFIX "%u", &cur_device_id) == 1 && \
            cur_device_id == device_id)
        {
            USBHSFS_LOG_MSG("Setting SD card as the default devoptab device.");
            setDefaultDevice(FindDevice("sdmc:"));
        }
        
        /* Update default device ID. */
        g_devoptabDefaultDeviceId = DEVOPTAB_INVALID_ID;
    }
}
