diff --git a/tools/README.md b/tools/README.md index e67393433..33427f767 100644 --- a/tools/README.md +++ b/tools/README.md @@ -3,7 +3,7 @@ This directory contains tools designed to help with the PCSX-Redux project somehow. The top level, directly usable tools are: * [exe2elf](exe2elf) - Converts a PS-EXE executable to an ELF file, which can be useful for loading and debugging through gdb. -* [exe2iso](exe2iso) - Converts a PS-EXE executable to a minimally bootable ISO file. The generated iso will not be conformant to the ISO9660 standard, but it will be bootable on a retail PlayStation 1. +* [exe2iso](exe2iso) - Converts a PS-EXE executable to a bootable ISO file containing a single PSX.EXE, conformant to the ISO9660 standard and bootable on a retail PlayStation 1. * [ghidra_scripts](ghidra_scripts) - A collection of Ghidra scripts that can be used to integrate some parts of PCSX-Redux into Ghidra and vice versa. * [ps1-packer](ps1-packer) - A tool for compressing PlayStation 1 executables into a single self-decompressing binary in various formats. * [psyq-obj-parser](psyq-obj-parser) - A tool for parsing the object files produced by the Psy-Q SDK, and converting them to ELF files. diff --git a/tools/exe2iso/README.md b/tools/exe2iso/README.md index 2d8558dfe..920d957f7 100644 --- a/tools/exe2iso/README.md +++ b/tools/exe2iso/README.md @@ -1,14 +1,16 @@ # EXE2ISO -This tool is more of a code golf than anything. It can take a ps-exe and create a bootable iso from it. The generated iso is not going to be valid according to the iso9660 standard, but will have enough data to be usable by the PS1 bios for booting, which should work in many emulators, the real hardware, and some ODEs. As a result, the image will be highly compressible, but many PC tools will fail to process it properly. +This tool takes a ps-exe and creates a bootable iso from it. The output is a standards +conformant ISO9660 image with a single `PSX.EXE;1` file in its root directory, built with +the same ISO9660 authoring code as the rest of PCSX-Redux. It boots in emulators, on real +hardware, and on ODEs. The sectors carry proper EDC/ECC, so PC tools that expect a valid +ISO9660 filesystem will read it just fine. ## arguments -Usage: exe2iso input.ps-exe [-offset value] [-pad] [-regen] [-license file] -o output.bin +Usage: exe2iso input.ps-exe [-license file] [-nopad] -o output.bin | Argument | Type | Description | |-|-|-| | input.ps-exe | mandatory | Specify the input ps-exe file. | | -o output.bin | mandatory | Name of the output file. | -| -offset value | optional | Move the exe data by value sectors. This can be useful to inject data at a known location. The default location for the exe is sector 19. | -| -pad | optional | Pads the iso with 150 blank sectors. This should only be useful when writing the iso to an actual disk, so the mechacon doesn't get confused when seeking close to the end. But since the read will be streaming from the beginning, this shouldn't be necessary. | -| -regen | optional | Generates proper ECC/EDC. This shouldn't be needed in most cases. Some emulators will be fussy and will want this data to be correct. Also some CD writing software will want this data to be correct. | | -license file | optional | Use this license file. Some emulators will want a proper license to recognize the disk. Also, Japanese consoles require a valid Japanese license to boot a disk properly. The file can either be from the official sdk, or a valid iso file from an existing game. | +| -nopad | optional | Don't append the 150 trailing blank sectors. Padding is on by default: real drives read ahead past the last data sector, so a couple of seconds of blank sectors keep the mechacon from choking near the end of the disc when burning the image. The padding sits past the end of the volume, so it's physical only and doesn't affect the filesystem. | diff --git a/tools/exe2iso/exe2iso.cc b/tools/exe2iso/exe2iso.cc index a049d0178..2ba29699e 100644 --- a/tools/exe2iso/exe2iso.cc +++ b/tools/exe2iso/exe2iso.cc @@ -18,74 +18,18 @@ ***************************************************************************/ #include +#include #include "flags.h" #include "fmt/format.h" -#include "iec-60908b/edcecc.h" #include "support/file.h" -#include "supportpsx/iec-60908b.h" +#include "supportpsx/iso9660-builder.h" +#include "supportpsx/iso9660-lowlevel.h" -static void storeU32(uint32_t value, uint8_t* buffer) { - buffer[0] = value & 0xff; - buffer[1] = (value >> 8) & 0xff; - buffer[2] = (value >> 16) & 0xff; - buffer[3] = (value >> 24) & 0xff; -} - -// make sure to call this with a sector that's memset to 0, as it relies on zeros being -// in the right place. -static void getSectorMinimal(uint8_t data[2048], uint32_t lba, uint32_t exeSize, uint32_t exeOffset = 19) { - switch (lba) { - // Minimal PVD - case 16: - data[0] = 1; - data[1] = 'C'; - data[2] = 'D'; - data[3] = '0'; - data[4] = '0'; - data[5] = '1'; - storeU32(1, data + 132); - storeU32(17, data + 140); - storeU32(18, data + 158); - break; - // Minimal path table - case 17: - data[0] = 1; - data[2] = 18; - data[6] = 1; - break; - // Minimal root directory - case 18: - data[0] = 42; - storeU32(exeOffset, data + 2); - storeU32(exeSize, data + 10); - data[32] = 9; - data[33] = 'P'; - data[34] = 'S'; - data[35] = 'X'; - data[36] = '.'; - data[37] = 'E'; - data[38] = 'X'; - data[39] = 'E'; - data[40] = ';'; - data[41] = '1'; - break; - } -} - -// Call this once on a sector that's been memset to 0, -// in order to set all of the immutable data. -static void makeHeaderOnce(uint8_t sector[2352]) { - memset(sector + 1, 0xff, 10); - sector[15] = 2; - sector[18] = sector[22] = 8; -} - -// This function sets the LBA in the header of the sector. -static void makeHeader(uint8_t sector[2352], uint32_t lba) { - PCSX::IEC60908b::MSF time(lba + 150); - time.toBCD(sector + 12); -} +// Number of blank sectors appended past the end of the volume when padding is on. +// 150 sectors is two seconds of disc time: enough slack that a real drive's read-ahead +// doesn't run off the end of the data while the BIOS is still reading the last sector. +static constexpr unsigned c_trailingPaddingSectors = 150; int main(int argc, char** argv) { CommandLine::args args(argc, argv); @@ -97,43 +41,33 @@ exe2iso by Nicolas "Pixel" Noble const auto output = args.get("o"); const auto inputs = args.positional(); + const auto license = args.get("license"); const bool asksForHelp = args.get("h").value_or(false); - const uint32_t offset = std::stoul(args.get("offset").value_or("0"), nullptr, 0); + // Padding is on by default; -nopad opts out of the trailing blank sectors. + const bool pad = !args.get("nopad").value_or(false); const bool hasOutput = output.has_value(); - const bool oneInput = inputs.size() == 1; - const bool pad = args.get("pad").value_or(false); - const bool regen = args.get("regen").value_or(false); - const auto license = args.get("license"); - const auto data = args.get("data"); - if (asksForHelp || !oneInput || !hasOutput) { + const bool hasExactlyOneInput = inputs.size() == 1; + + if (asksForHelp || !hasExactlyOneInput || !hasOutput) { fmt::print(R"( -Usage: {} input.ps-exe [-offset value] [-pad] [-regen] [-license file] -o output.bin +Usage: {} input.ps-exe [-license file] [-nopad] -o output.bin input.ps-exe mandatory: specify the input ps-exe file. -o output.bin mandatory: name of the output file. - -offset value optional: move the exe data by value sectors. - -data filename optional: insert this file into the iso after the exe. - -pad optional: pads the iso with 150 blank sectors. - -regen optional: generates proper ECC/EDC. -license file optional: use this license file. + -nopad optional: don't append {} trailing blank sectors. -h displays this help information and exit. )", - argv[0]); + argv[0], c_trailingPaddingSectors); return -1; } - auto& input = inputs[0]; - PCSX::IO file(new PCSX::PosixFile(input)); - if (file->failed()) { - fmt::print("Error opening input file {}\n", input); + PCSX::IO exeFile(new PCSX::PosixFile(inputs[0])); + if (exeFile->failed()) { + fmt::print("Error opening input file {}\n", inputs[0]); return -1; } + PCSX::IO licenseFile(new PCSX::FailedFile); - PCSX::IO dataFile(new PCSX::FailedFile); - PCSX::IO out(new PCSX::PosixFile(output.value(), PCSX::FileOps::TRUNCATE)); - if (out->failed()) { - fmt::print("Error opening output file {}\n", output.value()); - return -1; - } if (license.has_value()) { licenseFile.setFile(new PCSX::PosixFile(license.value())); if (licenseFile->failed()) { @@ -141,97 +75,42 @@ Usage: {} input.ps-exe [-offset value] [-pad] [-regen] [-license file] -o output return -1; } } - if (data.has_value()) { - dataFile.setFile(new PCSX::PosixFile(data.value())); - if (dataFile->failed()) { - fmt::print("Error opening data file {}\n", data.value()); - return -1; - } - } - uint32_t exeSize = file->size(); - exeSize += 2047; - exeSize /= 2048; - exeSize *= 2048; - uint32_t exeOffset = 19 + offset; - - uint8_t sector[2352]; - memset(sector, 0, sizeof(sector)); - makeHeaderOnce(sector); - bool wroteLicense = false; - unsigned LBA = 0; - auto writeSector = [&]() { - makeHeader(sector, LBA++); - if (regen) compute_edcecc(sector); - out->write(sector, sizeof(sector)); - }; - // Sectors 0-15 are the license. We can keep it to zeroes and it'll work most everywhere. - if (licenseFile && !licenseFile->failed()) { - uint8_t licenseData[2352 * 16]; - memset(licenseData, 0, sizeof(licenseData)); - licenseFile->read(licenseData, sizeof(licenseData)); - if ((licenseFile->size() == 2336 * 16) && (licenseData[0x2492] == 'L')) { - // official license file from the sdk, in 2336 bytes per sector. - for (unsigned i = 0; i < 16; i++) { - memcpy(sector + 16, licenseData + 2336 * i, 2336); - writeSector(); - } - wroteLicense = true; - } else if (licenseData[0x24e2] == 'L') { - // looks like an iso file itself - for (unsigned i = 0; i < 16; i++) { - memcpy(sector, licenseData + 2352 * i, 2352); - makeHeaderOnce(sector); - writeSector(); - } - wroteLicense = true; - } else { - fmt::print("Unrecognized LICENSE file format {}\n", output.value()); - } - } - if (!wroteLicense) { - memset(sector, 0, sizeof(sector)); - makeHeaderOnce(sector); - for (unsigned i = 0; i < 16; i++) { - writeSector(); - } - } - // The actual structure of the iso. We're only generating 3 sectors, - // from 16 to 18, as it's the only things necessary for the PS1 bios. - for (unsigned i = 0; i < 3; i++) { - memset(sector, 0, sizeof(sector)); - makeHeaderOnce(sector); - // This function will fill the sector with the right data, as - // necessary for the PS1 bios. - getSectorMinimal(sector + 24, LBA, exeSize, exeOffset); - writeSector(); - } - // Potential padding before the start of the exe. - memset(sector, 0, sizeof(sector)); - makeHeaderOnce(sector); - for (unsigned i = 0; i < offset; i++) { - writeSector(); - } - // The actual exe. - for (unsigned i = 0; i < exeSize; i += 2048) { - file->read(sector + 24, 2048); - writeSector(); - } - if (dataFile && !dataFile->failed()) { - // The additional data file. - unsigned sectors = (dataFile->size() + 2047) / 2048; - for (unsigned i = 0; i < sectors; i++) { - dataFile->read(sector + 24, 2048); - writeSector(); - } + PCSX::IO out(new PCSX::PosixFile(output.value(), PCSX::FileOps::TRUNCATE)); + if (out->failed()) { + fmt::print("Error opening output file {}\n", output.value()); + return -1; } - memset(sector, 0, sizeof(sector)); - makeHeaderOnce(sector); + + PCSX::ISO9660Builder builder(out); + + // PlayStation discs identify themselves through the PVD system identifier. + builder.getPVD().get().set("PLAYSTATION", ' '); + + // Sectors 0-15 are the license/system area. With no license file this writes zeroed + // sectors, which boot on most everything except a region-locked (e.g. Japanese) + // console; pass -license to embed a real one. + builder.writeLicense(licenseFile); + + // The whole disc: a single PSX.EXE in the root directory. The builder appends the + // ";1" version suffix and lays the file out as Mode 2 Form 1 data with valid EDC/ECC. + PCSX::ISO9660::DirTree* root = builder.createRoot(); + builder.createFile(root, "PSX.EXE", exeFile); + + // Compute the layout and emit the full image: volume descriptors, path tables, the + // root directory, and the executable. + builder.close(); + + // Optional trailing padding, sitting past the end of the declared volume so it's + // purely physical. See c_trailingPaddingSectors for the rationale. if (pad) { - // 150 sectors padding. - for (unsigned i = 0; i < 150; i++) { - writeSector(); + uint8_t blank[2048]; + memset(blank, 0, sizeof(blank)); + for (unsigned i = 0; i < c_trailingPaddingSectors; i++) { + builder.writeSector(blank, PCSX::IEC60908b::SectorMode::M2_FORM1); } } - fmt::print("Done."); + + fmt::print("Done.\n"); + return 0; }