diff --git a/CMakeLists.txt b/CMakeLists.txt
index 45b4971a69..f4c018c551 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1425,6 +1425,7 @@ if(UNITTEST)
unittest/TestVertexJit.h
unittest/TestVFS.cpp
unittest/TestZipSlip.cpp
+ unittest/TestPS1Eboot.cpp
unittest/TestLzrc.cpp
unittest/TestCwCheat.cpp
unittest/TestMpegCsc.cpp
diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index 9a585939ae..c5d74a0f96 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -612,6 +612,8 @@ add_library(Core STATIC
FileLoaders/HTTPFileLoader.h
FileLoaders/LocalFileLoader.cpp
FileLoaders/LocalFileLoader.h
+ FileLoaders/PS1EbootFileLoader.cpp
+ FileLoaders/PS1EbootFileLoader.h
FileLoaders/RamCachingFileLoader.cpp
FileLoaders/RamCachingFileLoader.h
FileLoaders/RetryingFileLoader.cpp
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index 4098967edf..010a5d2ea2 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -587,6 +587,7 @@
+
@@ -931,6 +932,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index a0b8550a09..4556eb843d 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -619,6 +619,9 @@
FileLoaders
+
+ FileLoaders
+
FileLoaders
@@ -1978,6 +1981,9 @@
FileLoaders
+
+ FileLoaders
+
FileLoaders
diff --git a/Core/FileLoaders/PS1EbootFileLoader.cpp b/Core/FileLoaders/PS1EbootFileLoader.cpp
new file mode 100644
index 0000000000..2702737849
--- /dev/null
+++ b/Core/FileLoaders/PS1EbootFileLoader.cpp
@@ -0,0 +1,755 @@
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#include
+#include
+#include
+
+#include "Common/File/DirListing.h"
+#include "Common/Log.h"
+#include "Common/StringUtils.h"
+#include "Common/Swap.h"
+#include "Core/ELF/ParamSFO.h"
+#include "Core/FileLoaders/PS1EbootFileLoader.h"
+
+static const u32 SECTOR_SIZE = 2352;
+// The PSAR's disc image is in blocks of 16 sectors, each stored as one uncompressed DEFLATE block: a
+// header byte (final, stored), the length, its complement, then the sectors. POPS doesn't take an
+// uncompressed block in its own format (0x9300 bytes), but its decoder call goes to an inflate for a
+// converted PBP (__PopsDeflateDecompress), and this way every block has the same size.
+static const u32 BLOCK_SECTORS = 16;
+static const u32 BLOCK_DATA_SIZE = BLOCK_SECTORS * SECTOR_SIZE;
+static const u8 STORED_HEADER[5] = { 0x01, BLOCK_DATA_SIZE & 0xFF, BLOCK_DATA_SIZE >> 8, (u8)~BLOCK_DATA_SIZE, (u8)(~BLOCK_DATA_SIZE >> 8) };
+static const u32 BLOCK_SIZE = BLOCK_DATA_SIZE + sizeof(STORED_HEADER);
+
+// The PSISOIMG header, as popstation writes it: the disc ID at 0x400, the TOC at 0x800, the block index
+// (32 bytes a block) from 0x4000, and the blocks from 0x100000.
+static const u32 PSAR_HEADER_SIZE = 0x4000;
+static const u32 PSAR_INDEX_ENTRY_SIZE = 32;
+static const u32 PSAR_DATA_OFFSET = 0x100000;
+static const u32 MAX_BLOCKS = (PSAR_DATA_OFFSET - PSAR_HEADER_SIZE) / PSAR_INDEX_ENTRY_SIZE;
+
+static const u8 SYNC[12] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 };
+
+static std::string AsciiToUpper(std::string_view s) {
+ std::string out(s);
+ for (char &c : out) {
+ c = (char)toupper((unsigned char)c);
+ }
+ return out;
+}
+
+static std::string AsciiToLower(std::string_view s) {
+ std::string out(s);
+ for (char &c : out) {
+ c = (char)tolower((unsigned char)c);
+ }
+ return out;
+}
+
+// The file name without its extension.
+static std::string FileStem(const Path &path) {
+ const std::string name = path.GetFilename();
+ return name.substr(0, name.size() - path.GetFileExtension().size());
+}
+
+static u8 ToBCD(u32 v) {
+ return (u8)(((v / 10) << 4) | (v % 10));
+}
+
+// The absolute time of an LBA (which starts at 00:02:00), in BCD.
+static void LBAToMSF(s32 lba, u8 *out) {
+ const u32 frames = (u32)(lba + 150);
+ out[0] = ToBCD(frames / (75 * 60));
+ out[1] = ToBCD((frames / 75) % 60);
+ out[2] = ToBCD(frames % 75);
+}
+
+// "mm:ss:ff" to a frame count, -1 if it isn't one.
+static int ParseMSF(std::string_view s) {
+ int m, sec, f;
+ char c1, c2;
+ if (sscanf(std::string(s).c_str(), "%d%c%d%c%d", &m, &c1, &sec, &c2, &f) != 5 || c1 != ':' || c2 != ':') {
+ return -1;
+ }
+ return (m * 60 + sec) * 75 + f;
+}
+
+static bool ReadSmallTextFile(const Path &path, std::string *text) {
+ std::unique_ptr loader(ConstructFileLoader(path));
+ if (!loader || !loader->Exists()) {
+ return false;
+ }
+ const s64 size = loader->FileSize();
+ if (size <= 0 || size > 64 * 1024) {
+ return false;
+ }
+ text->resize((size_t)size);
+ text->resize(loader->ReadAt(0, (size_t)size, &(*text)[0]));
+ if (startsWith(*text, "\xEF\xBB\xBF")) {
+ text->erase(0, 3);
+ }
+ return true;
+}
+
+// The next token of a cue sheet line: a quoted string, or up to the next space.
+static std::string_view NextToken(std::string_view *line) {
+ *line = StripSpaces(*line);
+ if (line->empty()) {
+ return {};
+ }
+ size_t end;
+ std::string_view token;
+ if ((*line)[0] == '"') {
+ end = line->find('"', 1);
+ if (end == std::string_view::npos) {
+ end = line->size();
+ }
+ token = line->substr(1, end - 1);
+ end = std::min(end + 1, line->size());
+ } else {
+ end = line->find_first_of(" \t");
+ if (end == std::string_view::npos) {
+ end = line->size();
+ }
+ token = line->substr(0, end);
+ }
+ line->remove_prefix(end);
+ return token;
+}
+
+bool IsPS1CueOrCcd(FileLoader *fileLoader) {
+ char buf[4096]{};
+ const size_t size = fileLoader->ReadAt(0, sizeof(buf) - 1, buf);
+ const std::string text(buf, size);
+ const std::string ext = fileLoader->GetFileExtension();
+ if (ext == ".ccd") {
+ return text.find("[CloneCD]") != std::string::npos;
+ }
+ if (ext == ".cue") {
+ const std::string upper = AsciiToUpper(text);
+ return upper.find("FILE") != std::string::npos && upper.find("TRACK") != std::string::npos;
+ }
+ return false;
+}
+
+std::vector CueSheetFileNames(const Path &cuePath) {
+ std::vector names;
+ std::string text;
+ if (!ReadSmallTextFile(cuePath, &text)) {
+ return names;
+ }
+ std::vector lines;
+ SplitString(text, '\n', lines);
+ for (std::string_view line : lines) {
+ if (equalsNoCase(NextToken(&line), "FILE")) {
+ names.emplace_back(NextToken(&line));
+ }
+ }
+ return names;
+}
+
+Path FindCueSheetFor(const Path &imagePath) {
+ if (!imagePath.CanNavigateUp()) {
+ return Path();
+ }
+ const std::string name = imagePath.GetFilename();
+ std::vector cues;
+ File::GetFilesInDir(imagePath.NavigateUp(), &cues, "cue:");
+ for (const File::FileInfo &cue : cues) {
+ if (cue.isDirectory) {
+ continue;
+ }
+ for (const std::string &file : CueSheetFileNames(cue.fullName)) {
+ if (equalsNoCase(Path(file).GetFilename(), name)) {
+ return cue.fullName;
+ }
+ }
+ }
+ return Path();
+}
+
+void RemoveDescribedDiscImages(std::vector *files) {
+ std::set described;
+ for (const File::FileInfo &info : *files) {
+ const std::string ext = info.fullName.GetFileExtension();
+ if (info.isDirectory || (ext != ".cue" && ext != ".ccd")) {
+ continue;
+ }
+ const std::string stem = AsciiToLower(FileStem(info.fullName));
+ described.insert(stem + ".bin");
+ described.insert(stem + ".img");
+ if (ext == ".cue") {
+ for (const std::string &file : CueSheetFileNames(info.fullName)) {
+ described.insert(AsciiToLower(Path(file).GetFilename()));
+ }
+ }
+ }
+ files->erase(std::remove_if(files->begin(), files->end(), [&](const File::FileInfo &info) {
+ const std::string ext = info.fullName.GetFileExtension();
+ if (info.isDirectory || (ext != ".bin" && ext != ".img")) {
+ return false;
+ }
+ return info.size % SECTOR_SIZE != 0 || described.count(AsciiToLower(info.name)) != 0;
+ }), files->end());
+}
+
+PS1EbootFileLoader::PS1EbootFileLoader(const Path &path) : path_(path) {
+ const std::string ext = path.GetFileExtension();
+ std::string text;
+ bool parsed = false;
+ if (ext == ".cue" || ext == ".ccd") {
+ if (!ReadSmallTextFile(path, &text)) {
+ error_ = "Can't read " + path.GetFilename();
+ return;
+ }
+ parsed = ext == ".cue" ? ParseCue(path, text) : ParseCcd(path, text);
+ } else {
+ // A bare image with a cue sheet or a .ccd next to it is described by that, which has its audio tracks.
+ Path cue = FindCueSheetFor(path);
+ if (cue.empty()) {
+ cue = path.WithReplacedExtension(".cue");
+ }
+ const Path ccd = path.WithReplacedExtension(".ccd");
+ if (ReadSmallTextFile(cue, &text) && ParseCue(cue, text)) {
+ parsed = true;
+ } else if (ReadSmallTextFile(ccd, &text) && ParseCcd(ccd, text)) {
+ parsed = true;
+ } else {
+ files_.clear();
+ extents_.clear();
+ tracks_.clear();
+ error_.clear();
+ parsed = OpenBare(path);
+ }
+ }
+ if (!parsed || tracks_.empty() || extents_.empty()) {
+ if (error_.empty()) {
+ error_ = "Not a PS1 disc image: " + path.GetFilename();
+ }
+ return;
+ }
+
+ // LBA 0 is the first track's INDEX 01. An image that has its pregap starts before it.
+ const u32 origin = tracks_[0].start;
+ if (origin != 0) {
+ std::vector shifted;
+ for (Extent e : extents_) {
+ if (e.lba + e.count <= origin) {
+ continue;
+ }
+ if (e.lba < origin) {
+ const u32 skip = origin - e.lba;
+ e.offset += (s64)skip * e.sectorSize;
+ e.count -= skip;
+ e.lba = origin;
+ }
+ e.lba -= origin;
+ shifted.push_back(e);
+ }
+ extents_ = shifted;
+ for (Track &t : tracks_) {
+ t.start -= origin;
+ }
+ }
+ tracks_[0].pregap = 150;
+ numSectors_ = extents_.back().lba + extents_.back().count;
+ numBlocks_ = (numSectors_ + BLOCK_SECTORS - 1) / BLOCK_SECTORS;
+ if (numBlocks_ > MAX_BLOCKS) {
+ error_ = "Disc image too large";
+ return;
+ }
+
+ ReadDiscID();
+ title_ = FileStem(path);
+ BuildHeaders();
+ INFO_LOG(Log::Loader, "PS1 disc %s: %s, %d tracks, %d sectors", path.c_str(), discID_.c_str(), (int)tracks_.size(), numSectors_);
+}
+
+PS1EbootFileLoader::~PS1EbootFileLoader() {}
+
+int PS1EbootFileLoader::AddFile(const Path &path) {
+ FileLoader *loader = ConstructFileLoader(path);
+ if (!loader || !loader->Exists()) {
+ delete loader;
+ return -1;
+ }
+ files_.emplace_back(loader);
+ return (int)files_.size() - 1;
+}
+
+void PS1EbootFileLoader::AddExtent(u32 lba, u32 count, int file, s64 offset, int sectorSize) {
+ if (count != 0) {
+ extents_.push_back(Extent{ lba, count, file, offset, sectorSize });
+ }
+}
+
+bool PS1EbootFileLoader::ParseCue(const Path &cuePath, const std::string &text) {
+ struct CueTrack {
+ int number;
+ bool audio;
+ int index0 = -1;
+ int index1 = -1;
+ int pregap = 0;
+ };
+ struct CueFile {
+ Path path;
+ int sectorSize = 0;
+ std::vector tracks;
+ };
+ std::vector cueFiles;
+
+ std::vector lines;
+ SplitString(text, '\n', lines);
+ for (std::string_view line : lines) {
+ const std::string command = AsciiToUpper(NextToken(&line));
+ if (command == "FILE") {
+ const std::string_view name = NextToken(&line);
+ const std::string type = AsciiToUpper(NextToken(&line));
+ if (type != "BINARY" && type != "MOTOROLA") {
+ error_ = "Unsupported file type in cue sheet: " + type;
+ return false;
+ }
+ CueFile file;
+ file.path = cuePath.NavigateUp() / name;
+ cueFiles.push_back(file);
+ } else if (command == "TRACK") {
+ if (cueFiles.empty()) {
+ return false;
+ }
+ const int number = atoi(std::string(NextToken(&line)).c_str());
+ const std::string mode = AsciiToUpper(NextToken(&line));
+ int sectorSize;
+ if (mode == "AUDIO" || mode == "MODE1/2352" || mode == "MODE2/2352") {
+ sectorSize = 2352;
+ } else if (mode == "MODE2/2336") {
+ sectorSize = 2336;
+ } else if (mode == "MODE1/2048" || mode == "MODE2/2048") {
+ sectorSize = 2048;
+ } else {
+ error_ = "Unsupported track mode in cue sheet: " + mode;
+ return false;
+ }
+ CueFile &file = cueFiles.back();
+ if (file.sectorSize == 0) {
+ file.sectorSize = sectorSize;
+ } else if (file.sectorSize != sectorSize) {
+ error_ = "Tracks with different sector sizes in one file";
+ return false;
+ }
+ CueTrack track;
+ track.number = number;
+ track.audio = mode == "AUDIO";
+ file.tracks.push_back(track);
+ } else if (command == "INDEX" || command == "PREGAP") {
+ if (cueFiles.empty() || cueFiles.back().tracks.empty()) {
+ return false;
+ }
+ CueTrack &track = cueFiles.back().tracks.back();
+ const int index = command == "INDEX" ? atoi(std::string(NextToken(&line)).c_str()) : -1;
+ const int frames = ParseMSF(NextToken(&line));
+ if (frames < 0) {
+ return false;
+ }
+ if (index == -1) {
+ track.pregap = frames;
+ } else if (index == 0) {
+ track.index0 = frames;
+ } else if (index == 1) {
+ track.index1 = frames;
+ }
+ }
+ }
+
+ // Lay the files out one after another, with any generated pregap before its track.
+ u32 lba = 0;
+ for (CueFile &cueFile : cueFiles) {
+ int file = AddFile(cueFile.path);
+ if (file < 0) {
+ // Renamed images are common: try the cue sheet's own name.
+ const std::string ext = cueFile.path.GetFileExtension();
+ for (const char *alt : { ext.c_str(), ".bin", ".img" }) {
+ file = AddFile(cuePath.WithReplacedExtension(alt));
+ if (file >= 0) {
+ break;
+ }
+ }
+ if (file < 0 || cueFiles.size() != 1) {
+ error_ = "Missing file in cue sheet: " + cueFile.path.GetFilename();
+ return false;
+ }
+ }
+ if (cueFile.tracks.empty()) {
+ continue;
+ }
+ const u32 fileSectors = (u32)(files_[file]->FileSize() / cueFile.sectorSize);
+ u32 fileSector = 0;
+ for (const CueTrack &cueTrack : cueFile.tracks) {
+ if (cueTrack.index1 < 0) {
+ error_ = "Track without INDEX 01 in cue sheet";
+ return false;
+ }
+ const u32 split = (u32)(cueTrack.index0 >= 0 ? cueTrack.index0 : cueTrack.index1);
+ if (split < fileSector || (u32)cueTrack.index1 < split) {
+ return false;
+ }
+ AddExtent(lba, split - fileSector, file, (s64)fileSector * cueFile.sectorSize, cueFile.sectorSize);
+ lba += split - fileSector;
+ AddExtent(lba, cueTrack.pregap, -1, 0, SECTOR_SIZE);
+ lba += cueTrack.pregap;
+ fileSector = split;
+
+ Track track;
+ track.number = cueTrack.number;
+ track.audio = cueTrack.audio;
+ track.start = lba + (cueTrack.index1 - split);
+ track.pregap = cueTrack.pregap + (cueTrack.index1 - split);
+ tracks_.push_back(track);
+ }
+ if (fileSectors > fileSector) {
+ AddExtent(lba, fileSectors - fileSector, file, (s64)fileSector * cueFile.sectorSize, cueFile.sectorSize);
+ lba += fileSectors - fileSector;
+ }
+ }
+ return !tracks_.empty();
+}
+
+bool PS1EbootFileLoader::ParseCcd(const Path &ccdPath, const std::string &text) {
+ const int file = AddFile(ccdPath.WithReplacedExtension(".img"));
+ if (file < 0) {
+ error_ = "Missing " + ccdPath.WithReplacedExtension(".img").GetFilename();
+ return false;
+ }
+ AddExtent(0, (u32)(files_[file]->FileSize() / SECTOR_SIZE), file, 0, SECTOR_SIZE);
+
+ // [TRACK n] sections: MODE=0 is audio, INDEX 1= is where the track starts, INDEX 0= its pregap.
+ std::vector lines;
+ SplitString(text, '\n', lines);
+ Track *track = nullptr;
+ int index0 = -1;
+ auto finishTrack = [&]() {
+ if (track && index0 >= 0 && (u32)index0 <= track->start) {
+ track->pregap = track->start - index0;
+ }
+ index0 = -1;
+ };
+ for (std::string_view line : lines) {
+ line = StripSpaces(line);
+ if (startsWith(line, "[")) {
+ finishTrack();
+ track = nullptr;
+ if (startsWithNoCase(line, "[TRACK ")) {
+ tracks_.push_back(Track{ atoi(std::string(line.substr(7)).c_str()), false, 0, 0 });
+ track = &tracks_.back();
+ }
+ continue;
+ }
+ std::string_view key, value;
+ if (!track || !SplitStringOnce(line, &key, &value, '=')) {
+ continue;
+ }
+ key = StripSpaces(key);
+ const int v = atoi(std::string(StripSpaces(value)).c_str());
+ if (equalsNoCase(key, "MODE")) {
+ track->audio = v == 0;
+ } else if (equalsNoCase(key, "INDEX 1")) {
+ track->start = v;
+ } else if (equalsNoCase(key, "INDEX 0")) {
+ index0 = v;
+ }
+ }
+ finishTrack();
+ std::sort(tracks_.begin(), tracks_.end(), [](const Track &a, const Track &b) { return a.number < b.number; });
+ if (tracks_.empty()) {
+ // No tracks listed: one data track.
+ tracks_.push_back(Track{ 1, false, 0, 150 });
+ }
+ return true;
+}
+
+bool PS1EbootFileLoader::OpenBare(const Path &path) {
+ const int file = AddFile(path);
+ if (file < 0) {
+ return false;
+ }
+ FileLoader *loader = files_[file].get();
+ const s64 size = loader->FileSize();
+ u8 sync[12]{};
+ u8 cd001[6]{};
+ loader->ReadAt(0, sizeof(sync), sync);
+ loader->ReadAt(16 * 2048, sizeof(cd001), cd001);
+ int sectorSize;
+ if (size % SECTOR_SIZE == 0 && memcmp(sync, SYNC, sizeof(SYNC)) == 0) {
+ sectorSize = SECTOR_SIZE;
+ } else if (size % 2048 == 0 && memcmp(cd001, "\x01" "CD001", 6) == 0) {
+ sectorSize = 2048;
+ } else {
+ return false;
+ }
+ AddExtent(0, (u32)(size / sectorSize), file, 0, sectorSize);
+ tracks_.push_back(Track{ 1, false, 0, 150 });
+ return true;
+}
+
+bool PS1EbootFileLoader::ReadRawSectors(u32 lba, u32 count, u8 *out) {
+ bool ok = true;
+ while (count > 0) {
+ auto it = std::upper_bound(extents_.begin(), extents_.end(), lba, [](u32 lba, const Extent &e) { return lba < e.lba; });
+ if (lba >= numSectors_ || it == extents_.begin()) {
+ memset(out, 0, (size_t)count * SECTOR_SIZE);
+ return ok;
+ }
+ const Extent &e = *(it - 1);
+ const u32 run = std::min(count, e.lba + e.count - lba);
+ const s64 offset = e.offset + (s64)(lba - e.lba) * e.sectorSize;
+ if (e.file < 0) {
+ memset(out, 0, (size_t)run * SECTOR_SIZE);
+ } else if (e.sectorSize == (int)SECTOR_SIZE) {
+ const size_t want = (size_t)run * SECTOR_SIZE;
+ const size_t got = files_[e.file]->ReadAt(offset, want, out);
+ if (got < want) {
+ memset(out + got, 0, want - got);
+ ok = false;
+ }
+ } else {
+ // Sectors without their sync and header (2336), or only their user data (2048, which can only
+ // be Mode 2 Form 1). The EDC and ECC of those stay zero.
+ for (u32 i = 0; i < run; i++) {
+ u8 *sector = out + (size_t)i * SECTOR_SIZE;
+ memset(sector, 0, SECTOR_SIZE);
+ memcpy(sector, SYNC, sizeof(SYNC));
+ LBAToMSF(lba + i, sector + 12);
+ sector[15] = 2;
+ u8 *data = sector + 16;
+ if (e.sectorSize == 2048) {
+ static const u8 subheader[8] = { 0, 0, 8, 0, 0, 0, 8, 0 };
+ memcpy(sector + 16, subheader, sizeof(subheader));
+ data = sector + 24;
+ }
+ const s64 pos = offset + (s64)i * e.sectorSize;
+ if (files_[e.file]->ReadAt(pos, e.sectorSize, data) != (size_t)e.sectorSize) {
+ ok = false;
+ }
+ }
+ }
+ lba += run;
+ count -= run;
+ out += (size_t)run * SECTOR_SIZE;
+ }
+ return ok;
+}
+
+bool PS1EbootFileLoader::ReadUserData(u32 lba, u8 *out) {
+ u8 sector[SECTOR_SIZE];
+ if (!ReadRawSectors(lba, 1, sector)) {
+ return false;
+ }
+ memcpy(out, sector + (sector[15] == 1 ? 16 : 24), 2048);
+ return true;
+}
+
+// The ID the disc boots by, from SYSTEM.CNF's BOOT line ("cdrom:\SLUS_007.13;1" is SLUS00713).
+void PS1EbootFileLoader::ReadDiscID() {
+ discID_ = "SLUS00000";
+ u8 pvd[2048];
+ if (!ReadUserData(16, pvd) || memcmp(pvd, "\x01" "CD001", 6) != 0) {
+ return;
+ }
+ const u32 rootLBA = pvd[156 + 2] | (pvd[156 + 3] << 8) | (pvd[156 + 4] << 16) | ((u32)pvd[156 + 5] << 24);
+ const u32 rootSize = pvd[156 + 10] | (pvd[156 + 11] << 8) | (pvd[156 + 12] << 16) | ((u32)pvd[156 + 13] << 24);
+ std::string cnf;
+ for (u32 s = 0; s < std::min(rootSize / 2048, 16U) && cnf.empty(); s++) {
+ u8 dir[2048];
+ if (!ReadUserData(rootLBA + s, dir)) {
+ return;
+ }
+ for (u32 pos = 0; pos + 33 < sizeof(dir) && dir[pos] != 0; pos += dir[pos]) {
+ const u8 nameLen = dir[pos + 32];
+ if (pos + 33 + nameLen > sizeof(dir)) {
+ break;
+ }
+ std::string_view name((const char *)&dir[pos + 33], nameLen);
+ name = name.substr(0, name.find(';'));
+ if (!equalsNoCase(name, "SYSTEM.CNF")) {
+ continue;
+ }
+ const u32 lba = dir[pos + 2] | (dir[pos + 3] << 8) | (dir[pos + 4] << 16) | ((u32)dir[pos + 5] << 24);
+ const u32 size = std::min(dir[pos + 10] | (dir[pos + 11] << 8) | (dir[pos + 12] << 16) | ((u32)dir[pos + 13] << 24), 2048U);
+ u8 data[2048];
+ if (ReadUserData(lba, data)) {
+ cnf.assign((const char *)data, size);
+ }
+ break;
+ }
+ }
+
+ std::vector lines;
+ SplitString(cnf, '\n', lines);
+ for (std::string_view line : lines) {
+ std::string_view key, value;
+ if (!SplitStringOnce(line, &key, &value, '=') || !equalsNoCase(StripSpaces(key), "BOOT")) {
+ continue;
+ }
+ value = StripSpaces(value);
+ const size_t slash = value.find_last_of("\\:/");
+ if (slash != std::string_view::npos) {
+ value = value.substr(slash + 1);
+ }
+ value = value.substr(0, value.find(';'));
+ std::string id;
+ for (char c : value) {
+ if (isalnum((unsigned char)c)) {
+ id.push_back((char)toupper((unsigned char)c));
+ }
+ }
+ if (id.size() == 9) {
+ discID_ = id;
+ }
+ break;
+ }
+}
+
+void PS1EbootFileLoader::BuildHeaders() {
+ ParamSFOData sfo;
+ sfo.SetValue("BOOTABLE", 1, 4);
+ sfo.SetValue("CATEGORY", "ME", 4);
+ sfo.SetValue("DISC_ID", discID_, 16);
+ sfo.SetValue("DISC_VERSION", "1.00", 8);
+ sfo.SetValue("PARENTAL_LEVEL", 1, 4);
+ sfo.SetValue("PSP_SYSTEM_VER", "3.01", 8);
+ sfo.SetValue("REGION", 0x8000, 4);
+ sfo.SetValue("TITLE", title_, 128);
+ u8 *sfoData = nullptr;
+ size_t sfoSize = 0;
+ sfo.WriteSFO(&sfoData, &sfoSize);
+
+ // PARAM.SFO, then no icons, pictures, sound or DATA.PSP.
+ const u32 sfoEnd = 0x28 + (u32)sfoSize;
+ psarOffset_ = (sfoEnd + 0xFFFF) & ~0xFFFF;
+ head_.assign(psarOffset_ + PSAR_HEADER_SIZE, 0);
+ u8 *pbp = head_.data();
+ memcpy(pbp, "\0PBP", 4);
+ u32_le *words = (u32_le *)pbp;
+ words[1] = 0x00010000;
+ words[2] = 0x28;
+ for (int i = 3; i < 9; i++) {
+ words[i] = sfoEnd;
+ }
+ words[9] = psarOffset_;
+ memcpy(pbp + 0x28, sfoData, sfoSize);
+ delete[] sfoData;
+
+ u8 *psar = pbp + psarOffset_;
+ auto write32 = [&](u32 offset, u32 value) {
+ *(u32_le *)(psar + offset) = value;
+ };
+ const u32 end = PSAR_DATA_OFFSET + numBlocks_ * BLOCK_SIZE;
+ memcpy(psar, "PSISOIMG0000", 12);
+ write32(0x0C, end);
+ const std::string gameID = "_" + discID_.substr(0, 4) + "_" + discID_.substr(4);
+ memcpy(psar + 0x400, gameID.data(), std::min(gameID.size(), (size_t)0x20));
+
+ // The TOC, as Q subchannel entries of 10 bytes (control/ADR, TNO, POINT, the absolute time, zero, then
+ // POINT's): the first track (and the disc type, 0x20 being XA), the last one, the lead-out, then each
+ // track with its pregap's start and its own.
+ const Track &first = tracks_.front();
+ const Track &last = tracks_.back();
+ auto control = [](const Track &t) -> u8 { return t.audio ? 0x01 : 0x41; };
+ u8 *toc = psar + 0x800;
+ toc[0] = control(first); toc[2] = 0xA0; toc[7] = ToBCD(first.number); toc[8] = 0x20;
+ toc += 10;
+ toc[0] = control(last); toc[2] = 0xA1; toc[7] = ToBCD(last.number);
+ toc += 10;
+ toc[0] = control(last); toc[2] = 0xA2; LBAToMSF(numSectors_, toc + 7);
+ toc += 10;
+ for (const Track &t : tracks_) {
+ if (toc + 10 > psar + 0xBFC) {
+ break;
+ }
+ toc[0] = control(t);
+ toc[2] = ToBCD(t.number);
+ LBAToMSF((s32)t.start - (s32)t.pregap, toc + 3);
+ LBAToMSF(t.start, toc + 7);
+ toc += 10;
+ }
+ write32(0xBFC, PSAR_DATA_OFFSET);
+ write32(0x1220, end + 0x2D31);
+ write32(0x1228, 0x7FF);
+ memcpy(psar + 0x122C, title_.data(), std::min(title_.size(), (size_t)0x7F));
+ write32(0x12AC, 3);
+
+ size_ = (s64)psarOffset_ + end;
+}
+
+size_t PS1EbootFileLoader::ReadAt(s64 absolutePos, size_t bytes, size_t count, void *data, Flags flags) {
+ if (!IsValid() || absolutePos < 0 || bytes == 0) {
+ return 0;
+ }
+ u8 *out = (u8 *)data;
+ size_t remaining = (size_t)std::min((s64)(bytes * count), std::max(size_ - absolutePos, (s64)0));
+ const size_t total = remaining;
+ s64 pos = absolutePos;
+ const s64 indexStart = (s64)head_.size();
+ const s64 dataStart = (s64)psarOffset_ + PSAR_DATA_OFFSET;
+ while (remaining > 0) {
+ size_t n;
+ if (pos < indexStart) {
+ n = std::min(remaining, (size_t)(indexStart - pos));
+ memcpy(out, head_.data() + pos, n);
+ } else if (pos < dataStart) {
+ const u32 entry = (u32)((pos - indexStart) / PSAR_INDEX_ENTRY_SIZE);
+ const u32 within = (u32)((pos - indexStart) % PSAR_INDEX_ENTRY_SIZE);
+ u8 buf[PSAR_INDEX_ENTRY_SIZE]{};
+ if (entry < numBlocks_) {
+ *(u32_le *)buf = entry * BLOCK_SIZE;
+ *(u16_le *)(buf + 4) = (u16)BLOCK_SIZE;
+ }
+ n = std::min(remaining, (size_t)(PSAR_INDEX_ENTRY_SIZE - within));
+ memcpy(out, buf + within, n);
+ } else {
+ const u32 block = (u32)((pos - dataStart) / BLOCK_SIZE);
+ const u32 within = (u32)((pos - dataStart) % BLOCK_SIZE);
+ if (within < sizeof(STORED_HEADER)) {
+ n = std::min(remaining, sizeof(STORED_HEADER) - within);
+ memcpy(out, STORED_HEADER + within, n);
+ } else {
+ const u32 offset = within - sizeof(STORED_HEADER);
+ n = std::min(remaining, (size_t)(BLOCK_DATA_SIZE - offset));
+ const u32 lba = block * BLOCK_SECTORS + offset / SECTOR_SIZE;
+ if (offset % SECTOR_SIZE == 0 && n % SECTOR_SIZE == 0) {
+ ReadRawSectors(lba, (u32)(n / SECTOR_SIZE), out);
+ } else {
+ const u32 first = offset % SECTOR_SIZE;
+ const u32 sectors = (u32)((first + n + SECTOR_SIZE - 1) / SECTOR_SIZE);
+ std::vector buf((size_t)sectors * SECTOR_SIZE);
+ ReadRawSectors(lba, sectors, buf.data());
+ memcpy(out, buf.data() + first, n);
+ }
+ }
+ }
+ out += n;
+ pos += n;
+ remaining -= n;
+ }
+ return total / bytes;
+}
+
+void PS1EbootFileLoader::Cancel() {
+ for (auto &file : files_) {
+ file->Cancel();
+ }
+}
diff --git a/Core/FileLoaders/PS1EbootFileLoader.h b/Core/FileLoaders/PS1EbootFileLoader.h
new file mode 100644
index 0000000000..fc8ff6b614
--- /dev/null
+++ b/Core/FileLoaders/PS1EbootFileLoader.h
@@ -0,0 +1,110 @@
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "Common/CommonTypes.h"
+#include "Core/Loaders.h"
+
+// A PS1 disc image (a cue sheet, a CloneCD .ccd, or a bare .bin/.img/.iso) presented as a converted
+// PS1 EBOOT.PBP, the kind popstation makes, for the firmware's POPS to run. Only the headers are kept
+// in memory. The disc image is stored uncompressed, as stored DEFLATE blocks, so a read of it goes
+// straight to the image's files. See docs/POPS.md.
+class PS1EbootFileLoader : public FileLoader {
+public:
+ explicit PS1EbootFileLoader(const Path &path);
+ ~PS1EbootFileLoader();
+
+ bool IsValid() const { return error_.empty(); }
+
+ bool Exists() override { return IsValid(); }
+ bool IsDirectory() override { return false; }
+ s64 FileSize() override { return size_; }
+ Path GetPath() const override { return path_; }
+ std::string GetFileExtension() const override { return ".pbp"; }
+ size_t ReadAt(s64 absolutePos, size_t bytes, size_t count, void *data, Flags flags = Flags::NONE) override;
+ void Cancel() override;
+ std::string LatestError() const override { return error_; }
+
+ const std::string &DiscID() const { return discID_; }
+ const std::string &Title() const { return title_; }
+
+private:
+ struct Track {
+ int number;
+ bool audio;
+ u32 start; // Disc LBA of INDEX 01.
+ u32 pregap; // Sectors before it that belong to the track (INDEX 00, or a generated PREGAP).
+ };
+ // A run of disc sectors, from a file or (file < 0) generated, all zeroes.
+ struct Extent {
+ u32 lba;
+ u32 count;
+ int file;
+ s64 offset;
+ int sectorSize; // 2352 raw, 2336 without sync and header, 2048 user data only.
+ };
+
+ bool ParseCue(const Path &cuePath, const std::string &text);
+ bool ParseCcd(const Path &ccdPath, const std::string &text);
+ bool OpenBare(const Path &path);
+ int AddFile(const Path &path);
+ void AddExtent(u32 lba, u32 count, int file, s64 offset, int sectorSize);
+
+ bool ReadRawSectors(u32 lba, u32 count, u8 *out);
+ bool ReadUserData(u32 lba, u8 *out);
+ void ReadDiscID();
+ void BuildHeaders();
+
+ Path path_;
+ std::string error_;
+ std::vector> files_;
+ std::vector extents_;
+ std::vector