diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 88644eeb69..eafb96b0b3 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -109,7 +109,6 @@ add_library(common STATIC
     cityhash.cpp
     cityhash.h
     common_funcs.h
-    common_paths.h
     common_sizes.h
     common_types.h
     concepts.h
@@ -118,8 +117,16 @@ add_library(common STATIC
     dynamic_library.h
     fiber.cpp
     fiber.h
-    file_util.cpp
-    file_util.h
+    fs/file.cpp
+    fs/file.h
+    fs/fs.cpp
+    fs/fs.h
+    fs/fs_paths.h
+    fs/fs_types.h
+    fs/fs_util.cpp
+    fs/fs_util.h
+    fs/path_util.cpp
+    fs/path_util.h
     hash.h
     hex_util.cpp
     hex_util.h
diff --git a/src/common/common_paths.h b/src/common/common_paths.h
deleted file mode 100644
index 3c593d5f6c..0000000000
--- a/src/common/common_paths.h
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-// Directory separators, do we need this?
-#define DIR_SEP "/"
-#define DIR_SEP_CHR '/'
-
-#ifndef MAX_PATH
-#define MAX_PATH 260
-#endif
-
-// The user data dir
-#define ROOT_DIR "."
-#define USERDATA_DIR "user"
-#ifdef USER_DIR
-#define EMU_DATA_DIR USER_DIR
-#else
-#define EMU_DATA_DIR "yuzu"
-#endif
-
-// Dirs in both User and Sys
-#define EUR_DIR "EUR"
-#define USA_DIR "USA"
-#define JAP_DIR "JAP"
-
-// Subdirs in the User dir returned by GetUserPath(UserPath::UserDir)
-#define CONFIG_DIR "config"
-#define CACHE_DIR "cache"
-#define SDMC_DIR "sdmc"
-#define NAND_DIR "nand"
-#define SYSDATA_DIR "sysdata"
-#define KEYS_DIR "keys"
-#define LOAD_DIR "load"
-#define DUMP_DIR "dump"
-#define SCREENSHOTS_DIR "screenshots"
-#define SHADER_DIR "shader"
-#define LOG_DIR "log"
-
-// Filenames
-// Files in the directory returned by GetUserPath(UserPath::ConfigDir)
-#define EMU_CONFIG "emu.ini"
-#define DEBUGGER_CONFIG "debugger.ini"
-#define LOGGER_CONFIG "logger.ini"
-// Files in the directory returned by GetUserPath(UserPath::LogDir)
-#define LOG_FILE "yuzu_log.txt"
-
-// Sys files
-#define SHARED_FONT "shared_font.bin"
-#define AES_KEYS "aes_keys.txt"
diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp
deleted file mode 100644
index 18fbfa25bf..0000000000
--- a/src/common/file_util.cpp
+++ /dev/null
@@ -1,1032 +0,0 @@
-// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#include <array>
-#include <limits>
-#include <memory>
-#include <sstream>
-#include <unordered_map>
-#include "common/assert.h"
-#include "common/common_funcs.h"
-#include "common/common_paths.h"
-#include "common/file_util.h"
-#include "common/logging/log.h"
-
-#ifdef _WIN32
-#include <windows.h>
-// windows.h needs to be included before other windows headers
-#include <direct.h> // getcwd
-#include <io.h>
-#include <shellapi.h>
-#include <shlobj.h> // for SHGetFolderPath
-#include <tchar.h>
-#include "common/string_util.h"
-
-#ifdef _MSC_VER
-// 64 bit offsets for MSVC
-#define fseeko _fseeki64
-#define ftello _ftelli64
-#define fileno _fileno
-#endif
-
-// 64 bit offsets for MSVC and MinGW. MinGW also needs this for using _wstat64
-#define stat _stat64
-#define fstat _fstat64
-
-#else
-#ifdef __APPLE__
-#include <sys/param.h>
-#endif
-#include <cctype>
-#include <cerrno>
-#include <cstdlib>
-#include <cstring>
-#include <dirent.h>
-#include <pwd.h>
-#include <unistd.h>
-#endif
-
-#if defined(__APPLE__)
-// CFURL contains __attribute__ directives that gcc does not know how to parse, so we need to just
-// ignore them if we're not using clang. The macro is only used to prevent linking against
-// functions that don't exist on older versions of macOS, and the worst case scenario is a linker
-// error, so this is perfectly safe, just inconvenient.
-#ifndef __clang__
-#define availability(...)
-#endif
-#include <CoreFoundation/CFBundle.h>
-#include <CoreFoundation/CFString.h>
-#include <CoreFoundation/CFURL.h>
-#ifdef availability
-#undef availability
-#endif
-
-#endif
-
-#include <algorithm>
-#include <sys/stat.h>
-
-#ifndef S_ISDIR
-#define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
-#endif
-
-// This namespace has various generic functions related to files and paths.
-// The code still needs a ton of cleanup.
-// REMEMBER: strdup considered harmful!
-namespace Common::FS {
-
-// Remove any ending forward slashes from directory paths
-// Modifies argument.
-static void StripTailDirSlashes(std::string& fname) {
-    if (fname.length() <= 1) {
-        return;
-    }
-
-    std::size_t i = fname.length();
-    while (i > 0 && fname[i - 1] == DIR_SEP_CHR) {
-        --i;
-    }
-    fname.resize(i);
-}
-
-bool Exists(const std::string& filename) {
-    struct stat file_info;
-
-    std::string copy(filename);
-    StripTailDirSlashes(copy);
-
-#ifdef _WIN32
-    // Windows needs a slash to identify a driver root
-    if (copy.size() != 0 && copy.back() == ':')
-        copy += DIR_SEP_CHR;
-
-    int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
-#else
-    int result = stat(copy.c_str(), &file_info);
-#endif
-
-    return (result == 0);
-}
-
-bool IsDirectory(const std::string& filename) {
-    struct stat file_info;
-
-    std::string copy(filename);
-    StripTailDirSlashes(copy);
-
-#ifdef _WIN32
-    // Windows needs a slash to identify a driver root
-    if (copy.size() != 0 && copy.back() == ':')
-        copy += DIR_SEP_CHR;
-
-    int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info);
-#else
-    int result = stat(copy.c_str(), &file_info);
-#endif
-
-    if (result < 0) {
-        LOG_DEBUG(Common_Filesystem, "stat failed on {}: {}", filename, GetLastErrorMsg());
-        return false;
-    }
-
-    return S_ISDIR(file_info.st_mode);
-}
-
-bool Delete(const std::string& filename) {
-    LOG_TRACE(Common_Filesystem, "file {}", filename);
-
-    // Return true because we care about the file no
-    // being there, not the actual delete.
-    if (!Exists(filename)) {
-        LOG_DEBUG(Common_Filesystem, "{} does not exist", filename);
-        return true;
-    }
-
-    // We can't delete a directory
-    if (IsDirectory(filename)) {
-        LOG_ERROR(Common_Filesystem, "Failed: {} is a directory", filename);
-        return false;
-    }
-
-#ifdef _WIN32
-    if (!DeleteFileW(Common::UTF8ToUTF16W(filename).c_str())) {
-        LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg());
-        return false;
-    }
-#else
-    if (unlink(filename.c_str()) == -1) {
-        LOG_ERROR(Common_Filesystem, "unlink failed on {}: {}", filename, GetLastErrorMsg());
-        return false;
-    }
-#endif
-
-    return true;
-}
-
-bool CreateDir(const std::string& path) {
-    LOG_TRACE(Common_Filesystem, "directory {}", path);
-#ifdef _WIN32
-    if (::CreateDirectoryW(Common::UTF8ToUTF16W(path).c_str(), nullptr))
-        return true;
-    DWORD error = GetLastError();
-    if (error == ERROR_ALREADY_EXISTS) {
-        LOG_DEBUG(Common_Filesystem, "CreateDirectory failed on {}: already exists", path);
-        return true;
-    }
-    LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error);
-    return false;
-#else
-    if (mkdir(path.c_str(), 0755) == 0)
-        return true;
-
-    int err = errno;
-
-    if (err == EEXIST) {
-        LOG_DEBUG(Common_Filesystem, "mkdir failed on {}: already exists", path);
-        return true;
-    }
-
-    LOG_ERROR(Common_Filesystem, "mkdir failed on {}: {}", path, strerror(err));
-    return false;
-#endif
-}
-
-bool CreateFullPath(const std::string& fullPath) {
-    int panicCounter = 100;
-    LOG_TRACE(Common_Filesystem, "path {}", fullPath);
-
-    if (Exists(fullPath)) {
-        LOG_DEBUG(Common_Filesystem, "path exists {}", fullPath);
-        return true;
-    }
-
-    std::size_t position = 0;
-    while (true) {
-        // Find next sub path
-        position = fullPath.find(DIR_SEP_CHR, position);
-
-        // we're done, yay!
-        if (position == fullPath.npos)
-            return true;
-
-        // Include the '/' so the first call is CreateDir("/") rather than CreateDir("")
-        std::string const subPath(fullPath.substr(0, position + 1));
-        if (!IsDirectory(subPath) && !CreateDir(subPath)) {
-            LOG_ERROR(Common, "CreateFullPath: directory creation failed");
-            return false;
-        }
-
-        // A safety check
-        panicCounter--;
-        if (panicCounter <= 0) {
-            LOG_ERROR(Common, "CreateFullPath: directory structure is too deep");
-            return false;
-        }
-        position++;
-    }
-}
-
-bool DeleteDir(const std::string& filename) {
-    LOG_TRACE(Common_Filesystem, "directory {}", filename);
-
-    // check if a directory
-    if (!IsDirectory(filename)) {
-        LOG_ERROR(Common_Filesystem, "Not a directory {}", filename);
-        return false;
-    }
-
-#ifdef _WIN32
-    if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str()))
-        return true;
-#else
-    if (rmdir(filename.c_str()) == 0)
-        return true;
-#endif
-    LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg());
-
-    return false;
-}
-
-bool Rename(const std::string& srcFilename, const std::string& destFilename) {
-    LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
-#ifdef _WIN32
-    if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(),
-                 Common::UTF8ToUTF16W(destFilename).c_str()) == 0)
-        return true;
-#else
-    if (rename(srcFilename.c_str(), destFilename.c_str()) == 0)
-        return true;
-#endif
-    LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
-              GetLastErrorMsg());
-    return false;
-}
-
-bool Copy(const std::string& srcFilename, const std::string& destFilename) {
-    LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename);
-#ifdef _WIN32
-    if (CopyFileW(Common::UTF8ToUTF16W(srcFilename).c_str(),
-                  Common::UTF8ToUTF16W(destFilename).c_str(), FALSE))
-        return true;
-
-    LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename,
-              GetLastErrorMsg());
-    return false;
-#else
-    using CFilePointer = std::unique_ptr<FILE, decltype(&std::fclose)>;
-
-    // Open input file
-    CFilePointer input{fopen(srcFilename.c_str(), "rb"), std::fclose};
-    if (!input) {
-        LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename,
-                  destFilename, GetLastErrorMsg());
-        return false;
-    }
-
-    // open output file
-    CFilePointer output{fopen(destFilename.c_str(), "wb"), std::fclose};
-    if (!output) {
-        LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename,
-                  destFilename, GetLastErrorMsg());
-        return false;
-    }
-
-    // copy loop
-    std::array<char, 1024> buffer;
-    while (!feof(input.get())) {
-        // read input
-        std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input.get());
-        if (rnum != buffer.size()) {
-            if (ferror(input.get()) != 0) {
-                LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}",
-                          srcFilename, destFilename, GetLastErrorMsg());
-                return false;
-            }
-        }
-
-        // write output
-        std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output.get());
-        if (wnum != rnum) {
-            LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename,
-                      destFilename, GetLastErrorMsg());
-            return false;
-        }
-    }
-
-    return true;
-#endif
-}
-
-u64 GetSize(const std::string& filename) {
-    if (!Exists(filename)) {
-        LOG_ERROR(Common_Filesystem, "failed {}: No such file", filename);
-        return 0;
-    }
-
-    if (IsDirectory(filename)) {
-        LOG_ERROR(Common_Filesystem, "failed {}: is a directory", filename);
-        return 0;
-    }
-
-    struct stat buf;
-#ifdef _WIN32
-    if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0)
-#else
-    if (stat(filename.c_str(), &buf) == 0)
-#endif
-    {
-        LOG_TRACE(Common_Filesystem, "{}: {}", filename, buf.st_size);
-        return buf.st_size;
-    }
-
-    LOG_ERROR(Common_Filesystem, "Stat failed {}: {}", filename, GetLastErrorMsg());
-    return 0;
-}
-
-u64 GetSize(const int fd) {
-    struct stat buf;
-    if (fstat(fd, &buf) != 0) {
-        LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg());
-        return 0;
-    }
-    return buf.st_size;
-}
-
-u64 GetSize(FILE* f) {
-    // can't use off_t here because it can be 32-bit
-    u64 pos = ftello(f);
-    if (fseeko(f, 0, SEEK_END) != 0) {
-        LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
-        return 0;
-    }
-    u64 size = ftello(f);
-    if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) {
-        LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg());
-        return 0;
-    }
-    return size;
-}
-
-bool CreateEmptyFile(const std::string& filename) {
-    LOG_TRACE(Common_Filesystem, "{}", filename);
-
-    if (!IOFile(filename, "wb").IsOpen()) {
-        LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg());
-        return false;
-    }
-
-    return true;
-}
-
-bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
-                           DirectoryEntryCallable callback) {
-    LOG_TRACE(Common_Filesystem, "directory {}", directory);
-
-    // How many files + directories we found
-    u64 found_entries = 0;
-
-    // Save the status of callback function
-    bool callback_error = false;
-
-#ifdef _WIN32
-    // Find the first file in the directory.
-    WIN32_FIND_DATAW ffd;
-
-    HANDLE handle_find = FindFirstFileW(Common::UTF8ToUTF16W(directory + "\\*").c_str(), &ffd);
-    if (handle_find == INVALID_HANDLE_VALUE) {
-        FindClose(handle_find);
-        return false;
-    }
-    // windows loop
-    do {
-        const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName));
-#else
-    DIR* dirp = opendir(directory.c_str());
-    if (!dirp)
-        return false;
-
-    // non windows loop
-    while (struct dirent* result = readdir(dirp)) {
-        const std::string virtual_name(result->d_name);
-#endif
-
-        if (virtual_name == "." || virtual_name == "..")
-            continue;
-
-        u64 ret_entries = 0;
-        if (!callback(&ret_entries, directory, virtual_name)) {
-            callback_error = true;
-            break;
-        }
-        found_entries += ret_entries;
-
-#ifdef _WIN32
-    } while (FindNextFileW(handle_find, &ffd) != 0);
-    FindClose(handle_find);
-#else
-    }
-    closedir(dirp);
-#endif
-
-    if (callback_error)
-        return false;
-
-    // num_entries_out is allowed to be specified nullptr, in which case we shouldn't try to set it
-    if (num_entries_out != nullptr)
-        *num_entries_out = found_entries;
-    return true;
-}
-
-u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
-                      unsigned int recursion) {
-    const auto callback = [recursion, &parent_entry](u64* num_entries_out,
-                                                     const std::string& directory,
-                                                     const std::string& virtual_name) -> bool {
-        FSTEntry entry;
-        entry.virtualName = virtual_name;
-        entry.physicalName = directory + DIR_SEP + virtual_name;
-
-        if (IsDirectory(entry.physicalName)) {
-            entry.isDirectory = true;
-            // is a directory, lets go inside if we didn't recurse to often
-            if (recursion > 0) {
-                entry.size = ScanDirectoryTree(entry.physicalName, entry, recursion - 1);
-                *num_entries_out += entry.size;
-            } else {
-                entry.size = 0;
-            }
-        } else { // is a file
-            entry.isDirectory = false;
-            entry.size = GetSize(entry.physicalName);
-        }
-        (*num_entries_out)++;
-
-        // Push into the tree
-        parent_entry.children.push_back(std::move(entry));
-        return true;
-    };
-
-    u64 num_entries;
-    return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0;
-}
-
-bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) {
-    const auto callback = [recursion](u64*, const std::string& directory,
-                                      const std::string& virtual_name) {
-        const std::string new_path = directory + DIR_SEP_CHR + virtual_name;
-
-        if (IsDirectory(new_path)) {
-            if (recursion == 0) {
-                return false;
-            }
-            return DeleteDirRecursively(new_path, recursion - 1);
-        }
-        return Delete(new_path);
-    };
-
-    if (!ForeachDirectoryEntry(nullptr, directory, callback))
-        return false;
-
-    // Delete the outermost directory
-    DeleteDir(directory);
-    return true;
-}
-
-void CopyDir([[maybe_unused]] const std::string& source_path,
-             [[maybe_unused]] const std::string& dest_path) {
-#ifndef _WIN32
-    if (source_path == dest_path) {
-        return;
-    }
-    if (!Exists(source_path)) {
-        return;
-    }
-    if (!Exists(dest_path)) {
-        CreateFullPath(dest_path);
-    }
-
-    DIR* dirp = opendir(source_path.c_str());
-    if (!dirp) {
-        return;
-    }
-
-    while (struct dirent* result = readdir(dirp)) {
-        const std::string virtualName(result->d_name);
-        // check for "." and ".."
-        if (((virtualName[0] == '.') && (virtualName[1] == '\0')) ||
-            ((virtualName[0] == '.') && (virtualName[1] == '.') && (virtualName[2] == '\0'))) {
-            continue;
-        }
-
-        std::string source, dest;
-        source = source_path + virtualName;
-        dest = dest_path + virtualName;
-        if (IsDirectory(source)) {
-            source += '/';
-            dest += '/';
-            if (!Exists(dest)) {
-                CreateFullPath(dest);
-            }
-            CopyDir(source, dest);
-        } else if (!Exists(dest)) {
-            Copy(source, dest);
-        }
-    }
-    closedir(dirp);
-#endif
-}
-
-std::optional<std::string> GetCurrentDir() {
-// Get the current working directory (getcwd uses malloc)
-#ifdef _WIN32
-    wchar_t* dir = _wgetcwd(nullptr, 0);
-    if (!dir) {
-#else
-    char* dir = getcwd(nullptr, 0);
-    if (!dir) {
-#endif
-        LOG_ERROR(Common_Filesystem, "GetCurrentDirectory failed: {}", GetLastErrorMsg());
-        return std::nullopt;
-    }
-#ifdef _WIN32
-    std::string strDir = Common::UTF16ToUTF8(dir);
-#else
-    std::string strDir = dir;
-#endif
-    free(dir);
-    return strDir;
-}
-
-bool SetCurrentDir(const std::string& directory) {
-#ifdef _WIN32
-    return _wchdir(Common::UTF8ToUTF16W(directory).c_str()) == 0;
-#else
-    return chdir(directory.c_str()) == 0;
-#endif
-}
-
-#if defined(__APPLE__)
-std::string GetBundleDirectory() {
-    CFURLRef BundleRef;
-    char AppBundlePath[MAXPATHLEN];
-    // Get the main bundle for the app
-    BundleRef = CFBundleCopyBundleURL(CFBundleGetMainBundle());
-    CFStringRef BundlePath = CFURLCopyFileSystemPath(BundleRef, kCFURLPOSIXPathStyle);
-    CFStringGetFileSystemRepresentation(BundlePath, AppBundlePath, sizeof(AppBundlePath));
-    CFRelease(BundleRef);
-    CFRelease(BundlePath);
-
-    return AppBundlePath;
-}
-#endif
-
-#ifdef _WIN32
-const std::string& GetExeDirectory() {
-    static std::string exe_path;
-    if (exe_path.empty()) {
-        wchar_t wchar_exe_path[2048];
-        GetModuleFileNameW(nullptr, wchar_exe_path, 2048);
-        exe_path = Common::UTF16ToUTF8(wchar_exe_path);
-        exe_path = exe_path.substr(0, exe_path.find_last_of('\\'));
-    }
-    return exe_path;
-}
-
-std::string AppDataRoamingDirectory() {
-    PWSTR pw_local_path = nullptr;
-    // Only supported by Windows Vista or later
-    SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pw_local_path);
-    std::string local_path = Common::UTF16ToUTF8(pw_local_path);
-    CoTaskMemFree(pw_local_path);
-    return local_path;
-}
-#else
-/**
- * @return The user’s home directory on POSIX systems
- */
-static const std::string& GetHomeDirectory() {
-    static std::string home_path;
-    if (home_path.empty()) {
-        const char* envvar = getenv("HOME");
-        if (envvar) {
-            home_path = envvar;
-        } else {
-            auto pw = getpwuid(getuid());
-            ASSERT_MSG(pw,
-                       "$HOME isn’t defined, and the current user can’t be found in /etc/passwd.");
-            home_path = pw->pw_dir;
-        }
-    }
-    return home_path;
-}
-
-/**
- * Follows the XDG Base Directory Specification to get a directory path
- * @param envvar The XDG environment variable to get the value from
- * @return The directory path
- * @sa http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
- */
-static const std::string GetUserDirectory(const std::string& envvar) {
-    const char* directory = getenv(envvar.c_str());
-
-    std::string user_dir;
-    if (directory) {
-        user_dir = directory;
-    } else {
-        std::string subdirectory;
-        if (envvar == "XDG_DATA_HOME")
-            subdirectory = DIR_SEP ".local" DIR_SEP "share";
-        else if (envvar == "XDG_CONFIG_HOME")
-            subdirectory = DIR_SEP ".config";
-        else if (envvar == "XDG_CACHE_HOME")
-            subdirectory = DIR_SEP ".cache";
-        else
-            ASSERT_MSG(false, "Unknown XDG variable {}.", envvar);
-        user_dir = GetHomeDirectory() + subdirectory;
-    }
-
-    ASSERT_MSG(!user_dir.empty(), "User directory {} mustn’t be empty.", envvar);
-    ASSERT_MSG(user_dir[0] == '/', "User directory {} must be absolute.", envvar);
-
-    return user_dir;
-}
-#endif
-
-std::string GetSysDirectory() {
-    std::string sysDir;
-
-#if defined(__APPLE__)
-    sysDir = GetBundleDirectory();
-    sysDir += DIR_SEP;
-    sysDir += SYSDATA_DIR;
-#else
-    sysDir = SYSDATA_DIR;
-#endif
-    sysDir += DIR_SEP;
-
-    LOG_DEBUG(Common_Filesystem, "Setting to {}:", sysDir);
-    return sysDir;
-}
-
-const std::string& GetUserPath(UserPath path, const std::string& new_path) {
-    static std::unordered_map<UserPath, std::string> paths;
-    auto& user_path = paths[UserPath::UserDir];
-
-    // Set up all paths and files on the first run
-    if (user_path.empty()) {
-#ifdef _WIN32
-        user_path = GetExeDirectory() + DIR_SEP USERDATA_DIR DIR_SEP;
-        if (!IsDirectory(user_path)) {
-            user_path = AppDataRoamingDirectory() + DIR_SEP EMU_DATA_DIR DIR_SEP;
-        } else {
-            LOG_INFO(Common_Filesystem, "Using the local user directory");
-        }
-
-        paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
-        paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
-#else
-        if (Exists(ROOT_DIR DIR_SEP USERDATA_DIR)) {
-            user_path = ROOT_DIR DIR_SEP USERDATA_DIR DIR_SEP;
-            paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP);
-            paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP);
-        } else {
-            std::string data_dir = GetUserDirectory("XDG_DATA_HOME");
-            std::string config_dir = GetUserDirectory("XDG_CONFIG_HOME");
-            std::string cache_dir = GetUserDirectory("XDG_CACHE_HOME");
-
-            user_path = data_dir + DIR_SEP EMU_DATA_DIR DIR_SEP;
-            paths.emplace(UserPath::ConfigDir, config_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
-            paths.emplace(UserPath::CacheDir, cache_dir + DIR_SEP EMU_DATA_DIR DIR_SEP);
-        }
-#endif
-        paths.emplace(UserPath::SDMCDir, user_path + SDMC_DIR DIR_SEP);
-        paths.emplace(UserPath::NANDDir, user_path + NAND_DIR DIR_SEP);
-        paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP);
-        paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP);
-        paths.emplace(UserPath::ScreenshotsDir, user_path + SCREENSHOTS_DIR DIR_SEP);
-        paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP);
-        paths.emplace(UserPath::SysDataDir, user_path + SYSDATA_DIR DIR_SEP);
-        paths.emplace(UserPath::KeysDir, user_path + KEYS_DIR DIR_SEP);
-        // TODO: Put the logs in a better location for each OS
-        paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP);
-    }
-
-    if (!new_path.empty()) {
-        if (!IsDirectory(new_path)) {
-            LOG_ERROR(Common_Filesystem, "Invalid path specified {}", new_path);
-            return paths[path];
-        } else {
-            paths[path] = new_path;
-        }
-
-        switch (path) {
-        case UserPath::RootDir:
-            user_path = paths[UserPath::RootDir] + DIR_SEP;
-            break;
-        case UserPath::UserDir:
-            user_path = paths[UserPath::RootDir] + DIR_SEP;
-            paths[UserPath::ConfigDir] = user_path + CONFIG_DIR DIR_SEP;
-            paths[UserPath::CacheDir] = user_path + CACHE_DIR DIR_SEP;
-            paths[UserPath::SDMCDir] = user_path + SDMC_DIR DIR_SEP;
-            paths[UserPath::NANDDir] = user_path + NAND_DIR DIR_SEP;
-            break;
-        default:
-            break;
-        }
-    }
-
-    return paths[path];
-}
-
-std::string GetHactoolConfigurationPath() {
-#ifdef _WIN32
-    PWSTR pw_local_path = nullptr;
-    if (SHGetKnownFolderPath(FOLDERID_Profile, 0, nullptr, &pw_local_path) != S_OK)
-        return "";
-    std::string local_path = Common::UTF16ToUTF8(pw_local_path);
-    CoTaskMemFree(pw_local_path);
-    return local_path + "\\.switch";
-#else
-    return GetHomeDirectory() + "/.switch";
-#endif
-}
-
-std::string GetNANDRegistrationDir(bool system) {
-    if (system)
-        return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/";
-    return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/";
-}
-
-std::size_t WriteStringToFile(bool text_file, const std::string& filename, std::string_view str) {
-    return IOFile(filename, text_file ? "w" : "wb").WriteString(str);
-}
-
-std::size_t ReadFileToString(bool text_file, const std::string& filename, std::string& str) {
-    IOFile file(filename, text_file ? "r" : "rb");
-
-    if (!file.IsOpen())
-        return 0;
-
-    str.resize(static_cast<u32>(file.GetSize()));
-    return file.ReadArray(&str[0], str.size());
-}
-
-void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name,
-                     std::array<char, 4>& extension) {
-    static constexpr std::string_view forbidden_characters = ".\"/\\[]:;=, ";
-
-    // On a FAT32 partition, 8.3 names are stored as a 11 bytes array, filled with spaces.
-    short_name = {{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\0'}};
-    extension = {{' ', ' ', ' ', '\0'}};
-
-    auto point = filename.rfind('.');
-    if (point == filename.size() - 1) {
-        point = filename.rfind('.', point);
-    }
-
-    // Get short name.
-    int j = 0;
-    for (char letter : filename.substr(0, point)) {
-        if (forbidden_characters.find(letter, 0) != std::string::npos) {
-            continue;
-        }
-        if (j == 8) {
-            // TODO(Link Mauve): also do that for filenames containing a space.
-            // TODO(Link Mauve): handle multiple files having the same short name.
-            short_name[6] = '~';
-            short_name[7] = '1';
-            break;
-        }
-        short_name[j++] = static_cast<char>(std::toupper(letter));
-    }
-
-    // Get extension.
-    if (point != std::string::npos) {
-        j = 0;
-        for (char letter : filename.substr(point + 1, 3)) {
-            extension[j++] = static_cast<char>(std::toupper(letter));
-        }
-    }
-}
-
-std::vector<std::string> SplitPathComponents(std::string_view filename) {
-    std::string copy(filename);
-    std::replace(copy.begin(), copy.end(), '\\', '/');
-    std::vector<std::string> out;
-
-    std::stringstream stream(copy);
-    std::string item;
-    while (std::getline(stream, item, '/')) {
-        out.push_back(std::move(item));
-    }
-
-    return out;
-}
-
-std::string_view GetParentPath(std::string_view path) {
-    const auto name_bck_index = path.rfind('\\');
-    const auto name_fwd_index = path.rfind('/');
-    std::size_t name_index;
-
-    if (name_bck_index == std::string_view::npos || name_fwd_index == std::string_view::npos) {
-        name_index = std::min(name_bck_index, name_fwd_index);
-    } else {
-        name_index = std::max(name_bck_index, name_fwd_index);
-    }
-
-    return path.substr(0, name_index);
-}
-
-std::string_view GetPathWithoutTop(std::string_view path) {
-    if (path.empty()) {
-        return path;
-    }
-
-    while (path[0] == '\\' || path[0] == '/') {
-        path.remove_prefix(1);
-        if (path.empty()) {
-            return path;
-        }
-    }
-
-    const auto name_bck_index = path.find('\\');
-    const auto name_fwd_index = path.find('/');
-    return path.substr(std::min(name_bck_index, name_fwd_index) + 1);
-}
-
-std::string_view GetFilename(std::string_view path) {
-    const auto name_index = path.find_last_of("\\/");
-
-    if (name_index == std::string_view::npos) {
-        return {};
-    }
-
-    return path.substr(name_index + 1);
-}
-
-std::string_view GetExtensionFromFilename(std::string_view name) {
-    const std::size_t index = name.rfind('.');
-
-    if (index == std::string_view::npos) {
-        return {};
-    }
-
-    return name.substr(index + 1);
-}
-
-std::string_view RemoveTrailingSlash(std::string_view path) {
-    if (path.empty()) {
-        return path;
-    }
-
-    if (path.back() == '\\' || path.back() == '/') {
-        path.remove_suffix(1);
-        return path;
-    }
-
-    return path;
-}
-
-std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
-    std::string path(path_);
-    char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
-    char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
-
-    if (directory_separator == DirectorySeparator::PlatformDefault) {
-#ifdef _WIN32
-        type1 = '/';
-        type2 = '\\';
-#endif
-    }
-
-    std::replace(path.begin(), path.end(), type1, type2);
-
-    auto start = path.begin();
-#ifdef _WIN32
-    // allow network paths which start with a double backslash (e.g. \\server\share)
-    if (start != path.end())
-        ++start;
-#endif
-    path.erase(std::unique(start, path.end(),
-                           [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }),
-               path.end());
-    return std::string(RemoveTrailingSlash(path));
-}
-
-IOFile::IOFile() = default;
-
-IOFile::IOFile(const std::string& filename, const char openmode[], int flags) {
-    void(Open(filename, openmode, flags));
-}
-
-IOFile::~IOFile() {
-    Close();
-}
-
-IOFile::IOFile(IOFile&& other) noexcept {
-    Swap(other);
-}
-
-IOFile& IOFile::operator=(IOFile&& other) noexcept {
-    Swap(other);
-    return *this;
-}
-
-void IOFile::Swap(IOFile& other) noexcept {
-    std::swap(m_file, other.m_file);
-}
-
-bool IOFile::Open(const std::string& filename, const char openmode[], int flags) {
-    Close();
-    bool m_good;
-#ifdef _WIN32
-    if (flags != 0) {
-        m_file = _wfsopen(Common::UTF8ToUTF16W(filename).c_str(),
-                          Common::UTF8ToUTF16W(openmode).c_str(), flags);
-        m_good = m_file != nullptr;
-    } else {
-        m_good = _wfopen_s(&m_file, Common::UTF8ToUTF16W(filename).c_str(),
-                           Common::UTF8ToUTF16W(openmode).c_str()) == 0;
-    }
-#else
-    m_file = std::fopen(filename.c_str(), openmode);
-    m_good = m_file != nullptr;
-#endif
-
-    return m_good;
-}
-
-bool IOFile::Close() {
-    if (!IsOpen() || 0 != std::fclose(m_file)) {
-        return false;
-    }
-
-    m_file = nullptr;
-    return true;
-}
-
-u64 IOFile::GetSize() const {
-    if (IsOpen()) {
-        return FS::GetSize(m_file);
-    }
-    return 0;
-}
-
-bool IOFile::Seek(s64 off, int origin) const {
-    return IsOpen() && 0 == fseeko(m_file, off, origin);
-}
-
-u64 IOFile::Tell() const {
-    if (IsOpen()) {
-        return ftello(m_file);
-    }
-    return std::numeric_limits<u64>::max();
-}
-
-bool IOFile::Flush() {
-    return IsOpen() && 0 == std::fflush(m_file);
-}
-
-std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) const {
-    if (!IsOpen()) {
-        return std::numeric_limits<std::size_t>::max();
-    }
-
-    if (length == 0) {
-        return 0;
-    }
-
-    DEBUG_ASSERT(data != nullptr);
-
-    return std::fread(data, data_size, length, m_file);
-}
-
-std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) {
-    if (!IsOpen()) {
-        return std::numeric_limits<std::size_t>::max();
-    }
-
-    if (length == 0) {
-        return 0;
-    }
-
-    DEBUG_ASSERT(data != nullptr);
-
-    return std::fwrite(data, data_size, length, m_file);
-}
-
-bool IOFile::Resize(u64 size) {
-    return IsOpen() && 0 ==
-#ifdef _WIN32
-                           // ector: _chsize sucks, not 64-bit safe
-                           // F|RES: changed to _chsize_s. i think it is 64-bit safe
-                           _chsize_s(_fileno(m_file), size)
-#else
-                           // TODO: handle 64bit and growing
-                           ftruncate(fileno(m_file), size)
-#endif
-        ;
-}
-
-} // namespace Common::FS
diff --git a/src/common/file_util.h b/src/common/file_util.h
deleted file mode 100644
index 840cde2a68..0000000000
--- a/src/common/file_util.h
+++ /dev/null
@@ -1,298 +0,0 @@
-// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-#include <array>
-#include <cstdio>
-#include <fstream>
-#include <functional>
-#include <limits>
-#include <optional>
-#include <string>
-#include <string_view>
-#include <type_traits>
-#include <vector>
-#include "common/common_types.h"
-#ifdef _MSC_VER
-#include "common/string_util.h"
-#endif
-
-namespace Common::FS {
-
-// User paths for GetUserPath
-enum class UserPath {
-    CacheDir,
-    ConfigDir,
-    KeysDir,
-    LogDir,
-    NANDDir,
-    RootDir,
-    SDMCDir,
-    LoadDir,
-    DumpDir,
-    ScreenshotsDir,
-    ShaderDir,
-    SysDataDir,
-    UserDir,
-};
-
-// FileSystem tree node/
-struct FSTEntry {
-    bool isDirectory;
-    u64 size;                 // file length or number of entries from children
-    std::string physicalName; // name on disk
-    std::string virtualName;  // name in FST names table
-    std::vector<FSTEntry> children;
-};
-
-// Returns true if file filename exists
-[[nodiscard]] bool Exists(const std::string& filename);
-
-// Returns true if filename is a directory
-[[nodiscard]] bool IsDirectory(const std::string& filename);
-
-// Returns the size of filename (64bit)
-[[nodiscard]] u64 GetSize(const std::string& filename);
-
-// Overloaded GetSize, accepts file descriptor
-[[nodiscard]] u64 GetSize(int fd);
-
-// Overloaded GetSize, accepts FILE*
-[[nodiscard]] u64 GetSize(FILE* f);
-
-// Returns true if successful, or path already exists.
-bool CreateDir(const std::string& filename);
-
-// Creates the full path of fullPath returns true on success
-bool CreateFullPath(const std::string& fullPath);
-
-// Deletes a given filename, return true on success
-// Doesn't supports deleting a directory
-bool Delete(const std::string& filename);
-
-// Deletes a directory filename, returns true on success
-bool DeleteDir(const std::string& filename);
-
-// renames file srcFilename to destFilename, returns true on success
-bool Rename(const std::string& srcFilename, const std::string& destFilename);
-
-// copies file srcFilename to destFilename, returns true on success
-bool Copy(const std::string& srcFilename, const std::string& destFilename);
-
-// creates an empty file filename, returns true on success
-bool CreateEmptyFile(const std::string& filename);
-
-/**
- * @param num_entries_out to be assigned by the callable with the number of iterated directory
- * entries, never null
- * @param directory the path to the enclosing directory
- * @param virtual_name the entry name, without any preceding directory info
- * @return whether handling the entry succeeded
- */
-using DirectoryEntryCallable = std::function<bool(
-    u64* num_entries_out, const std::string& directory, const std::string& virtual_name)>;
-
-/**
- * Scans a directory, calling the callback for each file/directory contained within.
- * If the callback returns failure, scanning halts and this function returns failure as well
- * @param num_entries_out assigned by the function with the number of iterated directory entries,
- * can be null
- * @param directory the directory to scan
- * @param callback The callback which will be called for each entry
- * @return whether scanning the directory succeeded
- */
-bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory,
-                           DirectoryEntryCallable callback);
-
-/**
- * Scans the directory tree, storing the results.
- * @param directory the parent directory to start scanning from
- * @param parent_entry FSTEntry where the filesystem tree results will be stored.
- * @param recursion Number of children directories to read before giving up.
- * @return the total number of files/directories found
- */
-u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry,
-                      unsigned int recursion = 0);
-
-// deletes the given directory and anything under it. Returns true on success.
-bool DeleteDirRecursively(const std::string& directory, unsigned int recursion = 256);
-
-// Returns the current directory
-[[nodiscard]] std::optional<std::string> GetCurrentDir();
-
-// Create directory and copy contents (does not overwrite existing files)
-void CopyDir(const std::string& source_path, const std::string& dest_path);
-
-// Set the current directory to given directory
-bool SetCurrentDir(const std::string& directory);
-
-// Returns a pointer to a string with a yuzu data dir in the user's home
-// directory. To be used in "multi-user" mode (that is, installed).
-const std::string& GetUserPath(UserPath path, const std::string& new_path = "");
-
-[[nodiscard]] std::string GetHactoolConfigurationPath();
-
-[[nodiscard]] std::string GetNANDRegistrationDir(bool system = false);
-
-// Returns the path to where the sys file are
-[[nodiscard]] std::string GetSysDirectory();
-
-#ifdef __APPLE__
-[[nodiscard]] std::string GetBundleDirectory();
-#endif
-
-#ifdef _WIN32
-[[nodiscard]] const std::string& GetExeDirectory();
-[[nodiscard]] std::string AppDataRoamingDirectory();
-#endif
-
-std::size_t WriteStringToFile(bool text_file, const std::string& filename, std::string_view str);
-
-std::size_t ReadFileToString(bool text_file, const std::string& filename, std::string& str);
-
-/**
- * Splits the filename into 8.3 format
- * Loosely implemented following https://en.wikipedia.org/wiki/8.3_filename
- * @param filename The normal filename to use
- * @param short_name A 9-char array in which the short name will be written
- * @param extension A 4-char array in which the extension will be written
- */
-void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name,
-                     std::array<char, 4>& extension);
-
-// Splits the path on '/' or '\' and put the components into a vector
-// i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" }
-[[nodiscard]] std::vector<std::string> SplitPathComponents(std::string_view filename);
-
-// Gets all of the text up to the last '/' or '\' in the path.
-[[nodiscard]] std::string_view GetParentPath(std::string_view path);
-
-// Gets all of the text after the first '/' or '\' in the path.
-[[nodiscard]] std::string_view GetPathWithoutTop(std::string_view path);
-
-// Gets the filename of the path
-[[nodiscard]] std::string_view GetFilename(std::string_view path);
-
-// Gets the extension of the filename
-[[nodiscard]] std::string_view GetExtensionFromFilename(std::string_view name);
-
-// Removes the final '/' or '\' if one exists
-[[nodiscard]] std::string_view RemoveTrailingSlash(std::string_view path);
-
-// Creates a new vector containing indices [first, last) from the original.
-template <typename T>
-[[nodiscard]] std::vector<T> SliceVector(const std::vector<T>& vector, std::size_t first,
-                                         std::size_t last) {
-    if (first >= last) {
-        return {};
-    }
-    last = std::min<std::size_t>(last, vector.size());
-    return std::vector<T>(vector.begin() + first, vector.begin() + first + last);
-}
-
-enum class DirectorySeparator {
-    ForwardSlash,
-    BackwardSlash,
-    PlatformDefault,
-};
-
-// Removes trailing slash, makes all '\\' into '/', and removes duplicate '/'. Makes '/' into '\\'
-// depending if directory_separator is BackwardSlash or PlatformDefault and running on windows
-[[nodiscard]] std::string SanitizePath(
-    std::string_view path,
-    DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash);
-
-// To deal with Windows being dumb at Unicode
-template <typename T>
-void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode) {
-#ifdef _MSC_VER
-    fstream.open(Common::UTF8ToUTF16W(filename), openmode);
-#else
-    fstream.open(filename, openmode);
-#endif
-}
-
-// simple wrapper for cstdlib file functions to
-// hopefully will make error checking easier
-// and make forgetting an fclose() harder
-class IOFile : public NonCopyable {
-public:
-    IOFile();
-    // flags is used for windows specific file open mode flags, which
-    // allows yuzu to open the logs in shared write mode, so that the file
-    // isn't considered "locked" while yuzu is open and people can open the log file and view it
-    IOFile(const std::string& filename, const char openmode[], int flags = 0);
-
-    ~IOFile();
-
-    IOFile(IOFile&& other) noexcept;
-    IOFile& operator=(IOFile&& other) noexcept;
-
-    void Swap(IOFile& other) noexcept;
-
-    bool Open(const std::string& filename, const char openmode[], int flags = 0);
-    bool Close();
-
-    template <typename T>
-    std::size_t ReadArray(T* data, std::size_t length) const {
-        static_assert(std::is_trivially_copyable_v<T>,
-                      "Given array does not consist of trivially copyable objects");
-
-        return ReadImpl(data, length, sizeof(T));
-    }
-
-    template <typename T>
-    std::size_t WriteArray(const T* data, std::size_t length) {
-        static_assert(std::is_trivially_copyable_v<T>,
-                      "Given array does not consist of trivially copyable objects");
-
-        return WriteImpl(data, length, sizeof(T));
-    }
-
-    template <typename T>
-    std::size_t ReadBytes(T* data, std::size_t length) const {
-        static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
-        return ReadArray(reinterpret_cast<char*>(data), length);
-    }
-
-    template <typename T>
-    std::size_t WriteBytes(const T* data, std::size_t length) {
-        static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
-        return WriteArray(reinterpret_cast<const char*>(data), length);
-    }
-
-    template <typename T>
-    std::size_t WriteObject(const T& object) {
-        static_assert(!std::is_pointer_v<T>, "WriteObject arguments must not be a pointer");
-        return WriteArray(&object, 1);
-    }
-
-    std::size_t WriteString(std::string_view str) {
-        return WriteArray(str.data(), str.length());
-    }
-
-    [[nodiscard]] bool IsOpen() const {
-        return nullptr != m_file;
-    }
-
-    bool Seek(s64 off, int origin) const;
-    [[nodiscard]] u64 Tell() const;
-    [[nodiscard]] u64 GetSize() const;
-    bool Resize(u64 size);
-    bool Flush();
-
-    // clear error state
-    void Clear() {
-        std::clearerr(m_file);
-    }
-
-private:
-    std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) const;
-    std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size);
-
-    std::FILE* m_file = nullptr;
-};
-
-} // namespace Common::FS
diff --git a/src/common/fs/file.cpp b/src/common/fs/file.cpp
new file mode 100644
index 0000000000..9f3de1cb03
--- /dev/null
+++ b/src/common/fs/file.cpp
@@ -0,0 +1,392 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+
+#ifdef _WIN32
+#include <io.h>
+#include <share.h>
+#else
+#include <unistd.h>
+#endif
+
+#ifdef _MSC_VER
+#define fileno _fileno
+#define fseeko _fseeki64
+#define ftello _ftelli64
+#endif
+
+namespace Common::FS {
+
+namespace fs = std::filesystem;
+
+namespace {
+
+#ifdef _WIN32
+
+/**
+ * Converts the file access mode and file type enums to a file access mode wide string.
+ *
+ * @param mode File access mode
+ * @param type File type
+ *
+ * @returns A pointer to a wide string representing the file access mode.
+ */
+[[nodiscard]] constexpr const wchar_t* AccessModeToWStr(FileAccessMode mode, FileType type) {
+    switch (type) {
+    case FileType::BinaryFile:
+        switch (mode) {
+        case FileAccessMode::Read:
+            return L"rb";
+        case FileAccessMode::Write:
+            return L"wb";
+        case FileAccessMode::Append:
+            return L"ab";
+        case FileAccessMode::ReadWrite:
+            return L"r+b";
+        case FileAccessMode::ReadAppend:
+            return L"a+b";
+        }
+        break;
+    case FileType::TextFile:
+        switch (mode) {
+        case FileAccessMode::Read:
+            return L"r";
+        case FileAccessMode::Write:
+            return L"w";
+        case FileAccessMode::Append:
+            return L"a";
+        case FileAccessMode::ReadWrite:
+            return L"r+";
+        case FileAccessMode::ReadAppend:
+            return L"a+";
+        }
+        break;
+    }
+
+    return L"";
+}
+
+/**
+ * Converts the file-share access flag enum to a Windows defined file-share access flag.
+ *
+ * @param flag File-share access flag
+ *
+ * @returns Windows defined file-share access flag.
+ */
+[[nodiscard]] constexpr int ToWindowsFileShareFlag(FileShareFlag flag) {
+    switch (flag) {
+    case FileShareFlag::ShareNone:
+    default:
+        return _SH_DENYRW;
+    case FileShareFlag::ShareReadOnly:
+        return _SH_DENYWR;
+    case FileShareFlag::ShareWriteOnly:
+        return _SH_DENYRD;
+    case FileShareFlag::ShareReadWrite:
+        return _SH_DENYNO;
+    }
+}
+
+#else
+
+/**
+ * Converts the file access mode and file type enums to a file access mode string.
+ *
+ * @param mode File access mode
+ * @param type File type
+ *
+ * @returns A pointer to a string representing the file access mode.
+ */
+[[nodiscard]] constexpr const char* AccessModeToStr(FileAccessMode mode, FileType type) {
+    switch (type) {
+    case FileType::BinaryFile:
+        switch (mode) {
+        case FileAccessMode::Read:
+            return "rb";
+        case FileAccessMode::Write:
+            return "wb";
+        case FileAccessMode::Append:
+            return "ab";
+        case FileAccessMode::ReadWrite:
+            return "r+b";
+        case FileAccessMode::ReadAppend:
+            return "a+b";
+        }
+        break;
+    case FileType::TextFile:
+        switch (mode) {
+        case FileAccessMode::Read:
+            return "r";
+        case FileAccessMode::Write:
+            return "w";
+        case FileAccessMode::Append:
+            return "a";
+        case FileAccessMode::ReadWrite:
+            return "r+";
+        case FileAccessMode::ReadAppend:
+            return "a+";
+        }
+        break;
+    }
+
+    return "";
+}
+
+#endif
+
+/**
+ * Converts the seek origin enum to a seek origin integer.
+ *
+ * @param origin Seek origin
+ *
+ * @returns Seek origin integer.
+ */
+[[nodiscard]] constexpr int ToSeekOrigin(SeekOrigin origin) {
+    switch (origin) {
+    case SeekOrigin::SetOrigin:
+    default:
+        return SEEK_SET;
+    case SeekOrigin::CurrentPosition:
+        return SEEK_CUR;
+    case SeekOrigin::End:
+        return SEEK_END;
+    }
+}
+
+} // Anonymous namespace
+
+std::string ReadStringFromFile(const std::filesystem::path& path, FileType type) {
+    if (!IsFile(path)) {
+        return "";
+    }
+
+    IOFile io_file{path, FileAccessMode::Read, type};
+
+    return io_file.ReadString(io_file.GetSize());
+}
+
+size_t WriteStringToFile(const std::filesystem::path& path, FileType type,
+                         std::string_view string) {
+    if (!IsFile(path)) {
+        return 0;
+    }
+
+    IOFile io_file{path, FileAccessMode::Write, type};
+
+    return io_file.WriteString(string);
+}
+
+size_t AppendStringToFile(const std::filesystem::path& path, FileType type,
+                          std::string_view string) {
+    if (!Exists(path)) {
+        return WriteStringToFile(path, type, string);
+    }
+
+    if (!IsFile(path)) {
+        return 0;
+    }
+
+    IOFile io_file{path, FileAccessMode::Append, type};
+
+    return io_file.WriteString(string);
+}
+
+IOFile::IOFile() = default;
+
+IOFile::IOFile(const std::string& path, FileAccessMode mode, FileType type, FileShareFlag flag) {
+    Open(path, mode, type, flag);
+}
+
+IOFile::IOFile(std::string_view path, FileAccessMode mode, FileType type, FileShareFlag flag) {
+    Open(path, mode, type, flag);
+}
+
+IOFile::IOFile(const fs::path& path, FileAccessMode mode, FileType type, FileShareFlag flag) {
+    Open(path, mode, type, flag);
+}
+
+IOFile::~IOFile() {
+    Close();
+}
+
+IOFile::IOFile(IOFile&& other) noexcept {
+    std::swap(file_path, other.file_path);
+    std::swap(file_access_mode, other.file_access_mode);
+    std::swap(file_type, other.file_type);
+    std::swap(file, other.file);
+}
+
+IOFile& IOFile::operator=(IOFile&& other) noexcept {
+    std::swap(file_path, other.file_path);
+    std::swap(file_access_mode, other.file_access_mode);
+    std::swap(file_type, other.file_type);
+    std::swap(file, other.file);
+    return *this;
+}
+
+fs::path IOFile::GetPath() const {
+    return file_path;
+}
+
+FileAccessMode IOFile::GetAccessMode() const {
+    return file_access_mode;
+}
+
+FileType IOFile::GetType() const {
+    return file_type;
+}
+
+void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, FileShareFlag flag) {
+    Close();
+
+    file_path = path;
+    file_access_mode = mode;
+    file_type = type;
+
+    errno = 0;
+
+#ifdef _WIN32
+    if (flag != FileShareFlag::ShareNone) {
+        file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag));
+    } else {
+        _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
+    }
+#else
+    file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
+#endif
+
+    if (!IsOpen()) {
+        const auto ec = std::error_code{errno, std::generic_category()};
+        LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}, ec_message={}",
+                  PathToUTF8String(file_path), ec.message());
+    }
+}
+
+void IOFile::Close() {
+    if (!IsOpen()) {
+        return;
+    }
+
+    errno = 0;
+
+    const auto close_result = std::fclose(file) == 0;
+
+    if (!close_result) {
+        const auto ec = std::error_code{errno, std::generic_category()};
+        LOG_ERROR(Common_Filesystem, "Failed to close the file at path={}, ec_message={}",
+                  PathToUTF8String(file_path), ec.message());
+    }
+
+    file = nullptr;
+}
+
+bool IOFile::IsOpen() const {
+    return file != nullptr;
+}
+
+std::string IOFile::ReadString(size_t length) const {
+    std::vector<char> string_buffer(length);
+
+    const auto chars_read = ReadSpan<char>(string_buffer);
+    const auto string_size = chars_read != length ? chars_read : length;
+
+    return std::string{string_buffer.data(), string_size};
+}
+
+size_t IOFile::WriteString(std::span<const char> string) const {
+    return WriteSpan(string);
+}
+
+bool IOFile::Flush() const {
+    if (!IsOpen()) {
+        return false;
+    }
+
+    errno = 0;
+
+    const auto flush_result = std::fflush(file) == 0;
+
+    if (!flush_result) {
+        const auto ec = std::error_code{errno, std::generic_category()};
+        LOG_ERROR(Common_Filesystem, "Failed to flush the file at path={}, ec_message={}",
+                  PathToUTF8String(file_path), ec.message());
+    }
+
+    return flush_result;
+}
+
+bool IOFile::SetSize(u64 size) const {
+    if (!IsOpen()) {
+        return false;
+    }
+
+    errno = 0;
+
+#ifdef _WIN32
+    const auto set_size_result = _chsize_s(fileno(file), static_cast<s64>(size)) == 0;
+#else
+    const auto set_size_result = ftruncate(fileno(file), static_cast<s64>(size)) == 0;
+#endif
+
+    if (!set_size_result) {
+        const auto ec = std::error_code{errno, std::generic_category()};
+        LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={}, size={}, ec_message={}",
+                  PathToUTF8String(file_path), size, ec.message());
+    }
+
+    return set_size_result;
+}
+
+u64 IOFile::GetSize() const {
+    if (!IsOpen()) {
+        return 0;
+    }
+
+    std::error_code ec;
+
+    const auto file_size = fs::file_size(file_path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to retrieve the file size of path={}, ec_message={}",
+                  PathToUTF8String(file_path), ec.message());
+        return 0;
+    }
+
+    return file_size;
+}
+
+bool IOFile::Seek(s64 offset, SeekOrigin origin) const {
+    if (!IsOpen()) {
+        return false;
+    }
+
+    errno = 0;
+
+    const auto seek_result = fseeko(file, offset, ToSeekOrigin(origin)) == 0;
+
+    if (!seek_result) {
+        const auto ec = std::error_code{errno, std::generic_category()};
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to seek the file at path={}, offset={}, origin={}, ec_message={}",
+                  PathToUTF8String(file_path), offset, origin, ec.message());
+    }
+
+    return seek_result;
+}
+
+s64 IOFile::Tell() const {
+    if (!IsOpen()) {
+        return 0;
+    }
+
+    errno = 0;
+
+    return ftello(file);
+}
+
+} // namespace Common::FS
diff --git a/src/common/fs/file.h b/src/common/fs/file.h
new file mode 100644
index 0000000000..209f9664b4
--- /dev/null
+++ b/src/common/fs/file.h
@@ -0,0 +1,450 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <cstdio>
+#include <filesystem>
+#include <fstream>
+#include <span>
+#include <type_traits>
+#include <vector>
+
+#include "common/concepts.h"
+#include "common/fs/fs_types.h"
+#include "common/fs/fs_util.h"
+
+namespace Common::FS {
+
+enum class SeekOrigin {
+    SetOrigin,       // Seeks from the start of the file.
+    CurrentPosition, // Seeks from the current file pointer position.
+    End,             // Seeks from the end of the file.
+};
+
+/**
+ * Opens a file stream at path with the specified open mode.
+ *
+ * @param file_stream Reference to file stream
+ * @param path Filesystem path
+ * @param open_mode File stream open mode
+ */
+template <typename FileStream>
+void OpenFileStream(FileStream& file_stream, const std::filesystem::path& path,
+                    std::ios_base::openmode open_mode) {
+    file_stream.open(path, open_mode);
+}
+
+#ifdef _WIN32
+template <typename FileStream, typename Path>
+void OpenFileStream(FileStream& file_stream, const Path& path, std::ios_base::openmode open_mode) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        file_stream.open(ToU8String(path), open_mode);
+    } else {
+        file_stream.open(std::filesystem::path{path}, open_mode);
+    }
+}
+#endif
+
+/**
+ * Reads an entire file at path and returns a string of the contents read from the file.
+ * If the filesystem object at path is not a file, this function returns an empty string.
+ *
+ * @param path Filesystem path
+ * @param type File type
+ *
+ * @returns A string of the contents read from the file.
+ */
+[[nodiscard]] std::string ReadStringFromFile(const std::filesystem::path& path, FileType type);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] std::string ReadStringFromFile(const Path& path, FileType type) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return ReadStringFromFile(ToU8String(path), type);
+    } else {
+        return ReadStringFromFile(std::filesystem::path{path}, type);
+    }
+}
+#endif
+
+/**
+ * Writes a string to a file at path and returns the number of characters successfully written.
+ * If an file already exists at path, its contents will be erased.
+ * If the filesystem object at path is not a file, this function returns 0.
+ *
+ * @param path Filesystem path
+ * @param type File type
+ *
+ * @returns Number of characters successfully written.
+ */
+[[nodiscard]] size_t WriteStringToFile(const std::filesystem::path& path, FileType type,
+                                       std::string_view string);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] size_t WriteStringToFile(const Path& path, FileType type, std::string_view string) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return WriteStringToFile(ToU8String(path), type, string);
+    } else {
+        return WriteStringToFile(std::filesystem::path{path}, type, string);
+    }
+}
+#endif
+
+/**
+ * Appends a string to a file at path and returns the number of characters successfully written.
+ * If a file does not exist at path, WriteStringToFile is called instead.
+ * If the filesystem object at path is not a file, this function returns 0.
+ *
+ * @param path Filesystem path
+ * @param type File type
+ *
+ * @returns Number of characters successfully written.
+ */
+[[nodiscard]] size_t AppendStringToFile(const std::filesystem::path& path, FileType type,
+                                        std::string_view string);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] size_t AppendStringToFile(const Path& path, FileType type, std::string_view string) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return AppendStringToFile(ToU8String(path), type, string);
+    } else {
+        return AppendStringToFile(std::filesystem::path{path}, type, string);
+    }
+}
+#endif
+
+class IOFile final : NonCopyable {
+public:
+    IOFile();
+
+    explicit IOFile(const std::string& path, FileAccessMode mode,
+                    FileType type = FileType::BinaryFile,
+                    FileShareFlag flag = FileShareFlag::ShareReadOnly);
+
+    explicit IOFile(std::string_view path, FileAccessMode mode,
+                    FileType type = FileType::BinaryFile,
+                    FileShareFlag flag = FileShareFlag::ShareReadOnly);
+
+    /**
+     * An IOFile is a lightweight wrapper on C Library file operations.
+     * Automatically closes an open file on the destruction of an IOFile object.
+     *
+     * @param path Filesystem path
+     * @param mode File access mode
+     * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
+     * @param flag (Windows only) File-share access flag, default is ShareReadOnly
+     */
+    explicit IOFile(const std::filesystem::path& path, FileAccessMode mode,
+                    FileType type = FileType::BinaryFile,
+                    FileShareFlag flag = FileShareFlag::ShareReadOnly);
+
+    virtual ~IOFile();
+
+    IOFile(IOFile&& other) noexcept;
+    IOFile& operator=(IOFile&& other) noexcept;
+
+    /**
+     * Gets the path of the file.
+     *
+     * @returns The path of the file.
+     */
+    [[nodiscard]] std::filesystem::path GetPath() const;
+
+    /**
+     * Gets the access mode of the file.
+     *
+     * @returns The access mode of the file.
+     */
+    [[nodiscard]] FileAccessMode GetAccessMode() const;
+
+    /**
+     * Gets the type of the file.
+     *
+     * @returns The type of the file.
+     */
+    [[nodiscard]] FileType GetType() const;
+
+    /**
+     * Opens a file at path with the specified file access mode.
+     * This function behaves differently depending on the FileAccessMode.
+     * These behaviors are documented in each enum value of FileAccessMode.
+     *
+     * @param path Filesystem path
+     * @param mode File access mode
+     * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
+     * @param flag (Windows only) File-share access flag, default is ShareReadOnly
+     */
+    void Open(const std::filesystem::path& path, FileAccessMode mode,
+              FileType type = FileType::BinaryFile,
+              FileShareFlag flag = FileShareFlag::ShareReadOnly);
+
+#ifdef _WIN32
+    template <typename Path>
+    [[nodiscard]] void Open(const Path& path, FileAccessMode mode,
+                            FileType type = FileType::BinaryFile,
+                            FileShareFlag flag = FileShareFlag::ShareReadOnly) {
+        using ValueType = typename Path::value_type;
+        if constexpr (IsChar<ValueType>) {
+            Open(ToU8String(path), mode, type, flag);
+        } else {
+            Open(std::filesystem::path{path}, mode, type, flag);
+        }
+    }
+#endif
+
+    /// Closes the file if it is opened.
+    void Close();
+
+    /**
+     * Checks whether the file is open.
+     * Use this to check whether the calls to Open() or Close() succeeded.
+     *
+     * @returns True if the file is open, false otherwise.
+     */
+    [[nodiscard]] bool IsOpen() const;
+
+    /**
+     * Helper function which deduces the value type of a contiguous STL container used in ReadSpan.
+     * If T is not a contiguous STL container as defined by the concept IsSTLContainer, this calls
+     * ReadObject and T must be a trivially copyable object.
+     *
+     * See ReadSpan for more details if T is a contiguous container.
+     * See ReadObject for more details if T is a trivially copyable object.
+     *
+     * @tparam T Contiguous container or trivially copyable object
+     *
+     * @param data Container of T::value_type data or reference to object
+     *
+     * @returns Count of T::value_type data or objects successfully read.
+     */
+    template <typename T>
+    [[nodiscard]] size_t Read(T& data) const {
+        if constexpr (IsSTLContainer<T>) {
+            using ContiguousType = typename T::value_type;
+            static_assert(std::is_trivially_copyable_v<ContiguousType>,
+                          "Data type must be trivially copyable.");
+            return ReadSpan<ContiguousType>(data);
+        } else {
+            return ReadObject(data) ? 1 : 0;
+        }
+    }
+
+    /**
+     * Helper function which deduces the value type of a contiguous STL container used in WriteSpan.
+     * If T is not a contiguous STL container as defined by the concept IsSTLContainer, this calls
+     * WriteObject and T must be a trivially copyable object.
+     *
+     * See WriteSpan for more details if T is a contiguous container.
+     * See WriteObject for more details if T is a trivially copyable object.
+     *
+     * @tparam T Contiguous container or trivially copyable object
+     *
+     * @param data Container of T::value_type data or const reference to object
+     *
+     * @returns Count of T::value_type data or objects successfully written.
+     */
+    template <typename T>
+    [[nodiscard]] size_t Write(const T& data) const {
+        if constexpr (IsSTLContainer<T>) {
+            using ContiguousType = typename T::value_type;
+            static_assert(std::is_trivially_copyable_v<ContiguousType>,
+                          "Data type must be trivially copyable.");
+            return WriteSpan<ContiguousType>(data);
+        } else {
+            static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
+            return WriteObject(data) ? 1 : 0;
+        }
+    }
+
+    /**
+     * Reads a span of T data from a file sequentially.
+     * This function reads from the current position of the file pointer and
+     * advances it by the (count of T * sizeof(T)) bytes successfully read.
+     *
+     * Failures occur when:
+     * - The file is not open
+     * - The opened file lacks read permissions
+     * - Attempting to read beyond the end-of-file
+     *
+     * @tparam T Data type
+     *
+     * @param data Span of T data
+     *
+     * @returns Count of T data successfully read.
+     */
+    template <typename T>
+    [[nodiscard]] size_t ReadSpan(std::span<T> data) const {
+        static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
+
+        if (!IsOpen()) {
+            return 0;
+        }
+
+        return std::fread(data.data(), sizeof(T), data.size(), file);
+    }
+
+    /**
+     * Writes a span of T data to a file sequentially.
+     * This function writes from the current position of the file pointer and
+     * advances it by the (count of T * sizeof(T)) bytes successfully written.
+     *
+     * Failures occur when:
+     * - The file is not open
+     * - The opened file lacks write permissions
+     *
+     * @tparam T Data type
+     *
+     * @param data Span of T data
+     *
+     * @returns Count of T data successfully written.
+     */
+    template <typename T>
+    [[nodiscard]] size_t WriteSpan(std::span<const T> data) const {
+        static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
+
+        if (!IsOpen()) {
+            return 0;
+        }
+
+        return std::fwrite(data.data(), sizeof(T), data.size(), file);
+    }
+
+    /**
+     * Reads a T object from a file sequentially.
+     * This function reads from the current position of the file pointer and
+     * advances it by the sizeof(T) bytes successfully read.
+     *
+     * Failures occur when:
+     * - The file is not open
+     * - The opened file lacks read permissions
+     * - Attempting to read beyond the end-of-file
+     *
+     * @tparam T Data type
+     *
+     * @param object Reference to object
+     *
+     * @returns True if the object is successfully read from the file, false otherwise.
+     */
+    template <typename T>
+    [[nodiscard]] bool ReadObject(T& object) const {
+        static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
+        static_assert(!std::is_pointer_v<T>, "T must not be a pointer to an object.");
+
+        if (!IsOpen()) {
+            return false;
+        }
+
+        return std::fread(&object, sizeof(T), 1, file) == 1;
+    }
+
+    /**
+     * Writes a T object to a file sequentially.
+     * This function writes from the current position of the file pointer and
+     * advances it by the sizeof(T) bytes successfully written.
+     *
+     * Failures occur when:
+     * - The file is not open
+     * - The opened file lacks write permissions
+     *
+     * @tparam T Data type
+     *
+     * @param object Const reference to object
+     *
+     * @returns True if the object is successfully written to the file, false otherwise.
+     */
+    template <typename T>
+    [[nodiscard]] bool WriteObject(const T& object) const {
+        static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
+        static_assert(!std::is_pointer_v<T>, "T must not be a pointer to an object.");
+
+        if (!IsOpen()) {
+            return false;
+        }
+
+        return std::fwrite(&object, sizeof(T), 1, file) == 1;
+    }
+
+    /**
+     * Specialized function to read a string of a given length from a file sequentially.
+     * This function writes from the current position of the file pointer and
+     * advances it by the number of characters successfully read.
+     * The size of the returned string may not match length if not all bytes are successfully read.
+     *
+     * @param length Length of the string
+     *
+     * @returns A string read from the file.
+     */
+    [[nodiscard]] std::string ReadString(size_t length) const;
+
+    /**
+     * Specialized function to write a string to a file sequentially.
+     * This function writes from the current position of the file pointer and
+     * advances it by the number of characters successfully written.
+     *
+     * @param string Span of const char backed std::string or std::string_view
+     *
+     * @returns Number of characters successfully written.
+     */
+    [[nodiscard]] size_t WriteString(std::span<const char> string) const;
+
+    /**
+     * Flushes any unwritten buffered data into the file.
+     *
+     * @returns True if the flush was successful, false otherwise.
+     */
+    [[nodiscard]] bool Flush() const;
+
+    /**
+     * Resizes the file to a given size.
+     * If the file is resized to a smaller size, the remainder of the file is discarded.
+     * If the file is resized to a larger size, the new area appears as if zero-filled.
+     *
+     * Failures occur when:
+     * - The file is not open
+     *
+     * @param size File size in bytes
+     *
+     * @returns True if the file resize succeeded, false otherwise.
+     */
+    [[nodiscard]] bool SetSize(u64 size) const;
+
+    /**
+     * Gets the size of the file.
+     *
+     * Failures occur when:
+     * - The file is not open
+     *
+     * @returns The file size in bytes of the file. Returns 0 on failure.
+     */
+    [[nodiscard]] u64 GetSize() const;
+
+    /**
+     * Moves the current position of the file pointer with the specified offset and seek origin.
+     *
+     * @param offset Offset from seek origin
+     * @param origin Seek origin
+     *
+     * @returns True if the file pointer has moved to the specified offset, false otherwise.
+     */
+    [[nodiscard]] bool Seek(s64 offset, SeekOrigin origin = SeekOrigin::SetOrigin) const;
+
+    /**
+     * Gets the current position of the file pointer.
+     *
+     * @returns The current position of the file pointer.
+     */
+    [[nodiscard]] s64 Tell() const;
+
+private:
+    std::filesystem::path file_path;
+    FileAccessMode file_access_mode;
+    FileType file_type;
+
+    std::FILE* file = nullptr;
+};
+
+} // namespace Common::FS
diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp
new file mode 100644
index 0000000000..d492480d94
--- /dev/null
+++ b/src/common/fs/fs.cpp
@@ -0,0 +1,610 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+
+namespace Common::FS {
+
+namespace fs = std::filesystem;
+
+// File Operations
+
+bool NewFile(const fs::path& path, u64 size) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path.parent_path())) {
+        LOG_ERROR(Common_Filesystem, "Parent directory of path={} does not exist",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    if (Exists(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} exists", PathToUTF8String(path));
+        return false;
+    }
+
+    IOFile io_file{path, FileAccessMode::Write};
+
+    if (!io_file.IsOpen()) {
+        LOG_ERROR(Common_Filesystem, "Failed to create a file at path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!io_file.SetSize(size)) {
+        LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={} to size={}",
+                  PathToUTF8String(path), size);
+        return false;
+    }
+
+    io_file.Close();
+
+    LOG_DEBUG(Common_Filesystem, "Successfully created a file at path={} with size={}",
+              PathToUTF8String(path), size);
+
+    return true;
+}
+
+bool RemoveFile(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    if (!IsFile(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a file",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    fs::remove(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to remove the file at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully removed the file at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool RenameFile(const fs::path& old_path, const fs::path& new_path) {
+    if (!ValidatePath(old_path) || !ValidatePath(new_path)) {
+        LOG_ERROR(Common_Filesystem,
+                  "One or both input path(s) is not valid, old_path={}, new_path={}",
+                  PathToUTF8String(old_path), PathToUTF8String(new_path));
+        return false;
+    }
+
+    if (!Exists(old_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} does not exist",
+                  PathToUTF8String(old_path));
+        return false;
+    }
+
+    if (!IsFile(old_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} is not a file",
+                  PathToUTF8String(old_path));
+        return false;
+    }
+
+    if (Exists(new_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} exists",
+                  PathToUTF8String(new_path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    fs::rename(old_path, new_path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to rename the file from old_path={} to new_path={}, ec_message={}",
+                  PathToUTF8String(old_path), PathToUTF8String(new_path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully renamed the file from old_path={} to new_path={}",
+              PathToUTF8String(old_path), PathToUTF8String(new_path));
+
+    return true;
+}
+
+std::shared_ptr<IOFile> FileOpen(const fs::path& path, FileAccessMode mode, FileType type,
+                                 FileShareFlag flag) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return nullptr;
+    }
+
+    if (!IsFile(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a file",
+                  PathToUTF8String(path));
+        return nullptr;
+    }
+
+    auto io_file = std::make_shared<IOFile>(path, mode, type, flag);
+
+    if (!io_file->IsOpen()) {
+        io_file.reset();
+
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to open the file at path={} with mode={}, type={}, flag={}",
+                  PathToUTF8String(path), mode, type, flag);
+
+        return nullptr;
+    }
+
+    LOG_DEBUG(Common_Filesystem,
+              "Successfully opened the file at path={} with mode={}, type={}, flag={}",
+              PathToUTF8String(path), mode, type, flag);
+
+    return io_file;
+}
+
+// Directory Operations
+
+bool CreateDir(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path.parent_path())) {
+        LOG_ERROR(Common_Filesystem, "Parent directory of path={} does not exist",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    if (IsDir(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} exists and is a directory",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    std::error_code ec;
+
+    fs::create_directory(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to create the directory at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully created the directory at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool CreateDirs(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (IsDir(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} exists and is a directory",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    std::error_code ec;
+
+    fs::create_directories(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to create the directories at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully created the directories at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool CreateParentDir(const fs::path& path) {
+    return CreateDir(path.parent_path());
+}
+
+bool CreateParentDirs(const fs::path& path) {
+    return CreateDirs(path.parent_path());
+}
+
+bool RemoveDir(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    if (!IsDir(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    fs::remove(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to remove the directory at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully removed the directory at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool RemoveDirRecursively(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    if (!IsDir(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    fs::remove_all(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to remove the directory and its contents at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully removed the directory and its contents at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool RemoveDirContentsRecursively(const fs::path& path) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+    if (!Exists(path)) {
+        LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return true;
+    }
+
+    if (!IsDir(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
+                  PathToUTF8String(path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    for (const auto& entry : fs::recursive_directory_iterator(path, ec)) {
+        if (ec) {
+            LOG_ERROR(Common_Filesystem,
+                      "Failed to completely enumerate the directory at path={}, ec_message={}",
+                      PathToUTF8String(path), ec.message());
+            break;
+        }
+
+        fs::remove(entry.path(), ec);
+
+        if (ec) {
+            LOG_ERROR(Common_Filesystem,
+                      "Failed to remove the filesystem object at path={}, ec_message={}",
+                      PathToUTF8String(entry.path()), ec.message());
+            break;
+        }
+    }
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to remove all the contents of the directory at path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem,
+              "Successfully removed all the contents of the directory at path={}",
+              PathToUTF8String(path));
+
+    return true;
+}
+
+bool RenameDir(const fs::path& old_path, const fs::path& new_path) {
+    if (!ValidatePath(old_path) || !ValidatePath(new_path)) {
+        LOG_ERROR(Common_Filesystem,
+                  "One or both input path(s) is not valid, old_path={}, new_path={}",
+                  PathToUTF8String(old_path), PathToUTF8String(new_path));
+        return false;
+    }
+
+    if (!Exists(old_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} does not exist",
+                  PathToUTF8String(old_path));
+        return false;
+    }
+
+    if (!IsDir(old_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} is not a directory",
+                  PathToUTF8String(old_path));
+        return false;
+    }
+
+    if (Exists(new_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} exists",
+                  PathToUTF8String(new_path));
+        return false;
+    }
+
+    std::error_code ec;
+
+    fs::rename(old_path, new_path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to rename the file from old_path={} to new_path={}, ec_message={}",
+                  PathToUTF8String(old_path), PathToUTF8String(new_path), ec.message());
+        return false;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully renamed the file from old_path={} to new_path={}",
+              PathToUTF8String(old_path), PathToUTF8String(new_path));
+
+    return true;
+}
+
+void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable& callback,
+                       DirEntryFilter filter) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return;
+    }
+
+    if (!Exists(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return;
+    }
+
+    if (!IsDir(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
+                  PathToUTF8String(path));
+        return;
+    }
+
+    bool callback_error = false;
+
+    std::error_code ec;
+
+    for (const auto& entry : fs::directory_iterator(path, ec)) {
+        if (ec) {
+            break;
+        }
+
+        if (True(filter & DirEntryFilter::File) &&
+            entry.status().type() == fs::file_type::regular) {
+            if (!callback(entry.path())) {
+                callback_error = true;
+                break;
+            }
+        }
+
+        if (True(filter & DirEntryFilter::Directory) &&
+            entry.status().type() == fs::file_type::directory) {
+            if (!callback(entry.path())) {
+                callback_error = true;
+                break;
+            }
+        }
+    }
+
+    if (callback_error || ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to visit all the directory entries of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}",
+              PathToUTF8String(path));
+}
+
+void IterateDirEntriesRecursively(const std::filesystem::path& path,
+                                  const DirEntryCallable& callback, DirEntryFilter filter) {
+    if (!ValidatePath(path)) {
+        LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path));
+        return;
+    }
+
+    if (!Exists(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist",
+                  PathToUTF8String(path));
+        return;
+    }
+
+    if (!IsDir(path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory",
+                  PathToUTF8String(path));
+        return;
+    }
+
+    bool callback_error = false;
+
+    std::error_code ec;
+
+    for (const auto& entry : fs::recursive_directory_iterator(path, ec)) {
+        if (ec) {
+            break;
+        }
+
+        if (True(filter & DirEntryFilter::File) &&
+            entry.status().type() == fs::file_type::regular) {
+            if (!callback(entry.path())) {
+                callback_error = true;
+                break;
+            }
+        }
+
+        if (True(filter & DirEntryFilter::Directory) &&
+            entry.status().type() == fs::file_type::directory) {
+            if (!callback(entry.path())) {
+                callback_error = true;
+                break;
+            }
+        }
+    }
+
+    if (callback_error || ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to visit all the directory entries of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return;
+    }
+
+    LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}",
+              PathToUTF8String(path));
+}
+
+// Generic Filesystem Operations
+
+bool Exists(const fs::path& path) {
+    return fs::exists(path);
+}
+
+bool IsFile(const fs::path& path) {
+    return fs::is_regular_file(path);
+}
+
+bool IsDir(const fs::path& path) {
+    return fs::is_directory(path);
+}
+
+fs::path GetCurrentDir() {
+    std::error_code ec;
+
+    const auto current_path = fs::current_path(ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to get the current path, ec_message={}", ec.message());
+        return {};
+    }
+
+    return current_path;
+}
+
+bool SetCurrentDir(const fs::path& path) {
+    std::error_code ec;
+
+    fs::current_path(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to set the current path to path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return false;
+    }
+
+    return true;
+}
+
+fs::file_type GetEntryType(const fs::path& path) {
+    std::error_code ec;
+
+    const auto file_status = fs::status(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to retrieve the entry type of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return fs::file_type::not_found;
+    }
+
+    return file_status.type();
+}
+
+u64 GetSize(const fs::path& path) {
+    std::error_code ec;
+
+    const auto file_size = fs::file_size(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem, "Failed to retrieve the file size of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return 0;
+    }
+
+    return file_size;
+}
+
+u64 GetFreeSpaceSize(const fs::path& path) {
+    std::error_code ec;
+
+    const auto space_info = fs::space(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to retrieve the available free space of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return 0;
+    }
+
+    return space_info.free;
+}
+
+u64 GetTotalSpaceSize(const fs::path& path) {
+    std::error_code ec;
+
+    const auto space_info = fs::space(path, ec);
+
+    if (ec) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to retrieve the total capacity of path={}, ec_message={}",
+                  PathToUTF8String(path), ec.message());
+        return 0;
+    }
+
+    return space_info.capacity;
+}
+
+} // namespace Common::FS
diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h
new file mode 100644
index 0000000000..f6f2563490
--- /dev/null
+++ b/src/common/fs/fs.h
@@ -0,0 +1,582 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <filesystem>
+#include <memory>
+
+#include "common/fs/fs_types.h"
+#include "common/fs/fs_util.h"
+
+namespace Common::FS {
+
+class IOFile;
+
+// File Operations
+
+/**
+ * Creates a new file at path with the specified size.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - The input path's parent directory does not exist
+ * - Filesystem object at path exists
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ * @param size File size
+ *
+ * @returns True if the file creation succeeds, false otherwise.
+ */
+[[nodiscard]] bool NewFile(const std::filesystem::path& path, u64 size = 0);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool NewFile(const Path& path, u64 size = 0) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return NewFile(ToU8String(path), size);
+    } else {
+        return NewFile(std::filesystem::path{path}, size);
+    }
+}
+#endif
+
+/**
+ * Removes a file at path.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a file
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if file removal succeeds or file does not exist, false otherwise.
+ */
+[[nodiscard]] bool RemoveFile(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool RemoveFile(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return RemoveFile(ToU8String(path));
+    } else {
+        return RemoveFile(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Renames a file from old_path to new_path.
+ *
+ * Failures occur when:
+ * - One or both input path(s) is not valid
+ * - Filesystem object at old_path does not exist
+ * - Filesystem object at old_path is not a file
+ * - Filesystem object at new_path exists
+ * - Filesystem at either path is read only
+ *
+ * @param old_path Old filesystem path
+ * @param new_path New filesystem path
+ *
+ * @returns True if file rename succeeds, false otherwise.
+ */
+[[nodiscard]] bool RenameFile(const std::filesystem::path& old_path,
+                              const std::filesystem::path& new_path);
+
+#ifdef _WIN32
+template <typename Path1, typename Path2>
+[[nodiscard]] bool RenameFile(const Path1& old_path, const Path2& new_path) {
+    using ValueType1 = typename Path1::value_type;
+    using ValueType2 = typename Path2::value_type;
+    if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
+        return RenameFile(ToU8String(old_path), ToU8String(new_path));
+    } else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
+        return RenameFile(ToU8String(old_path), new_path);
+    } else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
+        return RenameFile(old_path, ToU8String(new_path));
+    } else {
+        return RenameFile(std::filesystem::path{old_path}, std::filesystem::path{new_path});
+    }
+}
+#endif
+
+/**
+ * Opens a file at path with the specified file access mode.
+ * This function behaves differently depending on the FileAccessMode.
+ * These behaviors are documented in each enum value of FileAccessMode.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a file
+ * - The file is not opened
+ *
+ * @param path Filesystem path
+ * @param mode File access mode
+ * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file
+ * @param flag (Windows only) File-share access flag, default is ShareReadOnly
+ *
+ * @returns A shared pointer to the opened file. Returns nullptr on failure.
+ */
+[[nodiscard]] std::shared_ptr<IOFile> FileOpen(const std::filesystem::path& path,
+                                               FileAccessMode mode,
+                                               FileType type = FileType::BinaryFile,
+                                               FileShareFlag flag = FileShareFlag::ShareReadOnly);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] std::shared_ptr<IOFile> FileOpen(const Path& path, FileAccessMode mode,
+                                               FileType type = FileType::BinaryFile,
+                                               FileShareFlag flag = FileShareFlag::ShareReadOnly) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return FileOpen(ToU8String(path), mode, type, flag);
+    } else {
+        return FileOpen(std::filesystem::path{path}, mode, type, flag);
+    }
+}
+#endif
+
+// Directory Operations
+
+/**
+ * Creates a directory at path.
+ * Note that this function will *always* assume that the input path is a directory. For example,
+ * if the input path is /path/to/directory/file.txt, it will create a directory called "file.txt".
+ * If you intend to create the parent directory of a file, use CreateParentDir instead.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - The input path's parent directory does not exist
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if directory creation succeeds or directory already exists, false otherwise.
+ */
+[[nodiscard]] bool CreateDir(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool CreateDir(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return CreateDir(ToU8String(path));
+    } else {
+        return CreateDir(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Recursively creates a directory at path.
+ * Note that this function will *always* assume that the input path is a directory. For example,
+ * if the input path is /path/to/directory/file.txt, it will create a directory called "file.txt".
+ * If you intend to create the parent directory of a file, use CreateParentDirs instead.
+ * Unlike CreateDir, this creates all of input path's parent directories if they do not exist.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if directory creation succeeds or directory already exists, false otherwise.
+ */
+[[nodiscard]] bool CreateDirs(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool CreateDirs(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return CreateDirs(ToU8String(path));
+    } else {
+        return CreateDirs(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Creates the parent directory of a given path.
+ * This function calls CreateDir(path.parent_path()), see CreateDir for more details.
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if directory creation succeeds or directory already exists, false otherwise.
+ */
+[[nodiscard]] bool CreateParentDir(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool CreateParentDir(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return CreateParentDir(ToU8String(path));
+    } else {
+        return CreateParentDir(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Recursively creates the parent directory of a given path.
+ * This function calls CreateDirs(path.parent_path()), see CreateDirs for more details.
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if directory creation succeeds or directory already exists, false otherwise.
+ */
+[[nodiscard]] bool CreateParentDirs(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool CreateParentDirs(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return CreateParentDirs(ToU8String(path));
+    } else {
+        return CreateParentDirs(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Removes a directory at path.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a directory
+ * - The given directory is not empty
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if directory removal succeeds or directory does not exist, false otherwise.
+ */
+[[nodiscard]] bool RemoveDir(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool RemoveDir(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return RemoveDir(ToU8String(path));
+    } else {
+        return RemoveDir(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Removes all the contents within the given directory and removes the directory itself.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a directory
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if the directory and all of its contents are removed successfully, false otherwise.
+ */
+[[nodiscard]] bool RemoveDirRecursively(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool RemoveDirRecursively(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return RemoveDirRecursively(ToU8String(path));
+    } else {
+        return RemoveDirRecursively(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Removes all the contents within the given directory without removing the directory itself.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a directory
+ * - Filesystem at path is read only
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if all of the directory's contents are removed successfully, false otherwise.
+ */
+[[nodiscard]] bool RemoveDirContentsRecursively(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool RemoveDirContentsRecursively(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return RemoveDirContentsRecursively(ToU8String(path));
+    } else {
+        return RemoveDirContentsRecursively(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Renames a directory from old_path to new_path.
+ *
+ * Failures occur when:
+ * - One or both input path(s) is not valid
+ * - Filesystem object at old_path does not exist
+ * - Filesystem object at old_path is not a directory
+ * - Filesystem object at new_path exists
+ * - Filesystem at either path is read only
+ *
+ * @param old_path Old filesystem path
+ * @param new_path New filesystem path
+ *
+ * @returns True if directory rename succeeds, false otherwise.
+ */
+[[nodiscard]] bool RenameDir(const std::filesystem::path& old_path,
+                             const std::filesystem::path& new_path);
+
+#ifdef _WIN32
+template <typename Path1, typename Path2>
+[[nodiscard]] bool RenameDir(const Path1& old_path, const Path2& new_path) {
+    using ValueType1 = typename Path1::value_type;
+    using ValueType2 = typename Path2::value_type;
+    if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
+        return RenameDir(ToU8String(old_path), ToU8String(new_path));
+    } else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
+        return RenameDir(ToU8String(old_path), new_path);
+    } else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
+        return RenameDir(old_path, ToU8String(new_path));
+    } else {
+        return RenameDir(std::filesystem::path{old_path}, std::filesystem::path{new_path});
+    }
+}
+#endif
+
+/**
+ * Iterates over the directory entries of a given directory.
+ * This does not iterate over the sub-directories of the given directory.
+ * The DirEntryCallable callback is called for each visited directory entry.
+ * A filter can be set to control which directory entries are visited based on their type.
+ * By default, both files and directories are visited.
+ * If the callback returns false or there is an error, the iteration is immediately halted.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path is not a directory
+ *
+ * @param path Filesystem path
+ * @param callback Callback to be called for each visited directory entry
+ * @param filter Directory entry type filter
+ */
+void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable& callback,
+                       DirEntryFilter filter = DirEntryFilter::All);
+
+#ifdef _WIN32
+template <typename Path>
+void IterateDirEntries(const Path& path, const DirEntryCallable& callback,
+                       DirEntryFilter filter = DirEntryFilter::All) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        IterateDirEntries(ToU8String(path), callback, filter);
+    } else {
+        IterateDirEntries(std::filesystem::path{path}, callback, filter);
+    }
+}
+#endif
+
+/**
+ * Iterates over the directory entries of a given directory and its sub-directories.
+ * The DirEntryCallable callback is called for each visited directory entry.
+ * A filter can be set to control which directory entries are visited based on their type.
+ * By default, both files and directories are visited.
+ * If the callback returns false or there is an error, the iteration is immediately halted.
+ *
+ * Failures occur when:
+ * - Input path is not valid
+ * - Filesystem object at path does not exist
+ * - Filesystem object at path is not a directory
+ *
+ * @param path Filesystem path
+ * @param callback Callback to be called for each visited directory entry
+ * @param filter Directory entry type filter
+ */
+void IterateDirEntriesRecursively(const std::filesystem::path& path,
+                                  const DirEntryCallable& callback,
+                                  DirEntryFilter filter = DirEntryFilter::All);
+
+#ifdef _WIN32
+template <typename Path>
+void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback,
+                                  DirEntryFilter filter = DirEntryFilter::All) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        IterateDirEntriesRecursively(ToU8String(path), callback, filter);
+    } else {
+        IterateDirEntriesRecursively(std::filesystem::path{path}, callback, filter);
+    }
+}
+#endif
+
+// Generic Filesystem Operations
+
+/**
+ * Returns whether a filesystem object at path exists.
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if a filesystem object at path exists, false otherwise.
+ */
+[[nodiscard]] bool Exists(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool Exists(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return Exists(ToU8String(path));
+    } else {
+        return Exists(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Returns whether a filesystem object at path is a file.
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if a filesystem object at path is a file, false otherwise.
+ */
+[[nodiscard]] bool IsFile(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool IsFile(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return IsFile(ToU8String(path));
+    } else {
+        return IsFile(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Returns whether a filesystem object at path is a directory.
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if a filesystem object at path is a directory, false otherwise.
+ */
+[[nodiscard]] bool IsDir(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool IsDir(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return IsDir(ToU8String(path));
+    } else {
+        return IsDir(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the current working directory.
+ *
+ * @returns The current working directory. Returns an empty path on failure.
+ */
+[[nodiscard]] std::filesystem::path GetCurrentDir();
+
+/**
+ * Sets the current working directory to path.
+ *
+ * @returns True if the current working directory is successfully set, false otherwise.
+ */
+[[nodiscard]] bool SetCurrentDir(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool SetCurrentDir(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return SetCurrentDir(ToU8String(path));
+    } else {
+        return SetCurrentDir(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the entry type of the filesystem object at path.
+ *
+ * @param path Filesystem path
+ *
+ * @returns The entry type of the filesystem object. Returns file_type::not_found on failure.
+ */
+[[nodiscard]] std::filesystem::file_type GetEntryType(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] std::filesystem::file_type GetEntryType(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return GetEntryType(ToU8String(path));
+    } else {
+        return GetEntryType(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the size of the filesystem object at path.
+ *
+ * @param path Filesystem path
+ *
+ * @returns The size in bytes of the filesystem object. Returns 0 on failure.
+ */
+[[nodiscard]] u64 GetSize(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] u64 GetSize(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return GetSize(ToU8String(path));
+    } else {
+        return GetSize(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the free space size of the filesystem at path.
+ *
+ * @param path Filesystem path
+ *
+ * @returns The free space size in bytes of the filesystem at path. Returns 0 on failure.
+ */
+[[nodiscard]] u64 GetFreeSpaceSize(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] u64 GetFreeSpaceSize(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return GetFreeSpaceSize(ToU8String(path));
+    } else {
+        return GetFreeSpaceSize(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the total capacity of the filesystem at path.
+ *
+ * @param path Filesystem path
+ *
+ * @returns The total capacity in bytes of the filesystem at path. Returns 0 on failure.
+ */
+[[nodiscard]] u64 GetTotalSpaceSize(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] u64 GetTotalSpaceSize(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return GetTotalSpaceSize(ToU8String(path));
+    } else {
+        return GetTotalSpaceSize(std::filesystem::path{path});
+    }
+}
+#endif
+
+} // namespace Common::FS
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h
new file mode 100644
index 0000000000..b32614797c
--- /dev/null
+++ b/src/common/fs/fs_paths.h
@@ -0,0 +1,27 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+// yuzu data directories
+
+#define YUZU_DIR "yuzu"
+#define PORTABLE_DIR "user"
+
+// Sub-directories contained within a yuzu data directory
+
+#define CACHE_DIR "cache"
+#define CONFIG_DIR "config"
+#define DUMP_DIR "dump"
+#define KEYS_DIR "keys"
+#define LOAD_DIR "load"
+#define LOG_DIR "log"
+#define NAND_DIR "nand"
+#define SCREENSHOTS_DIR "screenshots"
+#define SDMC_DIR "sdmc"
+#define SHADER_DIR "shader"
+
+// yuzu-specific files
+
+#define LOG_FILE "yuzu_log.txt"
diff --git a/src/common/fs/fs_types.h b/src/common/fs/fs_types.h
new file mode 100644
index 0000000000..089980aee3
--- /dev/null
+++ b/src/common/fs/fs_types.h
@@ -0,0 +1,73 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace Common::FS {
+
+enum class FileAccessMode {
+    /**
+     * If the file at path exists, it opens the file for reading.
+     * If the file at path does not exist, it fails to open the file.
+     */
+    Read = 1 << 0,
+    /**
+     * If the file at path exists, the existing contents of the file are erased.
+     * The empty file is then opened for writing.
+     * If the file at path does not exist, it creates and opens a new empty file for writing.
+     */
+    Write = 1 << 1,
+    /**
+     * If the file at path exists, it opens the file for reading and writing.
+     * If the file at path does not exist, it fails to open the file.
+     */
+    ReadWrite = Read | Write,
+    /**
+     * If the file at path exists, it opens the file for appending.
+     * If the file at path does not exist, it creates and opens a new empty file for appending.
+     */
+    Append = 1 << 2,
+    /**
+     * If the file at path exists, it opens the file for both reading and appending.
+     * If the file at path does not exist, it creates and opens a new empty file for both
+     * reading and appending.
+     */
+    ReadAppend = Read | Append,
+};
+
+enum class FileType {
+    BinaryFile,
+    TextFile,
+};
+
+enum class FileShareFlag {
+    ShareNone,      // Provides exclusive access to the file.
+    ShareReadOnly,  // Provides read only shared access to the file.
+    ShareWriteOnly, // Provides write only shared access to the file.
+    ShareReadWrite, // Provides read and write shared access to the file.
+};
+
+enum class DirEntryFilter {
+    File = 1 << 0,
+    Directory = 1 << 1,
+    All = File | Directory,
+};
+DECLARE_ENUM_FLAG_OPERATORS(DirEntryFilter);
+
+/**
+ * A callback function which takes in the path of a directory entry.
+ *
+ * @param path The path of a directory entry
+ *
+ * @returns A boolean value.
+ *          Return true to indicate whether the callback is successful, false otherwise.
+ */
+using DirEntryCallable = std::function<bool(const std::filesystem::path& path)>;
+
+} // namespace Common::FS
diff --git a/src/common/fs/fs_util.cpp b/src/common/fs/fs_util.cpp
new file mode 100644
index 0000000000..0ddfc31316
--- /dev/null
+++ b/src/common/fs/fs_util.cpp
@@ -0,0 +1,13 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/fs/fs_util.h"
+
+namespace Common::FS {
+
+std::u8string ToU8String(std::string_view utf8_string) {
+    return std::u8string{utf8_string.begin(), utf8_string.end()};
+}
+
+} // namespace Common::FS
diff --git a/src/common/fs/fs_util.h b/src/common/fs/fs_util.h
new file mode 100644
index 0000000000..951df53b6c
--- /dev/null
+++ b/src/common/fs/fs_util.h
@@ -0,0 +1,25 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <concepts>
+#include <string>
+#include <string_view>
+
+namespace Common::FS {
+
+template <typename T>
+concept IsChar = std::same_as<T, char>;
+
+/**
+ * Converts a UTF-8 encoded std::string or std::string_view to a std::u8string.
+ *
+ * @param utf8_string UTF-8 encoded string
+ *
+ * @returns UTF-8 encoded std::u8string.
+ */
+[[nodiscard]] std::u8string ToU8String(std::string_view utf8_string);
+
+} // namespace Common::FS
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
new file mode 100644
index 0000000000..8b732a21c3
--- /dev/null
+++ b/src/common/fs/path_util.cpp
@@ -0,0 +1,432 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <unordered_map>
+
+#include "common/fs/fs.h"
+#include "common/fs/fs_paths.h"
+#include "common/fs/path_util.h"
+#include "common/logging/log.h"
+
+#ifdef _WIN32
+#include <shlobj.h> // Used in GetExeDirectory()
+#else
+#include <cstdlib>     // Used in Get(Home/Data)Directory()
+#include <pwd.h>       // Used in GetHomeDirectory()
+#include <sys/types.h> // Used in GetHomeDirectory()
+#include <unistd.h>    // Used in GetDataDirectory()
+#endif
+
+#ifdef __APPLE__
+#include <sys/param.h> // Used in GetBundleDirectory()
+
+// CFURL contains __attribute__ directives that gcc does not know how to parse, so we need to just
+// ignore them if we're not using clang. The macro is only used to prevent linking against
+// functions that don't exist on older versions of macOS, and the worst case scenario is a linker
+// error, so this is perfectly safe, just inconvenient.
+#ifndef __clang__
+#define availability(...)
+#endif
+#include <CoreFoundation/CFBundle.h> // Used in GetBundleDirectory()
+#include <CoreFoundation/CFString.h> // Used in GetBundleDirectory()
+#include <CoreFoundation/CFURL.h>    // Used in GetBundleDirectory()
+#ifdef availability
+#undef availability
+#endif
+#endif
+
+#ifndef MAX_PATH
+#ifdef _WIN32
+// This is the maximum number of UTF-16 code units permissible in Windows file paths
+#define MAX_PATH 260
+#else
+// This is the maximum number of UTF-8 code units permissible in all other OSes' file paths
+#define MAX_PATH 1024
+#endif
+#endif
+
+namespace Common::FS {
+
+namespace fs = std::filesystem;
+
+/**
+ * The PathManagerImpl is a singleton allowing to manage the mapping of
+ * YuzuPath enums to real filesystem paths.
+ * This class provides 2 functions: GetYuzuPathImpl and SetYuzuPathImpl.
+ * These are used by GetYuzuPath and SetYuzuPath respectively to get or modify
+ * the path mapped by the YuzuPath enum.
+ */
+class PathManagerImpl {
+public:
+    static PathManagerImpl& GetInstance() {
+        static PathManagerImpl path_manager_impl;
+
+        return path_manager_impl;
+    }
+
+    PathManagerImpl(const PathManagerImpl&) = delete;
+    PathManagerImpl& operator=(const PathManagerImpl&) = delete;
+
+    PathManagerImpl(PathManagerImpl&&) = delete;
+    PathManagerImpl& operator=(PathManagerImpl&&) = delete;
+
+    [[nodiscard]] const fs::path& GetYuzuPathImpl(YuzuPath yuzu_path) {
+        return yuzu_paths.at(yuzu_path);
+    }
+
+    void SetYuzuPathImpl(YuzuPath yuzu_path, const fs::path& new_path) {
+        yuzu_paths.insert_or_assign(yuzu_path, new_path);
+    }
+
+private:
+    PathManagerImpl() {
+#ifdef _WIN32
+        auto yuzu_path = GetExeDirectory() / PORTABLE_DIR;
+
+        if (!IsDir(yuzu_path)) {
+            yuzu_path = GetAppDataRoamingDirectory() / YUZU_DIR;
+        }
+
+        GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
+        GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR);
+        GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR);
+#else
+        auto yuzu_path = GetCurrentDir() / PORTABLE_DIR;
+
+        if (Exists(yuzu_path) && IsDir(yuzu_path)) {
+            GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
+            GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR);
+            GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR);
+        } else {
+            yuzu_path = GetDataDirectory("XDG_DATA_HOME") / YUZU_DIR;
+
+            GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path);
+            GenerateYuzuPath(YuzuPath::CacheDir, GetDataDirectory("XDG_CACHE_HOME") / YUZU_DIR);
+            GenerateYuzuPath(YuzuPath::ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / YUZU_DIR);
+        }
+#endif
+
+        GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR);
+        GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR);
+        GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR);
+        GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR);
+        GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR);
+        GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR);
+        GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
+        GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
+    }
+
+    ~PathManagerImpl() = default;
+
+    void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
+        void(FS::CreateDir(new_path));
+
+        SetYuzuPathImpl(yuzu_path, new_path);
+    }
+
+    std::unordered_map<YuzuPath, fs::path> yuzu_paths;
+};
+
+std::string PathToUTF8String(const fs::path& path) {
+    const auto utf8_string = path.u8string();
+
+    return std::string{utf8_string.begin(), utf8_string.end()};
+}
+
+bool ValidatePath(const fs::path& path) {
+    if (path.empty()) {
+        LOG_ERROR(Common_Filesystem, "Input path is empty, path={}", PathToUTF8String(path));
+        return false;
+    }
+
+#ifdef _WIN32
+    if (path.u16string().size() >= MAX_PATH) {
+        LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path));
+        return false;
+    }
+#else
+    if (path.u8string().size() >= MAX_PATH) {
+        LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path));
+        return false;
+    }
+#endif
+
+    return true;
+}
+
+fs::path ConcatPath(const fs::path& first, const fs::path& second) {
+    const bool second_has_dir_sep = IsDirSeparator(second.u8string().front());
+
+    if (!second_has_dir_sep) {
+        return (first / second).lexically_normal();
+    }
+
+    fs::path concat_path = first;
+    concat_path += second;
+
+    return concat_path.lexically_normal();
+}
+
+fs::path ConcatPathSafe(const fs::path& base, const fs::path& offset) {
+    const auto concatenated_path = ConcatPath(base, offset);
+
+    if (!IsPathSandboxed(base, concatenated_path)) {
+        return base;
+    }
+
+    return concatenated_path;
+}
+
+bool IsPathSandboxed(const fs::path& base, const fs::path& path) {
+    const auto base_string = RemoveTrailingSeparators(base.lexically_normal()).u8string();
+    const auto path_string = RemoveTrailingSeparators(path.lexically_normal()).u8string();
+
+    if (path_string.size() < base_string.size()) {
+        return false;
+    }
+
+    return base_string.compare(0, base_string.size(), path_string, 0, base_string.size()) == 0;
+}
+
+bool IsDirSeparator(char character) {
+    return character == '/' || character == '\\';
+}
+
+bool IsDirSeparator(char8_t character) {
+    return character == u8'/' || character == u8'\\';
+}
+
+fs::path RemoveTrailingSeparators(const fs::path& path) {
+    if (path.empty()) {
+        return path;
+    }
+
+    auto string_path = path.u8string();
+
+    while (IsDirSeparator(string_path.back())) {
+        string_path.pop_back();
+    }
+
+    return fs::path{string_path};
+}
+
+const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
+    return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
+}
+
+std::string GetYuzuPathString(YuzuPath yuzu_path) {
+    return PathToUTF8String(GetYuzuPath(yuzu_path));
+}
+
+void SetYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
+    if (!FS::IsDir(new_path)) {
+        LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} is not a directory",
+                  PathToUTF8String(new_path));
+        return;
+    }
+
+    PathManagerImpl::GetInstance().SetYuzuPathImpl(yuzu_path, new_path);
+}
+
+#ifdef _WIN32
+
+fs::path GetExeDirectory() {
+    wchar_t exe_path[MAX_PATH];
+
+    GetModuleFileNameW(nullptr, exe_path, MAX_PATH);
+
+    if (!exe_path) {
+        LOG_ERROR(Common_Filesystem,
+                  "Failed to get the path to the executable of the current process");
+    }
+
+    return fs::path{exe_path}.parent_path();
+}
+
+fs::path GetAppDataRoamingDirectory() {
+    PWSTR appdata_roaming_path = nullptr;
+
+    SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_roaming_path);
+
+    auto fs_appdata_roaming_path = fs::path{appdata_roaming_path};
+
+    CoTaskMemFree(appdata_roaming_path);
+
+    if (fs_appdata_roaming_path.empty()) {
+        LOG_ERROR(Common_Filesystem, "Failed to get the path to the %APPDATA% directory");
+    }
+
+    return fs_appdata_roaming_path;
+}
+
+#else
+
+fs::path GetHomeDirectory() {
+    const char* home_env_var = getenv("HOME");
+
+    if (home_env_var) {
+        return fs::path{home_env_var};
+    }
+
+    LOG_INFO(Common_Filesystem,
+             "$HOME is not defined in the environment variables, "
+             "attempting to query passwd to get the home path of the current user");
+
+    const auto* pw = getpwuid(getuid());
+
+    if (!pw) {
+        LOG_ERROR(Common_Filesystem, "Failed to get the home path of the current user");
+        return {};
+    }
+
+    return fs::path{pw->pw_dir};
+}
+
+fs::path GetDataDirectory(const std::string& env_name) {
+    const char* data_env_var = getenv(env_name.c_str());
+
+    if (data_env_var) {
+        return fs::path{data_env_var};
+    }
+
+    if (env_name == "XDG_DATA_HOME") {
+        return GetHomeDirectory() / ".local/share";
+    } else if (env_name == "XDG_CACHE_HOME") {
+        return GetHomeDirectory() / ".cache";
+    } else if (env_name == "XDG_CONFIG_HOME") {
+        return GetHomeDirectory() / ".config";
+    }
+
+    return {};
+}
+
+#endif
+
+#ifdef __APPLE__
+
+fs::path GetBundleDirectory() {
+    char app_bundle_path[MAXPATHLEN];
+
+    // Get the main bundle for the app
+    CFURLRef bundle_ref = CFBundleCopyBundleURL(CFBundleGetMainBundle());
+    CFStringRef bundle_path = CFURLCopyFileSystemPath(bundle_ref, kCFURLPOSIXPathStyle);
+
+    CFStringGetFileSystemRepresentation(bundle_path, app_bundle_path, sizeof(app_bundle_path));
+
+    CFRelease(bundle_ref);
+    CFRelease(bundle_path);
+
+    return fs::path{app_bundle_path};
+}
+
+#endif
+
+// vvvvvvvvvv Deprecated vvvvvvvvvv //
+
+std::string_view RemoveTrailingSlash(std::string_view path) {
+    if (path.empty()) {
+        return path;
+    }
+
+    if (path.back() == '\\' || path.back() == '/') {
+        path.remove_suffix(1);
+        return path;
+    }
+
+    return path;
+}
+
+std::vector<std::string> SplitPathComponents(std::string_view filename) {
+    std::string copy(filename);
+    std::replace(copy.begin(), copy.end(), '\\', '/');
+    std::vector<std::string> out;
+
+    std::stringstream stream(copy);
+    std::string item;
+    while (std::getline(stream, item, '/')) {
+        out.push_back(std::move(item));
+    }
+
+    return out;
+}
+
+std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
+    std::string path(path_);
+    char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
+    char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
+
+    if (directory_separator == DirectorySeparator::PlatformDefault) {
+#ifdef _WIN32
+        type1 = '/';
+        type2 = '\\';
+#endif
+    }
+
+    std::replace(path.begin(), path.end(), type1, type2);
+
+    auto start = path.begin();
+#ifdef _WIN32
+    // allow network paths which start with a double backslash (e.g. \\server\share)
+    if (start != path.end())
+        ++start;
+#endif
+    path.erase(std::unique(start, path.end(),
+                           [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }),
+               path.end());
+    return std::string(RemoveTrailingSlash(path));
+}
+
+std::string_view GetParentPath(std::string_view path) {
+    const auto name_bck_index = path.rfind('\\');
+    const auto name_fwd_index = path.rfind('/');
+    std::size_t name_index;
+
+    if (name_bck_index == std::string_view::npos || name_fwd_index == std::string_view::npos) {
+        name_index = std::min(name_bck_index, name_fwd_index);
+    } else {
+        name_index = std::max(name_bck_index, name_fwd_index);
+    }
+
+    return path.substr(0, name_index);
+}
+
+std::string_view GetPathWithoutTop(std::string_view path) {
+    if (path.empty()) {
+        return path;
+    }
+
+    while (path[0] == '\\' || path[0] == '/') {
+        path.remove_prefix(1);
+        if (path.empty()) {
+            return path;
+        }
+    }
+
+    const auto name_bck_index = path.find('\\');
+    const auto name_fwd_index = path.find('/');
+    return path.substr(std::min(name_bck_index, name_fwd_index) + 1);
+}
+
+std::string_view GetFilename(std::string_view path) {
+    const auto name_index = path.find_last_of("\\/");
+
+    if (name_index == std::string_view::npos) {
+        return {};
+    }
+
+    return path.substr(name_index + 1);
+}
+
+std::string_view GetExtensionFromFilename(std::string_view name) {
+    const std::size_t index = name.rfind('.');
+
+    if (index == std::string_view::npos) {
+        return {};
+    }
+
+    return name.substr(index + 1);
+}
+
+} // namespace Common::FS
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
new file mode 100644
index 0000000000..a9fadbceb9
--- /dev/null
+++ b/src/common/fs/path_util.h
@@ -0,0 +1,309 @@
+// Copyright 2021 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <filesystem>
+#include <vector>
+
+#include "common/fs/fs_util.h"
+
+namespace Common::FS {
+
+enum class YuzuPath {
+    YuzuDir,        // Where yuzu stores its data.
+    CacheDir,       // Where cached filesystem data is stored.
+    ConfigDir,      // Where config files are stored.
+    DumpDir,        // Where dumped data is stored.
+    KeysDir,        // Where key files are stored.
+    LoadDir,        // Where cheat/mod files are stored.
+    LogDir,         // Where log files are stored.
+    NANDDir,        // Where the emulated NAND is stored.
+    ScreenshotsDir, // Where yuzu screenshots are stored.
+    SDMCDir,        // Where the emulated SDMC is stored.
+    ShaderDir,      // Where shaders are stored.
+};
+
+/**
+ * Converts a filesystem path to a UTF-8 encoded std::string.
+ *
+ * @param path Filesystem path
+ *
+ * @returns UTF-8 encoded std::string.
+ */
+[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path);
+
+/**
+ * Validates a given path.
+ *
+ * A given path is valid if it meets these conditions:
+ * - The path is not empty
+ * - The path is not too long
+ *
+ * @param path Filesystem path
+ *
+ * @returns True if the path is valid, false otherwise.
+ */
+[[nodiscard]] bool ValidatePath(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] bool ValidatePath(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return ValidatePath(ToU8String(path));
+    } else {
+        return ValidatePath(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Concatenates two filesystem paths together.
+ *
+ * This is needed since the following occurs when using std::filesystem::path's operator/:
+ * first: "/first/path"
+ * second: "/second/path" (Note that the second path has a directory separator in the front)
+ * first / second yields "/second/path" when the desired result is first/path/second/path
+ *
+ * @param first First filesystem path
+ * @param second Second filesystem path
+ *
+ * @returns A concatenated filesystem path.
+ */
+[[nodiscard]] std::filesystem::path ConcatPath(const std::filesystem::path& first,
+                                               const std::filesystem::path& second);
+
+#ifdef _WIN32
+template <typename Path1, typename Path2>
+[[nodiscard]] std::filesystem::path ConcatPath(const Path1& first, const Path2& second) {
+    using ValueType1 = typename Path1::value_type;
+    using ValueType2 = typename Path2::value_type;
+    if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
+        return ConcatPath(ToU8String(first), ToU8String(second));
+    } else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
+        return ConcatPath(ToU8String(first), second);
+    } else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
+        return ConcatPath(first, ToU8String(second));
+    } else {
+        return ConcatPath(std::filesystem::path{first}, std::filesystem::path{second});
+    }
+}
+#endif
+
+/**
+ * Safe variant of ConcatPath that takes in a base path and an offset path from the given base path.
+ *
+ * If ConcatPath(base, offset) resolves to a path that is sandboxed within the base path,
+ * this will return the concatenated path. Otherwise this will return the base path.
+ *
+ * @param base Base filesystem path
+ * @param offset Offset filesystem path
+ *
+ * @returns A concatenated filesystem path if it is within the base path,
+ *          returns the base path otherwise.
+ */
+[[nodiscard]] std::filesystem::path ConcatPathSafe(const std::filesystem::path& base,
+                                                   const std::filesystem::path& offset);
+
+#ifdef _WIN32
+template <typename Path1, typename Path2>
+[[nodiscard]] std::filesystem::path ConcatPathSafe(const Path1& base, const Path2& offset) {
+    using ValueType1 = typename Path1::value_type;
+    using ValueType2 = typename Path2::value_type;
+    if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
+        return ConcatPathSafe(ToU8String(base), ToU8String(offset));
+    } else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
+        return ConcatPathSafe(ToU8String(base), offset);
+    } else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
+        return ConcatPathSafe(base, ToU8String(offset));
+    } else {
+        return ConcatPathSafe(std::filesystem::path{base}, std::filesystem::path{offset});
+    }
+}
+#endif
+
+/**
+ * Checks whether a given path is sandboxed within a given base path.
+ *
+ * @param base Base filesystem path
+ * @param path Filesystem path
+ *
+ * @returns True if the given path is sandboxed within the given base path, false otherwise.
+ */
+[[nodiscard]] bool IsPathSandboxed(const std::filesystem::path& base,
+                                   const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path1, typename Path2>
+[[nodiscard]] bool IsPathSandboxed(const Path1& base, const Path2& path) {
+    using ValueType1 = typename Path1::value_type;
+    using ValueType2 = typename Path2::value_type;
+    if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
+        return IsPathSandboxed(ToU8String(base), ToU8String(path));
+    } else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
+        return IsPathSandboxed(ToU8String(base), path);
+    } else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
+        return IsPathSandboxed(base, ToU8String(path));
+    } else {
+        return IsPathSandboxed(std::filesystem::path{base}, std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Checks if a character is a directory separator (either a forward slash or backslash).
+ *
+ * @param character Character
+ *
+ * @returns True if the character is a directory separator, false otherwise.
+ */
+[[nodiscard]] bool IsDirSeparator(char character);
+
+/**
+ * Checks if a character is a directory separator (either a forward slash or backslash).
+ *
+ * @param character Character
+ *
+ * @returns True if the character is a directory separator, false otherwise.
+ */
+[[nodiscard]] bool IsDirSeparator(char8_t character);
+
+/**
+ * Removes any trailing directory separators if they exist in the given path.
+ *
+ * @param path Filesystem path
+ *
+ * @returns The filesystem path without any trailing directory separators.
+ */
+[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const std::filesystem::path& path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const Path& path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        return RemoveTrailingSeparators(ToU8String(path));
+    } else {
+        return RemoveTrailingSeparators(std::filesystem::path{path});
+    }
+}
+#endif
+
+/**
+ * Gets the filesystem path associated with the YuzuPath enum.
+ *
+ * @param yuzu_path YuzuPath enum
+ *
+ * @returns The filesystem path associated with the YuzuPath enum.
+ */
+[[nodiscard]] const std::filesystem::path& GetYuzuPath(YuzuPath yuzu_path);
+
+/**
+ * Gets the filesystem path associated with the YuzuPath enum as a UTF-8 encoded std::string.
+ *
+ * @param yuzu_path YuzuPath enum
+ *
+ * @returns The filesystem path associated with the YuzuPath enum as a UTF-8 encoded std::string.
+ */
+[[nodiscard]] std::string GetYuzuPathString(YuzuPath yuzu_path);
+
+/**
+ * Sets a new filesystem path associated with the YuzuPath enum.
+ * If the filesystem object at new_path is not a directory, this function will not do anything.
+ *
+ * @param yuzu_path YuzuPath enum
+ * @param new_path New filesystem path
+ */
+void SetYuzuPath(YuzuPath yuzu_path, const std::filesystem::path& new_path);
+
+#ifdef _WIN32
+template <typename Path>
+[[nodiscard]] void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {
+    if constexpr (IsChar<typename Path::value_type>) {
+        SetYuzuPath(yuzu_path, ToU8String(new_path));
+    } else {
+        SetYuzuPath(yuzu_path, std::filesystem::path{new_path});
+    }
+}
+#endif
+
+#ifdef _WIN32
+
+/**
+ * Gets the path of the directory containing the executable of the current process.
+ *
+ * @returns The path of the directory containing the executable of the current process.
+ */
+[[nodiscard]] std::filesystem::path GetExeDirectory();
+
+/**
+ * Gets the path of the current user's %APPDATA% directory (%USERPROFILE%/AppData/Roaming).
+ *
+ * @returns The path of the current user's %APPDATA% directory.
+ */
+[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory();
+
+#else
+
+/**
+ * Gets the path of the directory specified by the #HOME environment variable.
+ * If $HOME is not defined, it will attempt to query the user database in passwd instead.
+ *
+ * @returns The path of the current user's home directory.
+ */
+[[nodiscard]] std::filesystem::path GetHomeDirectory();
+
+/**
+ * Gets the relevant paths for yuzu to store its data based on the given XDG environment variable.
+ * See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+ * Defaults to $HOME/.local/share for main application data,
+ * $HOME/.cache for cached data, and $HOME/.config for configuration files.
+ *
+ * @param env_name XDG environment variable name
+ *
+ * @returns The path where yuzu should store its data.
+ */
+[[nodiscard]] std::filesystem::path GetDataDirectory(const std::string& env_name);
+
+#endif
+
+#ifdef __APPLE__
+
+[[nodiscard]] std::filesystem::path GetBundleDirectory();
+
+#endif
+
+// vvvvvvvvvv Deprecated vvvvvvvvvv //
+
+// Removes the final '/' or '\' if one exists
+[[nodiscard]] std::string_view RemoveTrailingSlash(std::string_view path);
+
+enum class DirectorySeparator {
+    ForwardSlash,
+    BackwardSlash,
+    PlatformDefault,
+};
+
+// Splits the path on '/' or '\' and put the components into a vector
+// i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" }
+[[nodiscard]] std::vector<std::string> SplitPathComponents(std::string_view filename);
+
+// Removes trailing slash, makes all '\\' into '/', and removes duplicate '/'. Makes '/' into '\\'
+// depending if directory_separator is BackwardSlash or PlatformDefault and running on windows
+[[nodiscard]] std::string SanitizePath(
+    std::string_view path,
+    DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash);
+
+// Gets all of the text up to the last '/' or '\' in the path.
+[[nodiscard]] std::string_view GetParentPath(std::string_view path);
+
+// Gets all of the text after the first '/' or '\' in the path.
+[[nodiscard]] std::string_view GetPathWithoutTop(std::string_view path);
+
+// Gets the filename of the path
+[[nodiscard]] std::string_view GetFilename(std::string_view path);
+
+// Gets the extension of the filename
+[[nodiscard]] std::string_view GetExtensionFromFilename(std::string_view name);
+
+} // namespace Common::FS
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index 96efa977d8..6aa8ac960d 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -11,13 +11,13 @@
 #include <mutex>
 #include <thread>
 #include <vector>
+
 #ifdef _WIN32
-#include <share.h>   // For _SH_DENYWR
 #include <windows.h> // For OutputDebugStringW
-#else
-#define _SH_DENYWR 0
 #endif
+
 #include "common/assert.h"
+#include "common/fs/fs.h"
 #include "common/logging/backend.h"
 #include "common/logging/log.h"
 #include "common/logging/text_formatter.h"
@@ -148,19 +148,16 @@ void ColorConsoleBackend::Write(const Entry& entry) {
     PrintColoredMessage(entry);
 }
 
-FileBackend::FileBackend(const std::string& filename) {
-    const auto old_filename = filename + ".old.txt";
+FileBackend::FileBackend(const std::filesystem::path& filename) {
+    auto old_filename = filename;
+    old_filename += ".old.txt";
 
-    if (FS::Exists(old_filename)) {
-        FS::Delete(old_filename);
-    }
-    if (FS::Exists(filename)) {
-        FS::Rename(filename, old_filename);
-    }
+    // Existence checks are done within the functions themselves.
+    // We don't particularly care if these succeed or not.
+    void(FS::RemoveFile(old_filename));
+    void(FS::RenameFile(filename, old_filename));
 
-    // _SH_DENYWR allows read only access to the file for other programs.
-    // It is #defined to 0 on other platforms
-    file = FS::IOFile(filename, "w", _SH_DENYWR);
+    file = FS::IOFile(filename, FS::FileAccessMode::Write, FS::FileType::TextFile);
 }
 
 void FileBackend::Write(const Entry& entry) {
@@ -181,7 +178,7 @@ void FileBackend::Write(const Entry& entry) {
 
     bytes_written += file.WriteString(FormatLogMessage(entry).append(1, '\n'));
     if (entry.log_level >= Level::Error) {
-        file.Flush();
+        void(file.Flush());
     }
 }
 
diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h
index 9dd2589c3c..eb629a33f6 100644
--- a/src/common/logging/backend.h
+++ b/src/common/logging/backend.h
@@ -4,10 +4,11 @@
 #pragma once
 
 #include <chrono>
+#include <filesystem>
 #include <memory>
 #include <string>
 #include <string_view>
-#include "common/file_util.h"
+#include "common/fs/file.h"
 #include "common/logging/filter.h"
 #include "common/logging/log.h"
 
@@ -81,7 +82,7 @@ public:
  */
 class FileBackend : public Backend {
 public:
-    explicit FileBackend(const std::string& filename);
+    explicit FileBackend(const std::filesystem::path& filename);
 
     static const char* Name() {
         return "file";
diff --git a/src/common/nvidia_flags.cpp b/src/common/nvidia_flags.cpp
index d537517db9..d1afd1f1de 100644
--- a/src/common/nvidia_flags.cpp
+++ b/src/common/nvidia_flags.cpp
@@ -2,24 +2,30 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include <filesystem>
-#include <stdlib.h>
+#include <cstdlib>
 
 #include <fmt/format.h>
 
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/nvidia_flags.h"
 
 namespace Common {
 
 void ConfigureNvidiaEnvironmentFlags() {
 #ifdef _WIN32
-    const std::string shader_path = Common::FS::SanitizePath(
-        fmt::format("{}/nvidia/", Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir)));
-    const std::string windows_path =
-        Common::FS::SanitizePath(shader_path, Common::FS::DirectorySeparator::BackwardSlash);
-    void(Common::FS::CreateFullPath(shader_path + '/'));
-    void(_putenv(fmt::format("__GL_SHADER_DISK_CACHE_PATH={}", windows_path).c_str()));
+    const auto nvidia_shader_dir =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir) / "nvidia";
+
+    if (!Common::FS::CreateDirs(nvidia_shader_dir)) {
+        return;
+    }
+
+    const auto windows_path_string =
+        Common::FS::PathToUTF8String(nvidia_shader_dir.lexically_normal());
+
+    void(_putenv(fmt::format("__GL_SHADER_DISK_CACHE_PATH={}", windows_path_string).c_str()));
     void(_putenv("__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1"));
 #endif
 }
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index e29cbf506a..bcb4e4be1a 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -5,7 +5,7 @@
 #include <string_view>
 
 #include "common/assert.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/settings.h"
 
@@ -34,6 +34,10 @@ void LogSettings() {
         LOG_INFO(Config, "{}: {}", name, value);
     };
 
+    const auto log_path = [](std::string_view name, const std::filesystem::path& path) {
+        LOG_INFO(Config, "{}: {}", name, Common::FS::PathToUTF8String(path));
+    };
+
     LOG_INFO(Config, "yuzu Configuration:");
     log_setting("Controls_UseDockedMode", values.use_docked_mode.GetValue());
     log_setting("System_RngSeed", values.rng_seed.GetValue().value_or(0));
@@ -59,11 +63,11 @@ void LogSettings() {
     log_setting("Audio_EnableAudioStretching", values.enable_audio_stretching.GetValue());
     log_setting("Audio_OutputDevice", values.audio_device_id);
     log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd);
-    log_setting("DataStorage_CacheDir", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir));
-    log_setting("DataStorage_ConfigDir", Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir));
-    log_setting("DataStorage_LoadDir", Common::FS::GetUserPath(Common::FS::UserPath::LoadDir));
-    log_setting("DataStorage_NandDir", Common::FS::GetUserPath(Common::FS::UserPath::NANDDir));
-    log_setting("DataStorage_SdmcDir", Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir));
+    log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir));
+    log_path("DataStorage_ConfigDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir));
+    log_path("DataStorage_LoadDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::LoadDir));
+    log_path("DataStorage_NANDDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir));
+    log_path("DataStorage_SDMCDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir));
     log_setting("Debugging_ProgramArgs", values.program_args);
     log_setting("Services_BCATBackend", values.bcat_backend);
     log_setting("Services_BCATBoxcatLocal", values.bcat_boxcat_local);
diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp
index 7b614ad89f..e6344fd415 100644
--- a/src/common/string_util.cpp
+++ b/src/common/string_util.cpp
@@ -9,7 +9,6 @@
 #include <locale>
 #include <sstream>
 
-#include "common/common_paths.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 
@@ -93,18 +92,6 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _
     return true;
 }
 
-void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _Path,
-                           const std::string& _Filename) {
-    _CompleteFilename = _Path;
-
-    // check for seperator
-    if (DIR_SEP_CHR != *_CompleteFilename.rbegin())
-        _CompleteFilename += DIR_SEP_CHR;
-
-    // add the filename
-    _CompleteFilename += _Filename;
-}
-
 void SplitString(const std::string& str, const char delim, std::vector<std::string>& output) {
     std::istringstream iss(str);
     output.resize(1);
diff --git a/src/common/string_util.h b/src/common/string_util.h
index a32c07c062..7e90a9ca52 100644
--- a/src/common/string_util.h
+++ b/src/common/string_util.h
@@ -32,8 +32,6 @@ void SplitString(const std::string& str, char delim, std::vector<std::string>& o
 bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename,
                std::string* _pExtension);
 
-void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _Path,
-                           const std::string& _Filename);
 [[nodiscard]] std::string ReplaceAll(std::string result, const std::string& src,
                                      const std::string& dest);
 
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 826a00ad68..c5004b7b47 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -6,7 +6,7 @@
 #include <memory>
 #include <utility>
 
-#include "common/file_util.h"
+#include "common/fs/fs.h"
 #include "common/logging/log.h"
 #include "common/microprofile.h"
 #include "common/settings.h"
@@ -121,7 +121,7 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs,
                                                                   dir->GetName());
     }
 
-    if (Common::FS::IsDirectory(path)) {
+    if (Common::FS::IsDir(path)) {
         return vfs->OpenFile(path + "/main", FileSys::Mode::Read);
     }
 
diff --git a/src/core/crypto/key_manager.cpp b/src/core/crypto/key_manager.cpp
index a4b739c632..fb451a4230 100644
--- a/src/core/crypto/key_manager.cpp
+++ b/src/core/crypto/key_manager.cpp
@@ -18,8 +18,9 @@
 #include <mbedtls/cmac.h>
 #include <mbedtls/sha256.h>
 #include "common/common_funcs.h"
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
 #include "common/settings.h"
@@ -325,46 +326,55 @@ Key128 DeriveKeyblobMACKey(const Key128& keyblob_key, const Key128& mac_source)
 }
 
 std::optional<Key128> DeriveSDSeed() {
-    const Common::FS::IOFile save_43(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                                         "/system/save/8000000000000043",
-                                     "rb+");
+    const auto system_save_43_path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/8000000000000043";
+    const Common::FS::IOFile save_43{system_save_43_path, Common::FS::FileAccessMode::Read,
+                                     Common::FS::FileType::BinaryFile};
+
     if (!save_43.IsOpen()) {
         return std::nullopt;
     }
 
-    const Common::FS::IOFile sd_private(Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir) +
-                                            "/Nintendo/Contents/private",
-                                        "rb+");
+    const auto sd_private_path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "Nintendo/Contents/private";
+
+    const Common::FS::IOFile sd_private{sd_private_path, Common::FS::FileAccessMode::Read,
+                                        Common::FS::FileType::BinaryFile};
+
     if (!sd_private.IsOpen()) {
         return std::nullopt;
     }
 
     std::array<u8, 0x10> private_seed{};
-    if (sd_private.ReadBytes(private_seed.data(), private_seed.size()) != private_seed.size()) {
+    if (sd_private.Read(private_seed) != private_seed.size()) {
         return std::nullopt;
     }
 
     std::array<u8, 0x10> buffer{};
-    std::size_t offset = 0;
-    for (; offset + 0x10 < save_43.GetSize(); ++offset) {
-        if (!save_43.Seek(offset, SEEK_SET)) {
+    s64 offset = 0;
+    for (; offset + 0x10 < static_cast<s64>(save_43.GetSize()); ++offset) {
+        if (!save_43.Seek(offset)) {
+            return std::nullopt;
+        }
+
+        if (save_43.Read(buffer) != buffer.size()) {
             return std::nullopt;
         }
 
-        save_43.ReadBytes(buffer.data(), buffer.size());
         if (buffer == private_seed) {
             break;
         }
     }
 
-    if (!save_43.Seek(offset + 0x10, SEEK_SET)) {
+    if (!save_43.Seek(offset + 0x10)) {
         return std::nullopt;
     }
 
     Key128 seed{};
-    if (save_43.ReadBytes(seed.data(), seed.size()) != seed.size()) {
+    if (save_43.Read(seed) != seed.size()) {
         return std::nullopt;
     }
+
     return seed;
 }
 
@@ -435,7 +445,7 @@ std::vector<Ticket> GetTicketblob(const Common::FS::IOFile& ticket_save) {
     }
 
     std::vector<u8> buffer(ticket_save.GetSize());
-    if (ticket_save.ReadBytes(buffer.data(), buffer.size()) != buffer.size()) {
+    if (ticket_save.Read(buffer) != buffer.size()) {
         return {};
     }
 
@@ -566,27 +576,26 @@ std::optional<std::pair<Key128, Key128>> ParseTicket(const Ticket& ticket,
 
 KeyManager::KeyManager() {
     // Initialize keys
-    const std::string hactool_keys_dir = Common::FS::GetHactoolConfigurationPath();
-    const std::string yuzu_keys_dir = Common::FS::GetUserPath(Common::FS::UserPath::KeysDir);
+    const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir);
 
-    if (!Common::FS::Exists(yuzu_keys_dir)) {
-        Common::FS::CreateDir(yuzu_keys_dir);
+    if (!Common::FS::CreateDir(yuzu_keys_dir)) {
+        LOG_ERROR(Core, "Failed to create the keys directory.");
     }
 
     if (Settings::values.use_dev_keys) {
         dev_mode = true;
-        AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "dev.keys", false);
-        AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "dev.keys_autogenerated", false);
+        LoadFromFile(yuzu_keys_dir / "dev.keys", false);
+        LoadFromFile(yuzu_keys_dir / "dev.keys_autogenerated", false);
     } else {
         dev_mode = false;
-        AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "prod.keys", false);
-        AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "prod.keys_autogenerated", false);
+        LoadFromFile(yuzu_keys_dir / "prod.keys", false);
+        LoadFromFile(yuzu_keys_dir / "prod.keys_autogenerated", false);
     }
 
-    AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "title.keys", true);
-    AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "title.keys_autogenerated", true);
-    AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "console.keys", false);
-    AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "console.keys_autogenerated", false);
+    LoadFromFile(yuzu_keys_dir / "title.keys", true);
+    LoadFromFile(yuzu_keys_dir / "title.keys_autogenerated", true);
+    LoadFromFile(yuzu_keys_dir / "console.keys", false);
+    LoadFromFile(yuzu_keys_dir / "console.keys_autogenerated", false);
 }
 
 static bool ValidCryptoRevisionString(std::string_view base, size_t begin, size_t length) {
@@ -597,9 +606,14 @@ static bool ValidCryptoRevisionString(std::string_view base, size_t begin, size_
                        [](u8 c) { return std::isxdigit(c); });
 }
 
-void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
+void KeyManager::LoadFromFile(const std::filesystem::path& file_path, bool is_title_keys) {
+    if (!Common::FS::Exists(file_path)) {
+        return;
+    }
+
     std::ifstream file;
-    Common::FS::OpenFStream(file, filename, std::ios_base::in);
+    Common::FS::OpenFileStream(file, file_path, std::ios_base::in);
+
     if (!file.is_open()) {
         return;
     }
@@ -694,15 +708,6 @@ void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
     }
 }
 
-void KeyManager::AttemptLoadKeyFile(const std::string& dir1, const std::string& dir2,
-                                    const std::string& filename, bool title) {
-    if (Common::FS::Exists(dir1 + DIR_SEP + filename)) {
-        LoadFromFile(dir1 + DIR_SEP + filename, title);
-    } else if (Common::FS::Exists(dir2 + DIR_SEP + filename)) {
-        LoadFromFile(dir2 + DIR_SEP + filename, title);
-    }
-}
-
 bool KeyManager::BaseDeriveNecessary() const {
     const auto check_key_existence = [this](auto key_type, u64 index1 = 0, u64 index2 = 0) {
         return !HasKey(key_type, index1, index2);
@@ -766,30 +771,35 @@ Key256 KeyManager::GetBISKey(u8 partition_id) const {
 template <size_t Size>
 void KeyManager::WriteKeyToFile(KeyCategory category, std::string_view keyname,
                                 const std::array<u8, Size>& key) {
-    const std::string yuzu_keys_dir = Common::FS::GetUserPath(Common::FS::UserPath::KeysDir);
+    const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir);
+
     std::string filename = "title.keys_autogenerated";
+
     if (category == KeyCategory::Standard) {
         filename = dev_mode ? "dev.keys_autogenerated" : "prod.keys_autogenerated";
     } else if (category == KeyCategory::Console) {
         filename = "console.keys_autogenerated";
     }
 
-    const auto path = yuzu_keys_dir + DIR_SEP + filename;
+    const auto path = yuzu_keys_dir / filename;
     const auto add_info_text = !Common::FS::Exists(path);
-    Common::FS::CreateFullPath(path);
-    Common::FS::IOFile file{path, "a"};
+
+    Common::FS::IOFile file{path, Common::FS::FileAccessMode::Append,
+                            Common::FS::FileType::TextFile};
+
     if (!file.IsOpen()) {
         return;
     }
+
     if (add_info_text) {
-        file.WriteString(
+        void(file.WriteString(
             "# This file is autogenerated by Yuzu\n"
             "# It serves to store keys that were automatically generated from the normal keys\n"
-            "# If you are experiencing issues involving keys, it may help to delete this file\n");
+            "# If you are experiencing issues involving keys, it may help to delete this file\n"));
     }
 
-    file.WriteString(fmt::format("\n{} = {}", keyname, Common::HexToString(key)));
-    AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, filename, category == KeyCategory::Title);
+    void(file.WriteString(fmt::format("\n{} = {}", keyname, Common::HexToString(key))));
+    LoadFromFile(path, category == KeyCategory::Title);
 }
 
 void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
@@ -861,20 +871,17 @@ void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
 }
 
 bool KeyManager::KeyFileExists(bool title) {
-    const std::string hactool_keys_dir = Common::FS::GetHactoolConfigurationPath();
-    const std::string yuzu_keys_dir = Common::FS::GetUserPath(Common::FS::UserPath::KeysDir);
+    const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir);
+
     if (title) {
-        return Common::FS::Exists(hactool_keys_dir + DIR_SEP + "title.keys") ||
-               Common::FS::Exists(yuzu_keys_dir + DIR_SEP + "title.keys");
+        return Common::FS::Exists(yuzu_keys_dir / "title.keys");
     }
 
     if (Settings::values.use_dev_keys) {
-        return Common::FS::Exists(hactool_keys_dir + DIR_SEP + "dev.keys") ||
-               Common::FS::Exists(yuzu_keys_dir + DIR_SEP + "dev.keys");
+        return Common::FS::Exists(yuzu_keys_dir / "dev.keys");
     }
 
-    return Common::FS::Exists(hactool_keys_dir + DIR_SEP + "prod.keys") ||
-           Common::FS::Exists(yuzu_keys_dir + DIR_SEP + "prod.keys");
+    return Common::FS::Exists(yuzu_keys_dir / "prod.keys");
 }
 
 void KeyManager::DeriveSDSeedLazy() {
@@ -1115,15 +1122,21 @@ void KeyManager::PopulateTickets() {
         return;
     }
 
-    const Common::FS::IOFile save1(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                                       "/system/save/80000000000000e1",
-                                   "rb+");
-    const Common::FS::IOFile save2(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                                       "/system/save/80000000000000e2",
-                                   "rb+");
+    const auto system_save_e1_path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/80000000000000e1";
+
+    const Common::FS::IOFile save_e1{system_save_e1_path, Common::FS::FileAccessMode::Read,
+                                     Common::FS::FileType::BinaryFile};
+
+    const auto system_save_e2_path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/80000000000000e2";
+
+    const Common::FS::IOFile save_e2{system_save_e2_path, Common::FS::FileAccessMode::Read,
+                                     Common::FS::FileType::BinaryFile};
+
+    const auto blob2 = GetTicketblob(save_e2);
+    auto res = GetTicketblob(save_e1);
 
-    const auto blob2 = GetTicketblob(save2);
-    auto res = GetTicketblob(save1);
     const auto idx = res.size();
     res.insert(res.end(), blob2.begin(), blob2.end());
 
diff --git a/src/core/crypto/key_manager.h b/src/core/crypto/key_manager.h
index 0a72202866..e771625e1f 100644
--- a/src/core/crypto/key_manager.h
+++ b/src/core/crypto/key_manager.h
@@ -5,6 +5,7 @@
 #pragma once
 
 #include <array>
+#include <filesystem>
 #include <map>
 #include <optional>
 #include <string>
@@ -283,9 +284,8 @@ private:
     std::array<u8, 576> eticket_extended_kek{};
 
     bool dev_mode;
-    void LoadFromFile(const std::string& filename, bool is_title_keys);
-    void AttemptLoadKeyFile(const std::string& dir1, const std::string& dir2,
-                            const std::string& filename, bool title);
+    void LoadFromFile(const std::filesystem::path& file_path, bool is_title_keys);
+
     template <size_t Size>
     void WriteKeyToFile(KeyCategory category, std::string_view keyname,
                         const std::array<u8, Size>& key);
diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp
index 7c6304ff0e..f3891acf18 100644
--- a/src/core/file_sys/bis_factory.cpp
+++ b/src/core/file_sys/bis_factory.cpp
@@ -3,7 +3,7 @@
 // Refer to the license.txt file included.
 
 #include <fmt/format.h>
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "core/file_sys/bis_factory.h"
 #include "core/file_sys/mode.h"
 #include "core/file_sys/registered_cache.h"
@@ -85,7 +85,7 @@ VirtualFile BISFactory::OpenPartitionStorage(BisPartitionId id,
                                              VirtualFilesystem file_system) const {
     auto& keys = Core::Crypto::KeyManager::Instance();
     Core::Crypto::PartitionDataManager pdm{file_system->OpenDirectory(
-        Common::FS::GetUserPath(Common::FS::UserPath::SysDataDir), Mode::Read)};
+        Common::FS::GetYuzuPathString(Common::FS::YuzuPath::NANDDir), Mode::Read)};
     keys.PopulateFromPartitionData(pdm);
 
     switch (id) {
diff --git a/src/core/file_sys/mode.h b/src/core/file_sys/mode.h
index 2b4f210730..6c49a64e2e 100644
--- a/src/core/file_sys/mode.h
+++ b/src/core/file_sys/mode.h
@@ -10,11 +10,13 @@
 namespace FileSys {
 
 enum class Mode : u32 {
-    Read = 1,
-    Write = 2,
+    Read = 1 << 0,
+    Write = 1 << 1,
     ReadWrite = Read | Write,
-    Append = 4,
+    Append = 1 << 2,
+    ReadAppend = Read | Append,
     WriteAppend = Write | Append,
+    All = ReadWrite | Append,
 };
 
 DECLARE_ENUM_FLAG_OPERATORS(Mode)
diff --git a/src/core/file_sys/partition_filesystem.cpp b/src/core/file_sys/partition_filesystem.cpp
index 48a2ed4d4e..c5967049e8 100644
--- a/src/core/file_sys/partition_filesystem.cpp
+++ b/src/core/file_sys/partition_filesystem.cpp
@@ -8,7 +8,6 @@
 #include <iterator>
 #include <utility>
 
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "core/file_sys/partition_filesystem.h"
 #include "core/file_sys/vfs_offset.h"
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index cc9b4b6373..53b8b7ca0d 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.cpp
@@ -7,7 +7,6 @@
 #include <cstddef>
 #include <cstring>
 
-#include "common/file_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
 #include "common/settings.h"
diff --git a/src/core/file_sys/registered_cache.cpp b/src/core/file_sys/registered_cache.cpp
index b0cb659525..066c6789aa 100644
--- a/src/core/file_sys/registered_cache.cpp
+++ b/src/core/file_sys/registered_cache.cpp
@@ -7,7 +7,7 @@
 #include <regex>
 #include <mbedtls/sha256.h>
 #include "common/assert.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
 #include "core/crypto/key_manager.h"
diff --git a/src/core/file_sys/vfs.cpp b/src/core/file_sys/vfs.cpp
index f497e9396d..215e1cb1a5 100644
--- a/src/core/file_sys/vfs.cpp
+++ b/src/core/file_sys/vfs.cpp
@@ -5,8 +5,7 @@
 #include <algorithm>
 #include <numeric>
 #include <string>
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "core/file_sys/mode.h"
 #include "core/file_sys/vfs.h"
@@ -122,15 +121,14 @@ VirtualDir VfsFilesystem::CopyDirectory(std::string_view old_path_, std::string_
         return nullptr;
 
     for (const auto& file : old_dir->GetFiles()) {
-        const auto x =
-            CopyFile(old_path + DIR_SEP + file->GetName(), new_path + DIR_SEP + file->GetName());
+        const auto x = CopyFile(old_path + '/' + file->GetName(), new_path + '/' + file->GetName());
         if (x == nullptr)
             return nullptr;
     }
 
     for (const auto& dir : old_dir->GetSubdirectories()) {
         const auto x =
-            CopyDirectory(old_path + DIR_SEP + dir->GetName(), new_path + DIR_SEP + dir->GetName());
+            CopyDirectory(old_path + '/' + dir->GetName(), new_path + '/' + dir->GetName());
         if (x == nullptr)
             return nullptr;
     }
diff --git a/src/core/file_sys/vfs_libzip.cpp b/src/core/file_sys/vfs_libzip.cpp
index 618eb658a6..cd162c0c3f 100644
--- a/src/core/file_sys/vfs_libzip.cpp
+++ b/src/core/file_sys/vfs_libzip.cpp
@@ -13,6 +13,7 @@
 #pragma GCC diagnostic pop
 #endif
 
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "core/file_sys/vfs.h"
 #include "core/file_sys/vfs_libzip.h"
diff --git a/src/core/file_sys/vfs_real.cpp b/src/core/file_sys/vfs_real.cpp
index 3d89dd6449..d0b8fd0468 100644
--- a/src/core/file_sys/vfs_real.cpp
+++ b/src/core/file_sys/vfs_real.cpp
@@ -7,8 +7,9 @@
 #include <iterator>
 #include <utility>
 #include "common/assert.h"
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "core/file_sys/vfs_real.h"
 
@@ -16,33 +17,31 @@ namespace FileSys {
 
 namespace FS = Common::FS;
 
-static std::string ModeFlagsToString(Mode mode) {
-    std::string mode_str;
+namespace {
 
-    // Calculate the correct open mode for the file.
-    if (True(mode & Mode::Read) && True(mode & Mode::Write)) {
-        if (True(mode & Mode::Append)) {
-            mode_str = "a+";
-        } else {
-            mode_str = "r+";
-        }
-    } else {
-        if (True(mode & Mode::Read)) {
-            mode_str = "r";
-        } else if (True(mode & Mode::Append)) {
-            mode_str = "a";
-        } else if (True(mode & Mode::Write)) {
-            mode_str = "w";
-        } else {
-            UNREACHABLE_MSG("Invalid file open mode: {:02X}", static_cast<u8>(mode));
-        }
+constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(Mode mode) {
+    switch (mode) {
+    case Mode::Read:
+        return FS::FileAccessMode::Read;
+    case Mode::Write:
+        return FS::FileAccessMode::Write;
+    case Mode::ReadWrite:
+        return FS::FileAccessMode::ReadWrite;
+    case Mode::Append:
+        return FS::FileAccessMode::Append;
+    case Mode::ReadAppend:
+        return FS::FileAccessMode::ReadAppend;
+    case Mode::WriteAppend:
+        return FS::FileAccessMode::Append;
+    case Mode::All:
+        return FS::FileAccessMode::ReadAppend;
+    default:
+        return {};
     }
-
-    mode_str += "b";
-
-    return mode_str;
 }
 
+} // Anonymous namespace
+
 RealVfsFilesystem::RealVfsFilesystem() : VfsFilesystem(nullptr) {}
 RealVfsFilesystem::~RealVfsFilesystem() = default;
 
@@ -63,7 +62,7 @@ VfsEntryType RealVfsFilesystem::GetEntryType(std::string_view path_) const {
     if (!FS::Exists(path)) {
         return VfsEntryType::None;
     }
-    if (FS::IsDirectory(path)) {
+    if (FS::IsDir(path)) {
         return VfsEntryType::Directory;
     }
 
@@ -81,12 +80,13 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
         }
     }
 
-    if (!FS::Exists(path) && True(perms & Mode::WriteAppend)) {
-        FS::CreateEmptyFile(path);
+    auto backing = FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
+
+    if (!backing) {
+        return nullptr;
     }
 
-    auto backing = std::make_shared<FS::IOFile>(path, ModeFlagsToString(perms).c_str());
-    cache.insert_or_assign(path, backing);
+    cache.insert_or_assign(path, std::move(backing));
 
     // Cannot use make_shared as RealVfsFile constructor is private
     return std::shared_ptr<RealVfsFile>(new RealVfsFile(*this, backing, path, perms));
@@ -94,25 +94,29 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) {
 
 VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) {
     const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
-    const auto path_fwd = FS::SanitizePath(path, FS::DirectorySeparator::ForwardSlash);
-    if (!FS::Exists(path)) {
-        FS::CreateFullPath(path_fwd);
-        if (!FS::CreateEmptyFile(path)) {
+    // Current usages of CreateFile expect to delete the contents of an existing file.
+    if (FS::IsFile(path)) {
+        FS::IOFile temp{path, FS::FileAccessMode::Write, FS::FileType::BinaryFile};
+
+        if (!temp.IsOpen()) {
             return nullptr;
         }
+
+        temp.Close();
+
+        return OpenFile(path, perms);
     }
+
+    if (!FS::NewFile(path)) {
+        return nullptr;
+    }
+
     return OpenFile(path, perms);
 }
 
 VirtualFile RealVfsFilesystem::CopyFile(std::string_view old_path_, std::string_view new_path_) {
-    const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault);
-    const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
-
-    if (!FS::Exists(old_path) || FS::Exists(new_path) || FS::IsDirectory(old_path) ||
-        !FS::Copy(old_path, new_path)) {
-        return nullptr;
-    }
-    return OpenFile(new_path, Mode::ReadWrite);
+    // Unused
+    return nullptr;
 }
 
 VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_view new_path_) {
@@ -127,13 +131,13 @@ VirtualFile RealVfsFilesystem::MoveFile(std::string_view old_path_, std::string_
             file->Close();
         }
 
-        if (!FS::Exists(old_path) || FS::Exists(new_path) || FS::IsDirectory(old_path) ||
-            !FS::Rename(old_path, new_path)) {
+        if (!FS::RenameFile(old_path, new_path)) {
             return nullptr;
         }
 
         cache.erase(old_path);
-        if (file->Open(new_path, "r+b")) {
+        file->Open(new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
+        if (file->IsOpen()) {
             cache.insert_or_assign(new_path, std::move(file));
         } else {
             LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", new_path);
@@ -157,7 +161,7 @@ bool RealVfsFilesystem::DeleteFile(std::string_view path_) {
         cache.erase(path);
     }
 
-    return FS::Delete(path);
+    return FS::RemoveFile(path);
 }
 
 VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms) {
@@ -168,12 +172,8 @@ VirtualDir RealVfsFilesystem::OpenDirectory(std::string_view path_, Mode perms)
 
 VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms) {
     const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
-    const auto path_fwd = FS::SanitizePath(path, FS::DirectorySeparator::ForwardSlash);
-    if (!FS::Exists(path)) {
-        FS::CreateFullPath(path_fwd);
-        if (!FS::CreateDir(path)) {
-            return nullptr;
-        }
+    if (!FS::CreateDirs(path)) {
+        return nullptr;
     }
     // Cannot use make_shared as RealVfsDirectory constructor is private
     return std::shared_ptr<RealVfsDirectory>(new RealVfsDirectory(*this, path, perms));
@@ -181,13 +181,8 @@ VirtualDir RealVfsFilesystem::CreateDirectory(std::string_view path_, Mode perms
 
 VirtualDir RealVfsFilesystem::CopyDirectory(std::string_view old_path_,
                                             std::string_view new_path_) {
-    const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault);
-    const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
-    if (!FS::Exists(old_path) || FS::Exists(new_path) || !FS::IsDirectory(old_path)) {
-        return nullptr;
-    }
-    FS::CopyDir(old_path, new_path);
-    return OpenDirectory(new_path, Mode::ReadWrite);
+    // Unused
+    return nullptr;
 }
 
 VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
@@ -195,8 +190,7 @@ VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
     const auto old_path = FS::SanitizePath(old_path_, FS::DirectorySeparator::PlatformDefault);
     const auto new_path = FS::SanitizePath(new_path_, FS::DirectorySeparator::PlatformDefault);
 
-    if (!FS::Exists(old_path) || FS::Exists(new_path) || FS::IsDirectory(old_path) ||
-        !FS::Rename(old_path, new_path)) {
+    if (!FS::RenameDir(old_path, new_path)) {
         return nullptr;
     }
 
@@ -208,7 +202,7 @@ VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
 
         const auto file_old_path =
             FS::SanitizePath(kv.first, FS::DirectorySeparator::PlatformDefault);
-        auto file_new_path = FS::SanitizePath(new_path + DIR_SEP + kv.first.substr(old_path.size()),
+        auto file_new_path = FS::SanitizePath(new_path + '/' + kv.first.substr(old_path.size()),
                                               FS::DirectorySeparator::PlatformDefault);
         const auto& cached = cache[file_old_path];
 
@@ -218,7 +212,8 @@ VirtualDir RealVfsFilesystem::MoveDirectory(std::string_view old_path_,
 
         auto file = cached.lock();
         cache.erase(file_old_path);
-        if (file->Open(file_new_path, "r+b")) {
+        file->Open(file_new_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
+        if (file->IsOpen()) {
             cache.insert_or_assign(std::move(file_new_path), std::move(file));
         } else {
             LOG_ERROR(Service_FS, "Failed to open path {} in order to re-cache it", file_new_path);
@@ -245,15 +240,13 @@ bool RealVfsFilesystem::DeleteDirectory(std::string_view path_) {
         cache.erase(kv.first);
     }
 
-    return FS::DeleteDirRecursively(path);
+    return FS::RemoveDirRecursively(path);
 }
 
 RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::shared_ptr<FS::IOFile> backing_,
                          const std::string& path_, Mode perms_)
     : base(base_), backing(std::move(backing_)), path(path_), parent_path(FS::GetParentPath(path_)),
-      path_components(FS::SplitPathComponents(path_)),
-      parent_components(FS::SliceVector(path_components, 0, path_components.size() - 1)),
-      perms(perms_) {}
+      path_components(FS::SplitPathComponents(path_)), perms(perms_) {}
 
 RealVfsFile::~RealVfsFile() = default;
 
@@ -266,7 +259,7 @@ std::size_t RealVfsFile::GetSize() const {
 }
 
 bool RealVfsFile::Resize(std::size_t new_size) {
-    return backing->Resize(new_size);
+    return backing->SetSize(new_size);
 }
 
 VirtualDir RealVfsFile::GetContainingDirectory() const {
@@ -274,33 +267,33 @@ VirtualDir RealVfsFile::GetContainingDirectory() const {
 }
 
 bool RealVfsFile::IsWritable() const {
-    return True(perms & Mode::WriteAppend);
+    return True(perms & Mode::Write);
 }
 
 bool RealVfsFile::IsReadable() const {
-    return True(perms & Mode::ReadWrite);
+    return True(perms & Mode::Read);
 }
 
 std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const {
-    if (!backing->Seek(static_cast<s64>(offset), SEEK_SET)) {
+    if (!backing->Seek(static_cast<s64>(offset))) {
         return 0;
     }
-    return backing->ReadBytes(data, length);
+    return backing->ReadSpan(std::span{data, length});
 }
 
 std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) {
-    if (!backing->Seek(static_cast<s64>(offset), SEEK_SET)) {
+    if (!backing->Seek(static_cast<s64>(offset))) {
         return 0;
     }
-    return backing->WriteBytes(data, length);
+    return backing->WriteSpan(std::span{data, length});
 }
 
 bool RealVfsFile::Rename(std::string_view name) {
-    return base.MoveFile(path, parent_path + DIR_SEP + std::string(name)) != nullptr;
+    return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr;
 }
 
-bool RealVfsFile::Close() {
-    return backing->Close();
+void RealVfsFile::Close() {
+    backing->Close();
 }
 
 // TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if
@@ -313,15 +306,16 @@ std::vector<VirtualFile> RealVfsDirectory::IterateEntries<RealVfsFile, VfsFile>(
     }
 
     std::vector<VirtualFile> out;
-    FS::ForeachDirectoryEntry(
-        nullptr, path,
-        [&out, this](u64* entries_out, const std::string& directory, const std::string& filename) {
-            const std::string full_path = directory + DIR_SEP + filename;
-            if (!FS::IsDirectory(full_path)) {
-                out.emplace_back(base.OpenFile(full_path, perms));
-            }
-            return true;
-        });
+
+    const FS::DirEntryCallable callback = [this, &out](const std::filesystem::path& full_path) {
+        const auto full_path_string = FS::PathToUTF8String(full_path);
+
+        out.emplace_back(base.OpenFile(full_path_string, perms));
+
+        return true;
+    };
+
+    FS::IterateDirEntries(path, callback, FS::DirEntryFilter::File);
 
     return out;
 }
@@ -333,42 +327,41 @@ std::vector<VirtualDir> RealVfsDirectory::IterateEntries<RealVfsDirectory, VfsDi
     }
 
     std::vector<VirtualDir> out;
-    FS::ForeachDirectoryEntry(
-        nullptr, path,
-        [&out, this](u64* entries_out, const std::string& directory, const std::string& filename) {
-            const std::string full_path = directory + DIR_SEP + filename;
-            if (FS::IsDirectory(full_path)) {
-                out.emplace_back(base.OpenDirectory(full_path, perms));
-            }
-            return true;
-        });
+
+    const FS::DirEntryCallable callback = [this, &out](const std::filesystem::path& full_path) {
+        const auto full_path_string = FS::PathToUTF8String(full_path);
+
+        out.emplace_back(base.OpenDirectory(full_path_string, perms));
+
+        return true;
+    };
+
+    FS::IterateDirEntries(path, callback, FS::DirEntryFilter::Directory);
 
     return out;
 }
 
 RealVfsDirectory::RealVfsDirectory(RealVfsFilesystem& base_, const std::string& path_, Mode perms_)
     : base(base_), path(FS::RemoveTrailingSlash(path_)), parent_path(FS::GetParentPath(path)),
-      path_components(FS::SplitPathComponents(path)),
-      parent_components(FS::SliceVector(path_components, 0, path_components.size() - 1)),
-      perms(perms_) {
-    if (!FS::Exists(path) && True(perms & Mode::WriteAppend)) {
-        FS::CreateDir(path);
+      path_components(FS::SplitPathComponents(path)), perms(perms_) {
+    if (!FS::Exists(path) && True(perms & Mode::Write)) {
+        void(FS::CreateDirs(path));
     }
 }
 
 RealVfsDirectory::~RealVfsDirectory() = default;
 
 VirtualFile RealVfsDirectory::GetFileRelative(std::string_view relative_path) const {
-    const auto full_path = FS::SanitizePath(path + DIR_SEP + std::string(relative_path));
-    if (!FS::Exists(full_path) || FS::IsDirectory(full_path)) {
+    const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path));
+    if (!FS::Exists(full_path) || FS::IsDir(full_path)) {
         return nullptr;
     }
     return base.OpenFile(full_path, perms);
 }
 
 VirtualDir RealVfsDirectory::GetDirectoryRelative(std::string_view relative_path) const {
-    const auto full_path = FS::SanitizePath(path + DIR_SEP + std::string(relative_path));
-    if (!FS::Exists(full_path) || !FS::IsDirectory(full_path)) {
+    const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path));
+    if (!FS::Exists(full_path) || !FS::IsDir(full_path)) {
         return nullptr;
     }
     return base.OpenDirectory(full_path, perms);
@@ -383,17 +376,20 @@ VirtualDir RealVfsDirectory::GetSubdirectory(std::string_view name) const {
 }
 
 VirtualFile RealVfsDirectory::CreateFileRelative(std::string_view relative_path) {
-    const auto full_path = FS::SanitizePath(path + DIR_SEP + std::string(relative_path));
+    const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path));
+    if (!FS::CreateParentDirs(full_path)) {
+        return nullptr;
+    }
     return base.CreateFile(full_path, perms);
 }
 
 VirtualDir RealVfsDirectory::CreateDirectoryRelative(std::string_view relative_path) {
-    const auto full_path = FS::SanitizePath(path + DIR_SEP + std::string(relative_path));
+    const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path));
     return base.CreateDirectory(full_path, perms);
 }
 
 bool RealVfsDirectory::DeleteSubdirectoryRecursive(std::string_view name) {
-    const auto full_path = FS::SanitizePath(this->path + DIR_SEP + std::string(name));
+    const auto full_path = FS::SanitizePath(this->path + '/' + std::string(name));
     return base.DeleteDirectory(full_path);
 }
 
@@ -406,11 +402,11 @@ std::vector<VirtualDir> RealVfsDirectory::GetSubdirectories() const {
 }
 
 bool RealVfsDirectory::IsWritable() const {
-    return True(perms & Mode::WriteAppend);
+    return True(perms & Mode::Write);
 }
 
 bool RealVfsDirectory::IsReadable() const {
-    return True(perms & Mode::ReadWrite);
+    return True(perms & Mode::Read);
 }
 
 std::string RealVfsDirectory::GetName() const {
@@ -426,27 +422,27 @@ VirtualDir RealVfsDirectory::GetParentDirectory() const {
 }
 
 VirtualDir RealVfsDirectory::CreateSubdirectory(std::string_view name) {
-    const std::string subdir_path = (path + DIR_SEP).append(name);
+    const std::string subdir_path = (path + '/').append(name);
     return base.CreateDirectory(subdir_path, perms);
 }
 
 VirtualFile RealVfsDirectory::CreateFile(std::string_view name) {
-    const std::string file_path = (path + DIR_SEP).append(name);
+    const std::string file_path = (path + '/').append(name);
     return base.CreateFile(file_path, perms);
 }
 
 bool RealVfsDirectory::DeleteSubdirectory(std::string_view name) {
-    const std::string subdir_path = (path + DIR_SEP).append(name);
+    const std::string subdir_path = (path + '/').append(name);
     return base.DeleteDirectory(subdir_path);
 }
 
 bool RealVfsDirectory::DeleteFile(std::string_view name) {
-    const std::string file_path = (path + DIR_SEP).append(name);
+    const std::string file_path = (path + '/').append(name);
     return base.DeleteFile(file_path);
 }
 
 bool RealVfsDirectory::Rename(std::string_view name) {
-    const std::string new_name = (parent_path + DIR_SEP).append(name);
+    const std::string new_name = (parent_path + '/').append(name);
     return base.MoveFile(path, new_name) != nullptr;
 }
 
@@ -462,14 +458,17 @@ std::map<std::string, VfsEntryType, std::less<>> RealVfsDirectory::GetEntries()
     }
 
     std::map<std::string, VfsEntryType, std::less<>> out;
-    FS::ForeachDirectoryEntry(
-        nullptr, path,
-        [&out](u64* entries_out, const std::string& directory, const std::string& filename) {
-            const std::string full_path = directory + DIR_SEP + filename;
-            out.emplace(filename,
-                        FS::IsDirectory(full_path) ? VfsEntryType::Directory : VfsEntryType::File);
-            return true;
-        });
+
+    const FS::DirEntryCallable callback = [&out](const std::filesystem::path& full_path) {
+        const auto filename = FS::PathToUTF8String(full_path.filename());
+
+        out.insert_or_assign(filename,
+                             FS::IsDir(full_path) ? VfsEntryType::Directory : VfsEntryType::File);
+
+        return true;
+    };
+
+    FS::IterateDirEntries(path, callback);
 
     return out;
 }
diff --git a/src/core/file_sys/vfs_real.h b/src/core/file_sys/vfs_real.h
index 0666f26796..e4d1bba79a 100644
--- a/src/core/file_sys/vfs_real.h
+++ b/src/core/file_sys/vfs_real.h
@@ -61,14 +61,13 @@ private:
     RealVfsFile(RealVfsFilesystem& base, std::shared_ptr<Common::FS::IOFile> backing,
                 const std::string& path, Mode perms = Mode::Read);
 
-    bool Close();
+    void Close();
 
     RealVfsFilesystem& base;
     std::shared_ptr<Common::FS::IOFile> backing;
     std::string path;
     std::string parent_path;
     std::vector<std::string> path_components;
-    std::vector<std::string> parent_components;
     Mode perms;
 };
 
@@ -110,7 +109,6 @@ private:
     std::string path;
     std::string parent_path;
     std::vector<std::string> path_components;
-    std::vector<std::string> parent_components;
     Mode perms;
 };
 
diff --git a/src/core/file_sys/xts_archive.cpp b/src/core/file_sys/xts_archive.cpp
index 814fd56808..d6fe1af47e 100644
--- a/src/core/file_sys/xts_archive.cpp
+++ b/src/core/file_sys/xts_archive.cpp
@@ -11,7 +11,7 @@
 #include <mbedtls/md.h>
 #include <mbedtls/sha256.h>
 
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/hex_util.h"
 #include "common/string_util.h"
 #include "core/crypto/aes_util.h"
diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp
index 49c09a5703..39cd1efc17 100644
--- a/src/core/hle/service/acc/acc.cpp
+++ b/src/core/hle/service/acc/acc.cpp
@@ -4,9 +4,9 @@
 
 #include <algorithm>
 #include <array>
-#include "common/common_paths.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 #include "common/swap.h"
@@ -41,9 +41,9 @@ constexpr ResultCode ERR_FAILED_SAVE_DATA{ErrorModule::Account, 100};
 // Thumbnails are hard coded to be at least this size
 constexpr std::size_t THUMBNAIL_SIZE = 0x24000;
 
-static std::string GetImagePath(Common::UUID uuid) {
-    return Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-           "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg";
+static std::filesystem::path GetImagePath(Common::UUID uuid) {
+    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) /
+           fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormatSwitch());
 }
 
 static constexpr u32 SanitizeJPEGSize(std::size_t size) {
@@ -328,7 +328,8 @@ protected:
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(RESULT_SUCCESS);
 
-        const Common::FS::IOFile image(GetImagePath(user_id), "rb");
+        const Common::FS::IOFile image(GetImagePath(user_id), Common::FS::FileAccessMode::Read,
+                                       Common::FS::FileType::BinaryFile);
         if (!image.IsOpen()) {
             LOG_WARNING(Service_ACC,
                         "Failed to load user provided image! Falling back to built-in backup...");
@@ -339,7 +340,10 @@ protected:
 
         const u32 size = SanitizeJPEGSize(image.GetSize());
         std::vector<u8> buffer(size);
-        image.ReadBytes(buffer.data(), buffer.size());
+
+        if (image.Read(buffer) != buffer.size()) {
+            LOG_ERROR(Service_ACC, "Failed to read all the bytes in the user provided image.");
+        }
 
         ctx.WriteBuffer(buffer);
         rb.Push<u32>(size);
@@ -350,7 +354,8 @@ protected:
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(RESULT_SUCCESS);
 
-        const Common::FS::IOFile image(GetImagePath(user_id), "rb");
+        const Common::FS::IOFile image(GetImagePath(user_id), Common::FS::FileAccessMode::Read,
+                                       Common::FS::FileType::BinaryFile);
 
         if (!image.IsOpen()) {
             LOG_WARNING(Service_ACC,
@@ -415,10 +420,11 @@ protected:
         ProfileData data;
         std::memcpy(&data, user_data.data(), sizeof(ProfileData));
 
-        Common::FS::IOFile image(GetImagePath(user_id), "wb");
+        Common::FS::IOFile image(GetImagePath(user_id), Common::FS::FileAccessMode::Write,
+                                 Common::FS::FileType::BinaryFile);
 
-        if (!image.IsOpen() || !image.Resize(image_data.size()) ||
-            image.WriteBytes(image_data.data(), image_data.size()) != image_data.size() ||
+        if (!image.IsOpen() || !image.SetSize(image_data.size()) ||
+            image.Write(image_data) != image_data.size() ||
             !profile_manager.SetProfileBaseAndData(user_id, base, data)) {
             LOG_ERROR(Service_ACC, "Failed to update profile data, base, and image!");
             IPC::ResponseBuilder rb{ctx, 2};
diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp
index de83d82a4a..77510489c9 100644
--- a/src/core/hle/service/acc/profile_manager.cpp
+++ b/src/core/hle/service/acc/profile_manager.cpp
@@ -7,7 +7,9 @@
 
 #include <fmt/format.h>
 
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/settings.h"
 #include "core/hle/service/acc/profile_manager.h"
 
@@ -36,7 +38,7 @@ constexpr ResultCode ERROR_TOO_MANY_USERS(ErrorModule::Account, u32(-1));
 constexpr ResultCode ERROR_USER_ALREADY_EXISTS(ErrorModule::Account, u32(-2));
 constexpr ResultCode ERROR_ARGUMENT_IS_NULL(ErrorModule::Account, 20);
 
-constexpr char ACC_SAVE_AVATORS_BASE_PATH[] = "/system/save/8000000000000010/su/avators/";
+constexpr char ACC_SAVE_AVATORS_BASE_PATH[] = "system/save/8000000000000010/su/avators";
 
 ProfileManager::ProfileManager() {
     ParseUserSaveFile();
@@ -325,8 +327,9 @@ bool ProfileManager::SetProfileBaseAndData(Common::UUID uuid, const ProfileBase&
 }
 
 void ProfileManager::ParseUserSaveFile() {
-    const FS::IOFile save(
-        FS::GetUserPath(FS::UserPath::NANDDir) + ACC_SAVE_AVATORS_BASE_PATH + "profiles.dat", "rb");
+    const auto save_path(FS::GetYuzuPath(FS::YuzuPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH /
+                         "profiles.dat");
+    const FS::IOFile save(save_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
 
     if (!save.IsOpen()) {
         LOG_WARNING(Service_ACC, "Failed to load profile data from save data... Generating new "
@@ -335,7 +338,7 @@ void ProfileManager::ParseUserSaveFile() {
     }
 
     ProfileDataRaw data;
-    if (save.ReadBytes(&data, sizeof(ProfileDataRaw)) != sizeof(ProfileDataRaw)) {
+    if (!save.ReadObject(data)) {
         LOG_WARNING(Service_ACC, "profiles.dat is smaller than expected... Generating new user "
                                  "'yuzu' with random UUID.");
         return;
@@ -372,31 +375,27 @@ void ProfileManager::WriteUserSaveFile() {
         };
     }
 
-    const auto raw_path = FS::GetUserPath(FS::UserPath::NANDDir) + "/system/save/8000000000000010";
-    if (FS::Exists(raw_path) && !FS::IsDirectory(raw_path)) {
-        FS::Delete(raw_path);
+    const auto raw_path(FS::GetYuzuPath(FS::YuzuPath::NANDDir) / "system/save/8000000000000010");
+    if (FS::IsFile(raw_path) && !FS::RemoveFile(raw_path)) {
+        return;
     }
 
-    const auto path =
-        FS::GetUserPath(FS::UserPath::NANDDir) + ACC_SAVE_AVATORS_BASE_PATH + "profiles.dat";
+    const auto save_path(FS::GetYuzuPath(FS::YuzuPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH /
+                         "profiles.dat");
 
-    if (!FS::CreateFullPath(path)) {
+    if (!FS::CreateParentDirs(save_path)) {
         LOG_WARNING(Service_ACC, "Failed to create full path of profiles.dat. Create the directory "
                                  "nand/system/save/8000000000000010/su/avators to mitigate this "
                                  "issue.");
         return;
     }
 
-    FS::IOFile save(path, "wb");
+    FS::IOFile save(save_path, FS::FileAccessMode::Write, FS::FileType::BinaryFile);
 
-    if (!save.IsOpen()) {
+    if (!save.IsOpen() || !save.SetSize(sizeof(ProfileDataRaw)) || !save.WriteObject(raw)) {
         LOG_WARNING(Service_ACC, "Failed to write save data to file... No changes to user data "
                                  "made in current session will be saved.");
-        return;
     }
-
-    save.Resize(sizeof(ProfileDataRaw));
-    save.WriteBytes(&raw, sizeof(ProfileDataRaw));
 }
 
 }; // namespace Service::Account
diff --git a/src/core/hle/service/am/applets/web_browser.cpp b/src/core/hle/service/am/applets/web_browser.cpp
index e5f4a44854..3b28e829be 100644
--- a/src/core/hle/service/am/applets/web_browser.cpp
+++ b/src/core/hle/service/am/applets/web_browser.cpp
@@ -3,8 +3,9 @@
 // Refer to the license.txt file included.
 
 #include "common/assert.h"
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 #include "core/core.h"
@@ -135,14 +136,10 @@ void ExtractSharedFonts(Core::System& system) {
         "FontNintendoExtended2.ttf",
     };
 
-    for (std::size_t i = 0; i < NS::SHARED_FONTS.size(); ++i) {
-        const auto fonts_dir = Common::FS::SanitizePath(
-            fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
-            Common::FS::DirectorySeparator::PlatformDefault);
+    const auto fonts_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "fonts";
 
-        const auto font_file_path =
-            Common::FS::SanitizePath(fmt::format("{}/{}", fonts_dir, DECRYPTED_SHARED_FONTS[i]),
-                                     Common::FS::DirectorySeparator::PlatformDefault);
+    for (std::size_t i = 0; i < NS::SHARED_FONTS.size(); ++i) {
+        const auto font_file_path = fonts_dir / DECRYPTED_SHARED_FONTS[i];
 
         if (Common::FS::Exists(font_file_path)) {
             continue;
@@ -197,8 +194,8 @@ void ExtractSharedFonts(Core::System& system) {
         FileSys::VirtualFile decrypted_font = std::make_shared<FileSys::VectorVfsFile>(
             std::move(decrypted_data), DECRYPTED_SHARED_FONTS[i]);
 
-        const auto temp_dir =
-            system.GetFilesystem()->CreateDirectory(fonts_dir, FileSys::Mode::ReadWrite);
+        const auto temp_dir = system.GetFilesystem()->CreateDirectory(
+            Common::FS::PathToUTF8String(fonts_dir), FileSys::Mode::ReadWrite);
 
         const auto out_file = temp_dir->CreateFile(DECRYPTED_SHARED_FONTS[i]);
 
@@ -312,13 +309,14 @@ void WebBrowser::Execute() {
 }
 
 void WebBrowser::ExtractOfflineRomFS() {
-    LOG_DEBUG(Service_AM, "Extracting RomFS to {}", offline_cache_dir);
+    LOG_DEBUG(Service_AM, "Extracting RomFS to {}",
+              Common::FS::PathToUTF8String(offline_cache_dir));
 
     const auto extracted_romfs_dir =
         FileSys::ExtractRomFS(offline_romfs, FileSys::RomFSExtractionType::SingleDiscard);
 
-    const auto temp_dir =
-        system.GetFilesystem()->CreateDirectory(offline_cache_dir, FileSys::Mode::ReadWrite);
+    const auto temp_dir = system.GetFilesystem()->CreateDirectory(
+        Common::FS::PathToUTF8String(offline_cache_dir), FileSys::Mode::ReadWrite);
 
     FileSys::VfsRawCopyD(extracted_romfs_dir, temp_dir);
 }
@@ -397,15 +395,12 @@ void WebBrowser::InitializeOffline() {
         "system_data",
     };
 
-    offline_cache_dir = Common::FS::SanitizePath(
-        fmt::format("{}/offline_web_applet_{}/{:016X}",
-                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir),
-                    RESOURCE_TYPES[static_cast<u32>(document_kind) - 1], title_id),
-        Common::FS::DirectorySeparator::PlatformDefault);
+    offline_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                        fmt::format("offline_web_applet_{}/{:016X}",
+                                    RESOURCE_TYPES[static_cast<u32>(document_kind) - 1], title_id);
 
-    offline_document = Common::FS::SanitizePath(
-        fmt::format("{}/{}/{}", offline_cache_dir, additional_paths, document_path),
-        Common::FS::DirectorySeparator::PlatformDefault);
+    offline_document = Common::FS::ConcatPathSafe(
+        offline_cache_dir, fmt::format("{}/{}", additional_paths, document_path));
 }
 
 void WebBrowser::InitializeShare() {}
@@ -429,8 +424,7 @@ void WebBrowser::ExecuteLogin() {
 }
 
 void WebBrowser::ExecuteOffline() {
-    const auto main_url = Common::FS::SanitizePath(GetMainURL(offline_document),
-                                                   Common::FS::DirectorySeparator::PlatformDefault);
+    const auto main_url = GetMainURL(Common::FS::PathToUTF8String(offline_document));
 
     if (!Common::FS::Exists(main_url)) {
         offline_romfs = GetOfflineRomFS(system, title_id, nca_type);
@@ -444,10 +438,11 @@ void WebBrowser::ExecuteOffline() {
         }
     }
 
-    LOG_INFO(Service_AM, "Opening offline document at {}", offline_document);
+    LOG_INFO(Service_AM, "Opening offline document at {}",
+             Common::FS::PathToUTF8String(offline_document));
 
     frontend.OpenLocalWebPage(
-        offline_document, [this] { ExtractOfflineRomFS(); },
+        Common::FS::PathToUTF8String(offline_document), [this] { ExtractOfflineRomFS(); },
         [this](WebExitReason exit_reason, std::string last_url) {
             WebBrowserExit(exit_reason, last_url);
         });
diff --git a/src/core/hle/service/am/applets/web_browser.h b/src/core/hle/service/am/applets/web_browser.h
index 1e1812f363..cdeaf2c407 100644
--- a/src/core/hle/service/am/applets/web_browser.h
+++ b/src/core/hle/service/am/applets/web_browser.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <filesystem>
 #include <optional>
 
 #include "common/common_funcs.h"
@@ -75,8 +76,8 @@ private:
 
     u64 title_id{};
     FileSys::ContentRecordType nca_type{};
-    std::string offline_cache_dir;
-    std::string offline_document;
+    std::filesystem::path offline_cache_dir;
+    std::filesystem::path offline_document;
     FileSys::VirtualFile offline_romfs;
 
     std::string external_url;
diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp
index d6d2f52e57..3cc397604a 100644
--- a/src/core/hle/service/bcat/backend/boxcat.cpp
+++ b/src/core/hle/service/bcat/backend/boxcat.cpp
@@ -15,6 +15,9 @@
 #pragma GCC diagnostic pop
 #endif
 
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/hex_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/log.h"
@@ -96,14 +99,14 @@ constexpr u32 PORT = 443;
 constexpr u32 TIMEOUT_SECONDS = 30;
 [[maybe_unused]] constexpr u64 VFS_COPY_BLOCK_SIZE = 1ULL << 24; // 4MB
 
-std::string GetBINFilePath(u64 title_id) {
-    return fmt::format("{}bcat/{:016X}/launchparam.bin",
-                       Common::FS::GetUserPath(Common::FS::UserPath::CacheDir), title_id);
+std::filesystem::path GetBINFilePath(u64 title_id) {
+    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "bcat" /
+           fmt::format("{:016X}/launchparam.bin", title_id);
 }
 
-std::string GetZIPFilePath(u64 title_id) {
-    return fmt::format("{}bcat/{:016X}/data.zip",
-                       Common::FS::GetUserPath(Common::FS::UserPath::CacheDir), title_id);
+std::filesystem::path GetZIPFilePath(u64 title_id) {
+    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "bcat" /
+           fmt::format("{:016X}/data.zip", title_id);
 }
 
 // If the error is something the user should know about (build ID mismatch, bad client version),
@@ -187,7 +190,7 @@ bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest,
 
 class Boxcat::Client {
 public:
-    Client(std::string path_, u64 title_id_, u64 build_id_)
+    Client(std::filesystem::path path_, u64 title_id_, u64 build_id_)
         : path(std::move(path_)), title_id(title_id_), build_id(build_id_) {}
 
     DownloadResult DownloadDataZip() {
@@ -217,10 +220,11 @@ private:
         };
 
         if (Common::FS::Exists(path)) {
-            Common::FS::IOFile file{path, "rb"};
+            Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read,
+                                    Common::FS::FileType::BinaryFile};
             if (file.IsOpen()) {
                 std::vector<u8> bytes(file.GetSize());
-                file.ReadBytes(bytes.data(), bytes.size());
+                void(file.Read(bytes));
                 const auto digest = DigestFile(bytes);
                 headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)});
             }
@@ -247,14 +251,23 @@ private:
             return DownloadResult::InvalidContentType;
         }
 
-        Common::FS::CreateFullPath(path);
-        Common::FS::IOFile file{path, "wb"};
-        if (!file.IsOpen())
+        if (!Common::FS::CreateDirs(path)) {
             return DownloadResult::GeneralFSError;
-        if (!file.Resize(response->body.size()))
+        }
+
+        Common::FS::IOFile file{path, Common::FS::FileAccessMode::Append,
+                                Common::FS::FileType::BinaryFile};
+        if (!file.IsOpen()) {
             return DownloadResult::GeneralFSError;
-        if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size())
+        }
+
+        if (!file.SetSize(response->body.size())) {
             return DownloadResult::GeneralFSError;
+        }
+
+        if (file.Write(response->body) != response->body.size()) {
+            return DownloadResult::GeneralFSError;
+        }
 
         return DownloadResult::Success;
     }
@@ -267,7 +280,7 @@ private:
     }
 
     std::unique_ptr<httplib::SSLClient> client;
-    std::string path;
+    std::filesystem::path path;
     u64 title_id;
     u64 build_id;
 };
@@ -291,7 +304,7 @@ void SynchronizeInternal(AM::Applets::AppletManager& applet_manager, DirectoryGe
         return;
     }
 
-    const auto zip_path{GetZIPFilePath(title.title_id)};
+    const auto zip_path = GetZIPFilePath(title.title_id);
     Boxcat::Client client{zip_path, title.title_id, title.build_id};
 
     progress.StartConnecting();
@@ -301,7 +314,7 @@ void SynchronizeInternal(AM::Applets::AppletManager& applet_manager, DirectoryGe
         LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
 
         if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
-            Common::FS::Delete(zip_path);
+            void(Common::FS::RemoveFile(zip_path));
         }
 
         HandleDownloadDisplayResult(applet_manager, res);
@@ -311,11 +324,13 @@ void SynchronizeInternal(AM::Applets::AppletManager& applet_manager, DirectoryGe
 
     progress.StartProcessingDataList();
 
-    Common::FS::IOFile zip{zip_path, "rb"};
+    Common::FS::IOFile zip{zip_path, Common::FS::FileAccessMode::Read,
+                           Common::FS::FileType::BinaryFile};
     const auto size = zip.GetSize();
     std::vector<u8> bytes(size);
-    if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
-        LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path);
+    if (!zip.IsOpen() || size == 0 || zip.Read(bytes) != bytes.size()) {
+        LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!",
+                  Common::FS::PathToUTF8String(zip_path));
         progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
         return;
     }
@@ -419,19 +434,19 @@ void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
 }
 
 std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) {
-    const auto path{GetBINFilePath(title.title_id)};
+    const auto bin_file_path = GetBINFilePath(title.title_id);
 
     if (Settings::values.bcat_boxcat_local) {
         LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
     } else {
-        Client launch_client{path, title.title_id, title.build_id};
+        Client launch_client{bin_file_path, title.title_id, title.build_id};
 
         const auto res = launch_client.DownloadLaunchParam();
         if (res != DownloadResult::Success) {
             LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
 
             if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
-                Common::FS::Delete(path);
+                void(Common::FS::RemoveFile(bin_file_path));
             }
 
             HandleDownloadDisplayResult(applet_manager, res);
@@ -439,12 +454,13 @@ std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title)
         }
     }
 
-    Common::FS::IOFile bin{path, "rb"};
+    Common::FS::IOFile bin{bin_file_path, Common::FS::FileAccessMode::Read,
+                           Common::FS::FileType::BinaryFile};
     const auto size = bin.GetSize();
     std::vector<u8> bytes(size);
-    if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
+    if (!bin.IsOpen() || size == 0 || bin.Read(bytes) != bytes.size()) {
         LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!",
-                  path);
+                  Common::FS::PathToUTF8String(bin_file_path));
         return std::nullopt;
     }
 
diff --git a/src/core/hle/service/fatal/fatal.cpp b/src/core/hle/service/fatal/fatal.cpp
index 432abde760..b7666e95a6 100644
--- a/src/core/hle/service/fatal/fatal.cpp
+++ b/src/core/hle/service/fatal/fatal.cpp
@@ -6,7 +6,6 @@
 #include <cstring>
 #include <ctime>
 #include <fmt/chrono.h>
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "common/scm_rev.h"
 #include "common/swap.h"
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index 67baaee9b6..78664439d2 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -5,7 +5,7 @@
 #include <utility>
 
 #include "common/assert.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/settings.h"
 #include "core/core.h"
 #include "core/file_sys/bis_factory.h"
@@ -728,14 +728,17 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
         sdmc_factory = nullptr;
     }
 
-    auto nand_directory = vfs.OpenDirectory(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir),
-                                            FileSys::Mode::ReadWrite);
-    auto sd_directory = vfs.OpenDirectory(Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir),
-                                          FileSys::Mode::ReadWrite);
-    auto load_directory = vfs.OpenDirectory(Common::FS::GetUserPath(Common::FS::UserPath::LoadDir),
-                                            FileSys::Mode::ReadWrite);
-    auto dump_directory = vfs.OpenDirectory(Common::FS::GetUserPath(Common::FS::UserPath::DumpDir),
-                                            FileSys::Mode::ReadWrite);
+    using YuzuPath = Common::FS::YuzuPath;
+    const auto rw_mode = FileSys::Mode::ReadWrite;
+
+    auto nand_directory =
+        vfs.OpenDirectory(Common::FS::GetYuzuPathString(YuzuPath::NANDDir), rw_mode);
+    auto sd_directory =
+        vfs.OpenDirectory(Common::FS::GetYuzuPathString(YuzuPath::SDMCDir), rw_mode);
+    auto load_directory =
+        vfs.OpenDirectory(Common::FS::GetYuzuPathString(YuzuPath::LoadDir), FileSys::Mode::Read);
+    auto dump_directory =
+        vfs.OpenDirectory(Common::FS::GetYuzuPathString(YuzuPath::DumpDir), rw_mode);
 
     if (bis_factory == nullptr) {
         bis_factory =
diff --git a/src/core/hle/service/mii/manager.cpp b/src/core/hle/service/mii/manager.cpp
index 70350a2a3e..114aff31c7 100644
--- a/src/core/hle/service/mii/manager.cpp
+++ b/src/core/hle/service/mii/manager.cpp
@@ -6,7 +6,6 @@
 #include <random>
 
 #include "common/assert.h"
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 
diff --git a/src/core/hle/service/ns/pl_u.cpp b/src/core/hle/service/ns/pl_u.cpp
index e14acce581..90ba5c7520 100644
--- a/src/core/hle/service/ns/pl_u.cpp
+++ b/src/core/hle/service/ns/pl_u.cpp
@@ -7,9 +7,7 @@
 #include <vector>
 
 #include "common/assert.h"
-#include "common/common_paths.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "common/swap.h"
 #include "core/core.h"
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index 022885c1bb..a19bb220a6 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -5,7 +5,6 @@
 #include <cinttypes>
 #include <cstring>
 #include "common/common_funcs.h"
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "core/core.h"
 #include "core/file_sys/content_archive.h"
diff --git a/src/core/loader/elf.cpp b/src/core/loader/elf.cpp
index c062a42590..3d9276f15b 100644
--- a/src/core/loader/elf.cpp
+++ b/src/core/loader/elf.cpp
@@ -7,7 +7,6 @@
 #include <string>
 #include "common/common_funcs.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "core/hle/kernel/code_set.h"
 #include "core/hle/kernel/k_page_table.h"
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index d4808fb5be..228dc63891 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -7,7 +7,7 @@
 #include <ostream>
 #include <string>
 #include "common/concepts.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/string_util.h"
 #include "core/core.h"
diff --git a/src/core/loader/nca.cpp b/src/core/loader/nca.cpp
index 418cbf61b9..aa51b0daa1 100644
--- a/src/core/loader/nca.cpp
+++ b/src/core/loader/nca.cpp
@@ -4,7 +4,6 @@
 
 #include <utility>
 
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "core/core.h"
 #include "core/file_sys/content_archive.h"
diff --git a/src/core/loader/nro.cpp b/src/core/loader/nro.cpp
index ef54fa5748..6185552022 100644
--- a/src/core/loader/nro.cpp
+++ b/src/core/loader/nro.cpp
@@ -7,7 +7,6 @@
 
 #include "common/common_funcs.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
 #include "common/logging/log.h"
 #include "common/settings.h"
 #include "common/swap.h"
diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp
index df59412cfe..0f5cfda682 100644
--- a/src/core/loader/nso.cpp
+++ b/src/core/loader/nso.cpp
@@ -7,7 +7,6 @@
 #include <vector>
 
 #include "common/common_funcs.h"
-#include "common/file_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
 #include "common/lz4_compression.h"
diff --git a/src/core/perf_stats.cpp b/src/core/perf_stats.cpp
index c42c437b72..6635a13393 100644
--- a/src/core/perf_stats.cpp
+++ b/src/core/perf_stats.cpp
@@ -11,7 +11,9 @@
 #include <thread>
 #include <fmt/chrono.h>
 #include <fmt/format.h>
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/math_util.h"
 #include "common/settings.h"
 #include "core/perf_stats.h"
@@ -38,12 +40,17 @@ PerfStats::~PerfStats() {
     std::ostringstream stream;
     std::copy(perf_history.begin() + IgnoreFrames, perf_history.begin() + current_index,
               std::ostream_iterator<double>(stream, "\n"));
-    const std::string& path = Common::FS::GetUserPath(Common::FS::UserPath::LogDir);
+
+    const auto path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::LogDir);
     // %F Date format expanded is "%Y-%m-%d"
-    const std::string filename =
-        fmt::format("{}/{:%F-%H-%M}_{:016X}.csv", path, *std::localtime(&t), title_id);
-    Common::FS::IOFile file(filename, "w");
-    file.WriteString(stream.str());
+    const auto filename = fmt::format("{:%F-%H-%M}_{:016X}.csv", *std::localtime(&t), title_id);
+    const auto filepath = path / filename;
+
+    if (Common::FS::CreateParentDir(filepath)) {
+        Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Write,
+                                Common::FS::FileType::TextFile);
+        void(file.WriteString(stream.str()));
+    }
 }
 
 void PerfStats::BeginSystemFrame() {
diff --git a/src/core/reporter.cpp b/src/core/reporter.cpp
index d1e807dd4a..a9596fe4d3 100644
--- a/src/core/reporter.cpp
+++ b/src/core/reporter.cpp
@@ -11,7 +11,9 @@
 #include <fmt/ostream.h>
 #include <nlohmann/json.hpp>
 
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/hex_util.h"
 #include "common/scm_rev.h"
 #include "common/settings.h"
@@ -26,10 +28,9 @@
 
 namespace {
 
-std::string GetPath(std::string_view type, u64 title_id, std::string_view timestamp) {
-    return fmt::format("{}{}/{:016X}_{}.json",
-                       Common::FS::GetUserPath(Common::FS::UserPath::LogDir), type, title_id,
-                       timestamp);
+std::filesystem::path GetPath(std::string_view type, u64 title_id, std::string_view timestamp) {
+    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::LogDir) / type /
+           fmt::format("{:016X}_{}.json", title_id, timestamp);
 }
 
 std::string GetTimestamp() {
@@ -39,14 +40,16 @@ std::string GetTimestamp() {
 
 using namespace nlohmann;
 
-void SaveToFile(json json, const std::string& filename) {
-    if (!Common::FS::CreateFullPath(filename)) {
-        LOG_ERROR(Core, "Failed to create path for '{}' to save report!", filename);
+void SaveToFile(json json, const std::filesystem::path& filename) {
+    if (!Common::FS::CreateParentDirs(filename)) {
+        LOG_ERROR(Core, "Failed to create path for '{}' to save report!",
+                  Common::FS::PathToUTF8String(filename));
         return;
     }
 
-    std::ofstream file(
-        Common::FS::SanitizePath(filename, Common::FS::DirectorySeparator::PlatformDefault));
+    std::ofstream file;
+    Common::FS::OpenFileStream(file, filename, std::ios_base::out | std::ios_base::trunc);
+
     file << std::setw(4) << json << std::endl;
 }
 
diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp
index 6dcff54002..ad1a9ffb49 100644
--- a/src/core/telemetry_session.cpp
+++ b/src/core/telemetry_session.cpp
@@ -9,7 +9,9 @@
 
 #include "common/assert.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 
 #include "common/settings.h"
@@ -72,31 +74,41 @@ static const char* TranslateGPUAccuracyLevel(Settings::GPUAccuracy backend) {
 
 u64 GetTelemetryId() {
     u64 telemetry_id{};
-    const std::string filename{Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir) +
-                               "telemetry_id"};
+    const auto filename = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "telemetry_id";
 
     bool generate_new_id = !Common::FS::Exists(filename);
+
     if (!generate_new_id) {
-        Common::FS::IOFile file(filename, "rb");
+        Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Read,
+                                Common::FS::FileType::BinaryFile};
+
         if (!file.IsOpen()) {
-            LOG_ERROR(Core, "failed to open telemetry_id: {}", filename);
+            LOG_ERROR(Core, "failed to open telemetry_id: {}",
+                      Common::FS::PathToUTF8String(filename));
             return {};
         }
-        file.ReadBytes(&telemetry_id, sizeof(u64));
-        if (telemetry_id == 0) {
+
+        if (!file.ReadObject(telemetry_id) || telemetry_id == 0) {
             LOG_ERROR(Frontend, "telemetry_id is 0. Generating a new one.", telemetry_id);
             generate_new_id = true;
         }
     }
 
     if (generate_new_id) {
-        Common::FS::IOFile file(filename, "wb");
+        Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Write,
+                                Common::FS::FileType::BinaryFile};
+
         if (!file.IsOpen()) {
-            LOG_ERROR(Core, "failed to open telemetry_id: {}", filename);
+            LOG_ERROR(Core, "failed to open telemetry_id: {}",
+                      Common::FS::PathToUTF8String(filename));
             return {};
         }
+
         telemetry_id = GenerateTelemetryId();
-        file.WriteBytes(&telemetry_id, sizeof(u64));
+
+        if (!file.WriteObject(telemetry_id)) {
+            LOG_ERROR(Core, "Failed to write telemetry_id to file.");
+        }
     }
 
     return telemetry_id;
@@ -104,15 +116,20 @@ u64 GetTelemetryId() {
 
 u64 RegenerateTelemetryId() {
     const u64 new_telemetry_id{GenerateTelemetryId()};
-    const std::string filename{Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir) +
-                               "telemetry_id"};
+    const auto filename = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "telemetry_id";
+
+    Common::FS::IOFile file{filename, Common::FS::FileAccessMode::Write,
+                            Common::FS::FileType::BinaryFile};
 
-    Common::FS::IOFile file(filename, "wb");
     if (!file.IsOpen()) {
-        LOG_ERROR(Core, "failed to open telemetry_id: {}", filename);
+        LOG_ERROR(Core, "failed to open telemetry_id: {}", Common::FS::PathToUTF8String(filename));
         return {};
     }
-    file.WriteBytes(&new_telemetry_id, sizeof(u64));
+
+    if (!file.WriteObject(new_telemetry_id)) {
+        LOG_ERROR(Core, "Failed to write telemetry_id to file.");
+    }
+
     return new_telemetry_id;
 }
 
diff --git a/src/tests/core/core_timing.cpp b/src/tests/core/core_timing.cpp
index b354591523..e0c66fa2eb 100644
--- a/src/tests/core/core_timing.cpp
+++ b/src/tests/core/core_timing.cpp
@@ -11,7 +11,6 @@
 #include <memory>
 #include <string>
 
-#include "common/file_util.h"
 #include "core/core.h"
 #include "core/core_timing.h"
 
diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
index dbcb751cb1..0deb86517e 100644
--- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp
@@ -7,9 +7,10 @@
 #include <fmt/format.h>
 
 #include "common/assert.h"
-#include "common/common_paths.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/scm_rev.h"
 #include "common/settings.h"
@@ -26,11 +27,7 @@ using Tegra::Engines::ShaderType;
 using VideoCommon::Shader::BindlessSamplerMap;
 using VideoCommon::Shader::BoundSamplerMap;
 using VideoCommon::Shader::KeyMap;
-
-namespace {
-
 using VideoCommon::Shader::SeparateSamplerKey;
-
 using ShaderCacheVersionHash = std::array<u8, 64>;
 
 struct ConstBufferKey {
@@ -58,6 +55,8 @@ struct BindlessSamplerEntry {
     Tegra::Engines::SamplerDescriptor sampler;
 };
 
+namespace {
+
 constexpr u32 NativeVersion = 21;
 
 ShaderCacheVersionHash GetShaderCacheVersionHash() {
@@ -74,22 +73,20 @@ ShaderDiskCacheEntry::ShaderDiskCacheEntry() = default;
 ShaderDiskCacheEntry::~ShaderDiskCacheEntry() = default;
 
 bool ShaderDiskCacheEntry::Load(Common::FS::IOFile& file) {
-    if (file.ReadBytes(&type, sizeof(u32)) != sizeof(u32)) {
+    if (!file.ReadObject(type)) {
         return false;
     }
     u32 code_size;
     u32 code_size_b;
-    if (file.ReadBytes(&code_size, sizeof(u32)) != sizeof(u32) ||
-        file.ReadBytes(&code_size_b, sizeof(u32)) != sizeof(u32)) {
+    if (!file.ReadObject(code_size) || !file.ReadObject(code_size_b)) {
         return false;
     }
     code.resize(code_size);
     code_b.resize(code_size_b);
-
-    if (file.ReadArray(code.data(), code_size) != code_size) {
+    if (file.Read(code) != code_size) {
         return false;
     }
-    if (HasProgramA() && file.ReadArray(code_b.data(), code_size_b) != code_size_b) {
+    if (HasProgramA() && file.Read(code_b) != code_size_b) {
         return false;
     }
 
@@ -99,13 +96,12 @@ bool ShaderDiskCacheEntry::Load(Common::FS::IOFile& file) {
     u32 num_bound_samplers;
     u32 num_separate_samplers;
     u32 num_bindless_samplers;
-    if (file.ReadArray(&unique_identifier, 1) != 1 || file.ReadArray(&bound_buffer, 1) != 1 ||
-        file.ReadArray(&is_texture_handler_size_known, 1) != 1 ||
-        file.ReadArray(&texture_handler_size_value, 1) != 1 ||
-        file.ReadArray(&graphics_info, 1) != 1 || file.ReadArray(&compute_info, 1) != 1 ||
-        file.ReadArray(&num_keys, 1) != 1 || file.ReadArray(&num_bound_samplers, 1) != 1 ||
-        file.ReadArray(&num_separate_samplers, 1) != 1 ||
-        file.ReadArray(&num_bindless_samplers, 1) != 1) {
+    if (!file.ReadObject(unique_identifier) || !file.ReadObject(bound_buffer) ||
+        !file.ReadObject(is_texture_handler_size_known) ||
+        !file.ReadObject(texture_handler_size_value) || !file.ReadObject(graphics_info) ||
+        !file.ReadObject(compute_info) || !file.ReadObject(num_keys) ||
+        !file.ReadObject(num_bound_samplers) || !file.ReadObject(num_separate_samplers) ||
+        !file.ReadObject(num_bindless_samplers)) {
         return false;
     }
     if (is_texture_handler_size_known) {
@@ -116,13 +112,10 @@ bool ShaderDiskCacheEntry::Load(Common::FS::IOFile& file) {
     std::vector<BoundSamplerEntry> flat_bound_samplers(num_bound_samplers);
     std::vector<SeparateSamplerEntry> flat_separate_samplers(num_separate_samplers);
     std::vector<BindlessSamplerEntry> flat_bindless_samplers(num_bindless_samplers);
-    if (file.ReadArray(flat_keys.data(), flat_keys.size()) != flat_keys.size() ||
-        file.ReadArray(flat_bound_samplers.data(), flat_bound_samplers.size()) !=
-            flat_bound_samplers.size() ||
-        file.ReadArray(flat_separate_samplers.data(), flat_separate_samplers.size()) !=
-            flat_separate_samplers.size() ||
-        file.ReadArray(flat_bindless_samplers.data(), flat_bindless_samplers.size()) !=
-            flat_bindless_samplers.size()) {
+    if (file.Read(flat_keys) != flat_keys.size() ||
+        file.Read(flat_bound_samplers) != flat_bound_samplers.size() ||
+        file.Read(flat_separate_samplers) != flat_separate_samplers.size() ||
+        file.Read(flat_bindless_samplers) != flat_bindless_samplers.size()) {
         return false;
     }
     for (const auto& entry : flat_keys) {
@@ -145,26 +138,25 @@ bool ShaderDiskCacheEntry::Load(Common::FS::IOFile& file) {
 }
 
 bool ShaderDiskCacheEntry::Save(Common::FS::IOFile& file) const {
-    if (file.WriteObject(static_cast<u32>(type)) != 1 ||
-        file.WriteObject(static_cast<u32>(code.size())) != 1 ||
-        file.WriteObject(static_cast<u32>(code_b.size())) != 1) {
+    if (!file.WriteObject(static_cast<u32>(type)) ||
+        !file.WriteObject(static_cast<u32>(code.size())) ||
+        !file.WriteObject(static_cast<u32>(code_b.size()))) {
         return false;
     }
-    if (file.WriteArray(code.data(), code.size()) != code.size()) {
+    if (file.Write(code) != code.size()) {
         return false;
     }
-    if (HasProgramA() && file.WriteArray(code_b.data(), code_b.size()) != code_b.size()) {
+    if (HasProgramA() && file.Write(code_b) != code_b.size()) {
         return false;
     }
 
-    if (file.WriteObject(unique_identifier) != 1 || file.WriteObject(bound_buffer) != 1 ||
-        file.WriteObject(static_cast<u8>(texture_handler_size.has_value())) != 1 ||
-        file.WriteObject(texture_handler_size.value_or(0)) != 1 ||
-        file.WriteObject(graphics_info) != 1 || file.WriteObject(compute_info) != 1 ||
-        file.WriteObject(static_cast<u32>(keys.size())) != 1 ||
-        file.WriteObject(static_cast<u32>(bound_samplers.size())) != 1 ||
-        file.WriteObject(static_cast<u32>(separate_samplers.size())) != 1 ||
-        file.WriteObject(static_cast<u32>(bindless_samplers.size())) != 1) {
+    if (!file.WriteObject(unique_identifier) || !file.WriteObject(bound_buffer) ||
+        !file.WriteObject(static_cast<u8>(texture_handler_size.has_value())) ||
+        !file.WriteObject(texture_handler_size.value_or(0)) || !file.WriteObject(graphics_info) ||
+        !file.WriteObject(compute_info) || !file.WriteObject(static_cast<u32>(keys.size())) ||
+        !file.WriteObject(static_cast<u32>(bound_samplers.size())) ||
+        !file.WriteObject(static_cast<u32>(separate_samplers.size())) ||
+        !file.WriteObject(static_cast<u32>(bindless_samplers.size()))) {
         return false;
     }
 
@@ -197,13 +189,10 @@ bool ShaderDiskCacheEntry::Save(Common::FS::IOFile& file) const {
             BindlessSamplerEntry{address.first, address.second, sampler});
     }
 
-    return file.WriteArray(flat_keys.data(), flat_keys.size()) == flat_keys.size() &&
-           file.WriteArray(flat_bound_samplers.data(), flat_bound_samplers.size()) ==
-               flat_bound_samplers.size() &&
-           file.WriteArray(flat_separate_samplers.data(), flat_separate_samplers.size()) ==
-               flat_separate_samplers.size() &&
-           file.WriteArray(flat_bindless_samplers.data(), flat_bindless_samplers.size()) ==
-               flat_bindless_samplers.size();
+    return file.Write(flat_keys) == flat_keys.size() &&
+           file.Write(flat_bound_samplers) == flat_bound_samplers.size() &&
+           file.Write(flat_separate_samplers) == flat_separate_samplers.size() &&
+           file.Write(flat_bindless_samplers) == flat_bindless_samplers.size();
 }
 
 ShaderDiskCacheOpenGL::ShaderDiskCacheOpenGL() = default;
@@ -221,7 +210,8 @@ std::optional<std::vector<ShaderDiskCacheEntry>> ShaderDiskCacheOpenGL::LoadTran
         return std::nullopt;
     }
 
-    Common::FS::IOFile file(GetTransferablePath(), "rb");
+    Common::FS::IOFile file{GetTransferablePath(), Common::FS::FileAccessMode::Read,
+                            Common::FS::FileType::BinaryFile};
     if (!file.IsOpen()) {
         LOG_INFO(Render_OpenGL, "No transferable shader cache found");
         is_usable = true;
@@ -229,7 +219,7 @@ std::optional<std::vector<ShaderDiskCacheEntry>> ShaderDiskCacheOpenGL::LoadTran
     }
 
     u32 version{};
-    if (file.ReadBytes(&version, sizeof(version)) != sizeof(version)) {
+    if (!file.ReadObject(version)) {
         LOG_ERROR(Render_OpenGL, "Failed to get transferable cache version, skipping it");
         return std::nullopt;
     }
@@ -249,7 +239,7 @@ std::optional<std::vector<ShaderDiskCacheEntry>> ShaderDiskCacheOpenGL::LoadTran
 
     // Version is valid, load the shaders
     std::vector<ShaderDiskCacheEntry> entries;
-    while (file.Tell() < file.GetSize()) {
+    while (static_cast<u64>(file.Tell()) < file.GetSize()) {
         ShaderDiskCacheEntry& entry = entries.emplace_back();
         if (!entry.Load(file)) {
             LOG_ERROR(Render_OpenGL, "Failed to load transferable raw entry, skipping");
@@ -266,7 +256,8 @@ std::vector<ShaderDiskCachePrecompiled> ShaderDiskCacheOpenGL::LoadPrecompiled()
         return {};
     }
 
-    Common::FS::IOFile file(GetPrecompiledPath(), "rb");
+    Common::FS::IOFile file{GetPrecompiledPath(), Common::FS::FileAccessMode::Read,
+                            Common::FS::FileType::BinaryFile};
     if (!file.IsOpen()) {
         LOG_INFO(Render_OpenGL, "No precompiled shader cache found");
         return {};
@@ -286,7 +277,9 @@ std::optional<std::vector<ShaderDiskCachePrecompiled>> ShaderDiskCacheOpenGL::Lo
     Common::FS::IOFile& file) {
     // Read compressed file from disk and decompress to virtual precompiled cache file
     std::vector<u8> compressed(file.GetSize());
-    file.ReadBytes(compressed.data(), compressed.size());
+    if (file.Read(compressed) != file.GetSize()) {
+        return std::nullopt;
+    }
     const std::vector<u8> decompressed = Common::Compression::DecompressDataZSTD(compressed);
     SaveArrayToPrecompiled(decompressed.data(), decompressed.size());
     precompiled_cache_virtual_file_offset = 0;
@@ -321,9 +314,9 @@ std::optional<std::vector<ShaderDiskCachePrecompiled>> ShaderDiskCacheOpenGL::Lo
 }
 
 void ShaderDiskCacheOpenGL::InvalidateTransferable() {
-    if (!Common::FS::Delete(GetTransferablePath())) {
+    if (!Common::FS::RemoveFile(GetTransferablePath())) {
         LOG_ERROR(Render_OpenGL, "Failed to invalidate transferable file={}",
-                  GetTransferablePath());
+                  Common::FS::PathToUTF8String(GetTransferablePath()));
     }
     InvalidatePrecompiled();
 }
@@ -332,8 +325,9 @@ void ShaderDiskCacheOpenGL::InvalidatePrecompiled() {
     // Clear virtaul precompiled cache file
     precompiled_cache_virtual_file.Resize(0);
 
-    if (!Common::FS::Delete(GetPrecompiledPath())) {
-        LOG_ERROR(Render_OpenGL, "Failed to invalidate precompiled file={}", GetPrecompiledPath());
+    if (!Common::FS::RemoveFile(GetPrecompiledPath())) {
+        LOG_ERROR(Render_OpenGL, "Failed to invalidate precompiled file={}",
+                  Common::FS::PathToUTF8String(GetPrecompiledPath()));
     }
 }
 
@@ -398,16 +392,18 @@ Common::FS::IOFile ShaderDiskCacheOpenGL::AppendTransferableFile() const {
     const auto transferable_path{GetTransferablePath()};
     const bool existed = Common::FS::Exists(transferable_path);
 
-    Common::FS::IOFile file(transferable_path, "ab");
+    Common::FS::IOFile file{transferable_path, Common::FS::FileAccessMode::Append,
+                            Common::FS::FileType::BinaryFile};
     if (!file.IsOpen()) {
-        LOG_ERROR(Render_OpenGL, "Failed to open transferable cache in path={}", transferable_path);
+        LOG_ERROR(Render_OpenGL, "Failed to open transferable cache in path={}",
+                  Common::FS::PathToUTF8String(transferable_path));
         return {};
     }
     if (!existed || file.GetSize() == 0) {
         // If the file didn't exist, write its version
-        if (file.WriteObject(NativeVersion) != 1) {
+        if (!file.WriteObject(NativeVersion)) {
             LOG_ERROR(Render_OpenGL, "Failed to write transferable cache version in path={}",
-                      transferable_path);
+                      Common::FS::PathToUTF8String(transferable_path));
             return {};
         }
     }
@@ -429,51 +425,54 @@ void ShaderDiskCacheOpenGL::SaveVirtualPrecompiledFile() {
     const std::vector<u8> compressed =
         Common::Compression::CompressDataZSTDDefault(uncompressed.data(), uncompressed.size());
 
-    const auto precompiled_path{GetPrecompiledPath()};
-    Common::FS::IOFile file(precompiled_path, "wb");
+    const auto precompiled_path = GetPrecompiledPath();
+    Common::FS::IOFile file{precompiled_path, Common::FS::FileAccessMode::Write,
+                            Common::FS::FileType::BinaryFile};
 
     if (!file.IsOpen()) {
-        LOG_ERROR(Render_OpenGL, "Failed to open precompiled cache in path={}", precompiled_path);
+        LOG_ERROR(Render_OpenGL, "Failed to open precompiled cache in path={}",
+                  Common::FS::PathToUTF8String(precompiled_path));
         return;
     }
-    if (file.WriteBytes(compressed.data(), compressed.size()) != compressed.size()) {
+    if (file.Write(compressed) != compressed.size()) {
         LOG_ERROR(Render_OpenGL, "Failed to write precompiled cache version in path={}",
-                  precompiled_path);
+                  Common::FS::PathToUTF8String(precompiled_path));
     }
 }
 
 bool ShaderDiskCacheOpenGL::EnsureDirectories() const {
-    const auto CreateDir = [](const std::string& dir) {
+    const auto CreateDir = [](const std::filesystem::path& dir) {
         if (!Common::FS::CreateDir(dir)) {
-            LOG_ERROR(Render_OpenGL, "Failed to create directory={}", dir);
+            LOG_ERROR(Render_OpenGL, "Failed to create directory={}",
+                      Common::FS::PathToUTF8String(dir));
             return false;
         }
         return true;
     };
 
-    return CreateDir(Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir)) &&
+    return CreateDir(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir)) &&
            CreateDir(GetBaseDir()) && CreateDir(GetTransferableDir()) &&
            CreateDir(GetPrecompiledDir());
 }
 
-std::string ShaderDiskCacheOpenGL::GetTransferablePath() const {
-    return Common::FS::SanitizePath(GetTransferableDir() + DIR_SEP_CHR + GetTitleID() + ".bin");
+std::filesystem::path ShaderDiskCacheOpenGL::GetTransferablePath() const {
+    return GetTransferableDir() / fmt::format("{}.bin", GetTitleID());
 }
 
-std::string ShaderDiskCacheOpenGL::GetPrecompiledPath() const {
-    return Common::FS::SanitizePath(GetPrecompiledDir() + DIR_SEP_CHR + GetTitleID() + ".bin");
+std::filesystem::path ShaderDiskCacheOpenGL::GetPrecompiledPath() const {
+    return GetPrecompiledDir() / fmt::format("{}.bin", GetTitleID());
 }
 
-std::string ShaderDiskCacheOpenGL::GetTransferableDir() const {
-    return GetBaseDir() + DIR_SEP "transferable";
+std::filesystem::path ShaderDiskCacheOpenGL::GetTransferableDir() const {
+    return GetBaseDir() / "transferable";
 }
 
-std::string ShaderDiskCacheOpenGL::GetPrecompiledDir() const {
-    return GetBaseDir() + DIR_SEP "precompiled";
+std::filesystem::path ShaderDiskCacheOpenGL::GetPrecompiledDir() const {
+    return GetBaseDir() / "precompiled";
 }
 
-std::string ShaderDiskCacheOpenGL::GetBaseDir() const {
-    return Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir) + DIR_SEP "opengl";
+std::filesystem::path ShaderDiskCacheOpenGL::GetBaseDir() const {
+    return Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir) / "opengl";
 }
 
 std::string ShaderDiskCacheOpenGL::GetTitleID() const {
diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.h b/src/video_core/renderer_opengl/gl_shader_disk_cache.h
index aef841c1d3..f8bc23868e 100644
--- a/src/video_core/renderer_opengl/gl_shader_disk_cache.h
+++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <filesystem>
 #include <optional>
 #include <string>
 #include <tuple>
@@ -108,19 +109,19 @@ private:
     bool EnsureDirectories() const;
 
     /// Gets current game's transferable file path
-    std::string GetTransferablePath() const;
+    std::filesystem::path GetTransferablePath() const;
 
     /// Gets current game's precompiled file path
-    std::string GetPrecompiledPath() const;
+    std::filesystem::path GetPrecompiledPath() const;
 
     /// Get user's transferable directory path
-    std::string GetTransferableDir() const;
+    std::filesystem::path GetTransferableDir() const;
 
     /// Get user's precompiled directory path
-    std::string GetPrecompiledDir() const;
+    std::filesystem::path GetPrecompiledDir() const;
 
     /// Get user's shader directory path
-    std::string GetBaseDir() const;
+    std::filesystem::path GetBaseDir() const;
 
     /// Get current game's title id
     std::string GetTitleID() const;
diff --git a/src/video_core/vulkan_common/nsight_aftermath_tracker.cpp b/src/video_core/vulkan_common/nsight_aftermath_tracker.cpp
index 7a9d00d4fe..f0ee765197 100644
--- a/src/video_core/vulkan_common/nsight_aftermath_tracker.cpp
+++ b/src/video_core/vulkan_common/nsight_aftermath_tracker.cpp
@@ -5,6 +5,7 @@
 #ifdef HAS_NSIGHT_AFTERMATH
 
 #include <mutex>
+#include <span>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -12,9 +13,10 @@
 
 #include <fmt/format.h>
 
-#include "common/common_paths.h"
 #include "common/common_types.h"
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/scope_exit.h"
 #include "video_core/vulkan_common/nsight_aftermath_tracker.h"
@@ -46,9 +48,9 @@ NsightAftermathTracker::NsightAftermathTracker() {
         LOG_ERROR(Render_Vulkan, "Failed to load Nsight Aftermath function pointers");
         return;
     }
-    dump_dir = Common::FS::GetUserPath(Common::FS::UserPath::LogDir) + "gpucrash";
+    dump_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::LogDir) / "gpucrash";
 
-    void(Common::FS::DeleteDirRecursively(dump_dir));
+    void(Common::FS::RemoveDirRecursively(dump_dir));
     if (!Common::FS::CreateDir(dump_dir)) {
         LOG_ERROR(Render_Vulkan, "Failed to create Nsight Aftermath dump directory");
         return;
@@ -60,7 +62,8 @@ NsightAftermathTracker::NsightAftermathTracker() {
         LOG_ERROR(Render_Vulkan, "GFSDK_Aftermath_EnableGpuCrashDumps failed");
         return;
     }
-    LOG_INFO(Render_Vulkan, "Nsight Aftermath dump directory is \"{}\"", dump_dir);
+    LOG_INFO(Render_Vulkan, "Nsight Aftermath dump directory is \"{}\"",
+             Common::FS::PathToUTF8String(dump_dir));
     initialized = true;
 }
 
@@ -89,12 +92,15 @@ void NsightAftermathTracker::SaveShader(const std::vector<u32>& spirv) const {
         return;
     }
 
-    Common::FS::IOFile file(fmt::format("{}/source_{:016x}.spv", dump_dir, hash.hash), "wb");
+    const auto shader_file = dump_dir / fmt::format("source_{:016x}.spv", hash.hash);
+
+    Common::FS::IOFile file{shader_file, Common::FS::FileAccessMode::Write,
+                            Common::FS::FileType::BinaryFile};
     if (!file.IsOpen()) {
         LOG_ERROR(Render_Vulkan, "Failed to dump SPIR-V module with hash={:016x}", hash.hash);
         return;
     }
-    if (file.WriteArray(spirv.data(), spirv.size()) != spirv.size()) {
+    if (file.Write(spirv) != spirv.size()) {
         LOG_ERROR(Render_Vulkan, "Failed to write SPIR-V module with hash={:016x}", hash.hash);
         return;
     }
@@ -129,22 +135,24 @@ void NsightAftermathTracker::OnGpuCrashDumpCallback(const void* gpu_crash_dump,
         return;
     }
 
-    const std::string base_name = [this] {
+    std::filesystem::path base_name = [this] {
         const int id = dump_id++;
         if (id == 0) {
-            return fmt::format("{}/crash.nv-gpudmp", dump_dir);
+            return dump_dir / "crash.nv-gpudmp";
         } else {
-            return fmt::format("{}/crash_{}.nv-gpudmp", dump_dir, id);
+            return dump_dir / fmt::format("crash_{}.nv-gpudmp", id);
         }
     }();
 
     std::string_view dump_view(static_cast<const char*>(gpu_crash_dump), gpu_crash_dump_size);
-    if (Common::FS::WriteStringToFile(false, base_name, dump_view) != gpu_crash_dump_size) {
+    if (Common::FS::WriteStringToFile(base_name, Common::FS::FileType::BinaryFile, dump_view) !=
+        gpu_crash_dump_size) {
         LOG_ERROR(Render_Vulkan, "Failed to write dump file");
         return;
     }
     const std::string_view json_view(json.data(), json.size());
-    if (Common::FS::WriteStringToFile(true, base_name + ".json", json_view) != json.size()) {
+    if (Common::FS::WriteStringToFile(base_name.concat(".json"), Common::FS::FileType::TextFile,
+                                      json_view) != json.size()) {
         LOG_ERROR(Render_Vulkan, "Failed to write JSON");
         return;
     }
@@ -161,16 +169,17 @@ void NsightAftermathTracker::OnShaderDebugInfoCallback(const void* shader_debug_
         return;
     }
 
-    const std::string path =
-        fmt::format("{}/shader_{:016x}{:016x}.nvdbg", dump_dir, identifier.id[0], identifier.id[1]);
-    Common::FS::IOFile file(path, "wb");
+    const auto path =
+        dump_dir / fmt::format("shader_{:016x}{:016x}.nvdbg", identifier.id[0], identifier.id[1]);
+    Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write,
+                            Common::FS::FileType::BinaryFile};
     if (!file.IsOpen()) {
-        LOG_ERROR(Render_Vulkan, "Failed to create file {}", path);
+        LOG_ERROR(Render_Vulkan, "Failed to create file {}", Common::FS::PathToUTF8String(path));
         return;
     }
-    if (file.WriteBytes(static_cast<const u8*>(shader_debug_info), shader_debug_info_size) !=
-        shader_debug_info_size) {
-        LOG_ERROR(Render_Vulkan, "Failed to write file {}", path);
+    if (file.WriteSpan(std::span(static_cast<const u8*>(shader_debug_info),
+                                 shader_debug_info_size)) != shader_debug_info_size) {
+        LOG_ERROR(Render_Vulkan, "Failed to write file {}", Common::FS::PathToUTF8String(path));
         return;
     }
 }
diff --git a/src/video_core/vulkan_common/nsight_aftermath_tracker.h b/src/video_core/vulkan_common/nsight_aftermath_tracker.h
index 1ce8d4e8e7..4fe2b14d91 100644
--- a/src/video_core/vulkan_common/nsight_aftermath_tracker.h
+++ b/src/video_core/vulkan_common/nsight_aftermath_tracker.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <filesystem>
 #include <mutex>
 #include <string>
 #include <vector>
@@ -54,7 +55,7 @@ private:
 
     mutable std::mutex mutex;
 
-    std::string dump_dir;
+    std::filesystem::path dump_dir;
     int dump_id = 0;
 
     bool initialized = false;
diff --git a/src/video_core/vulkan_common/vulkan_library.cpp b/src/video_core/vulkan_common/vulkan_library.cpp
index 557871d817..22833fa566 100644
--- a/src/video_core/vulkan_common/vulkan_library.cpp
+++ b/src/video_core/vulkan_common/vulkan_library.cpp
@@ -6,7 +6,7 @@
 #include <string>
 
 #include "common/dynamic_library.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "video_core/vulkan_common/vulkan_library.h"
 
 namespace Vulkan {
@@ -18,9 +18,9 @@ Common::DynamicLibrary OpenLibrary() {
     char* const libvulkan_env = std::getenv("LIBVULKAN_PATH");
     if (!libvulkan_env || !library.Open(libvulkan_env)) {
         // Use the libvulkan.dylib from the application bundle.
-        const std::string filename =
-            Common::FS::GetBundleDirectory() + "/Contents/Frameworks/libvulkan.dylib";
-        void(library.Open(filename.c_str()));
+        const auto filename =
+            Common::FS::GetBundleDirectory() / "Contents/Frameworks/libvulkan.dylib";
+        void(library.Open(Common::FS::PathToUTF8String(filename).c_str()));
     }
 #else
     std::string filename = Common::DynamicLibrary::GetVersionedFilename("vulkan", 1);
diff --git a/src/yuzu/applets/profile_select.cpp b/src/yuzu/applets/profile_select.cpp
index 0a4c48b3da..62fd1141c8 100644
--- a/src/yuzu/applets/profile_select.cpp
+++ b/src/yuzu/applets/profile_select.cpp
@@ -10,7 +10,7 @@
 #include <QScrollArea>
 #include <QStandardItemModel>
 #include <QVBoxLayout>
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/string_util.h"
 #include "core/constants.h"
 #include "core/hle/lock.h"
@@ -26,9 +26,10 @@ QString FormatUserEntryText(const QString& username, Common::UUID uuid) {
 }
 
 QString GetImagePath(Common::UUID uuid) {
-    const auto path = Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                      "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg";
-    return QString::fromStdString(path);
+    const auto path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) /
+        fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormatSwitch());
+    return QString::fromStdString(Common::FS::PathToUTF8String(path));
 }
 
 QPixmap GetIcon(Common::UUID uuid) {
diff --git a/src/yuzu/applets/web_browser.cpp b/src/yuzu/applets/web_browser.cpp
index 93e3a4f6f2..34d3feb550 100644
--- a/src/yuzu/applets/web_browser.cpp
+++ b/src/yuzu/applets/web_browser.cpp
@@ -12,7 +12,7 @@
 #include <QWebEngineUrlScheme>
 #endif
 
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "core/core.h"
 #include "core/frontend/input_interpreter.h"
 #include "input_common/keyboard.h"
@@ -322,21 +322,25 @@ void QtNXWebEngineView::LoadExtractedFonts() {
     QWebEngineScript nx_font_css;
     QWebEngineScript load_nx_font;
 
-    const QString fonts_dir = QString::fromStdString(Common::FS::SanitizePath(
-        fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir))));
+    auto fonts_dir_str = Common::FS::PathToUTF8String(
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "fonts/");
+
+    std::replace(fonts_dir_str.begin(), fonts_dir_str.end(), '\\', '/');
+
+    const auto fonts_dir = QString::fromStdString(fonts_dir_str);
 
     nx_font_css.setName(QStringLiteral("nx_font_css.js"));
     load_nx_font.setName(QStringLiteral("load_nx_font.js"));
 
     nx_font_css.setSourceCode(
         QString::fromStdString(NX_FONT_CSS)
-            .arg(fonts_dir + QStringLiteral("/FontStandard.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontChineseSimplified.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontExtendedChineseSimplified.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontChineseTraditional.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontKorean.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontNintendoExtended.ttf"))
-            .arg(fonts_dir + QStringLiteral("/FontNintendoExtended2.ttf")));
+            .arg(fonts_dir + QStringLiteral("FontStandard.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontChineseSimplified.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontExtendedChineseSimplified.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontChineseTraditional.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontKorean.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontNintendoExtended.ttf"))
+            .arg(fonts_dir + QStringLiteral("FontNintendoExtended2.ttf")));
     load_nx_font.setSourceCode(QString::fromStdString(LOAD_NX_FONT));
 
     nx_font_css.setInjectionPoint(QWebEngineScript::DocumentReady);
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 21d1dc1749..eb58bfa5b9 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -5,8 +5,8 @@
 #include <array>
 #include <QKeySequence>
 #include <QSettings>
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "core/core.h"
 #include "core/hle/service/acc/profile_manager.h"
 #include "core/hle/service/hid/controllers/npad.h"
@@ -243,27 +243,27 @@ const std::array<UISettings::Shortcut, 17> Config::default_hotkeys{{
 // clang-format on
 
 void Config::Initialize(const std::string& config_name) {
+    const auto fs_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir);
+    const auto config_file = fmt::format("{}.ini", config_name);
+
     switch (type) {
     case ConfigType::GlobalConfig:
-        qt_config_loc = fmt::format("{}" DIR_SEP "{}.ini", FS::GetUserPath(FS::UserPath::ConfigDir),
-                                    config_name);
-        FS::CreateFullPath(qt_config_loc);
+        qt_config_loc = FS::PathToUTF8String(fs_config_loc / config_file);
+        void(FS::CreateParentDir(qt_config_loc));
         qt_config = std::make_unique<QSettings>(QString::fromStdString(qt_config_loc),
                                                 QSettings::IniFormat);
         Reload();
         break;
     case ConfigType::PerGameConfig:
-        qt_config_loc = fmt::format("{}custom" DIR_SEP "{}.ini",
-                                    FS::GetUserPath(FS::UserPath::ConfigDir), config_name);
-        FS::CreateFullPath(qt_config_loc);
+        qt_config_loc = FS::PathToUTF8String(fs_config_loc / "custom" / config_file);
+        void(FS::CreateParentDir(qt_config_loc));
         qt_config = std::make_unique<QSettings>(QString::fromStdString(qt_config_loc),
                                                 QSettings::IniFormat);
         Reload();
         break;
     case ConfigType::InputProfile:
-        qt_config_loc = fmt::format("{}input" DIR_SEP "{}.ini",
-                                    FS::GetUserPath(FS::UserPath::ConfigDir), config_name);
-        FS::CreateFullPath(qt_config_loc);
+        qt_config_loc = FS::PathToUTF8String(fs_config_loc / "input" / config_file);
+        void(FS::CreateParentDir(qt_config_loc));
         qt_config = std::make_unique<QSettings>(QString::fromStdString(qt_config_loc),
                                                 QSettings::IniFormat);
         break;
@@ -598,30 +598,34 @@ void Config::ReadDataStorageValues() {
     qt_config->beginGroup(QStringLiteral("Data Storage"));
 
     Settings::values.use_virtual_sd = ReadSetting(QStringLiteral("use_virtual_sd"), true).toBool();
-    FS::GetUserPath(FS::UserPath::NANDDir,
-                    qt_config
-                        ->value(QStringLiteral("nand_directory"),
-                                QString::fromStdString(FS::GetUserPath(FS::UserPath::NANDDir)))
-                        .toString()
-                        .toStdString());
-    FS::GetUserPath(FS::UserPath::SDMCDir,
-                    qt_config
-                        ->value(QStringLiteral("sdmc_directory"),
-                                QString::fromStdString(FS::GetUserPath(FS::UserPath::SDMCDir)))
-                        .toString()
-                        .toStdString());
-    FS::GetUserPath(FS::UserPath::LoadDir,
-                    qt_config
-                        ->value(QStringLiteral("load_directory"),
-                                QString::fromStdString(FS::GetUserPath(FS::UserPath::LoadDir)))
-                        .toString()
-                        .toStdString());
-    FS::GetUserPath(FS::UserPath::DumpDir,
-                    qt_config
-                        ->value(QStringLiteral("dump_directory"),
-                                QString::fromStdString(FS::GetUserPath(FS::UserPath::DumpDir)))
-                        .toString()
-                        .toStdString());
+    FS::SetYuzuPath(
+        FS::YuzuPath::NANDDir,
+        qt_config
+            ->value(QStringLiteral("nand_directory"),
+                    QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::NANDDir)))
+            .toString()
+            .toStdString());
+    FS::SetYuzuPath(
+        FS::YuzuPath::SDMCDir,
+        qt_config
+            ->value(QStringLiteral("sdmc_directory"),
+                    QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)))
+            .toString()
+            .toStdString());
+    FS::SetYuzuPath(
+        FS::YuzuPath::LoadDir,
+        qt_config
+            ->value(QStringLiteral("load_directory"),
+                    QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::LoadDir)))
+            .toString()
+            .toStdString());
+    FS::SetYuzuPath(
+        FS::YuzuPath::DumpDir,
+        qt_config
+            ->value(QStringLiteral("dump_directory"),
+                    QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)))
+            .toString()
+            .toStdString());
     Settings::values.gamecard_inserted =
         ReadSetting(QStringLiteral("gamecard_inserted"), false).toBool();
     Settings::values.gamecard_current_game =
@@ -817,11 +821,11 @@ void Config::ReadScreenshotValues() {
 
     UISettings::values.enable_screenshot_save_as =
         ReadSetting(QStringLiteral("enable_screenshot_save_as"), true).toBool();
-    FS::GetUserPath(
-        FS::UserPath::ScreenshotsDir,
+    FS::SetYuzuPath(
+        FS::YuzuPath::ScreenshotsDir,
         qt_config
             ->value(QStringLiteral("screenshot_path"),
-                    QString::fromStdString(FS::GetUserPath(FS::UserPath::ScreenshotsDir)))
+                    QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::ScreenshotsDir)))
             .toString()
             .toStdString());
 
@@ -1220,17 +1224,17 @@ void Config::SaveDataStorageValues() {
 
     WriteSetting(QStringLiteral("use_virtual_sd"), Settings::values.use_virtual_sd, true);
     WriteSetting(QStringLiteral("nand_directory"),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::NANDDir)),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::NANDDir)));
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::NANDDir)),
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
     WriteSetting(QStringLiteral("sdmc_directory"),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::SDMCDir)),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::SDMCDir)));
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)),
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
     WriteSetting(QStringLiteral("load_directory"),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::LoadDir)),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::LoadDir)));
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::LoadDir)),
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
     WriteSetting(QStringLiteral("dump_directory"),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::DumpDir)),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::DumpDir)));
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)),
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
     WriteSetting(QStringLiteral("gamecard_inserted"), Settings::values.gamecard_inserted, false);
     WriteSetting(QStringLiteral("gamecard_current_game"), Settings::values.gamecard_current_game,
                  false);
@@ -1397,7 +1401,7 @@ void Config::SaveScreenshotValues() {
     WriteSetting(QStringLiteral("enable_screenshot_save_as"),
                  UISettings::values.enable_screenshot_save_as);
     WriteSetting(QStringLiteral("screenshot_path"),
-                 QString::fromStdString(FS::GetUserPath(FS::UserPath::ScreenshotsDir)));
+                 QString::fromStdString(FS::GetYuzuPathString(FS::YuzuPath::ScreenshotsDir)));
 
     qt_config->endGroup();
 }
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index 6730eb3563..b207e07cb0 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -4,7 +4,7 @@
 
 #include <QDesktopServices>
 #include <QUrl>
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
 #include "common/settings.h"
@@ -20,7 +20,7 @@ ConfigureDebug::ConfigureDebug(QWidget* parent) : QWidget(parent), ui(new Ui::Co
 
     connect(ui->open_log_button, &QPushButton::clicked, []() {
         const auto path =
-            QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::LogDir));
+            QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LogDir));
         QDesktopServices::openUrl(QUrl::fromLocalFile(path));
     });
 }
diff --git a/src/yuzu/configuration/configure_filesystem.cpp b/src/yuzu/configuration/configure_filesystem.cpp
index 006eda4b00..d223c40ea2 100644
--- a/src/yuzu/configuration/configure_filesystem.cpp
+++ b/src/yuzu/configuration/configure_filesystem.cpp
@@ -4,8 +4,8 @@
 
 #include <QFileDialog>
 #include <QMessageBox>
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/settings.h"
 #include "ui_configure_filesystem.h"
 #include "yuzu/configuration/configure_filesystem.h"
@@ -40,14 +40,14 @@ ConfigureFilesystem::~ConfigureFilesystem() = default;
 
 void ConfigureFilesystem::setConfiguration() {
     ui->nand_directory_edit->setText(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir)));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::NANDDir)));
     ui->sdmc_directory_edit->setText(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir)));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::SDMCDir)));
     ui->gamecard_path_edit->setText(QString::fromStdString(Settings::values.gamecard_path));
     ui->dump_path_edit->setText(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::DumpDir)));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::DumpDir)));
     ui->load_path_edit->setText(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::LoadDir)));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LoadDir)));
 
     ui->gamecard_inserted->setChecked(Settings::values.gamecard_inserted);
     ui->gamecard_current_game->setChecked(Settings::values.gamecard_current_game);
@@ -60,13 +60,13 @@ void ConfigureFilesystem::setConfiguration() {
 }
 
 void ConfigureFilesystem::applyConfiguration() {
-    Common::FS::GetUserPath(Common::FS::UserPath::NANDDir,
+    Common::FS::SetYuzuPath(Common::FS::YuzuPath::NANDDir,
                             ui->nand_directory_edit->text().toStdString());
-    Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir,
+    Common::FS::SetYuzuPath(Common::FS::YuzuPath::SDMCDir,
                             ui->sdmc_directory_edit->text().toStdString());
-    Common::FS::GetUserPath(Common::FS::UserPath::DumpDir,
+    Common::FS::SetYuzuPath(Common::FS::YuzuPath::DumpDir,
                             ui->dump_path_edit->text().toStdString());
-    Common::FS::GetUserPath(Common::FS::UserPath::LoadDir,
+    Common::FS::SetYuzuPath(Common::FS::YuzuPath::LoadDir,
                             ui->load_path_edit->text().toStdString());
 
     Settings::values.gamecard_inserted = ui->gamecard_inserted->isChecked();
@@ -104,25 +104,26 @@ void ConfigureFilesystem::SetDirectory(DirectoryTarget target, QLineEdit* edit)
                                            QStringLiteral("NX Gamecard;*.xci"));
     } else {
         str = QFileDialog::getExistingDirectory(this, caption, edit->text());
-        if (!str.isNull() && str.back() != QDir::separator()) {
-            str.append(QDir::separator());
-        }
     }
 
-    if (str.isEmpty())
+    if (str.isNull() || str.isEmpty()) {
         return;
+    }
+
+    if (str.back() != QChar::fromLatin1('/')) {
+        str.append(QChar::fromLatin1('/'));
+    }
 
     edit->setText(str);
 }
 
 void ConfigureFilesystem::ResetMetadata() {
-    if (!Common::FS::Exists(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                            "game_list")) {
+    if (!Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                            "game_list/")) {
         QMessageBox::information(this, tr("Reset Metadata Cache"),
                                  tr("The metadata cache is already empty."));
-    } else if (Common::FS::DeleteDirRecursively(
-                   Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                   "game_list")) {
+    } else if (Common::FS::RemoveDirRecursively(
+                   Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "game_list")) {
         QMessageBox::information(this, tr("Reset Metadata Cache"),
                                  tr("The operation completed successfully."));
         UISettings::values.is_game_list_reload_pending.exchange(true);
diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp
index f550567e25..3e13bd4387 100644
--- a/src/yuzu/configuration/configure_per_game.cpp
+++ b/src/yuzu/configuration/configure_per_game.cpp
@@ -14,8 +14,6 @@
 #include <QTimer>
 #include <QTreeView>
 
-#include "common/common_paths.h"
-#include "common/file_util.h"
 #include "core/core.h"
 #include "core/file_sys/control_metadata.h"
 #include "core/file_sys/patch_manager.h"
diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp
index cdeeec01c0..9b709d4051 100644
--- a/src/yuzu/configuration/configure_per_game_addons.cpp
+++ b/src/yuzu/configuration/configure_per_game_addons.cpp
@@ -13,8 +13,8 @@
 #include <QTimer>
 #include <QTreeView>
 
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "core/core.h"
 #include "core/file_sys/patch_manager.h"
 #include "core/file_sys/xts_archive.h"
@@ -79,8 +79,8 @@ void ConfigurePerGameAddons::ApplyConfiguration() {
     std::sort(disabled_addons.begin(), disabled_addons.end());
     std::sort(current.begin(), current.end());
     if (disabled_addons != current) {
-        Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                           "game_list" + DIR_SEP + fmt::format("{:016X}.pv.txt", title_id));
+        void(Common::FS::RemoveFile(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                                    "game_list" / fmt::format("{:016X}.pv.txt", title_id)));
     }
 
     Settings::values.disabled_addons[title_id] = disabled_addons;
diff --git a/src/yuzu/configuration/configure_profile_manager.cpp b/src/yuzu/configuration/configure_profile_manager.cpp
index d61b5e29b1..f5881e58dc 100644
--- a/src/yuzu/configuration/configure_profile_manager.cpp
+++ b/src/yuzu/configuration/configure_profile_manager.cpp
@@ -12,7 +12,7 @@
 #include <QTreeView>
 #include <QVBoxLayout>
 #include "common/assert.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/settings.h"
 #include "common/string_util.h"
 #include "core/core.h"
@@ -34,9 +34,10 @@ constexpr std::array<u8, 107> backup_jpeg{
 };
 
 QString GetImagePath(Common::UUID uuid) {
-    const auto path = Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                      "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg";
-    return QString::fromStdString(path);
+    const auto path =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) /
+        fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormatSwitch());
+    return QString::fromStdString(Common::FS::PathToUTF8String(path));
 }
 
 QString GetAccountUsername(const Service::Account::ProfileManager& manager, Common::UUID uuid) {
@@ -281,8 +282,8 @@ void ConfigureProfileManager::SetUserImage() {
         return;
     }
 
-    const auto raw_path = QString::fromStdString(
-        Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) + "/system/save/8000000000000010");
+    const auto raw_path = QString::fromStdString(Common::FS::PathToUTF8String(
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/save/8000000000000010"));
     const QFileInfo raw_info{raw_path};
     if (raw_info.exists() && !raw_info.isDir() && !QFile::remove(raw_path)) {
         QMessageBox::warning(this, tr("Error deleting file"),
diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp
index 85418f9699..99a5df241c 100644
--- a/src/yuzu/configuration/configure_system.cpp
+++ b/src/yuzu/configuration/configure_system.cpp
@@ -10,7 +10,6 @@
 #include <QGraphicsItem>
 #include <QMessageBox>
 #include "common/assert.h"
-#include "common/file_util.h"
 #include "common/settings.h"
 #include "core/core.h"
 #include "core/hle/service/time/time.h"
diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp
index 0cdaea8a4a..0a28c87c06 100644
--- a/src/yuzu/configuration/configure_ui.cpp
+++ b/src/yuzu/configuration/configure_ui.cpp
@@ -8,7 +8,7 @@
 
 #include <QDirIterator>
 #include "common/common_types.h"
-#include "common/file_util.h"
+#include "common/fs/path_util.h"
 #include "common/settings.h"
 #include "core/core.h"
 #include "ui_configure_ui.h"
@@ -62,13 +62,16 @@ ConfigureUi::ConfigureUi(QWidget* parent) : QWidget(parent), ui(new Ui::Configur
 
     // Set screenshot path to user specification.
     connect(ui->screenshot_path_button, &QToolButton::pressed, this, [this] {
-        const QString& filename =
+        auto dir =
             QFileDialog::getExistingDirectory(this, tr("Select Screenshots Path..."),
-                                              QString::fromStdString(Common::FS::GetUserPath(
-                                                  Common::FS::UserPath::ScreenshotsDir))) +
-            QDir::separator();
-        if (!filename.isEmpty()) {
-            ui->screenshot_path_edit->setText(filename);
+                                              QString::fromStdString(Common::FS::GetYuzuPathString(
+                                                  Common::FS::YuzuPath::ScreenshotsDir)));
+        if (!dir.isEmpty()) {
+            if (dir.back() != QChar::fromLatin1('/')) {
+                dir.append(QChar::fromLatin1('/'));
+            }
+
+            ui->screenshot_path_edit->setText(dir);
         }
     });
 }
@@ -84,7 +87,7 @@ void ConfigureUi::ApplyConfiguration() {
     UISettings::values.row_2_text_id = ui->row_2_text_combobox->currentData().toUInt();
 
     UISettings::values.enable_screenshot_save_as = ui->enable_screenshot_save_as->isChecked();
-    Common::FS::GetUserPath(Common::FS::UserPath::ScreenshotsDir,
+    Common::FS::SetYuzuPath(Common::FS::YuzuPath::ScreenshotsDir,
                             ui->screenshot_path_edit->text().toStdString());
     Core::System::GetInstance().ApplySettings();
 }
@@ -102,8 +105,8 @@ void ConfigureUi::SetConfiguration() {
         ui->icon_size_combobox->findData(UISettings::values.icon_size));
 
     ui->enable_screenshot_save_as->setChecked(UISettings::values.enable_screenshot_save_as);
-    ui->screenshot_path_edit->setText(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ScreenshotsDir)));
+    ui->screenshot_path_edit->setText(QString::fromStdString(
+        Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ScreenshotsDir)));
 }
 
 void ConfigureUi::changeEvent(QEvent* event) {
diff --git a/src/yuzu/configuration/input_profiles.cpp b/src/yuzu/configuration/input_profiles.cpp
index e87aededb3..333eeb84ec 100644
--- a/src/yuzu/configuration/input_profiles.cpp
+++ b/src/yuzu/configuration/input_profiles.cpp
@@ -4,8 +4,8 @@
 
 #include <fmt/format.h>
 
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "yuzu/configuration/config.h"
 #include "yuzu/configuration/input_profiles.h"
 
@@ -14,47 +14,43 @@ namespace FS = Common::FS;
 namespace {
 
 bool ProfileExistsInFilesystem(std::string_view profile_name) {
-    return FS::Exists(fmt::format("{}input" DIR_SEP "{}.ini",
-                                  FS::GetUserPath(FS::UserPath::ConfigDir), profile_name));
+    return FS::Exists(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "input" /
+                      fmt::format("{}.ini", profile_name));
 }
 
-bool IsINI(std::string_view filename) {
-    const std::size_t index = filename.rfind('.');
-
-    if (index == std::string::npos) {
-        return false;
-    }
-
-    return filename.substr(index) == ".ini";
+bool IsINI(const std::filesystem::path& filename) {
+    return filename.extension() == ".ini";
 }
 
-std::string GetNameWithoutExtension(const std::string& filename) {
-    const std::size_t index = filename.rfind('.');
-
-    if (index == std::string::npos) {
-        return filename;
-    }
-
-    return filename.substr(0, index);
+std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
+    return filename.replace_extension();
 }
 
 } // namespace
 
 InputProfiles::InputProfiles() {
-    const std::string input_profile_loc =
-        fmt::format("{}input", FS::GetUserPath(FS::UserPath::ConfigDir));
+    const auto input_profile_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "input";
 
-    FS::ForeachDirectoryEntry(
-        nullptr, input_profile_loc,
-        [this](u64* entries_out, const std::string& directory, const std::string& filename) {
-            if (IsINI(filename) && IsProfileNameValid(GetNameWithoutExtension(filename))) {
+    if (!FS::IsDir(input_profile_loc)) {
+        return;
+    }
+
+    FS::IterateDirEntries(
+        input_profile_loc,
+        [this](const std::filesystem::path& full_path) {
+            const auto filename = full_path.filename();
+            const auto name_without_ext =
+                Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
+
+            if (IsINI(filename) && IsProfileNameValid(name_without_ext)) {
                 map_profiles.insert_or_assign(
-                    GetNameWithoutExtension(filename),
-                    std::make_unique<Config>(GetNameWithoutExtension(filename),
-                                             Config::ConfigType::InputProfile));
+                    name_without_ext,
+                    std::make_unique<Config>(name_without_ext, Config::ConfigType::InputProfile));
             }
+
             return true;
-        });
+        },
+        FS::DirEntryFilter::File);
 }
 
 InputProfiles::~InputProfiles() = default;
@@ -96,7 +92,7 @@ bool InputProfiles::DeleteProfile(const std::string& profile_name) {
     }
 
     if (!ProfileExistsInFilesystem(profile_name) ||
-        FS::Delete(map_profiles[profile_name]->GetConfigFilePath())) {
+        FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
         map_profiles.erase(profile_name);
     }
 
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index 23643aea22..4850453340 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -12,8 +12,8 @@
 #include <QFileInfo>
 #include <QSettings>
 
-#include "common/common_paths.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "core/core.h"
 #include "core/file_sys/card_image.h"
 #include "core/file_sys/content_archive.h"
@@ -39,10 +39,11 @@ QString GetGameListCachedObject(const std::string& filename, const std::string&
         return generator();
     }
 
-    const auto path = Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                      "game_list" + DIR_SEP + filename + '.' + ext;
+    const auto path =
+        Common::FS::PathToUTF8String(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                                     "game_list" / fmt::format("{}.{}", filename, ext));
 
-    Common::FS::CreateFullPath(path);
+    void(Common::FS::CreateParentDirs(path));
 
     if (!Common::FS::Exists(path)) {
         const auto str = generator();
@@ -70,12 +71,15 @@ std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
         return generator();
     }
 
-    const auto path1 = Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                       "game_list" + DIR_SEP + filename + ".jpeg";
-    const auto path2 = Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) + DIR_SEP +
-                       "game_list" + DIR_SEP + filename + ".appname.txt";
+    const auto game_list_dir =
+        Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "game_list";
+    const auto jpeg_name = fmt::format("{}.jpeg", filename);
+    const auto app_name = fmt::format("{}.appname.txt", filename);
 
-    Common::FS::CreateFullPath(path1);
+    const auto path1 = Common::FS::PathToUTF8String(game_list_dir / jpeg_name);
+    const auto path2 = Common::FS::PathToUTF8String(game_list_dir / app_name);
+
+    void(Common::FS::CreateParentDirs(path1));
 
     if (!Common::FS::Exists(path1) || !Common::FS::Exists(path2)) {
         const auto [icon, nacp] = generator();
@@ -281,23 +285,27 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
     }
 }
 
-void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
-                                    unsigned int recursion, GameListDir* parent_dir) {
+void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
+                                    GameListDir* parent_dir) {
     auto& system = Core::System::GetInstance();
 
-    const auto callback = [this, target, recursion, parent_dir,
-                           &system](u64* num_entries_out, const std::string& directory,
-                                    const std::string& virtual_name) -> bool {
+    const auto callback = [this, target, parent_dir,
+                           &system](const std::filesystem::path& path) -> bool {
         if (stop_processing) {
             // Breaks the callback loop.
             return false;
         }
 
-        const std::string physical_name = directory + DIR_SEP + virtual_name;
-        const bool is_dir = Common::FS::IsDirectory(physical_name);
+        const auto physical_name = Common::FS::PathToUTF8String(path);
+        const auto is_dir = Common::FS::IsDir(path);
+
         if (!is_dir &&
             (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
             const auto file = vfs->OpenFile(physical_name, FileSys::Mode::Read);
+            if (!file) {
+                return true;
+            }
+
             auto loader = Loader::GetLoader(system, file);
             if (!loader) {
                 return true;
@@ -343,15 +351,19 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
                                                   compatibility_list, patch),
                                 parent_dir);
             }
-        } else if (is_dir && recursion > 0) {
+        } else if (is_dir) {
             watch_list.append(QString::fromStdString(physical_name));
-            ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
         }
 
         return true;
     };
 
-    Common::FS::ForeachDirectoryEntry(nullptr, dir_path, callback);
+    if (deep_scan) {
+        Common::FS::IterateDirEntriesRecursively(dir_path, callback,
+                                                 Common::FS::DirEntryFilter::All);
+    } else {
+        Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
+    }
 }
 
 void GameListWorker::run() {
@@ -376,9 +388,9 @@ void GameListWorker::run() {
             auto* const game_list_dir = new GameListDir(game_dir);
             emit DirEntryReady(game_list_dir);
             ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(),
-                           game_dir.deep_scan ? 256 : 0, game_list_dir);
+                           game_dir.deep_scan, game_list_dir);
             ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
-                           game_dir.deep_scan ? 256 : 0, game_list_dir);
+                           game_dir.deep_scan, game_list_dir);
         }
     }
 
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h
index 84e4e1b42c..396bb2623e 100644
--- a/src/yuzu/game_list_worker.h
+++ b/src/yuzu/game_list_worker.h
@@ -70,7 +70,7 @@ private:
         PopulateGameList,
     };
 
-    void ScanFileSystem(ScanTarget target, const std::string& dir_path, unsigned int recursion,
+    void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
                         GameListDir* parent_dir);
 
     std::shared_ptr<FileSys::VfsFilesystem> vfs;
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index bc97f9d537..37ef629672 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -66,9 +66,10 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
 #include <QtConcurrent/QtConcurrent>
 
 #include <fmt/format.h>
-#include "common/common_paths.h"
 #include "common/detached_tasks.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/fs_paths.h"
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
 #include "common/logging/log.h"
@@ -178,36 +179,25 @@ static void InitializeLogging() {
     log_filter.ParseFilterString(Settings::values.log_filter);
     Log::SetGlobalFilter(log_filter);
 
-    const std::string& log_dir = FS::GetUserPath(FS::UserPath::LogDir);
-    FS::CreateFullPath(log_dir);
-    Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
+    const auto log_dir = FS::GetYuzuPath(FS::YuzuPath::LogDir);
+    void(FS::CreateDir(log_dir));
+    Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir / LOG_FILE));
 #ifdef _WIN32
     Log::AddBackend(std::make_unique<Log::DebuggerBackend>());
 #endif
 }
 
 static void RemoveCachedContents() {
-    const auto offline_fonts = Common::FS::SanitizePath(
-        fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
-        Common::FS::DirectorySeparator::PlatformDefault);
+    const auto cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir);
+    const auto offline_fonts = cache_dir / "fonts";
+    const auto offline_manual = cache_dir / "offline_web_applet_manual";
+    const auto offline_legal_information = cache_dir / "offline_web_applet_legal_information";
+    const auto offline_system_data = cache_dir / "offline_web_applet_system_data";
 
-    const auto offline_manual = Common::FS::SanitizePath(
-        fmt::format("{}/offline_web_applet_manual",
-                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
-        Common::FS::DirectorySeparator::PlatformDefault);
-    const auto offline_legal_information = Common::FS::SanitizePath(
-        fmt::format("{}/offline_web_applet_legal_information",
-                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
-        Common::FS::DirectorySeparator::PlatformDefault);
-    const auto offline_system_data = Common::FS::SanitizePath(
-        fmt::format("{}/offline_web_applet_system_data",
-                    Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)),
-        Common::FS::DirectorySeparator::PlatformDefault);
-
-    Common::FS::DeleteDirRecursively(offline_fonts);
-    Common::FS::DeleteDirRecursively(offline_manual);
-    Common::FS::DeleteDirRecursively(offline_legal_information);
-    Common::FS::DeleteDirRecursively(offline_system_data);
+    void(Common::FS::RemoveDirRecursively(offline_fonts));
+    void(Common::FS::RemoveDirRecursively(offline_manual));
+    void(Common::FS::RemoveDirRecursively(offline_legal_information));
+    void(Common::FS::RemoveDirRecursively(offline_system_data));
 }
 
 GMainWindow::GMainWindow()
@@ -1418,7 +1408,8 @@ void GMainWindow::BootGame(const QString& filename, std::size_t program_index) {
         title_name = metadata.first->GetApplicationName();
     }
     if (res != Loader::ResultStatus::Success || title_name.empty()) {
-        title_name = Common::FS::GetFilename(filename.toStdString());
+        title_name = Common::FS::PathToUTF8String(
+            std::filesystem::path{filename.toStdU16String()}.filename());
     }
     LOG_INFO(Frontend, "Booting game: {:016X} | {} | {}", title_id, title_name, title_version);
     UpdateWindowTitle(title_name, title_version);
@@ -1538,7 +1529,7 @@ void GMainWindow::OnGameListLoadFile(QString game_path) {
 
 void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target,
                                        const std::string& game_path) {
-    std::string path;
+    std::filesystem::path path;
     QString open_target;
     auto& system = Core::System::GetInstance();
 
@@ -1567,7 +1558,7 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
     switch (target) {
     case GameListOpenTarget::SaveData: {
         open_target = tr("Save Data");
-        const std::string nand_dir = Common::FS::GetUserPath(Common::FS::UserPath::NANDDir);
+        const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
 
         if (has_user_save) {
             // User save data
@@ -1592,34 +1583,38 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
             Service::Account::ProfileManager manager;
             const auto user_id = manager.GetUser(static_cast<std::size_t>(index));
             ASSERT(user_id);
-            path = nand_dir + FileSys::SaveDataFactory::GetFullPath(
-                                  system, FileSys::SaveDataSpaceId::NandUser,
-                                  FileSys::SaveDataType::SaveData, program_id, user_id->uuid, 0);
+
+            const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
+                system, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
+                program_id, user_id->uuid, 0);
+
+            path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
         } else {
             // Device save data
-            path = nand_dir + FileSys::SaveDataFactory::GetFullPath(
-                                  system, FileSys::SaveDataSpaceId::NandUser,
-                                  FileSys::SaveDataType::SaveData, program_id, {}, 0);
+            const auto device_save_data_path = FileSys::SaveDataFactory::GetFullPath(
+                system, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
+                program_id, {}, 0);
+
+            path = Common::FS::ConcatPathSafe(nand_dir, device_save_data_path);
         }
 
-        if (!Common::FS::Exists(path)) {
-            Common::FS::CreateFullPath(path);
-            Common::FS::CreateDir(path);
+        if (!Common::FS::CreateDirs(path)) {
+            LOG_ERROR(Frontend, "Unable to create the directories for save data");
         }
 
         break;
     }
     case GameListOpenTarget::ModData: {
         open_target = tr("Mod Data");
-        const auto load_dir = Common::FS::GetUserPath(Common::FS::UserPath::LoadDir);
-        path = fmt::format("{}{:016X}", load_dir, program_id);
+        path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::LoadDir) /
+               fmt::format("{:016X}", program_id);
         break;
     }
     default:
         UNIMPLEMENTED();
     }
 
-    const QString qpath = QString::fromStdString(path);
+    const QString qpath = QString::fromStdString(Common::FS::PathToUTF8String(path));
     const QDir dir(qpath);
     if (!dir.exists()) {
         QMessageBox::warning(this, tr("Error Opening %1 Folder").arg(open_target),
@@ -1632,33 +1627,35 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
 }
 
 void GMainWindow::OnTransferableShaderCacheOpenFile(u64 program_id) {
-    const QString shader_dir =
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir));
-    const QString transferable_shader_cache_folder_path =
-        shader_dir + QStringLiteral("opengl") + QDir::separator() + QStringLiteral("transferable");
-    const QString transferable_shader_cache_file_path =
-        transferable_shader_cache_folder_path + QDir::separator() +
-        QString::fromStdString(fmt::format("{:016X}.bin", program_id));
+    const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
+    const auto transferable_shader_cache_folder_path = shader_cache_dir / "opengl" / "transferable";
+    const auto transferable_shader_cache_file_path =
+        transferable_shader_cache_folder_path / fmt::format("{:016X}.bin", program_id);
 
-    if (!QFile::exists(transferable_shader_cache_file_path)) {
+    if (!Common::FS::Exists(transferable_shader_cache_file_path)) {
         QMessageBox::warning(this, tr("Error Opening Transferable Shader Cache"),
                              tr("A shader cache for this title does not exist."));
         return;
     }
 
+    const auto qt_shader_cache_folder_path =
+        QString::fromStdString(Common::FS::PathToUTF8String(transferable_shader_cache_folder_path));
+    const auto qt_shader_cache_file_path =
+        QString::fromStdString(Common::FS::PathToUTF8String(transferable_shader_cache_file_path));
+
     // Windows supports opening a folder with selecting a specified file in explorer. On every other
     // OS we just open the transferable shader cache folder without preselecting the transferable
     // shader cache file for the selected game.
 #if defined(Q_OS_WIN)
     const QString explorer = QStringLiteral("explorer");
     QStringList param;
-    if (!QFileInfo(transferable_shader_cache_file_path).isDir()) {
+    if (!QFileInfo(qt_shader_cache_file_path).isDir()) {
         param << QStringLiteral("/select,");
     }
-    param << QDir::toNativeSeparators(transferable_shader_cache_file_path);
+    param << QDir::toNativeSeparators(qt_shader_cache_file_path);
     QProcess::startDetached(explorer, param);
 #else
-    QDesktopServices::openUrl(QUrl::fromLocalFile(transferable_shader_cache_folder_path));
+    QDesktopServices::openUrl(QUrl::fromLocalFile(qt_shader_cache_folder_path));
 #endif
 }
 
@@ -1736,8 +1733,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
         RemoveAddOnContent(program_id, entry_type);
         break;
     }
-    Common::FS::DeleteDirRecursively(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) +
-                                     DIR_SEP + "game_list");
+    void(Common::FS::RemoveDirRecursively(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                                          "game_list"));
     game_list->PopulateAsync(UISettings::values.game_dirs);
 }
 
@@ -1826,21 +1823,17 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ
 }
 
 void GMainWindow::RemoveTransferableShaderCache(u64 program_id) {
-    const QString shader_dir =
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir));
-    const QString transferable_shader_cache_folder_path =
-        shader_dir + QStringLiteral("opengl") + QDir::separator() + QStringLiteral("transferable");
-    const QString transferable_shader_cache_file_path =
-        transferable_shader_cache_folder_path + QDir::separator() +
-        QString::fromStdString(fmt::format("{:016X}.bin", program_id));
+    const auto shader_cache_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir);
+    const auto transferable_shader_cache_file_path =
+        shader_cache_dir / "opengl" / "transferable" / fmt::format("{:016X}.bin", program_id);
 
-    if (!QFile::exists(transferable_shader_cache_file_path)) {
+    if (!Common::FS::Exists(transferable_shader_cache_file_path)) {
         QMessageBox::warning(this, tr("Error Removing Transferable Shader Cache"),
                              tr("A shader cache for this title does not exist."));
         return;
     }
 
-    if (QFile::remove(transferable_shader_cache_file_path)) {
+    if (Common::FS::RemoveFile(transferable_shader_cache_file_path)) {
         QMessageBox::information(this, tr("Successfully Removed"),
                                  tr("Successfully removed the transferable shader cache."));
     } else {
@@ -1850,19 +1843,16 @@ void GMainWindow::RemoveTransferableShaderCache(u64 program_id) {
 }
 
 void GMainWindow::RemoveCustomConfiguration(u64 program_id) {
-    const QString config_dir =
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir));
-    const QString custom_config_file_path =
-        config_dir + QStringLiteral("custom") + QDir::separator() +
-        QString::fromStdString(fmt::format("{:016X}.ini", program_id));
+    const auto custom_config_file_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) /
+                                         "custom" / fmt::format("{:016X}.ini", program_id);
 
-    if (!QFile::exists(custom_config_file_path)) {
+    if (!Common::FS::Exists(custom_config_file_path)) {
         QMessageBox::warning(this, tr("Error Removing Custom Configuration"),
                              tr("A custom configuration for this title does not exist."));
         return;
     }
 
-    if (QFile::remove(custom_config_file_path)) {
+    if (Common::FS::RemoveFile(custom_config_file_path)) {
         QMessageBox::information(this, tr("Successfully Removed"),
                                  tr("Successfully removed the custom game configuration."));
     } else {
@@ -1899,8 +1889,10 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa
         return;
     }
 
-    const auto path = fmt::format(
-        "{}{:016X}/romfs", Common::FS::GetUserPath(Common::FS::UserPath::DumpDir), *romfs_title_id);
+    const auto dump_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::DumpDir);
+    const auto romfs_dir = fmt::format("{:016X}/romfs", *romfs_title_id);
+
+    const auto path = Common::FS::PathToUTF8String(dump_dir / romfs_dir);
 
     FileSys::VirtualFile romfs;
 
@@ -1978,24 +1970,29 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
 }
 
 void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
-    QString path;
+    std::filesystem::path fs_path;
     if (directory == QStringLiteral("SDMC")) {
-        path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir) +
-                                      "Nintendo/Contents/registered");
+        fs_path =
+            Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "Nintendo/Contents/registered";
     } else if (directory == QStringLiteral("UserNAND")) {
-        path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                                      "user/Contents/registered");
+        fs_path =
+            Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "user/Contents/registered";
     } else if (directory == QStringLiteral("SysNAND")) {
-        path = QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::NANDDir) +
-                                      "system/Contents/registered");
+        fs_path =
+            Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir) / "system/Contents/registered";
     } else {
-        path = directory;
+        fs_path = directory.toStdString();
     }
-    if (!QFileInfo::exists(path)) {
-        QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!"));
+
+    const auto qt_path = QString::fromStdString(Common::FS::PathToUTF8String(fs_path));
+
+    if (!Common::FS::IsDir(fs_path)) {
+        QMessageBox::critical(this, tr("Error Opening %1").arg(qt_path),
+                              tr("Folder does not exist!"));
         return;
     }
-    QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+
+    QDesktopServices::openUrl(QUrl::fromLocalFile(qt_path));
 }
 
 void GMainWindow::OnGameListAddDirectory() {
@@ -2189,8 +2186,8 @@ void GMainWindow::OnMenuInstallToNAND() {
                                 : tr("%n file(s) failed to install\n", "", failed_files.size()));
 
     QMessageBox::information(this, tr("Install Results"), install_results);
-    Common::FS::DeleteDirRecursively(Common::FS::GetUserPath(Common::FS::UserPath::CacheDir) +
-                                     DIR_SEP + "game_list");
+    void(Common::FS::RemoveDirRecursively(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
+                                          "game_list"));
     game_list->PopulateAsync(UISettings::values.game_dirs);
     ui.action_Install_File_NAND->setEnabled(true);
 }
@@ -2706,7 +2703,7 @@ void GMainWindow::LoadAmiibo(const QString& filename) {
 
 void GMainWindow::OnOpenYuzuFolder() {
     QDesktopServices::openUrl(QUrl::fromLocalFile(
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::UserDir))));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::YuzuDir))));
 }
 
 void GMainWindow::OnAbout() {
@@ -2728,7 +2725,7 @@ void GMainWindow::OnCaptureScreenshot() {
 
     const u64 title_id = Core::System::GetInstance().CurrentProcess()->GetTitleID();
     const auto screenshot_path =
-        QString::fromStdString(Common::FS::GetUserPath(Common::FS::UserPath::ScreenshotsDir));
+        QString::fromStdString(Common::FS::GetYuzuPathString(Common::FS::YuzuPath::ScreenshotsDir));
     const auto date =
         QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_hh-mm-ss-zzz"));
     QString filename = QStringLiteral("%1%2_%3.png")
@@ -2757,23 +2754,26 @@ void GMainWindow::OnCaptureScreenshot() {
 
 // TODO: Written 2020-10-01: Remove per-game config migration code when it is irrelevant
 void GMainWindow::MigrateConfigFiles() {
-    const std::string& config_dir_str = Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir);
-    const QDir config_dir = QDir(QString::fromStdString(config_dir_str));
+    const auto config_dir_fs_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir);
+    const QDir config_dir =
+        QDir(QString::fromStdString(Common::FS::PathToUTF8String(config_dir_fs_path)));
     const QStringList config_dir_list = config_dir.entryList(QStringList(QStringLiteral("*.ini")));
 
-    Common::FS::CreateFullPath(fmt::format("{}custom" DIR_SEP, config_dir_str));
-    for (QStringList::const_iterator it = config_dir_list.constBegin();
-         it != config_dir_list.constEnd(); ++it) {
+    if (!Common::FS::CreateDirs(config_dir_fs_path / "custom")) {
+        LOG_ERROR(Frontend, "Failed to create new config file directory");
+    }
+
+    for (auto it = config_dir_list.constBegin(); it != config_dir_list.constEnd(); ++it) {
         const auto filename = it->toStdString();
         if (filename.find_first_not_of("0123456789abcdefACBDEF", 0) < 16) {
             continue;
         }
-        const auto origin = fmt::format("{}{}", config_dir_str, filename);
-        const auto destination = fmt::format("{}custom" DIR_SEP "{}", config_dir_str, filename);
+        const auto origin = config_dir_fs_path / filename;
+        const auto destination = config_dir_fs_path / "custom" / filename;
         LOG_INFO(Frontend, "Migrating config file from {} to {}", origin, destination);
-        if (!Common::FS::Rename(origin, destination)) {
+        if (!Common::FS::RenameFile(origin, destination)) {
             // Delete the old config file if one already exists in the new location.
-            Common::FS::Delete(origin);
+            void(Common::FS::RemoveFile(origin));
         }
     }
 }
@@ -2965,18 +2965,16 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
         if (res == QMessageBox::Cancel)
             return;
 
-        Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
-                           "prod.keys_autogenerated");
-        Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
-                           "console.keys_autogenerated");
-        Common::FS::Delete(Common::FS::GetUserPath(Common::FS::UserPath::KeysDir) +
-                           "title.keys_autogenerated");
+        const auto keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir);
+
+        void(Common::FS::RemoveFile(keys_dir / "prod.keys_autogenerated"));
+        void(Common::FS::RemoveFile(keys_dir / "console.keys_autogenerated"));
+        void(Common::FS::RemoveFile(keys_dir / "title.keys_autogenerated"));
     }
 
     Core::Crypto::KeyManager& keys = Core::Crypto::KeyManager::Instance();
     if (keys.BaseDeriveNecessary()) {
-        Core::Crypto::PartitionDataManager pdm{vfs->OpenDirectory(
-            Common::FS::GetUserPath(Common::FS::UserPath::SysDataDir), FileSys::Mode::Read)};
+        Core::Crypto::PartitionDataManager pdm{vfs->OpenDirectory("", FileSys::Mode::Read)};
 
         const auto function = [this, &keys, &pdm] {
             keys.PopulateFromPartitionData(pdm);
@@ -3289,12 +3287,17 @@ int main(int argc, char* argv[]) {
     QCoreApplication::setOrganizationName(QStringLiteral("yuzu team"));
     QCoreApplication::setApplicationName(QStringLiteral("yuzu"));
 
+#ifdef _WIN32
+    // Increases the maximum open file limit to 4096
+    _setmaxstdio(4096);
+#endif
+
 #ifdef __APPLE__
     // If you start a bundle (binary) on OSX without the Terminal, the working directory is "/".
     // But since we require the working directory to be the executable path for the location of
     // the user folder in the Qt Frontend, we need to cd into that working directory
-    const std::string bin_path = Common::FS::GetBundleDirectory() + DIR_SEP + "..";
-    chdir(bin_path.c_str());
+    const auto bin_path = Common::FS::GetBundleDirectory() / "..";
+    chdir(Common::FS::PathToUTF8String(bin_path).c_str());
 #endif
 
 #ifdef __linux__
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index 38d896d657..a2ab69cdd6 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -16,7 +16,9 @@
 #endif
 
 #include <inih/cpp/INIReader.h>
-#include "common/file_util.h"
+#include "common/fs/file.h"
+#include "common/fs/fs.h"
+#include "common/fs/path_util.h"
 #include "common/logging/log.h"
 #include "common/param_package.h"
 #include "common/settings.h"
@@ -30,8 +32,8 @@ namespace FS = Common::FS;
 
 Config::Config() {
     // TODO: Don't hardcode the path; let the frontend decide where to put the config files.
-    sdl2_config_loc = FS::GetUserPath(FS::UserPath::ConfigDir) + "sdl2-config.ini";
-    sdl2_config = std::make_unique<INIReader>(sdl2_config_loc);
+    sdl2_config_loc = FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "sdl2-config.ini";
+    sdl2_config = std::make_unique<INIReader>(FS::PathToUTF8String(sdl2_config_loc));
 
     Reload();
 }
@@ -39,20 +41,23 @@ Config::Config() {
 Config::~Config() = default;
 
 bool Config::LoadINI(const std::string& default_contents, bool retry) {
-    const std::string& location = this->sdl2_config_loc;
+    const auto config_loc_str = FS::PathToUTF8String(sdl2_config_loc);
     if (sdl2_config->ParseError() < 0) {
         if (retry) {
-            LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location);
-            FS::CreateFullPath(location);
-            FS::WriteStringToFile(true, location, default_contents);
-            sdl2_config = std::make_unique<INIReader>(location); // Reopen file
+            LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...",
+                        config_loc_str);
+
+            void(FS::CreateParentDir(sdl2_config_loc));
+            void(FS::WriteStringToFile(sdl2_config_loc, FS::FileType::TextFile, default_contents));
+
+            sdl2_config = std::make_unique<INIReader>(config_loc_str);
 
             return LoadINI(default_contents, false);
         }
         LOG_ERROR(Config, "Failed.");
         return false;
     }
-    LOG_INFO(Config, "Successfully loaded {}", location);
+    LOG_INFO(Config, "Successfully loaded {}", config_loc_str);
     return true;
 }
 
@@ -327,18 +332,18 @@ void Config::ReadValues() {
     // Data Storage
     Settings::values.use_virtual_sd =
         sdl2_config->GetBoolean("Data Storage", "use_virtual_sd", true);
-    FS::GetUserPath(
-        FS::UserPath::NANDDir,
-        sdl2_config->Get("Data Storage", "nand_directory", FS::GetUserPath(FS::UserPath::NANDDir)));
-    FS::GetUserPath(
-        FS::UserPath::SDMCDir,
-        sdl2_config->Get("Data Storage", "sdmc_directory", FS::GetUserPath(FS::UserPath::SDMCDir)));
-    FS::GetUserPath(
-        FS::UserPath::LoadDir,
-        sdl2_config->Get("Data Storage", "load_directory", FS::GetUserPath(FS::UserPath::LoadDir)));
-    FS::GetUserPath(
-        FS::UserPath::DumpDir,
-        sdl2_config->Get("Data Storage", "dump_directory", FS::GetUserPath(FS::UserPath::DumpDir)));
+    FS::SetYuzuPath(FS::YuzuPath::NANDDir,
+                    sdl2_config->Get("Data Storage", "nand_directory",
+                                     FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
+    FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
+                    sdl2_config->Get("Data Storage", "sdmc_directory",
+                                     FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
+    FS::SetYuzuPath(FS::YuzuPath::LoadDir,
+                    sdl2_config->Get("Data Storage", "load_directory",
+                                     FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
+    FS::SetYuzuPath(FS::YuzuPath::DumpDir,
+                    sdl2_config->Get("Data Storage", "dump_directory",
+                                     FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
     Settings::values.gamecard_inserted =
         sdl2_config->GetBoolean("Data Storage", "gamecard_inserted", false);
     Settings::values.gamecard_current_game =
diff --git a/src/yuzu_cmd/config.h b/src/yuzu_cmd/config.h
index abc90f642a..8071992784 100644
--- a/src/yuzu_cmd/config.h
+++ b/src/yuzu_cmd/config.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <filesystem>
 #include <memory>
 #include <string>
 
@@ -11,7 +12,7 @@ class INIReader;
 
 class Config {
     std::unique_ptr<INIReader> sdl2_config;
-    std::string sdl2_config_loc;
+    std::filesystem::path sdl2_config_loc;
 
     bool LoadINI(const std::string& default_contents = "", bool retry = true);
     void ReadValues();
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index e2812ca616..584967f5c9 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -10,9 +10,10 @@
 
 #include <fmt/ostream.h>
 
-#include "common/common_paths.h"
 #include "common/detached_tasks.h"
-#include "common/file_util.h"
+#include "common/fs/fs.h"
+#include "common/fs/fs_paths.h"
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/filter.h"
 #include "common/logging/log.h"
@@ -82,9 +83,9 @@ static void InitializeLogging() {
 
     Log::AddBackend(std::make_unique<Log::ColorConsoleBackend>());
 
-    const std::string& log_dir = FS::GetUserPath(FS::UserPath::LogDir);
-    FS::CreateFullPath(log_dir);
-    Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir + LOG_FILE));
+    const auto& log_dir = FS::GetYuzuPath(FS::YuzuPath::LogDir);
+    void(FS::CreateDir(log_dir));
+    Log::AddBackend(std::make_unique<Log::FileBackend>(log_dir / LOG_FILE));
 #ifdef _WIN32
     Log::AddBackend(std::make_unique<Log::DebuggerBackend>());
 #endif