Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6bde05a
Add filesystem browsing, extraction, and patching to ISO browser
nicolasnoble Apr 7, 2026
fcfe8d5
Add directory listing to Lua ISO reader API
nicolasnoble Apr 7, 2026
ecd09ee
Add flat view mode to ISO browser filesystem display
nicolasnoble Apr 7, 2026
0bf6a3d
Display and support import/export of gaps in flat view
nicolasnoble Apr 7, 2026
f45d399
Merge branch 'feature/memory-editor-io-file' into feature/isobrowser-…
nicolasnoble Apr 7, 2026
a7ea25e
Add hex editor for ISO file and gap contents
nicolasnoble Apr 7, 2026
4fb0edd
Scan gap sectors for hidden files using Mode/subheader analysis
nicolasnoble Apr 15, 2026
dd5fe0e
Add reader:findGaps() to Lua ISO API
nicolasnoble Apr 15, 2026
f7e1c1e
Account for ISO9660 system structures in flat view and gap finder
nicolasnoble Apr 16, 2026
a093a9e
Make filesystem tables fill available vertical space
nicolasnoble Apr 16, 2026
e081cc7
Match hex editor options with main memory editors
nicolasnoble Apr 16, 2026
77312ff
Move actions to context menu, support multiple hex editors
nicolasnoble Apr 16, 2026
f56c000
Merge remote-tracking branch 'upstream/main' into feature/isobrowser-…
nicolasnoble Apr 20, 2026
d45e683
Address CodeRabbit PR 2013 review feedback
nicolasnoble Apr 20, 2026
a9c3bee
Address CodeRabbit second review round on PR 2013
nicolasnoble Apr 20, 2026
e97564b
Proper include.
nicolasnoble Apr 20, 2026
3113760
Address CodeRabbit third review round on PR 2013
nicolasnoble Apr 20, 2026
2e05407
Address CodeRabbit fourth review round on PR 2013
nicolasnoble Apr 20, 2026
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
24 changes: 16 additions & 8 deletions src/cdrom/iso9660-reader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,41 @@

PCSX::ISO9660Reader::ISO9660Reader(std::shared_ptr<CDRIso> iso) : m_iso(iso) {
unsigned pvdSector = 16;
bool foundPVD = false;

// Scan the full Volume Descriptor Set (LBA 16..terminator) to both find
// the PVD and record where the set actually ends.
while (true) {
IO<File> pvdFile(new CDRIsoFile(iso, pvdSector++, 2048));
IO<File> pvdFile(new CDRIsoFile(iso, pvdSector, 2048));
if (pvdFile->failed()) {
m_failed = true;
return;
}

uint8_t vd[7];
pvdFile->readAt(vd, 7, 0);
if ((vd[1] != 'C') || (vd[2] != 'D') || (vd[3] != '0') || (vd[4] != '0') || (vd[5] != '1') || (vd[6] != 1)) {
// Malformed descriptor set: even if we already have the PVD, the
// set is broken so we can't trust m_vdEnd.
m_failed = true;
return;
}

if (vd[0] == 255) {
m_failed = true;
// Terminator: the VD set ends at the sector after this one.
m_vdEnd = pvdSector + 1;
if (!foundPVD) m_failed = true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return;
}

if (vd[0] != 1) continue;

ISO9660LowLevel::PVD pvd;
pvd.deserialize(pvdFile);
if (vd[0] == 1 && !foundPVD) {
ISO9660LowLevel::PVD pvd;
pvd.deserialize(pvdFile);
m_pvd = pvd;
foundPVD = true;
}

m_pvd = pvd;
break;
pvdSector++;

Check warning on line 62 in src/cdrom/iso9660-reader.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Bumpy Road Ahead

PCSX::ISO9660Reader::ISO9660Reader has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/cdrom/iso9660-reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,20 @@ class ISO9660Reader {
return std::string_view(m_pvd.get<ISO9660LowLevel::PVD_VolumeIdent>());
}

typedef std::pair<ISO9660LowLevel::DirEntry, ISO9660LowLevel::DirEntry_XA> FullDirEntry;
std::vector<FullDirEntry> listAllEntriesFrom(const ISO9660LowLevel::DirEntry& entry);
const ISO9660LowLevel::DirEntry& getRootDirEntry() { return m_pvd.get<ISO9660LowLevel::PVD_RootDir>(); }
const ISO9660LowLevel::PVD& getPVD() { return m_pvd; }
// Returns the LBA just past the end of the volume descriptor set, i.e. the
// first sector after the VD terminator (type 255). Returns 17 if not found.
uint32_t getVDEnd() { return m_vdEnd; }

private:
std::shared_ptr<CDRIso> m_iso;
bool m_failed = false;
typedef std::pair<ISO9660LowLevel::DirEntry, ISO9660LowLevel::DirEntry_XA> FullDirEntry;
uint32_t m_vdEnd = 17;

std::optional<FullDirEntry> findEntry(const std::string_view& filename);
std::vector<FullDirEntry> listAllEntriesFrom(const ISO9660LowLevel::DirEntry& entry);
ISO9660LowLevel::PVD m_pvd;
};

Expand Down
44 changes: 44 additions & 0 deletions src/core/isoffi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ bool isReaderFailed(IsoReader* reader);
LuaFile* readerOpen(IsoReader* reader, const char* path);
LuaFile* fileisoOpen(LuaIso* wrapper, uint32_t lba, uint32_t size, enum SectorMode mode);

typedef struct { char opaque[?]; } DirEntries;
DirEntries* readerListDir(IsoReader* reader, const char* path);
void deleteDirEntries(DirEntries* entries);
uint32_t dirEntriesCount(DirEntries* entries);
const char* dirEntryName(DirEntries* entries, uint32_t index);
uint32_t dirEntryLBA(DirEntries* entries, uint32_t index);
uint32_t dirEntrySize(DirEntries* entries, uint32_t index);
bool dirEntryIsDir(DirEntries* entries, uint32_t index);

typedef struct { char opaque[?]; } GapList;
GapList* readerFindGaps(IsoReader* reader);
void deleteGapList(GapList* list);
uint32_t gapListCount(GapList* list);
uint32_t gapEntryLBA(GapList* list, uint32_t index);
uint32_t gapEntrySectors(GapList* list, uint32_t index);

typedef struct { char opaque[?]; } ISO9660Builder;
ISO9660Builder* createIsoBuilder(LuaFile* out);
void deleteIsoBuilder(ISO9660Builder* builder);
Expand All @@ -58,6 +74,34 @@ local function createIsoReaderWrapper(isoReader)
local reader = {
_wrapper = ffi.gc(isoReader, C.deleteIsoReader),
open = function(self, fname) return Support.File._createFileWrapper(C.readerOpen(self._wrapper, fname)) end,
findGaps = function(self)
local gapList = ffi.gc(C.readerFindGaps(self._wrapper), C.deleteGapList)
local count = C.gapListCount(gapList)
local result = {}
for i = 0, count - 1 do
table.insert(result, {
lba = C.gapEntryLBA(gapList, i),
sectors = C.gapEntrySectors(gapList, i),
})
end
return result
end,
listDir = function(self, path)
if path == nil then path = '' end
local entries = ffi.gc(C.readerListDir(self._wrapper, path), C.deleteDirEntries)
local count = C.dirEntriesCount(entries)
local result = {}
for i = 0, count - 1 do
local name = ffi.string(C.dirEntryName(entries, i))
table.insert(result, {
name = name,
lba = C.dirEntryLBA(entries, i),
size = C.dirEntrySize(entries, i),
isDir = C.dirEntryIsDir(entries, i),
})
end
return result
end,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return reader
end
Expand Down
177 changes: 177 additions & 0 deletions src/core/luaiso.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,17 @@

#include "core/luaiso.h"

#include <algorithm>
#include <memory>
#include <unordered_set>

#include "cdrom/cdriso.h"
#include "cdrom/file.h"
#include "cdrom/iso9660-reader.h"
#include "core/cdrom.h"
#include "lua/luafile.h"
#include "lua/luawrapper.h"
#include "support/strings-helpers.h"
#include "supportpsx/iso9660-builder.h"

namespace {
Expand Down Expand Up @@ -57,6 +60,166 @@
return new PCSX::LuaFFI::LuaFile(new PCSX::CDRIsoFile(wrapper->iso, lba, size, mode));
}

struct DirEntries {
std::vector<PCSX::ISO9660Reader::FullDirEntry> entries;
};

// Drop ISO9660 "." (\0) and ".." (\1) sentinel entries from a listing.
static std::vector<PCSX::ISO9660Reader::FullDirEntry> stripSelfParent(
std::vector<PCSX::ISO9660Reader::FullDirEntry>&& entries) {
std::vector<PCSX::ISO9660Reader::FullDirEntry> out;
out.reserve(entries.size());
for (auto& e : entries) {
const auto& name = e.first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value;
if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue;

Check warning on line 74 in src/core/luaiso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Conditional

stripSelfParent has 1 complex conditionals with 2 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
out.push_back(std::move(e));
}
return out;
}

DirEntries* readerListDir(PCSX::ISO9660Reader* reader, const char* path) {
if (reader->failed()) return new DirEntries{};
auto root = reader->getRootDirEntry();
if (path == nullptr || path[0] == '\0') {
return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(root))};
}
// Walk the path using listAllEntriesFrom. ISO9660 directory entries
// don't carry version suffixes (only files do), so exact match on each
// path component is correct for directory listing.
auto parts = PCSX::StringsHelpers::split(std::string_view(path), "/");
PCSX::ISO9660LowLevel::DirEntry current = root;
for (auto& part : parts) {
auto entries = reader->listAllEntriesFrom(current);
bool found = false;
for (auto& [entry, xa] : entries) {
if (entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value == part) {
current = entry;
found = true;
break;
}
}
if (!found) return new DirEntries{};
}
// Only return children if the target is actually a directory.
if ((current.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) == 0) {
return new DirEntries{};
}
return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(current))};
}

Check warning on line 108 in src/core/luaiso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

readerListDir has a cyclomatic complexity of 9, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void deleteDirEntries(DirEntries* entries) { delete entries; }
uint32_t dirEntriesCount(DirEntries* entries) { return entries->entries.size(); }
const char* dirEntryName(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return "";
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value.c_str();
}
uint32_t dirEntryLBA(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return 0;
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
}
uint32_t dirEntrySize(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return 0;
return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
}
bool dirEntryIsDir(DirEntries* entries, uint32_t index) {
if (index >= entries->entries.size()) return false;
return (entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0;
}

struct GapEntry {
uint32_t lba;
uint32_t sectors;
};

struct GapList {
std::vector<GapEntry> gaps;
};

// XA attribute bits (CD-XA spec, stored big-endian in directory record).
// Bit 11 (0x0800): Mode 2 Form 1 data file
// Bit 12 (0x1000): Mode 2 Form 2 interleaved audio/video
static constexpr uint16_t XA_ATTR_FORM2 = 0x1000;

static void collectAllEntries(PCSX::ISO9660Reader* reader, const PCSX::ISO9660LowLevel::DirEntry& dir,
std::vector<std::pair<uint32_t, uint32_t>>& out,
std::unordered_set<uint32_t>& visitedDirs) {
auto entries = reader->listAllEntriesFrom(dir);
for (auto& [entry, xa] : entries) {
const auto& name = entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value;
if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue;

Check warning on line 149 in src/core/luaiso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Conditional

collectAllEntries has 1 complex conditionals with 2 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
uint32_t lba = entry.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
uint32_t size = entry.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
uint16_t attribs = xa.get<PCSX::ISO9660LowLevel::DirEntry_XA_Attribs>();
uint32_t sectorSize = (attribs & XA_ATTR_FORM2) ? 2324 : 2048;
uint32_t sectors = (size + sectorSize - 1) / sectorSize;
// Skip zero-length extents: they don't consume sectors, and emitting
// them would confuse the gap aggregation pass.
if (sectors != 0) out.push_back({lba, sectors});
bool isDir = (entry.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0;
// Guard against malformed ISOs with directory cycles.
if (isDir && visitedDirs.insert(lba).second) {
collectAllEntries(reader, entry, out, visitedDirs);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Check warning on line 164 in src/core/luaiso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

collectAllEntries has a cyclomatic complexity of 10, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

GapList* readerFindGaps(PCSX::ISO9660Reader* reader) {
if (reader->failed()) return new GapList{};
std::vector<std::pair<uint32_t, uint32_t>> allFiles;

// Account for ISO9660 system structures
allFiles.push_back({0, 16}); // License/system area
auto& pvd = reader->getPVD();
uint32_t vdEnd = reader->getVDEnd();
allFiles.push_back({16, vdEnd > 16 ? vdEnd - 16 : 1}); // Volume descriptors including terminator
uint32_t lPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableLocation>();
uint32_t pathTableSize = pvd.get<PCSX::ISO9660LowLevel::PVD_PathTableSize>();
uint32_t pathTableSectors = (pathTableSize + 2047) / 2048;
allFiles.push_back({lPathLoc, pathTableSectors});
uint32_t lPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableOptLocation>();
if (lPathOptLoc != 0) allFiles.push_back({lPathOptLoc, pathTableSectors});
uint32_t mPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableLocation>();
allFiles.push_back({mPathLoc, pathTableSectors});
uint32_t mPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableOptLocation>();
if (mPathOptLoc != 0) allFiles.push_back({mPathOptLoc, pathTableSectors});
auto& rootDir = reader->getRootDirEntry();
uint32_t rootLBA = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_LBA>();
uint32_t rootSize = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_Size>();
allFiles.push_back({rootLBA, (rootSize + 2047) / 2048});

std::unordered_set<uint32_t> visitedDirs;
visitedDirs.insert(rootLBA);
collectAllEntries(reader, reader->getRootDirEntry(), allFiles, visitedDirs);
std::sort(allFiles.begin(), allFiles.end());

auto* result = new GapList{};
uint32_t nextExpected = 0;
for (auto& [lba, sectors] : allFiles) {
if (lba > nextExpected) {
result->gaps.push_back({nextExpected, lba - nextExpected});
}
uint32_t end = lba + sectors;
if (end > nextExpected) nextExpected = end;
}
// Trailing gap: anything between the last occupied extent and the disc end.
uint32_t volumeSpaceSize = pvd.get<PCSX::ISO9660LowLevel::PVD_VolumeSpaceSize>();
if (volumeSpaceSize > nextExpected) {
result->gaps.push_back({nextExpected, volumeSpaceSize - nextExpected});
}
return result;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

Check warning on line 210 in src/core/luaiso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

readerFindGaps has a cyclomatic complexity of 9, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void deleteGapList(GapList* list) { delete list; }
uint32_t gapListCount(GapList* list) { return list->gaps.size(); }
uint32_t gapEntryLBA(GapList* list, uint32_t index) {
if (index >= list->gaps.size()) return 0;
return list->gaps[index].lba;
}
uint32_t gapEntrySectors(GapList* list, uint32_t index) {
if (index >= list->gaps.size()) return 0;
return list->gaps[index].sectors;
}

PCSX::ISO9660Builder* createIsoBuilder(PCSX::LuaFFI::LuaFile* wrapper) {
return new PCSX::ISO9660Builder(wrapper->file);
}
Expand Down Expand Up @@ -98,6 +261,20 @@
REGISTER(L, readerOpen);
REGISTER(L, fileisoOpen);

REGISTER(L, readerListDir);
REGISTER(L, deleteDirEntries);
REGISTER(L, dirEntriesCount);
REGISTER(L, dirEntryName);
REGISTER(L, dirEntryLBA);
REGISTER(L, dirEntrySize);
REGISTER(L, dirEntryIsDir);

REGISTER(L, readerFindGaps);
REGISTER(L, deleteGapList);
REGISTER(L, gapListCount);
REGISTER(L, gapEntryLBA);
REGISTER(L, gapEntrySectors);

REGISTER(L, createIsoBuilder);
REGISTER(L, deleteIsoBuilder);
REGISTER(L, isoBuilderWriteLicense);
Expand Down
2 changes: 1 addition & 1 deletion src/gui/gui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ PCSX::GUI::GUI(std::vector<std::string>& favorites)
m_openArchiveDialog(l_("Open Archive"), favorites),
m_selectBiosDialog(l_("Select BIOS"), favorites),
m_selectEXP1Dialog(l_("Select EXP1"), favorites),
m_isoBrowser(settings.get<ShowIsoBrowser>().value, favorites),
m_isoBrowser(settings.get<ShowIsoBrowser>().value, favorites, [this]() { useMonoFont(); }),
m_pioCart(settings.get<ShowPIOCartConfig>().value, favorites) {
assert(g_gui == nullptr);
g_gui = this;
Expand Down
Loading
Loading