rom-properties/src/libromdata/disc/IsoPartition.cpp
David Korth 3f1f92b18a [libromdata] IsoPartition: Add basic support for Joliet directories.
If a Supplementary Volume Descriptor is present, check for the Joliet
UCS-2 escape sequences. If found, use the SVD root directory.

Filenames in the SVD root directory are encoded in UCS-2 (big-endian).
The filename length is in bytes, not code points.

NOTE: Currently using a very cheap hack to convert UCS-2 to cp1252,
or more accurately, ISO-8859-1. It's good enough for our purposes
for now. (specifically, getting the icon from AUTORUN.INF.)

TODO: Better character set conversion.
2025-06-08 14:46:56 -04:00

1077 lines
29 KiB
C++

/***************************************************************************
* ROM Properties Page shell extension. (libromdata) *
* IsoPartition.cpp: ISO-9660 partition reader. *
* *
* Copyright (c) 2016-2024 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
#include "stdafx.h"
#include "IsoPartition.hpp"
#include "iso_structs.h"
// Other rom-properties libraries
using namespace LibRpBase;
using namespace LibRpFile;
using namespace LibRpText;
// C++ STL classes
using std::string;
using std::unordered_map;
// TODO: HSFS/CDI support?
namespace LibRomData {
class IsoPartitionPrivate
{
public:
IsoPartitionPrivate(IsoPartition *q, off64_t partition_offset, int iso_start_offset);
~IsoPartitionPrivate();
private:
RP_DISABLE_COPY(IsoPartitionPrivate)
protected:
IsoPartition *const q_ptr;
public:
// Partition start offset (in bytes)
off64_t partition_offset;
off64_t partition_size; // Calculated partition size
// ISO volume descriptors
ISO_Primary_Volume_Descriptor pvd, svd;
enum class JolietSVDType : uint8_t {
None = 0,
UCS2_Level1 = 1, // NOTE: UCS-2 BE
UCS2_Level2 = 2, // NOTE: UCS-2 BE
UCS2_Level3 = 3, // NOTE: UCS-2 BE
};
JolietSVDType jolietSVDType;
// Directories
// - Key: Directory name, WITHOUT leading slash. (Root == empty string) [cp1252]
// - Value: Directory entries.
// NOTE: Directory entries are variable-length, so this
// is a byte array, not an ISO_DirEntry array.
typedef rp::uvector<uint8_t> DirData_t;
unordered_map<string, DirData_t> dir_data;
// ISO start offset (in blocks)
// -1 == unknown
int iso_start_offset;
// IFst::Dir* reference counter
int fstDirCount;
/**
* Is this character a slash or backslash?
* @return True if it is; false if it isn't.
*/
static inline bool is_slash(char c)
{
return (c == '/') || (c == '\\');
}
private:
/**
* Find the last slash or backslash in a path.
* (Internal function!)
* @param path Path
* @param size Size of path
* @return Last slash or backslash, or nullptr if not found.
*/
static inline const char *findLastSlash(const char *path, size_t size)
{
const char *p = path + size - 1;
for (; size > 0; size--, p--) {
if (is_slash(*p)) {
return p;
}
}
return nullptr;
}
public:
/**
* Find the last slash or backslash in a path.
* @param path Path
* @return Last slash or backslash, or nullptr if not found.
*/
static inline const char *findLastSlash(const char *path)
{
return findLastSlash(path, strlen(path));
}
/**
* Find the last slash or backslash in a path.
* @param path Path
* @return Last slash or backslash, or nullptr if not found.
*/
static inline const char *findLastSlash(const string &path)
{
return findLastSlash(path.c_str(), path.size());
}
/**
* Sanitize an incoming path for ISO-9660 lookup.
*
* This function does the following:
* - Converts the path from UTF-8 to cp1252.
* - Removes leading and trailing slashes.
*
* @param path Path to sanitize
* @return Sanitized path (empty string for "/")
*/
static std::string sanitize_path(const char *path);
/**
* Look up a directory entry from a base filename and directory.
* @param pDir [in] Directory
* @param filename [in] Base filename [cp1252]
* @param bFindDir [in] True to find a subdirectory; false to find a file.
* @return ISO directory entry.
*/
const ISO_DirEntry *lookup_int(const DirData_t *pDir, const char *filename, bool bFindDir);
/**
* Get a directory.
* @param path [in] Pathname [cp1252] (For root, specify "" or "/".)
* @param pError [out] POSIX error code on error.
* @return Directory on success; nullptr on error.
*/
const DirData_t *getDirectory(const char *path, int *pError = nullptr);
/**
* Look up a directory entry from a filename.
* @param filename Filename [UTF-8]
* @return ISO directory entry
*/
const ISO_DirEntry *lookup(const char *filename);
/**
* Parse an ISO-9660 timestamp.
* @param isofiletime File timestamp
* @return Unix time
*/
static time_t parseTimestamp(const ISO_Dir_DateTime_t *isofiletime);
};
/** IsoPartitionPrivate **/
IsoPartitionPrivate::IsoPartitionPrivate(IsoPartition *q,
off64_t partition_offset, int iso_start_offset)
: q_ptr(q)
, partition_offset(partition_offset)
, partition_size(0)
, jolietSVDType(JolietSVDType::None)
, iso_start_offset(iso_start_offset)
, fstDirCount(0)
{
// Clear the Volume Descriptor structs.
memset(&pvd, 0, sizeof(pvd));
memset(&svd, 0, sizeof(svd));
if (!q->m_file) {
q->m_lastError = EIO;
return;
} else if (!q->m_file->isOpen()) {
q->m_lastError = q->m_file->lastError();
if (q->m_lastError == 0) {
q->m_lastError = EIO;
}
q->m_file.reset();
return;
}
// Calculated partition size.
partition_size = q->m_file->size() - partition_offset;
// Load the primary volume descriptor.
// TODO: Assuming this is the first one.
// Check for multiple?
size_t size = q->m_file->seekAndRead(partition_offset + ISO_PVD_ADDRESS_2048, &pvd, sizeof(pvd));
if (size != sizeof(pvd)) {
// Seek and/or read error.
q->m_file.reset();
return;
}
// Verify the signature and volume descriptor type.
if (pvd.header.type != ISO_VDT_PRIMARY || pvd.header.version != ISO_VD_VERSION ||
memcmp(pvd.header.identifier, ISO_VD_MAGIC, sizeof(pvd.header.identifier)) != 0)
{
// Invalid volume descriptor.
q->m_file.reset();
return;
}
// Attempt to load the Supplementary Volume Descriptor.
// TODO: Keep loading VDs until we reach 0xFF?
size = q->m_file->seekAndRead(partition_offset + ISO_SVD_ADDRESS_2048, &svd, sizeof(svd));
// Verify the signature and volume descriptor type.
if (size == sizeof(svd) &&
svd.header.type == ISO_VDT_SUPPLEMENTARY && svd.header.version == ISO_VD_VERSION &&
!memcmp(svd.header.identifier, ISO_VD_MAGIC, sizeof(svd.header.identifier)))
{
// This is a supplementary volume descriptor.
// Check the escape sequences.
// Escape sequence format: '%', '/', x
const char *const p_end = &svd.svd_escape_sequences[sizeof(svd.svd_escape_sequences)-3];
for (const char *p = svd.svd_escape_sequences; p < p_end && *p != '\0'; p++) {
if (p[0] != '%' || p[1] != '/') {
continue;
}
// Check if this is a valid UCS-2 level seqeunce.
// NOTE: Using the highest level specified.
switch (p[2]) {
case '@':
if (jolietSVDType < JolietSVDType::UCS2_Level1) {
jolietSVDType = JolietSVDType::UCS2_Level1;
}
break;
case 'C':
if (jolietSVDType < JolietSVDType::UCS2_Level2) {
jolietSVDType = JolietSVDType::UCS2_Level2;
}
break;
case 'E':
if (jolietSVDType < JolietSVDType::UCS2_Level3) {
jolietSVDType = JolietSVDType::UCS2_Level3;
}
break;
default:
break;
}
}
}
// Load the root directory.
getDirectory("/");
}
IsoPartitionPrivate::~IsoPartitionPrivate()
{
assert(fstDirCount == 0);
}
/**
* Sanitize an incoming path for ISO-9660 lookup.
*
* This function does the following:
* - Converts the path from UTF-8 to cp1252.
* - Removes leading and trailing slashes.
*
* @param path Path to sanitize
* @return Sanitized path (empty string for "/")
*/
std::string IsoPartitionPrivate::sanitize_path(const char *path)
{
// Remove leading slashes.
while (is_slash(*path)) {
path++;
}
if (*path == '\0') {
// Nothing but slashes?
return {};
}
// Convert to cp1252, then remove trailing slashes.
string s_path = utf8_to_cp1252(path, -1);
size_t s_path_len = s_path.size();
while (s_path_len > 0 && is_slash(s_path[s_path_len-1])) {
s_path_len--;
}
s_path.resize(s_path_len);
return s_path;
}
/**
* Look up a directory entry from a base filename and directory.
* @param pDir [in] Directory
* @param filename [in] Base filename [cp1252]
* @param bFindDir [in] True to find a subdirectory; false to find a file.
* @return ISO directory entry.
*/
const ISO_DirEntry *IsoPartitionPrivate::lookup_int(const DirData_t *pDir, const char *filename, bool bFindDir)
{
// Find the file in the directory.
// NOTE: Filenames are case-insensitive.
// NOTE: File might have a ";1" suffix.
int err = ENOENT;
const unsigned int filename_len = static_cast<unsigned int>(strlen(filename));
const ISO_DirEntry *dirEntry_found = nullptr;
const uint8_t *p = pDir->data();
const uint8_t *const p_end = p + pDir->size();
// Temporary buffer for converting Joliet UCS-2 filenames to cp1252.
char joliet_cp1252_buf[128];
while ((p + sizeof(ISO_DirEntry)) < p_end) {
const ISO_DirEntry *dirEntry = reinterpret_cast<const ISO_DirEntry*>(p);
if (dirEntry->entry_length == 0) {
// Directory entries cannot span multiple sectors in
// multi-sector directories, so if needed, the rest
// of the sector is padded with 00.
// Find the next non-zero byte.
for (p++; p < p_end; p++) {
if (*p != '\0') {
// Found a non-zero byte.
break;
}
}
if (p >= p_end) {
// No more non-zero bytes.
break;
}
continue;
} else if (dirEntry->entry_length < sizeof(*dirEntry)) {
// Invalid directory entry?
break;
}
const char *entry_filename = reinterpret_cast<const char*>(p) + sizeof(*dirEntry);
if (entry_filename + dirEntry->filename_length > reinterpret_cast<const char*>(p_end)) {
// Filename is out of bounds.
break;
}
// Skip subdirectories with names "\x00" and "\x01".
// These are Joliet "special directory identifiers".
// TODO: Joliet subdirectory support?
if ((dirEntry->flags & ISO_FLAG_DIRECTORY) && dirEntry->filename_length == 1) {
if (static_cast<uint8_t>(entry_filename[0]) <= 0x01) {
// Skip this filename.
p += dirEntry->entry_length;
continue;
}
}
// If using Joliet, the filename is encoded as UCS-2 (UTF-16).
// Use a quick-and-dirty (and not necessarily accurate) conversion to cp1252.
// FIXME: Proper conversion?
uint8_t dirEntry_filename_len = dirEntry->filename_length;
if (jolietSVDType > JolietSVDType::None) {
// dirEntry_filename_len is in bytes, which means it's double
// the number of UCS-2 code points.
// NOTE: UCS-2 *Big-Endian*.
dirEntry_filename_len /= 2;
unsigned int i = 0;
for (; i < dirEntry_filename_len; i++) {
joliet_cp1252_buf[i] = entry_filename[(i * 2) + 1];
}
joliet_cp1252_buf[i] = '\0';
entry_filename = joliet_cp1252_buf;
}
// Check the filename.
// 1990s and early 2000s CD-ROM games usually have
// ";1" filenames, so check for that first.
if (dirEntry_filename_len == filename_len + 2) {
// +2 length match.
// This might have ";1".
if (!strncasecmp(entry_filename, filename, filename_len)) {
// Check for ";1".
// TODO: Also allow other version numbers?
if (entry_filename[filename_len] == ';' &&
entry_filename[filename_len+1] == '1')
{
// Found it!
// Verify directory vs. file.
const bool isDir = !!(dirEntry->flags & ISO_FLAG_DIRECTORY);
if (isDir == bFindDir) {
// Directory attribute matches.
dirEntry_found = dirEntry;
} else {
// Not a match.
err = (isDir ? EISDIR : ENOTDIR);
}
break;
}
}
} else if (dirEntry_filename_len == filename_len) {
// Exact length match.
if (!strncasecmp(entry_filename, filename, filename_len)) {
// Found it!
// Verify directory vs. file.
const bool isDir = !!(dirEntry->flags & ISO_FLAG_DIRECTORY);
if (isDir == bFindDir) {
// Directory attribute matches.
dirEntry_found = dirEntry;
} else {
// Not a match.
err = (isDir ? EISDIR : ENOTDIR);
}
break;
}
}
// Next entry.
p += dirEntry->entry_length;
}
if (!dirEntry_found) {
RP_Q(IsoPartition);
q->m_lastError = err;
}
return dirEntry_found;
}
/**
* Get a directory.
* @param path [in] Pathname [cp1252] (For root, specify "" or "/".)
* @param pError [out] POSIX error code on error.
* @return Directory on success; nullptr on error.
*/
const IsoPartitionPrivate::DirData_t *IsoPartitionPrivate::getDirectory(const char *path, int *pError)
{
RP_Q(IsoPartition);
if (!path || !strcmp(path, "/")) {
// Root directory. Use "".
path = "";
}
// Check if this directory was already loaded.
auto iter = dir_data.find(path);
if (iter != dir_data.end()) {
// Directory is already loaded.
return &iter->second;
}
if (unlikely(!q->m_file)) {
// DiscReader isn't open.
q->m_lastError = EIO;
if (pError) {
*pError = EIO;
}
return nullptr;
} else if (unlikely(pvd.header.type != ISO_VDT_PRIMARY || pvd.header.version != ISO_VD_VERSION)) {
// PVD isn't loaded.
q->m_lastError = EIO;
if (pError) {
*pError = EIO;
}
return nullptr;
}
// Block size.
// Should be 2048, but other values are possible.
const unsigned int block_size = pvd.logical_block_size.he;
// Determine the directory size and address.
DirData_t dir;
off64_t dir_addr;
if (path[0] == '\0') {
// Loading the root directory.
// Check the root directory entry.
const ISO_DirEntry *const rootdir = (jolietSVDType > JolietSVDType::None)
? &svd.dir_entry_root
: &pvd.dir_entry_root;
if (rootdir->size.he > 16*1024*1024) {
// Root directory is too big.
q->m_lastError = EIO;
if (pError) {
*pError = EIO;
}
return nullptr;
}
if (iso_start_offset >= 0) {
// ISO start address was already determined.
if (rootdir->block.he < (static_cast<unsigned int>(iso_start_offset) + 2)) {
// Starting block is invalid.
q->m_lastError = EIO;
if (pError) {
*pError = EIO;
}
return nullptr;
}
} else {
// We didn't find the ISO start address yet.
// This might be a 2048-byte single-track image,
// in which case, we'll need to assume that the
// root directory starts at block 20.
// TODO: Better heuristics.
// TODO: Find the block that starts with "CD001" instead of this heuristic.
if (rootdir->block.he < 20) {
// Starting block is invalid.
q->m_lastError = EIO;
if (pError) {
*pError = EIO;
}
return nullptr;
}
iso_start_offset = static_cast<int>(rootdir->block.he - 20);
}
dir.resize(rootdir->size.he);
dir_addr = partition_offset + static_cast<off64_t>(rootdir->block.he - iso_start_offset) * block_size;
} else {
// Get the parent directory.
const DirData_t *pDir;
const char *const sl = findLastSlash(path);
if (!sl) {
// No slash. Parent is root.
pDir = getDirectory("");
} else {
// Found a slash.
const string s_parentDir(path, (sl - path));
path = sl + 1;
pDir = getDirectory(s_parentDir.c_str());
}
if (!pDir) {
// Can't find the parent directory.
// getDirectory() already set q->m_lastError().
return nullptr;
}
// Find this directory.
const ISO_DirEntry *const entry = lookup_int(pDir, path, true);
if (!entry) {
// Not found.
// lookup_int() already set q->m_lastError().
return nullptr;
} else if (!(entry->flags & ISO_FLAG_DIRECTORY)) {
// Entry found, but it's a directory.
q->m_lastError = ENOTDIR;
return nullptr;
}
dir.resize(entry->size.he);
dir_addr = partition_offset + static_cast<off64_t>(entry->block.he - iso_start_offset) * block_size;
}
// Load the directory.
// NOTE: Due to variable-length entries, we need to load
// the entire directory all at once.
size_t size = q->m_file->seekAndRead(dir_addr, dir.data(), dir.size());
if (size != dir.size()) {
// Seek and/or read error.
dir.clear();
q->m_lastError = q->m_file->lastError();
if (q->m_lastError == 0) {
q->m_lastError = EIO;
}
if (pError) {
*pError = q->m_lastError;
}
return nullptr;
}
// Directory loaded.
auto ins = dir_data.emplace(path, std::move(dir));
return &(ins.first->second);
}
/**
* Look up a directory entry from a filename.
* @param filename Filename [UTF-8]
* @return ISO directory entry
*/
const ISO_DirEntry *IsoPartitionPrivate::lookup(const char *filename)
{
assert(filename != nullptr);
assert(filename[0] != '\0');
// Sanitize the filename.
// If the return value is an empty string, that means root directory.
string s_filename = sanitize_path(filename);
// TODO: Which encoding?
// Assuming cp1252...
const DirData_t *pDir;
// Is this file in a subdirectory?
const char *const sl = findLastSlash(s_filename);
if (sl) {
// This file is in a subdirectory.
const string s_parentDir = s_filename.substr(0, sl - s_filename.c_str());
s_filename.assign(sl + 1);
pDir = getDirectory(s_parentDir.c_str());
} else {
// Not in a subdirectory.
// Parent directory is root.
pDir = getDirectory("");
}
if (!pDir) {
// Error getting the directory.
// getDirectory() has already set q->m_lastError.
return nullptr;
}
// Find the file in the directory.
return lookup_int(pDir, s_filename.c_str(), false);
}
/**
* Parse an ISO-9660 timestamp.
* @param isofiletime File timestamp
* @return Unix time
*/
time_t IsoPartitionPrivate::parseTimestamp(const ISO_Dir_DateTime_t *isofiletime)
{
// Convert to Unix time.
// NOTE: struct tm has some oddities:
// - tm_year: year - 1900
// - tm_mon: 0 == January
struct tm isotime;
isotime.tm_year = isofiletime->year;
isotime.tm_mon = isofiletime->month - 1;
isotime.tm_mday = isofiletime->day;
isotime.tm_hour = isofiletime->hour;
isotime.tm_min = isofiletime->minute;
isotime.tm_sec = isofiletime->second;
// tm_wday and tm_yday are output variables.
isotime.tm_wday = 0;
isotime.tm_yday = 0;
isotime.tm_isdst = 0;
// If conversion fails, this will return -1.
time_t unixtime = timegm(&isotime);
if (unixtime == -1) {
return unixtime;
}
// Adjust for the timezone offset.
// NOTE: Restricting to [-52, 52] as per the Linux kernel's isofs module.
if (-52 <= isofiletime->tz_offset && isofiletime->tz_offset <= 52) {
unixtime -= (static_cast<int>(isofiletime->tz_offset) * (15*60));
}
return unixtime;
}
/** IsoPartition **/
/**
* Construct an IsoPartition with the specified IDiscReader (or IRpFile).
*
* @param discReader IDiscReader (or IRpFile)
* @param partition_offset Partition start offset.
* @param iso_start_offset ISO start offset, in blocks. (If -1, uses heuristics.)
*/
IsoPartition::IsoPartition(const IRpFilePtr &discReader, off64_t partition_offset, int iso_start_offset)
: super(discReader)
, d_ptr(new IsoPartitionPrivate(this, partition_offset, iso_start_offset))
{ }
IsoPartition::~IsoPartition()
{
delete d_ptr;
}
/** IDiscReader **/
/**
* Read data from the file.
* @param ptr Output data buffer.
* @param size Amount of data to read, in bytes.
* @return Number of bytes read.
*/
size_t IsoPartition::read(void *ptr, size_t size)
{
assert(m_file != nullptr);
assert(m_file->isOpen());
if (!m_file || !m_file->isOpen()) {
m_lastError = EBADF;
return 0;
}
// GCN partitions are stored as-is.
// TODO: data_size checks?
return m_file->read(ptr, size);
}
/**
* Set the partition position.
* @param pos Partition position.
* @return 0 on success; -1 on error.
*/
int IsoPartition::seek(off64_t pos)
{
RP_D(IsoPartition);
assert(m_file != nullptr);
assert(m_file->isOpen());
if (!m_file || !m_file->isOpen()) {
m_lastError = EBADF;
return -1;
}
int ret = m_file->seek(d->partition_offset + pos);
if (ret != 0) {
m_lastError = m_file->lastError();
}
return ret;
}
/**
* Get the partition position.
* @return Partition position on success; -1 on error.
*/
off64_t IsoPartition::tell(void)
{
RP_D(IsoPartition);
assert(m_file != nullptr);
assert(m_file->isOpen());
if (!m_file || !m_file->isOpen()) {
m_lastError = EBADF;
return -1;
}
off64_t ret = m_file->tell() - d->partition_offset;
if (ret < 0) {
m_lastError = m_file->lastError();
}
return ret;
}
/**
* Get the data size.
* This size does not include the partition header,
* and it's adjusted to exclude hashes.
* @return Data size, or -1 on error.
*/
off64_t IsoPartition::size(void)
{
// TODO: Restrict partition size?
RP_D(const IsoPartition);
if (!m_file)
return -1;
return d->partition_size;
}
/** Device file functions **/
/** IPartition **/
/**
* Get the partition size.
* This size includes the partition header and hashes.
* @return Partition size, or -1 on error.
*/
off64_t IsoPartition::partition_size(void) const
{
// TODO: Restrict partition size?
RP_D(const IsoPartition);
if (!m_file)
return -1;
return d->partition_size;
}
/**
* Get the used partition size.
* This size includes the partition header and hashes,
* but does not include "empty" sectors.
* @return Used partition size, or -1 on error.
*/
off64_t IsoPartition::partition_size_used(void) const
{
// TODO: Implement for ISO?
// For now, just use partition_size().
return partition_size();
}
/** IsoPartition **/
/** IFst wrapper functions **/
/**
* Open a directory.
* @param path [in] Directory path
* @return IFst::Dir*, or nullptr on error.
*/
IFst::Dir *IsoPartition::opendir(const char *path)
{
RP_D(IsoPartition);
// Sanitize the path.
// If the return value is an empty string, that means root directory.
string s_path = d->sanitize_path(path);
const IsoPartitionPrivate::DirData_t *const pDir = d->getDirectory(s_path.c_str());
if (!pDir) {
// Path not found.
// TODO: Return an error code?
return nullptr;
}
// FIXME: Create an IsoFst class? Cannot pass `this` as IFst*.
IFst::Dir *const dirp = new IFst::Dir(nullptr, (void*)pDir);
d->fstDirCount++;
// Initialize the entry to this directory.
// readdir() will automatically seek to the next entry.
dirp->entry.extra = nullptr; // temporary filename storage
dirp->entry.ptnum = 0; // not used for ISO
dirp->entry.idx = 0;
dirp->entry.type = DT_UNKNOWN;
dirp->entry.name = nullptr;
// offset and size are not valid for directories.
dirp->entry.offset = 0;
dirp->entry.size = 0;
// Return the IFst::Dir*.
return dirp;
}
/**
* Read a directory entry.
* @param dirp FstDir pointer
* @return IFst::DirEnt*, or nullptr if end of directory or on error.
* (TODO: Add lastError()?)
*/
const IFst::DirEnt *IsoPartition::readdir(IFst::Dir *dirp)
{
assert(dirp != nullptr);
//assert(dirp->parent == this);
if (!dirp /*|| dirp->parent != this*/) {
// No directory pointer, or the dirp
// doesn't belong to this IFst.
return nullptr;
}
const IsoPartitionPrivate::DirData_t *const pDir =
reinterpret_cast<const IsoPartitionPrivate::DirData_t*>(dirp->dir_idx);
const uint8_t *p = pDir->data();
const uint8_t *const p_end = p + pDir->size();
p += dirp->entry.idx;
// NOTE: Using a loop in order to skip files that aren't really files.
const ISO_DirEntry *dirEntry = nullptr;
const char *entry_filename = nullptr;
while (p < p_end) {
dirEntry = reinterpret_cast<const ISO_DirEntry*>(p);
if (dirEntry->entry_length == 0) {
// Directory entries cannot span multiple sectors in
// multi-sector directories, so if needed, the rest
// of the sector is padded with 00.
// Find the next non-zero byte.
for (p++; p < p_end; p++) {
if (*p != '\0') {
// Found a non-zero byte.
dirp->entry.idx = static_cast<int>(p - pDir->data());
break;
}
}
if (p >= p_end) {
// No more non-zero bytes.
dirp->entry.idx = static_cast<int>(pDir->size());
return nullptr;
}
continue;
} else if (dirEntry->entry_length < sizeof(*dirEntry)) {
// Invalid directory entry?
return nullptr;
}
entry_filename = reinterpret_cast<const char*>(p) + sizeof(*dirEntry);
if (entry_filename + dirEntry->filename_length > reinterpret_cast<const char*>(p_end)) {
// Filename is out of bounds.
return nullptr;
}
// Skip subdirectories with names "\x00" and "\x01".
// These are Joliet "special directory identifiers".
// TODO: Joliet subdirectory support?
if ((dirEntry->flags & ISO_FLAG_DIRECTORY) && dirEntry->filename_length == 1) {
if (static_cast<uint8_t>(entry_filename[0]) <= 0x01) {
// Skip this filename.
dirp->entry.idx += dirEntry->entry_length;
p += dirEntry->entry_length;
continue;
}
}
// Found a valid file.
break;
}
if (!dirEntry) {
// Could not find a valid file. (End of directory?)
return nullptr;
}
const bool isDir = !!(dirEntry->flags & ISO_FLAG_DIRECTORY);
if (isDir) {
// Technically, offset/size are valid for directories on ISO-9660,
// but we're going to set them to 0.
dirp->entry.type = DT_DIR;
dirp->entry.offset = 0;
dirp->entry.size = 0;
} else {
RP_D(IsoPartition);
const unsigned int block_size = d->pvd.logical_block_size.he;
dirp->entry.type = DT_REG;
dirp->entry.offset = static_cast<off64_t>(dirEntry->block.he) * block_size;
dirp->entry.size = dirEntry->size.he;
}
// NOTE: Need to copy the filename in order to have NULL-termination.
// TODO: Remove ";1" from the filename, if present?
char *extra = static_cast<char*>(dirp->entry.extra);
delete[] extra;
// If using Joliet, the filename is encoded as UCS-2 (UTF-16).
// Use a quick-and-dirty (and not necessarily accurate) conversion to cp1252.
// FIXME: Proper conversion?
// TODO: Convert to UTF-8 for readdir()?
RP_D(IsoPartition);
uint8_t dirEntry_filename_len = dirEntry->filename_length;
if (d->jolietSVDType > IsoPartitionPrivate::JolietSVDType::None) {
// dirEntry_filename_len is in bytes, which means it's double
// the number of UCS-2 code points.
// NOTE: UCS-2 *Big-Endian*.
dirEntry_filename_len /= 2;
extra = new char[dirEntry_filename_len + 1];
unsigned int i = 0;
for (; i < dirEntry_filename_len; i++) {
extra[i] = entry_filename[(i * 2) + 1];
}
extra[i] = '\0';
} else {
// TODO: Convert from cp1252 to UTF-8 for readdir()?
extra = new char[dirEntry_filename_len + 1];
memcpy(extra, entry_filename, dirEntry_filename_len);
extra[dirEntry_filename_len] = '\0';
}
dirp->entry.name = extra;
dirp->entry.extra = extra;
// Next file entry.
dirp->entry.idx += dirEntry->entry_length;
return &dirp->entry;
}
/**
* Close an opened directory.
* @param dirp FstDir pointer
* @return 0 on success; negative POSIX error code on error.
*/
int IsoPartition::closedir(IFst::Dir *dirp)
{
assert(dirp != nullptr);
//assert(dirp->parent == this);
if (!dirp) {
// No directory pointer.
// In release builds, this is a no-op.
return 0;
} /*else if (dirp->parent != this) {
// The dirp doesn't belong to this IFst.
return -EINVAL;
}*/
RP_D(IsoPartition);
assert(d->fstDirCount > 0);
if (dirp->entry.extra) {
delete[] static_cast<char*>(dirp->entry.extra);
}
delete dirp;
d->fstDirCount--;
return 0;
}
/**
* Open a file. (read-only)
* @param filename Filename
* @return IRpFile*, or nullptr on error.
*/
IRpFilePtr IsoPartition::open(const char *filename)
{
RP_D(IsoPartition);
assert((bool)m_file);
assert(m_file->isOpen());
if (!m_file || !m_file->isOpen()) {
m_lastError = EBADF;
return nullptr;
}
assert(filename != nullptr);
if (!filename || filename[0] == 0) {
// No filename.
m_lastError = EINVAL;
return nullptr;
}
// TODO: File reference counter.
// This might be difficult to do because PartitionFile is a separate class.
const ISO_DirEntry *const dirEntry = d->lookup(filename);
if (!dirEntry) {
// Not found.
return nullptr;
}
// Make sure this is a regular file.
// TODO: What is an "associated" file?
if (dirEntry->flags & (ISO_FLAG_ASSOCIATED | ISO_FLAG_DIRECTORY)) {
// Not a regular file.
m_lastError = ((dirEntry->flags & ISO_FLAG_DIRECTORY) ? EISDIR : EPERM);
return nullptr;
}
// Block size.
// Should be 2048, but other values are possible.
const unsigned int block_size = d->pvd.logical_block_size.he;
// Make sure the file is in bounds.
const off64_t file_addr = (static_cast<off64_t>(dirEntry->block.he) - d->iso_start_offset) * block_size;
if (file_addr >= d->partition_size + d->partition_offset ||
file_addr > d->partition_size + d->partition_offset - dirEntry->size.he)
{
// File is out of bounds.
m_lastError = EIO;
return nullptr;
}
// Create the PartitionFile.
// This is an IRpFile implementation that uses an
// IPartition as the reader and takes an offset
// and size as the file parameters.
return std::make_shared<PartitionFile>(this->shared_from_this(), file_addr, dirEntry->size.he);
}
/** IsoPartition-specific functions **/
/**
* Get a file's timestamp.
* @param filename Filename
* @return Timestamp, or -1 on error.
*/
time_t IsoPartition::get_mtime(const char *filename)
{
RP_D(IsoPartition);
assert(m_file != nullptr);
assert(m_file->isOpen());
if (!m_file || !m_file->isOpen()) {
m_lastError = EBADF;
return -1;
}
assert(filename != nullptr);
if (!filename || filename[0] == 0) {
// No filename.
m_lastError = EINVAL;
return -1;
}
// TODO: File reference counter.
// This might be difficult to do because PartitionFile is a separate class.
const ISO_DirEntry *const dirEntry = d->lookup(filename);
if (!dirEntry) {
// Not found.
return -1;
}
// Parse the timestamp.
return d->parseTimestamp(&dirEntry->mtime);
}
}