Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 7 additions & 5 deletions tools/exe2iso/README.md
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 |
|-|-|-|
Comment on lines +10 to 12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Usage: exe2iso input.ps-exe [-license file] [-nopad] -o output.bin
| Argument | Type | Description |
|-|-|-|
Usage: exe2iso input.ps-exe [-license file] [-nopad] -o output.bin
| Argument | Type | Description |
|-|-|-|
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 11-11: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/exe2iso/README.md` around lines 10 - 12, The markdown linter rule MD058
(blanks-around-tables) requires a blank line before table content. In the
README.md file, add a blank line between the usage example line starting with
"Usage: exe2iso input.ps-exe" and the table header row that starts with "|
Argument". This blank line separation is necessary to satisfy the markdownlint
requirements for proper table formatting.

Source: Linters/SAST tools

| 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. |
227 changes: 53 additions & 174 deletions tools/exe2iso/exe2iso.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return success when -h is requested.

Line 51 currently routes help requests and invalid-usage errors through the same branch, so -h exits with -1. That makes help look like a failure in scripts/CI.

🔧 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/exe2iso/exe2iso.cc` around lines 51 - 62, The condition at line 51
combines the help flag check with usage error checks, causing the help message
to exit with -1 (failure) instead of 0 (success). Refactor the logic to check
asksForHelp separately first: if true, print the help message and return 0, then
check the remaining conditions (hasExactlyOneInput and hasOutput) separately and
only return -1 if those validations fail. This ensures that requesting help with
the -h flag exits successfully while actual usage errors still indicate failure.


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

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Complex Method

main decreases in cyclomatic complexity from 28 to 10, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check notice on line 115 in tools/exe2iso/exe2iso.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Bumpy Road Ahead

main decreases from 7 to 2 logical blocks with deeply nested code, threshold is 2 blocks per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
}
Loading