|
19 | 19 |
|
20 | 20 | #include "core/luaiso.h" |
21 | 21 |
|
| 22 | +#include <algorithm> |
22 | 23 | #include <memory> |
| 24 | +#include <unordered_set> |
23 | 25 |
|
24 | 26 | #include "cdrom/cdriso.h" |
25 | 27 | #include "cdrom/file.h" |
26 | 28 | #include "cdrom/iso9660-reader.h" |
27 | 29 | #include "core/cdrom.h" |
28 | 30 | #include "lua/luafile.h" |
29 | 31 | #include "lua/luawrapper.h" |
| 32 | +#include "support/strings-helpers.h" |
30 | 33 | #include "supportpsx/iso9660-builder.h" |
31 | 34 |
|
32 | 35 | namespace { |
@@ -57,6 +60,166 @@ PCSX::LuaFFI::LuaFile* fileisoOpen(LuaIso* wrapper, uint32_t lba, uint32_t size, |
57 | 60 | return new PCSX::LuaFFI::LuaFile(new PCSX::CDRIsoFile(wrapper->iso, lba, size, mode)); |
58 | 61 | } |
59 | 62 |
|
| 63 | +struct DirEntries { |
| 64 | + std::vector<PCSX::ISO9660Reader::FullDirEntry> entries; |
| 65 | +}; |
| 66 | + |
| 67 | +// Drop ISO9660 "." (\0) and ".." (\1) sentinel entries from a listing. |
| 68 | +static std::vector<PCSX::ISO9660Reader::FullDirEntry> stripSelfParent( |
| 69 | + std::vector<PCSX::ISO9660Reader::FullDirEntry>&& entries) { |
| 70 | + std::vector<PCSX::ISO9660Reader::FullDirEntry> out; |
| 71 | + out.reserve(entries.size()); |
| 72 | + for (auto& e : entries) { |
| 73 | + const auto& name = e.first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value; |
| 74 | + if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue; |
| 75 | + out.push_back(std::move(e)); |
| 76 | + } |
| 77 | + return out; |
| 78 | +} |
| 79 | + |
| 80 | +DirEntries* readerListDir(PCSX::ISO9660Reader* reader, const char* path) { |
| 81 | + if (reader->failed()) return new DirEntries{}; |
| 82 | + auto root = reader->getRootDirEntry(); |
| 83 | + if (path == nullptr || path[0] == '\0') { |
| 84 | + return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(root))}; |
| 85 | + } |
| 86 | + // Walk the path using listAllEntriesFrom. ISO9660 directory entries |
| 87 | + // don't carry version suffixes (only files do), so exact match on each |
| 88 | + // path component is correct for directory listing. |
| 89 | + auto parts = PCSX::StringsHelpers::split(std::string_view(path), "/"); |
| 90 | + PCSX::ISO9660LowLevel::DirEntry current = root; |
| 91 | + for (auto& part : parts) { |
| 92 | + auto entries = reader->listAllEntriesFrom(current); |
| 93 | + bool found = false; |
| 94 | + for (auto& [entry, xa] : entries) { |
| 95 | + if (entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value == part) { |
| 96 | + current = entry; |
| 97 | + found = true; |
| 98 | + break; |
| 99 | + } |
| 100 | + } |
| 101 | + if (!found) return new DirEntries{}; |
| 102 | + } |
| 103 | + // Only return children if the target is actually a directory. |
| 104 | + if ((current.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) == 0) { |
| 105 | + return new DirEntries{}; |
| 106 | + } |
| 107 | + return new DirEntries{stripSelfParent(reader->listAllEntriesFrom(current))}; |
| 108 | +} |
| 109 | + |
| 110 | +void deleteDirEntries(DirEntries* entries) { delete entries; } |
| 111 | +uint32_t dirEntriesCount(DirEntries* entries) { return entries->entries.size(); } |
| 112 | +const char* dirEntryName(DirEntries* entries, uint32_t index) { |
| 113 | + if (index >= entries->entries.size()) return ""; |
| 114 | + return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value.c_str(); |
| 115 | +} |
| 116 | +uint32_t dirEntryLBA(DirEntries* entries, uint32_t index) { |
| 117 | + if (index >= entries->entries.size()) return 0; |
| 118 | + return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_LBA>(); |
| 119 | +} |
| 120 | +uint32_t dirEntrySize(DirEntries* entries, uint32_t index) { |
| 121 | + if (index >= entries->entries.size()) return 0; |
| 122 | + return entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Size>(); |
| 123 | +} |
| 124 | +bool dirEntryIsDir(DirEntries* entries, uint32_t index) { |
| 125 | + if (index >= entries->entries.size()) return false; |
| 126 | + return (entries->entries[index].first.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0; |
| 127 | +} |
| 128 | + |
| 129 | +struct GapEntry { |
| 130 | + uint32_t lba; |
| 131 | + uint32_t sectors; |
| 132 | +}; |
| 133 | + |
| 134 | +struct GapList { |
| 135 | + std::vector<GapEntry> gaps; |
| 136 | +}; |
| 137 | + |
| 138 | +// XA attribute bits (CD-XA spec, stored big-endian in directory record). |
| 139 | +// Bit 11 (0x0800): Mode 2 Form 1 data file |
| 140 | +// Bit 12 (0x1000): Mode 2 Form 2 interleaved audio/video |
| 141 | +static constexpr uint16_t XA_ATTR_FORM2 = 0x1000; |
| 142 | + |
| 143 | +static void collectAllEntries(PCSX::ISO9660Reader* reader, const PCSX::ISO9660LowLevel::DirEntry& dir, |
| 144 | + std::vector<std::pair<uint32_t, uint32_t>>& out, |
| 145 | + std::unordered_set<uint32_t>& visitedDirs) { |
| 146 | + auto entries = reader->listAllEntriesFrom(dir); |
| 147 | + for (auto& [entry, xa] : entries) { |
| 148 | + const auto& name = entry.get<PCSX::ISO9660LowLevel::DirEntry_Filename>().value; |
| 149 | + if (name.size() == 1 && (name[0] == '\0' || name[0] == '\1')) continue; |
| 150 | + uint32_t lba = entry.get<PCSX::ISO9660LowLevel::DirEntry_LBA>(); |
| 151 | + uint32_t size = entry.get<PCSX::ISO9660LowLevel::DirEntry_Size>(); |
| 152 | + uint16_t attribs = xa.get<PCSX::ISO9660LowLevel::DirEntry_XA_Attribs>(); |
| 153 | + uint32_t sectorSize = (attribs & XA_ATTR_FORM2) ? 2324 : 2048; |
| 154 | + uint32_t sectors = (size + sectorSize - 1) / sectorSize; |
| 155 | + // Skip zero-length extents: they don't consume sectors, and emitting |
| 156 | + // them would confuse the gap aggregation pass. |
| 157 | + if (sectors != 0) out.push_back({lba, sectors}); |
| 158 | + bool isDir = (entry.get<PCSX::ISO9660LowLevel::DirEntry_Flags>().value & 2) != 0; |
| 159 | + // Guard against malformed ISOs with directory cycles. |
| 160 | + if (isDir && visitedDirs.insert(lba).second) { |
| 161 | + collectAllEntries(reader, entry, out, visitedDirs); |
| 162 | + } |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +GapList* readerFindGaps(PCSX::ISO9660Reader* reader) { |
| 167 | + if (reader->failed()) return new GapList{}; |
| 168 | + std::vector<std::pair<uint32_t, uint32_t>> allFiles; |
| 169 | + |
| 170 | + // Account for ISO9660 system structures |
| 171 | + allFiles.push_back({0, 16}); // License/system area |
| 172 | + auto& pvd = reader->getPVD(); |
| 173 | + uint32_t vdEnd = reader->getVDEnd(); |
| 174 | + allFiles.push_back({16, vdEnd > 16 ? vdEnd - 16 : 1}); // Volume descriptors including terminator |
| 175 | + uint32_t lPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableLocation>(); |
| 176 | + uint32_t pathTableSize = pvd.get<PCSX::ISO9660LowLevel::PVD_PathTableSize>(); |
| 177 | + uint32_t pathTableSectors = (pathTableSize + 2047) / 2048; |
| 178 | + allFiles.push_back({lPathLoc, pathTableSectors}); |
| 179 | + uint32_t lPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_LPathTableOptLocation>(); |
| 180 | + if (lPathOptLoc != 0) allFiles.push_back({lPathOptLoc, pathTableSectors}); |
| 181 | + uint32_t mPathLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableLocation>(); |
| 182 | + allFiles.push_back({mPathLoc, pathTableSectors}); |
| 183 | + uint32_t mPathOptLoc = pvd.get<PCSX::ISO9660LowLevel::PVD_MPathTableOptLocation>(); |
| 184 | + if (mPathOptLoc != 0) allFiles.push_back({mPathOptLoc, pathTableSectors}); |
| 185 | + auto& rootDir = reader->getRootDirEntry(); |
| 186 | + uint32_t rootLBA = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_LBA>(); |
| 187 | + uint32_t rootSize = rootDir.get<PCSX::ISO9660LowLevel::DirEntry_Size>(); |
| 188 | + allFiles.push_back({rootLBA, (rootSize + 2047) / 2048}); |
| 189 | + |
| 190 | + std::unordered_set<uint32_t> visitedDirs; |
| 191 | + visitedDirs.insert(rootLBA); |
| 192 | + collectAllEntries(reader, reader->getRootDirEntry(), allFiles, visitedDirs); |
| 193 | + std::sort(allFiles.begin(), allFiles.end()); |
| 194 | + |
| 195 | + auto* result = new GapList{}; |
| 196 | + uint32_t nextExpected = 0; |
| 197 | + for (auto& [lba, sectors] : allFiles) { |
| 198 | + if (lba > nextExpected) { |
| 199 | + result->gaps.push_back({nextExpected, lba - nextExpected}); |
| 200 | + } |
| 201 | + uint32_t end = lba + sectors; |
| 202 | + if (end > nextExpected) nextExpected = end; |
| 203 | + } |
| 204 | + // Trailing gap: anything between the last occupied extent and the disc end. |
| 205 | + uint32_t volumeSpaceSize = pvd.get<PCSX::ISO9660LowLevel::PVD_VolumeSpaceSize>(); |
| 206 | + if (volumeSpaceSize > nextExpected) { |
| 207 | + result->gaps.push_back({nextExpected, volumeSpaceSize - nextExpected}); |
| 208 | + } |
| 209 | + return result; |
| 210 | +} |
| 211 | + |
| 212 | +void deleteGapList(GapList* list) { delete list; } |
| 213 | +uint32_t gapListCount(GapList* list) { return list->gaps.size(); } |
| 214 | +uint32_t gapEntryLBA(GapList* list, uint32_t index) { |
| 215 | + if (index >= list->gaps.size()) return 0; |
| 216 | + return list->gaps[index].lba; |
| 217 | +} |
| 218 | +uint32_t gapEntrySectors(GapList* list, uint32_t index) { |
| 219 | + if (index >= list->gaps.size()) return 0; |
| 220 | + return list->gaps[index].sectors; |
| 221 | +} |
| 222 | + |
60 | 223 | PCSX::ISO9660Builder* createIsoBuilder(PCSX::LuaFFI::LuaFile* wrapper) { |
61 | 224 | return new PCSX::ISO9660Builder(wrapper->file); |
62 | 225 | } |
@@ -98,6 +261,20 @@ static void registerAllSymbols(PCSX::Lua L) { |
98 | 261 | REGISTER(L, readerOpen); |
99 | 262 | REGISTER(L, fileisoOpen); |
100 | 263 |
|
| 264 | + REGISTER(L, readerListDir); |
| 265 | + REGISTER(L, deleteDirEntries); |
| 266 | + REGISTER(L, dirEntriesCount); |
| 267 | + REGISTER(L, dirEntryName); |
| 268 | + REGISTER(L, dirEntryLBA); |
| 269 | + REGISTER(L, dirEntrySize); |
| 270 | + REGISTER(L, dirEntryIsDir); |
| 271 | + |
| 272 | + REGISTER(L, readerFindGaps); |
| 273 | + REGISTER(L, deleteGapList); |
| 274 | + REGISTER(L, gapListCount); |
| 275 | + REGISTER(L, gapEntryLBA); |
| 276 | + REGISTER(L, gapEntrySectors); |
| 277 | + |
101 | 278 | REGISTER(L, createIsoBuilder); |
102 | 279 | REGISTER(L, deleteIsoBuilder); |
103 | 280 | REGISTER(L, isoBuilderWriteLicense); |
|
0 commit comments