mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-31 19:47:06 +01:00
POPS: Boot PS1 disc images as converted EBOOTs made on the fly
A cue sheet, a CloneCD .ccd, or a bare .bin, .img or .iso of a PS1 disc now boots on the firmware's POPS, as the converted EBOOT.PBP popstation would make from it. PS1EbootFileLoader builds the PBP header, PARAM.SFO (the disc ID from SYSTEM.CNF) and the PSISOIMG header, TOC and block index, and serves the disc image from the image's own files. Each block of 16 sectors is a stored DEFLATE block: the manager won't read an image of uncompressed blocks in its own format, but the converted PBP's inflate takes stored ones, and with every block the same size, reads map straight onto the files. Nothing is compressed or written anywhere. Cue sheets can have any number of files, AUDIO tracks, INDEX 00 and PREGAP, and a missing FILE falls back to the cue sheet's own name. A 2048-byte .iso gets Mode 2 Form 1 sectors made around its user data. The EBOOT is mounted as the only file in host0:, through BlobFileSystem, which now saves its open handles in savestates. The game browser lists .cue, .ccd, .bin and .img, leaving out the .bin and .img files a cue sheet or .ccd next to them describes, and a bare image opened directly goes through the cue sheet that names it. GameInfoCache reads the title and ID of these, and of real PS1 EBOOTs, from their PARAM.SFO. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
da92d72d81
commit
23dffa9db3
24 files changed
+1256
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -587,6 +587,7 @@
|
||||
<ClCompile Include="FileLoaders\DiskCachingFileLoader.cpp" />
|
||||
<ClCompile Include="FileLoaders\HTTPFileLoader.cpp" />
|
||||
<ClCompile Include="FileLoaders\LocalFileLoader.cpp" />
|
||||
<ClCompile Include="FileLoaders\PS1EbootFileLoader.cpp" />
|
||||
<ClCompile Include="FileLoaders\RamCachingFileLoader.cpp" />
|
||||
<ClCompile Include="FileLoaders\RetryingFileLoader.cpp" />
|
||||
<ClCompile Include="FileSystems\BlockDevices.cpp" />
|
||||
@@ -931,6 +932,7 @@
|
||||
<ClInclude Include="FileLoaders\DiskCachingFileLoader.h" />
|
||||
<ClInclude Include="FileLoaders\HTTPFileLoader.h" />
|
||||
<ClInclude Include="FileLoaders\LocalFileLoader.h" />
|
||||
<ClInclude Include="FileLoaders\PS1EbootFileLoader.h" />
|
||||
<ClInclude Include="FileLoaders\RamCachingFileLoader.h" />
|
||||
<ClInclude Include="FileLoaders\RetryingFileLoader.h" />
|
||||
<ClInclude Include="FileSystems\BlockDevices.h" />
|
||||
|
||||
@@ -619,6 +619,9 @@
|
||||
<ClCompile Include="FileLoaders\LocalFileLoader.cpp">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FileLoaders\PS1EbootFileLoader.cpp">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FileLoaders\HTTPFileLoader.cpp">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClCompile>
|
||||
@@ -1978,6 +1981,9 @@
|
||||
<ClInclude Include="FileLoaders\LocalFileLoader.h">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FileLoaders\PS1EbootFileLoader.h">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FileLoaders\HTTPFileLoader.h">
|
||||
<Filter>FileLoaders</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
|
||||
#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<FileLoader> 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<std::string> CueSheetFileNames(const Path &cuePath) {
|
||||
std::vector<std::string> names;
|
||||
std::string text;
|
||||
if (!ReadSmallTextFile(cuePath, &text)) {
|
||||
return names;
|
||||
}
|
||||
std::vector<std::string_view> 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<File::FileInfo> 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<File::FileInfo> *files) {
|
||||
std::set<std::string> 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<Extent> 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<CueTrack> tracks;
|
||||
};
|
||||
std::vector<CueFile> cueFiles;
|
||||
|
||||
std::vector<std::string_view> 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<std::string_view> 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<std::string_view> 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<u8> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<std::unique_ptr<FileLoader>> files_;
|
||||
std::vector<Extent> extents_;
|
||||
std::vector<Track> tracks_;
|
||||
u32 numSectors_ = 0;
|
||||
|
||||
std::string discID_;
|
||||
std::string title_;
|
||||
|
||||
// The PBP up to the PSAR's block index, then the index and the blocks, which are made on the fly.
|
||||
std::vector<u8> head_;
|
||||
u32 psarOffset_ = 0;
|
||||
u32 numBlocks_ = 0;
|
||||
s64 size_ = 0;
|
||||
};
|
||||
|
||||
namespace File {
|
||||
struct FileInfo;
|
||||
}
|
||||
|
||||
// Whether a .cue or .ccd describes a disc, by its contents.
|
||||
bool IsPS1CueOrCcd(FileLoader *fileLoader);
|
||||
|
||||
// The file names a cue sheet's FILE lines give.
|
||||
std::vector<std::string> CueSheetFileNames(const Path &cuePath);
|
||||
|
||||
// The cue sheet next to a disc image whose FILE lines name it, or an empty path.
|
||||
Path FindCueSheetFor(const Path &imagePath);
|
||||
|
||||
// Removes the .bin and .img files from a directory listing that a cue sheet or .ccd in it describes, so
|
||||
// that each game shows up once, and those that can't be a raw disc image (a size that isn't a whole
|
||||
// number of sectors).
|
||||
void RemoveDescribedDiscImages(std::vector<File::FileInfo> *files);
|
||||
@@ -15,6 +15,8 @@
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
#include "Common/Serialize/SerializeMap.h"
|
||||
#include "Core/FileSystems/BlobFileSystem.h"
|
||||
|
||||
BlobFileSystem::BlobFileSystem(IHandleAllocator *hAlloc, FileLoader *fileLoader, std::string alias)
|
||||
@@ -27,7 +29,11 @@ BlobFileSystem::~BlobFileSystem() {
|
||||
}
|
||||
|
||||
void BlobFileSystem::DoState(PointerWrap &p) {
|
||||
// Not used in real emulation.
|
||||
auto s = p.Section("BlobFileSystem", 1);
|
||||
if (!s)
|
||||
return;
|
||||
// The open handles and their positions. The blob is the one booted.
|
||||
Do(p, entries_);
|
||||
}
|
||||
|
||||
std::vector<PSPFileInfo> BlobFileSystem::GetDirListing(std::string_view path, bool *exists) {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// This is used for opening a debug file as a blob, and mounting it.
|
||||
// This is used for opening a file as a blob, and mounting it.
|
||||
// Importantly, uses a fileLoader for all access, so http:// URLs are supported.
|
||||
// As of writing, only used by GE dump replay.
|
||||
// Used by GE dump replay, and for the EBOOT a PS1 disc image is presented as.
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
@@ -235,6 +235,10 @@ static bool ReadDiscImage(const PBPReader &pbp, size_t size, u8 *out) {
|
||||
|
||||
// The content ID (DATA.PSP + 0x560, as in the license) and the disc image's PGD header.
|
||||
static bool ReadPBPDrmHeaders(u8 *pgdHeader, u8 *contentId) {
|
||||
// A converted one has no PGD, and one made from a disc image isn't at its path.
|
||||
if (g_popsConvertedPBP) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<FileLoader> loader(ConstructFileLoader(g_popsPBPPath));
|
||||
PBPReader pbp(loader.get());
|
||||
return pbp.IsValid() && pbp.ReadSubFile(PBP_EXECUTABLE_PSP, 0x560, 0x30, contentId) &&
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "Core/FileLoaders/DiskCachingFileLoader.h"
|
||||
#include "Core/FileLoaders/HTTPFileLoader.h"
|
||||
#include "Core/FileLoaders/LocalFileLoader.h"
|
||||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||||
#include "Core/FileLoaders/RetryingFileLoader.h"
|
||||
#include "Core/FileLoaders/ZipFileLoader.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
@@ -111,6 +112,9 @@ IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorStrin
|
||||
bool isDiscImage = false;
|
||||
if (extension == ".iso" || extension == ".cso" || extension == ".chd") {
|
||||
isDiscImage = true;
|
||||
} else if (extension == ".cue" || extension == ".ccd") {
|
||||
// A PS1 disc, which boots as a converted EBOOT (see PS1EbootFileLoader).
|
||||
return IsPS1CueOrCcd(fileLoader) ? IdentifiedFileType::PSX_ISO : IdentifiedFileType::UNKNOWN;
|
||||
} else if (extension == ".ppst") {
|
||||
return IdentifiedFileType::PPSSPP_SAVESTATE;
|
||||
} else if (extension == ".ppdmp") {
|
||||
@@ -302,6 +306,17 @@ FileLoader *ResolveFileLoaderTarget(FileLoader *fileLoader, IdentifiedFileType *
|
||||
// Re-identify the file.
|
||||
*fileType = Identify_File(fileLoader, errorString);
|
||||
}
|
||||
} else if (*fileType == IdentifiedFileType::PSX_ISO) {
|
||||
// A PS1 disc image runs on the firmware's POPS, as a converted EBOOT made from it.
|
||||
PS1EbootFileLoader *eboot = new PS1EbootFileLoader(fileLoader->GetPath());
|
||||
if (!eboot->IsValid()) {
|
||||
*errorString = eboot->LatestError();
|
||||
delete eboot;
|
||||
return fileLoader;
|
||||
}
|
||||
delete fileLoader;
|
||||
*fileType = IdentifiedFileType::PSP_PS1_PBP;
|
||||
return eboot;
|
||||
} else if (*fileType == IdentifiedFileType::ARCHIVE_ZIP) {
|
||||
// Handle zip files, take automatic action depending on contents.
|
||||
// Can also return nullptr.
|
||||
|
||||
+11
-4
@@ -35,6 +35,7 @@
|
||||
#include "Core/ELF/ParamSFO.h"
|
||||
#include "Core/ELF/PBPReader.h"
|
||||
#include "Core/ELF/PrxDecrypter.h"
|
||||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||||
|
||||
#include "Core/FileSystems/BlockDevices.h"
|
||||
#include "Core/FileSystems/BlobFileSystem.h"
|
||||
@@ -671,14 +672,20 @@ bool Load_PSP_POPS(FileLoader *fileLoader, std::string *error_string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Its directory as host0:, as for a homebrew EBOOT.
|
||||
// Its directory as host0:, as for a homebrew EBOOT. An EBOOT made from a disc image is the only file there.
|
||||
const Path fullPath = fileLoader->GetPath();
|
||||
auto fs = std::make_shared<DirectoryFileSystem>(&pspFileSystem, fullPath.NavigateUp(), FileSystemFlags::SIMULATE_FAT32 | FileSystemFlags::CARD);
|
||||
pspFileSystem.Mount("host0:", fs);
|
||||
std::string gamePath;
|
||||
if (dynamic_cast<PS1EbootFileLoader *>(fileLoader)) {
|
||||
pspFileSystem.Mount("host0:", std::make_shared<BlobFileSystem>(&pspFileSystem, fileLoader, "EBOOT.PBP"));
|
||||
gamePath = "host0:/EBOOT.PBP";
|
||||
} else {
|
||||
auto fs = std::make_shared<DirectoryFileSystem>(&pspFileSystem, fullPath.NavigateUp(), FileSystemFlags::SIMULATE_FAT32 | FileSystemFlags::CARD);
|
||||
pspFileSystem.Mount("host0:", fs);
|
||||
gamePath = "host0:/" + fullPath.GetFilename();
|
||||
}
|
||||
pspFileSystem.SetStartingDirectory("host0:/");
|
||||
|
||||
__PopsBootInit();
|
||||
const std::string gamePath = "host0:/" + fullPath.GetFilename();
|
||||
INFO_LOG(Log::Loader, "POPS: booting %s through %s", gamePath.c_str(), popsmanPath);
|
||||
if (!__KernelLoadExec(currentMIPS, popsmanPath, 0, error_string)) {
|
||||
return false;
|
||||
|
||||
+6
-1
@@ -288,7 +288,12 @@ static void GetBootError(IdentifiedFileType type, std::string *errorString) {
|
||||
|
||||
case IdentifiedFileType::ARCHIVE_7Z: *errorString = "7z file detected (Require 7-Zip)"; break;
|
||||
case IdentifiedFileType::PSP_PKG: *errorString = "PKG game updates need to be installed, not booted."; break;
|
||||
case IdentifiedFileType::PSX_ISO: *errorString = "PSX game image detected."; break;
|
||||
case IdentifiedFileType::PSX_ISO:
|
||||
// It only gets here if it couldn't be made into an EBOOT, which says why.
|
||||
if (errorString->empty()) {
|
||||
*errorString = "PSX game image detected.";
|
||||
}
|
||||
break;
|
||||
case IdentifiedFileType::PS2_ISO: *errorString = "PS2 game image detected."; break;
|
||||
case IdentifiedFileType::PS3_ISO: *errorString = "PS2 game image detected."; break;
|
||||
case IdentifiedFileType::NORMAL_DIRECTORY: *errorString = "Just a directory."; break;
|
||||
|
||||
+4
-1
@@ -38,6 +38,7 @@
|
||||
#include "Core/Util/RecentFiles.h"
|
||||
#include "Core/HLE/sceCtrl.h"
|
||||
#include "Core/ELF/PBPReader.h"
|
||||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/Util/VideoPlayer.h"
|
||||
|
||||
@@ -1116,7 +1117,9 @@ void GameBrowser::Refresh() {
|
||||
}
|
||||
} else if (!listingPending_) {
|
||||
std::vector<File::FileInfo> fileInfo;
|
||||
path_.GetListing(fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:");
|
||||
path_.GetListing(fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:cue:ccd:bin:img:");
|
||||
// PS1 discs: a .bin or .img shows up as its cue sheet or .ccd, when it has one.
|
||||
RemoveDescribedDiscImages(&fileInfo);
|
||||
for (size_t i = 0; i < fileInfo.size(); i++) {
|
||||
bool isGame = !fileInfo[i].isDirectory;
|
||||
bool isSaveData = false;
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "Common/System/Request.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||||
#include "Core/FileSystems/ISOFileSystem.h"
|
||||
#include "Core/FileSystems/DirectoryFileSystem.h"
|
||||
#include "Core/FileSystems/VirtualDiscFileSystem.h"
|
||||
@@ -597,6 +598,8 @@ public:
|
||||
switch (fileType) {
|
||||
case IdentifiedFileType::PSP_PBP:
|
||||
case IdentifiedFileType::PSP_PBP_DIRECTORY:
|
||||
case IdentifiedFileType::PSP_PS1_PBP:
|
||||
case IdentifiedFileType::PSX_ISO:
|
||||
{
|
||||
auto pbpLoader = info_->GetFileLoader();
|
||||
if (fileType == IdentifiedFileType::PSP_PBP_DIRECTORY) {
|
||||
@@ -604,6 +607,9 @@ public:
|
||||
if (ebootPath != gamePath_) {
|
||||
pbpLoader.reset(ConstructFileLoader(ebootPath));
|
||||
}
|
||||
} else if (fileType == IdentifiedFileType::PSX_ISO) {
|
||||
// The EBOOT a PS1 disc image boots as, which has its title and disc ID.
|
||||
pbpLoader.reset(new PS1EbootFileLoader(gamePath_));
|
||||
}
|
||||
|
||||
if (!pbpLoader->Exists()) {
|
||||
|
||||
@@ -233,7 +233,6 @@ static bool FileTypeIsPlayable(IdentifiedFileType fileType) {
|
||||
switch (fileType) {
|
||||
case IdentifiedFileType::ERROR_IDENTIFYING:
|
||||
case IdentifiedFileType::UNKNOWN:
|
||||
case IdentifiedFileType::PSX_ISO:
|
||||
case IdentifiedFileType::PS2_ISO:
|
||||
case IdentifiedFileType::PS3_ISO:
|
||||
case IdentifiedFileType::UNKNOWN_BIN:
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
<ClInclude Include="..\..\Core\FileLoaders\DiskCachingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\HTTPFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\LocalFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\PS1EbootFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\RamCachingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\RetryingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\ZipFileLoader.h" />
|
||||
@@ -450,6 +451,7 @@
|
||||
<ClCompile Include="..\..\Core\FileLoaders\DiskCachingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\HTTPFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\LocalFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\PS1EbootFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\RamCachingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\RetryingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\ZipFileLoader.cpp" />
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<ClCompile Include="..\..\Core\FileLoaders\DiskCachingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\HTTPFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\LocalFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\PS1EbootFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\RamCachingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\RetryingFileLoader.cpp" />
|
||||
<ClCompile Include="..\..\Core\FileLoaders\ZipFileLoader.cpp" />
|
||||
@@ -477,6 +478,7 @@
|
||||
<ClInclude Include="..\..\Core\FileLoaders\DiskCachingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\HTTPFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\LocalFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\PS1EbootFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\RamCachingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\RetryingFileLoader.h" />
|
||||
<ClInclude Include="..\..\Core\FileLoaders\ZipFileLoader.h" />
|
||||
|
||||
@@ -690,6 +690,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/Core/FileLoaders/DiskCachingFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/HTTPFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/LocalFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/PS1EbootFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/RamCachingFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/RetryingFileLoader.cpp \
|
||||
$(SRC)/Core/FileLoaders/ZipFileLoader.cpp \
|
||||
@@ -1131,6 +1132,7 @@ ifeq ($(UNITTEST),1)
|
||||
$(SRC)/unittest/TestSplineTessellation.cpp \
|
||||
$(SRC)/unittest/TestGEMath.cpp \
|
||||
$(SRC)/unittest/TestZipSlip.cpp \
|
||||
$(SRC)/unittest/TestPS1Eboot.cpp \
|
||||
$(SRC)/unittest/UnitTest.cpp
|
||||
|
||||
include $(BUILD_EXECUTABLE)
|
||||
|
||||
@@ -58,6 +58,22 @@ A converted EBOOT (popstation and similar) has a plain disc image of DEFLATE blo
|
||||
does, the emulator's decoder calls go to an inflate, its CD audio index check takes the full-length branch,
|
||||
and the two MAC checks (+0A28, +0AB8) pass.
|
||||
|
||||
## Disc images
|
||||
|
||||
A PS1 disc image boots as the converted EBOOT it would make (`Core/FileLoaders/PS1EbootFileLoader.cpp`),
|
||||
which never exists as a whole. It takes a cue sheet (with any number of files, AUDIO tracks, INDEX 00
|
||||
and PREGAP), a CloneCD `.ccd` with its `.img`, or a bare raw (2352-byte sector) `.bin`, `.img` or `.iso`,
|
||||
or a 2048-byte `.iso`, around whose user data Mode 2 Form 1 sectors are made (with no EDC/ECC). A bare
|
||||
image that a cue sheet next to it names, or that has a `.ccd` of the same name, is read through that.
|
||||
The disc ID is the one SYSTEM.CNF boots.
|
||||
|
||||
It's laid out as popstation lays out a PSISOIMG: the disc ID at +0x400, the TOC at +0x800, a block
|
||||
index at +0x4000 and the blocks from +0x100000, each of 16 sectors. The manager doesn't read the disc
|
||||
image of one whose blocks are its own uncompressed size (0x9300), so each block is a stored DEFLATE
|
||||
block, which the inflate above decodes: 0x9305 bytes, so reads of the image map straight onto its files.
|
||||
There are no icons, pictures or DATA.PSP, and none of the STARTDAT that follows a popstation image. The
|
||||
EBOOT is the only file in `host0:`.
|
||||
|
||||
For any EBOOT, also as popcorn does: the manual's name check passes, and an ICON0 the emulator can't use
|
||||
(anything but an 80x80 PNG, such as a missing one or a PSP game's 144x80 one, which crashes it) reads as a
|
||||
blank 80x80 one.
|
||||
@@ -78,5 +94,7 @@ The HOME button opens the emulator's menu. Savestates work.
|
||||
- The anti-LibCrypt word some PAL games need in a converted image (PSISOIMG + 0x12B0). ARK-4's popcorn has
|
||||
a table of them by disc ID, switched off. Converting tools can patch the image instead.
|
||||
- ARK-4's per-game CONFIG.BIN next to the EBOOT, copied over the image's config (PSISOIMG + 0x420).
|
||||
- Multi-disc games from disc images (PSTITLEIMG, which has up to five PSISOIMGs), and `.mds`/`.mdf`, CHD
|
||||
and archived images.
|
||||
- A byte popcorn changes in reads of converted images (0x41B, from 0x27 to 0x55 when 0x41B-0x41E read
|
||||
27 19 22 41) without knowing why.
|
||||
@@ -724,6 +724,7 @@ SOURCES_CXX += \
|
||||
$(COREDIR)/FileLoaders/RetryingFileLoader.cpp \
|
||||
$(COREDIR)/FileLoaders/RamCachingFileLoader.cpp \
|
||||
$(COREDIR)/FileLoaders/LocalFileLoader.cpp \
|
||||
$(COREDIR)/FileLoaders/PS1EbootFileLoader.cpp \
|
||||
$(COREDIR)/FileLoaders/ZipFileLoader.cpp \
|
||||
$(COREDIR)/CoreTiming.cpp \
|
||||
$(COREDIR)/CwCheat.cpp \
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// 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/.
|
||||
|
||||
// PS1EbootFileLoader: small disc images in a temp directory, read back through the EBOOT made from them.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/File/DirListing.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/Path.h"
|
||||
#include "Core/ELF/PBPReader.h"
|
||||
#include "Core/ELF/ParamSFO.h"
|
||||
#include "Core/FileLoaders/PS1EbootFileLoader.h"
|
||||
#include "Core/Loaders.h"
|
||||
|
||||
#include "UnitTest.h"
|
||||
|
||||
static const u32 SECTOR = 2352;
|
||||
static const u32 BLOCK = 16 * SECTOR + 5;
|
||||
static const u8 SYNC[12] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 };
|
||||
|
||||
static void Put32(u8 *p, u32 v) {
|
||||
p[0] = v & 0xFF;
|
||||
p[1] = (v >> 8) & 0xFF;
|
||||
p[2] = (v >> 16) & 0xFF;
|
||||
p[3] = v >> 24;
|
||||
}
|
||||
|
||||
static u32 Get32(const u8 *p) {
|
||||
return p[0] | (p[1] << 8) | (p[2] << 16) | ((u32)p[3] << 24);
|
||||
}
|
||||
|
||||
static u8 BCD(u32 v) {
|
||||
return (u8)(((v / 10) << 4) | (v % 10));
|
||||
}
|
||||
|
||||
// The user data of a small Mode 2 Form 1 data track: a PVD at 16, its root directory at 18 with one
|
||||
// SYSTEM.CNF at 20, and a pattern everywhere else.
|
||||
static std::vector<u8> MakeUserData(u32 sectors) {
|
||||
std::vector<u8> data(sectors * 2048);
|
||||
for (size_t i = 0; i < data.size(); i++) {
|
||||
data[i] = (u8)(i * 7 + i / 2048);
|
||||
}
|
||||
u8 *pvd = &data[16 * 2048];
|
||||
memset(pvd, 0, 2048);
|
||||
memcpy(pvd, "\x01" "CD001", 6);
|
||||
Put32(pvd + 156 + 2, 18);
|
||||
Put32(pvd + 156 + 10, 2048);
|
||||
u8 *dir = &data[18 * 2048];
|
||||
memset(dir, 0, 2048);
|
||||
const char name[] = "SYSTEM.CNF;1";
|
||||
dir[0] = (u8)(33 + sizeof(name) - 1 + 1);
|
||||
Put32(dir + 2, 20);
|
||||
const std::string cnf = "BOOT = cdrom:\\SLUS_123.45;1\r\nTCB = 4\r\n";
|
||||
Put32(dir + 10, (u32)cnf.size());
|
||||
dir[32] = (u8)(sizeof(name) - 1);
|
||||
memcpy(dir + 33, name, sizeof(name) - 1);
|
||||
memset(&data[20 * 2048], 0, 2048);
|
||||
memcpy(&data[20 * 2048], cnf.data(), cnf.size());
|
||||
return data;
|
||||
}
|
||||
|
||||
// Raw sectors around user data, as a .bin has them. The EDC and ECC are a pattern, which nothing checks.
|
||||
static std::vector<u8> MakeRawTrack(const std::vector<u8> &user) {
|
||||
const u32 sectors = (u32)(user.size() / 2048);
|
||||
std::vector<u8> raw(sectors * SECTOR);
|
||||
for (u32 s = 0; s < sectors; s++) {
|
||||
u8 *p = &raw[s * SECTOR];
|
||||
memcpy(p, SYNC, 12);
|
||||
const u32 f = s + 150;
|
||||
p[12] = BCD(f / 4500);
|
||||
p[13] = BCD(f / 75 % 60);
|
||||
p[14] = BCD(f % 75);
|
||||
p[15] = 2;
|
||||
static const u8 subheader[8] = { 0, 0, 8, 0, 0, 0, 8, 0 };
|
||||
memcpy(p + 16, subheader, 8);
|
||||
memcpy(p + 24, &user[s * 2048], 2048);
|
||||
for (u32 i = 24 + 2048; i < SECTOR; i++) {
|
||||
p[i] = (u8)(s + i);
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
static std::vector<u8> MakeAudio(u32 sectors) {
|
||||
std::vector<u8> audio(sectors * SECTOR);
|
||||
for (size_t i = 0; i < audio.size(); i++) {
|
||||
audio[i] = (u8)(i * 13 + 5);
|
||||
}
|
||||
return audio;
|
||||
}
|
||||
|
||||
static std::vector<u8> ReadAll(FileLoader *loader, s64 pos, size_t size) {
|
||||
std::vector<u8> out(size);
|
||||
out.resize(loader->ReadAt(pos, size, out.data()));
|
||||
return out;
|
||||
}
|
||||
|
||||
// The PSAR's disc image as it should be: each block of 16 sectors, stored, after its DEFLATE header.
|
||||
static std::vector<u8> ExpectedBlocks(const std::vector<u8> &disc) {
|
||||
const u32 blocks = (u32)((disc.size() / SECTOR + 15) / 16);
|
||||
std::vector<u8> out;
|
||||
for (u32 b = 0; b < blocks; b++) {
|
||||
static const u8 header[5] = { 0x01, 0x00, 0x93, 0xFF, 0x6C };
|
||||
out.insert(out.end(), header, header + 5);
|
||||
for (u32 i = 0; i < 16 * SECTOR; i++) {
|
||||
const size_t pos = (size_t)b * 16 * SECTOR + i;
|
||||
out.push_back(pos < disc.size() ? disc[pos] : 0);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool CheckEboot(PS1EbootFileLoader &eboot, const std::vector<u8> &disc, u32 numTracks) {
|
||||
EXPECT_TRUE(eboot.IsValid());
|
||||
EXPECT_EQ_STR(eboot.DiscID(), std::string("SLUS12345"));
|
||||
|
||||
std::string error;
|
||||
EXPECT_TRUE(Identify_File(&eboot, &error) == IdentifiedFileType::PSP_PS1_PBP);
|
||||
PBPReader pbp(&eboot);
|
||||
EXPECT_TRUE(pbp.IsValid());
|
||||
std::vector<u8> sfoData;
|
||||
EXPECT_TRUE(pbp.GetSubFile(PBP_PARAM_SFO, &sfoData));
|
||||
ParamSFOData sfo;
|
||||
EXPECT_TRUE(sfo.ReadSFO(sfoData));
|
||||
EXPECT_EQ_STR(sfo.GetValueString("CATEGORY"), std::string("ME"));
|
||||
EXPECT_EQ_STR(sfo.GetValueString("DISC_ID"), std::string("SLUS12345"));
|
||||
|
||||
const std::vector<u8> header = ReadAll(&eboot, 0, 0x28);
|
||||
EXPECT_EQ_INT(header.size(), 0x28);
|
||||
const u32 psar = Get32(&header[0x24]);
|
||||
const std::vector<u8> psarHeader = ReadAll(&eboot, psar, 0x1300);
|
||||
EXPECT_EQ_MEM(&psarHeader[0], "PSISOIMG0000", 12);
|
||||
EXPECT_EQ_MEM(&psarHeader[0x400], "_SLUS_12345", 12);
|
||||
|
||||
// The TOC: the last track, the lead-out (at 00:02:00 plus the disc's length), and the tracks.
|
||||
const u32 sectors = (u32)(disc.size() / SECTOR);
|
||||
const u8 *toc = &psarHeader[0x800];
|
||||
EXPECT_EQ_INT(toc[0], 0x41);
|
||||
EXPECT_EQ_INT(toc[2], 0xA0);
|
||||
EXPECT_EQ_INT(toc[10 + 2], 0xA1);
|
||||
EXPECT_EQ_INT(toc[10 + 7], BCD(numTracks));
|
||||
EXPECT_EQ_INT(toc[20 + 2], 0xA2);
|
||||
EXPECT_EQ_INT(toc[20 + 8], BCD((sectors + 150) / 75 % 60));
|
||||
EXPECT_EQ_INT(toc[20 + 9], BCD((sectors + 150) % 75));
|
||||
EXPECT_EQ_INT(toc[30 + 2], 0x01);
|
||||
EXPECT_EQ_INT(toc[30 + 8], 0x02);
|
||||
EXPECT_EQ_INT(toc[30 + 9], 0x00);
|
||||
|
||||
// The index, then the blocks: all of them at once, and reads that start and end mid-sector and
|
||||
// cross blocks, as POPS's 0x200-aligned ones do.
|
||||
const u32 blocks = (sectors + 15) / 16;
|
||||
const std::vector<u8> index = ReadAll(&eboot, psar + 0x4000, (blocks + 1) * 32);
|
||||
for (u32 b = 0; b < blocks; b++) {
|
||||
EXPECT_EQ_INT(Get32(&index[b * 32]), b * BLOCK);
|
||||
EXPECT_EQ_INT(index[b * 32 + 4] | (index[b * 32 + 5] << 8), BLOCK);
|
||||
}
|
||||
EXPECT_EQ_INT(Get32(&index[blocks * 32]), 0);
|
||||
const std::vector<u8> expected = ExpectedBlocks(disc);
|
||||
const s64 data = psar + 0x100000;
|
||||
EXPECT_EQ_INT(eboot.FileSize(), data + expected.size());
|
||||
const std::vector<u8> all = ReadAll(&eboot, data, expected.size() + 100);
|
||||
EXPECT_EQ_INT(all.size(), expected.size());
|
||||
EXPECT_TRUE(all == expected);
|
||||
const size_t starts[] = { 0, 3, 0xB3, BLOCK - 1, BLOCK + 5 + SECTOR - 7, expected.size() - 0x300 };
|
||||
for (size_t start : starts) {
|
||||
const size_t len = std::min((size_t)0x9400, expected.size() - start);
|
||||
const std::vector<u8> part = ReadAll(&eboot, data + start, len);
|
||||
EXPECT_EQ_INT(part.size(), len);
|
||||
EXPECT_TRUE(memcmp(part.data(), &expected[start], len) == 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TestPS1Eboot() {
|
||||
const Path dir = UnitTestTempPath("unittest_ps1eboot");
|
||||
File::DeleteDirRecursively(dir);
|
||||
EXPECT_TRUE(File::CreateFullPath(dir));
|
||||
|
||||
const std::vector<u8> user = MakeUserData(40);
|
||||
const std::vector<u8> data = MakeRawTrack(user);
|
||||
const std::vector<u8> audio = MakeAudio(20);
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, data.data(), data.size(), dir / "Game (Track 1).bin"));
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, audio.data(), audio.size(), dir / "Game (Track 2).bin"));
|
||||
|
||||
// A data track and an audio track in two files, the audio track's pregap ripped (INDEX 00).
|
||||
const std::string cue =
|
||||
"FILE \"Game (Track 1).bin\" BINARY\r\n"
|
||||
" TRACK 01 MODE2/2352\r\n"
|
||||
" INDEX 01 00:00:00\r\n"
|
||||
"FILE \"Game (Track 2).bin\" BINARY\r\n"
|
||||
" TRACK 02 AUDIO\r\n"
|
||||
" INDEX 00 00:00:00\r\n"
|
||||
" INDEX 01 00:00:05\r\n";
|
||||
EXPECT_TRUE(File::WriteStringToFile(true, cue, dir / "Game.cue"));
|
||||
std::vector<u8> disc = data;
|
||||
disc.insert(disc.end(), audio.begin(), audio.end());
|
||||
{
|
||||
PS1EbootFileLoader eboot(dir / "Game.cue");
|
||||
if (!CheckEboot(eboot, disc, 2)) {
|
||||
return false;
|
||||
}
|
||||
EXPECT_EQ_STR(eboot.Title(), std::string("Game"));
|
||||
// Track 2's pregap starts at LBA 40, and the track at 45.
|
||||
const std::vector<u8> toc = ReadAll(&eboot, Get32(&ReadAll(&eboot, 0, 0x28)[0x24]) + 0x800 + 40, 10);
|
||||
EXPECT_EQ_INT(toc[0], 0x01);
|
||||
EXPECT_EQ_INT(toc[2], 0x02);
|
||||
EXPECT_EQ_INT(toc[5], BCD(190 % 75));
|
||||
EXPECT_EQ_INT(toc[9], BCD(195 % 75));
|
||||
}
|
||||
// The first file opened on its own: the cue sheet that names it describes it.
|
||||
{
|
||||
EXPECT_TRUE(FindCueSheetFor(dir / "Game (Track 1).bin") == dir / "Game.cue");
|
||||
PS1EbootFileLoader eboot(dir / "Game (Track 1).bin");
|
||||
if (!CheckEboot(eboot, disc, 2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A PREGAP that isn't in the file: generated silence before the track.
|
||||
const std::string pregapCue =
|
||||
"FILE \"Game (Track 1).bin\" BINARY\r\n"
|
||||
" TRACK 01 MODE2/2352\r\n"
|
||||
" INDEX 01 00:00:00\r\n"
|
||||
"FILE \"Game (Track 2).bin\" BINARY\r\n"
|
||||
" TRACK 02 AUDIO\r\n"
|
||||
" PREGAP 00:00:03\r\n"
|
||||
" INDEX 01 00:00:00\r\n";
|
||||
EXPECT_TRUE(File::WriteStringToFile(true, pregapCue, dir / "Pregap.cue"));
|
||||
{
|
||||
std::vector<u8> gapped = data;
|
||||
gapped.resize(gapped.size() + 3 * SECTOR, 0);
|
||||
gapped.insert(gapped.end(), audio.begin(), audio.end());
|
||||
PS1EbootFileLoader eboot(dir / "Pregap.cue");
|
||||
if (!CheckEboot(eboot, gapped, 2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// A CloneCD image, and a bare .iso of only the user data, whose sectors are made around it.
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, data.data(), data.size(), dir / "Clone.img"));
|
||||
EXPECT_TRUE(File::WriteStringToFile(true, "[CloneCD]\r\nVersion=3\r\n[TRACK 1]\r\nMODE=2\r\nINDEX 1=0\r\n", dir / "Clone.ccd"));
|
||||
{
|
||||
PS1EbootFileLoader eboot(dir / "Clone.ccd");
|
||||
if (!CheckEboot(eboot, data, 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, user.data(), user.size(), dir / "Cooked.iso"));
|
||||
{
|
||||
std::vector<u8> cooked = data;
|
||||
for (u32 s = 0; s < 40; s++) {
|
||||
memset(&cooked[s * SECTOR + 24 + 2048], 0, SECTOR - 24 - 2048);
|
||||
}
|
||||
PS1EbootFileLoader eboot(dir / "Cooked.iso");
|
||||
if (!CheckEboot(eboot, cooked, 1)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The browser shows each disc once: by its cue sheet or .ccd, and a .bin of its own only if it
|
||||
// could be a disc image.
|
||||
const std::vector<u8> loose(3 * SECTOR, 1);
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, loose.data(), loose.size(), dir / "Loose.bin"));
|
||||
EXPECT_TRUE(File::WriteDataToFile(false, loose.data(), 100, dir / "Junk.bin"));
|
||||
std::vector<File::FileInfo> files;
|
||||
File::GetFilesInDir(dir, &files);
|
||||
RemoveDescribedDiscImages(&files);
|
||||
std::vector<std::string> names;
|
||||
for (const File::FileInfo &info : files) {
|
||||
names.push_back(info.name);
|
||||
}
|
||||
std::sort(names.begin(), names.end());
|
||||
const std::vector<std::string> expectedNames = { "Clone.ccd", "Cooked.iso", "Game.cue", "Loose.bin", "Pregap.cue" };
|
||||
EXPECT_TRUE(names == expectedNames);
|
||||
|
||||
File::DeleteDirRecursively(dir);
|
||||
return true;
|
||||
}
|
||||
@@ -3551,6 +3551,7 @@ bool TestIRPassSimplify();
|
||||
bool TestThreadManager();
|
||||
bool TestVFS();
|
||||
bool TestZipSlip();
|
||||
bool TestPS1Eboot();
|
||||
bool TestLzrc();
|
||||
bool TestCwCheat();
|
||||
bool TestMpegCsc();
|
||||
@@ -4076,6 +4077,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(Lang),
|
||||
TEST_ITEM(CmdLine),
|
||||
TEST_ITEM(ZipSlip),
|
||||
TEST_ITEM(PS1Eboot),
|
||||
TEST_ITEM(Lzrc),
|
||||
TEST_ITEM(CwCheat),
|
||||
TEST_ITEM(MpegCsc),
|
||||
|
||||
@@ -306,6 +306,7 @@
|
||||
<ClCompile Include="TestVertexJit.cpp" />
|
||||
<ClCompile Include="TestVFS.cpp" />
|
||||
<ClCompile Include="TestZipSlip.cpp" />
|
||||
<ClCompile Include="TestPS1Eboot.cpp" />
|
||||
<ClCompile Include="UnitTest.cpp" />
|
||||
<ClCompile Include="TestArmEmitter.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">true</ExcludedFromBuild>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ClCompile Include="TestRiscVEmitter.cpp" />
|
||||
<ClCompile Include="TestVFS.cpp" />
|
||||
<ClCompile Include="TestZipSlip.cpp" />
|
||||
<ClCompile Include="TestPS1Eboot.cpp" />
|
||||
<ClCompile Include="TestLoongArch64Emitter.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in new issue
Block a user