-
-
Notifications
You must be signed in to change notification settings - Fork 141
exe2iso: rewrite on top of the ISO9660 builder #2036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,74 +18,18 @@ | |
| ***************************************************************************/ | ||
|
|
||
| #include <stdint.h> | ||
| #include <string.h> | ||
|
|
||
| #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,141 +41,76 @@ | |
|
|
||
| const auto output = args.get<std::string>("o"); | ||
| const auto inputs = args.positional(); | ||
| const auto license = args.get<std::string>("license"); | ||
| const bool asksForHelp = args.get<bool>("h").value_or(false); | ||
| const uint32_t offset = std::stoul(args.get<std::string>("offset").value_or("0"), nullptr, 0); | ||
| // Padding is on by default; -nopad opts out of the trailing blank sectors. | ||
| const bool pad = !args.get<bool>("nopad").value_or(false); | ||
| const bool hasOutput = output.has_value(); | ||
| const bool oneInput = inputs.size() == 1; | ||
| const bool pad = args.get<bool>("pad").value_or(false); | ||
| const bool regen = args.get<bool>("regen").value_or(false); | ||
| const auto license = args.get<std::string>("license"); | ||
| const auto data = args.get<std::string>("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; | ||
| } | ||
|
Comment on lines
+51
to
62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return success when Line 51 currently routes help requests and invalid-usage errors through the same branch, so 🔧 Suggested fix- if (asksForHelp || !hasExactlyOneInput || !hasOutput) {
+ if (asksForHelp) {
+ fmt::print(R"(
+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.
+ -license file optional: use this license file.
+ -nopad optional: don't append {} trailing blank sectors.
+ -h displays this help information and exit.
+)",
+ argv[0], c_trailingPaddingSectors);
+ return 0;
+ }
+
+ if (!hasExactlyOneInput || !hasOutput) {
fmt::print(R"(
Usage: {} input.ps-exe [-license file] [-nopad] -o output.bin
input.ps-exe mandatory: specify the input ps-exe file.
@@
)",
argv[0], c_trailingPaddingSectors);
return -1;
}🤖 Prompt for AI Agents |
||
|
|
||
| auto& input = inputs[0]; | ||
| PCSX::IO<PCSX::File> file(new PCSX::PosixFile(input)); | ||
| if (file->failed()) { | ||
| fmt::print("Error opening input file {}\n", input); | ||
| PCSX::IO<PCSX::File> exeFile(new PCSX::PosixFile(inputs[0])); | ||
| if (exeFile->failed()) { | ||
| fmt::print("Error opening input file {}\n", inputs[0]); | ||
| return -1; | ||
| } | ||
|
|
||
| PCSX::IO<PCSX::File> licenseFile(new PCSX::FailedFile); | ||
| PCSX::IO<PCSX::File> dataFile(new PCSX::FailedFile); | ||
| PCSX::IO<PCSX::File> 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()) { | ||
| fmt::print("Error opening license file {}\n", license.value()); | ||
| 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<PCSX::File> 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<PCSX::ISO9660LowLevel::PVD_SystemIdent>().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; | ||
|
Check notice on line 115 in tools/exe2iso/exe2iso.cc
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a blank line before the table to satisfy markdownlint.
Line 11 triggers MD058 (
blanks-around-tables). Add an empty line between the usage line and the table header.📝 Suggested fix
Usage: exe2iso input.ps-exe [-license file] [-nopad] -o output.bin + | Argument | Type | Description | |-|-|-|📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 11-11: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Source: Linters/SAST tools