Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
290 changes: 274 additions & 16 deletions Utilities/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ static fs::error to_error(DWORD e)
#include <sys/statvfs.h>
#include <sys/file.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <dirent.h>
#include <fcntl.h>
#include <libgen.h>
Expand All @@ -159,12 +160,19 @@ static fs::error to_error(DWORD e)
#include <copyfile.h>
#include <mach-o/dyld.h>
#include <limits.h>
#include <sys/disk.h>
#include <sys/param.h>
#include <sys/mount.h>
#elif defined(__linux__) || defined(__sun)
#include <sys/sendfile.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <linux/fs.h>
#else
#include <fstream>
#include <sys/disk.h>
#include <sys/param.h>
#include <sys/mount.h>
#endif

static fs::error to_error(int e)
Expand All @@ -185,6 +193,91 @@ static fs::error to_error(int e)
}
}

#if defined(__linux__)
// Major numbers of the block devices an optical disc can be accessed through (see "Documentation/admin-guide/devices.txt")
constexpr unsigned int s_scsi_cdrom_major = 11; // Optical drive (e.g. "/dev/sr0")
constexpr unsigned int s_loopback_major = 7; // Disc image attached with "mount"/"losetup" (e.g. "/dev/loop0")
#endif

// Check whether the provided device node is an optical drive (e.g. a Blu-Ray Disc drive) or a mounted disc image
static bool is_optical_device_node(const struct ::stat& file_info)
{
#if defined(__linux__)
// An optical drive is exposed as a block device using the SCSI CD-ROM major number (e.g. "/dev/sr0", "/dev/scd0" or the "/dev/cdrom" link).
// A disc image attached through "mount -o loop" (or "losetup") is exposed as a loopback block device instead (e.g. "/dev/loop0"):
// it is the counterpart of an ISO file mounted on Windows, where the resulting virtual drive is reported as DRIVE_CDROM
if (!S_ISBLK(file_info.st_mode))
{
return false;
}

const unsigned int device_major = major(file_info.st_rdev);

return device_major == s_scsi_cdrom_major || device_major == s_loopback_major;
#elif defined(__APPLE__)
// The raw (unbuffered) device is a character device (e.g. "/dev/rdisk2").
// NOTE: there is no cheap way to tell an optical drive from any other raw disk here, so any raw disk node is accepted
return S_ISCHR(file_info.st_mode);
#else
// On the BSDs an optical drive is a character device (e.g. "/dev/cd0")
return S_ISCHR(file_info.st_mode) || S_ISBLK(file_info.st_mode);
#endif
}

// Check whether the path is the root of a mounted filesystem (e.g. the mount point of a disc): "st_dev" changes when crossing a mount point
[[maybe_unused]] static bool is_mount_point(const std::string& path, const struct ::stat& file_info)
{
// A file is never a mount point: discard it without any syscall (the "stat()" below would fail with ENOTDIR anyway)
if (!S_ISDIR(file_info.st_mode))
{
return false;
}

struct ::stat parent_info;

if (::stat((path + "/..").c_str(), &parent_info) != 0)
{
return false;
}

return parent_info.st_dev != file_info.st_dev;
}

// Retrieve the size of a raw device: "fstat()" reports no size (0) on such a device
static u64 get_raw_device_size(int fd)
{
#if defined(__linux__)
if (u64 size = 0; ::ioctl(fd, BLKGETSIZE64, &size) == 0)
{
return size;
}
#elif defined(__APPLE__)
u64 block_count = 0;
u32 block_size = 0;

if (::ioctl(fd, DKIOCGETBLOCKCOUNT, &block_count) == 0 && ::ioctl(fd, DKIOCGETBLOCKSIZE, &block_size) == 0)
{
return block_count * block_size;
Comment thread
digant73 marked this conversation as resolved.
}
#elif defined(__FreeBSD__) || defined(__DragonFly__)
if (off_t media_size = 0; ::ioctl(fd, DIOCGMEDIASIZE, &media_size) == 0)
{
return static_cast<u64>(media_size);
}
#endif

// Fallback: seek to the end of the device (restoring the current position afterwards)
const off_t old_pos = ::lseek(fd, 0, SEEK_CUR);
const off_t end_pos = ::lseek(fd, 0, SEEK_END);

if (old_pos >= 0)
{
::lseek(fd, old_pos, SEEK_SET);
}

return end_pos > 0 ? static_cast<u64>(end_pos) : 0;
}

#endif

static std::string path_append(std::string_view path, std::string_view more)
Expand Down Expand Up @@ -577,20 +670,20 @@ namespace fs

u64 size() override
{
if (!m_raw_device)
if (m_raw_device)
{
// NOTE: this can fail if we access a mounted empty drive (e.g. after unmounting an iso).
LARGE_INTEGER size;
// For a raw device, we need to use DeviceIoControl.
DISK_GEOMETRY_EX geometry;

ensure(GetFileSizeEx(m_handle, &size)); // "file::size"
return size.QuadPart;
ensure(DeviceIoControl(m_handle, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, nullptr, 0, &geometry, sizeof(geometry), nullptr, nullptr));
return geometry.DiskSize.QuadPart;
}

// For a raw device, we need to use DeviceIoControl.
DISK_GEOMETRY_EX geometry;
// NOTE: this can fail if we access a mounted empty drive (e.g. after unmounting an iso).
LARGE_INTEGER size;

ensure(DeviceIoControl(m_handle, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, nullptr, 0, &geometry, sizeof(geometry), nullptr, nullptr));
return geometry.DiskSize.QuadPart;
ensure(GetFileSizeEx(m_handle, &size)); // "file::size"
return size.QuadPart;
}

native_handle get_handle() override
Expand Down Expand Up @@ -629,10 +722,11 @@ namespace fs
class unix_file final : public file_base
{
int m_fd;
bool m_raw_device;

public:
unix_file(int fd)
: m_fd(fd)
unix_file(int fd, bool raw_device = false)
: m_fd(fd), m_raw_device(raw_device)
{
}

Expand All @@ -651,8 +745,8 @@ namespace fs

stat_t info {};
info.is_directory = S_ISDIR(file_info.st_mode);
info.is_writable = file_info.st_mode & 0200; // HACK: approximation
info.size = file_info.st_size;
info.is_writable = !m_raw_device && (file_info.st_mode & 0200); // HACK: approximation
info.size = m_raw_device ? get_raw_device_size(m_fd) : file_info.st_size; // A raw device reports no size through "fstat()"
info.atime = file_info.st_atime;
info.mtime = file_info.st_mtime;
info.ctime = info.mtime;
Expand Down Expand Up @@ -758,6 +852,12 @@ namespace fs

u64 size() override
{
if (m_raw_device)
{
// For a raw device, we need to use an ioctl ("fstat()" would always report a null size)
return get_raw_device_size(m_fd);
}

struct ::stat file_info;
ensure(::fstat(m_fd, &file_info) == 0); // "file::size"

Expand Down Expand Up @@ -1113,15 +1213,31 @@ bool fs::is_symlink(const std::string& path)
return true;
}

bool fs::is_optical_raw_device([[maybe_unused]] const std::string& path)
bool fs::is_optical_raw_device(const std::string& path)
{
#ifdef _WIN32
if (path.starts_with("\\\\.\\"))
{
return true;
}
#endif

return false;
#else
// Skip a useless check if the path cannot point to a device node (device nodes always live in "/dev")
if (!path.starts_with("/dev/"))
{
return false;
}

struct ::stat file_info;

if (::stat(path.c_str(), &file_info) != 0)
{
return false;
}

return is_optical_device_node(file_info);
#endif
}

bool fs::get_optical_raw_device(const std::string& path, std::string* raw_device)
Expand Down Expand Up @@ -1158,8 +1274,132 @@ bool fs::get_optical_raw_device(const std::string& path, std::string* raw_device

return true;
}

return false;
#elif defined(__linux__)
// Here the path points to a mounted optical disc (e.g. "/media/user/PS3_DISC") or to a mounted disc image
// (e.g. "mount -o loop game.iso /mnt/game"), so retrieve the device backing the filesystem it belongs to
struct ::stat file_info;

if (::stat(path.c_str(), &file_info) != 0)
{
return false;
}

// "st_dev" of anything stored on a filesystem backed by a block device matches "st_rdev" of the device itself
const dev_t device_id = file_info.st_dev;
const unsigned int device_major = major(device_id);

// Discard at once anything which is not backed by an optical drive or by a loopback device, so that the checks
// below (each one costing at least a syscall) only run on an actual candidate
if (device_major != s_scsi_cdrom_major && device_major != s_loopback_major)
{
return false;
}

// Skip a useless check to detect an optical raw device if the path is not the mount point of the disc, it means we are
// navigating on subfolders (on Windows, in the same way, only the drive root is accepted)
if (!is_mount_point(path, file_info))
{
return false;
}

const unsigned int device_minor = minor(device_id);

// Retrieve the device name from sysfs (e.g. "/sys/dev/block/11:0" -> "../../devices/[...]/block/sr0")
std::string device_path;
char link_target[1024]; // NOTE: "readlink()" never null terminates, so the buffer is used through the returned length only

if (const ssize_t len = ::readlink(("/sys/dev/block/" + std::to_string(device_major) + ":" + std::to_string(device_minor)).c_str(), link_target, sizeof(link_target)); len > 0)
{
const std::string_view target{link_target, static_cast<usz>(len)};

device_path = "/dev/";
device_path += target.substr(target.find_last_of('/') + 1);
}
else if (device_major == s_scsi_cdrom_major)
{
// Fallback (sysfs not available): the minor number matches the index of the SCSI CD-ROM device
device_path = "/dev/sr" + std::to_string(device_minor);
}
else
{
return false;
}

// Ensure the resolved node really points to the same optical device
struct ::stat device_info;

if (::stat(device_path.c_str(), &device_info) != 0 || device_info.st_rdev != device_id || !is_optical_device_node(device_info))
{
return false;
}

if (raw_device)
{
*raw_device = device_path;
}

return true;
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
// Here the path points to a mounted optical disc or to a mounted disc image (e.g. attached with "hdiutil"/"mdconfig"),
// so retrieve the device backing the filesystem it belongs to
struct ::stat file_info;

if (::stat(path.c_str(), &file_info) != 0)
{
return false;
}

// Skip a useless check to detect an optical raw device if the path is not the mount point of the disc, it means we are
// navigating on subfolders (on Windows, in the same way, only the drive root is accepted). There is no cheap way to
// discard a device here (unlike the major number on Linux), so this is the filter which must come first: it rejects
// any file without a single syscall and any other folder with just one more
if (!is_mount_point(path, file_info))
{
return false;
}

struct ::statfs mount_info;

if (::statfs(path.c_str(), &mount_info) != 0)
{
return false;
}

std::string device_path = mount_info.f_mntfromname; // e.g. "/dev/disk2"

if (!device_path.starts_with("/dev/"))
{
return false;
}

#ifdef __APPLE__
// Use the raw (unbuffered) device (e.g. "/dev/disk2" -> "/dev/rdisk2")
if (!device_path.starts_with("/dev/r"))
{
device_path.insert(5, 1, 'r');
}
#endif

// Ensure the resolved node really points to a raw device
struct ::stat device_info;

if (::stat(device_path.c_str(), &device_info) != 0 || !is_optical_device_node(device_info))
{
return false;
}

if (raw_device)
{
*raw_device = device_path;
}

return true;
#else
// Not supported on this platform
return false;
#endif
}

bool fs::statfs(const std::string& path, fs::device_stat& info)
Expand Down Expand Up @@ -1790,6 +2030,9 @@ fs::file::file(const std::string& path, bs_t<open_mode> mode)

m_file = std::make_unique<windows_file>(handle);
#else
// An optical raw device can only be read: any write related flag makes the opening fail (handled as any other error)
const bool raw_device = is_optical_raw_device(path);

int flags = O_CLOEXEC; // Ensures all files are closed on execl for auto updater

if (mode & fs::read && mode & fs::write) flags |= O_RDWR;
Expand All @@ -1805,7 +2048,7 @@ fs::file::file(const std::string& path, bs_t<open_mode> mode)

if (mode & fs::write && mode & fs::unread)
{
if (!(mode & (fs::excl + fs::lock)) && mode & fs::trunc)
if (!raw_device && !(mode & (fs::excl + fs::lock)) && mode & fs::trunc)
{
// Alternative to truncation for "unread" flag (TODO)
if (mode & fs::create)
Expand Down Expand Up @@ -1838,6 +2081,21 @@ fs::file::file(const std::string& path, bs_t<open_mode> mode)
ensure(::ftruncate(fd, 0) == 0);
}

// If path points to an optical raw device, complete the file opening
if (raw_device)
{
// Try to retrieve the size of the content. If it fails, no disc is probably mounted so abort the file opening
if (!get_raw_device_size(fd))
{
::close(fd);
g_tls_error = fs::error::noent;
return;
}

m_file = std::make_unique<unix_file>(fd, true);
return;
}

m_file = std::make_unique<unix_file>(fd);

if (mode & fs::isfile && !(mode & fs::write) && get_stat().is_directory)
Expand Down
Loading
Loading