573in1/tools/convertExecutable.py

278 lines
6.8 KiB
Python
Raw Normal View History

2023-05-30 18:08:52 +02:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ELF executable to PlayStation 1 .EXE converter
A very simple script to convert ELF files compiled for the PS1 to the executable
format used by the BIOS, with support for setting initial $sp/$gp values and
customizing the region string (used by some emulators to determine whether they
should start in PAL or NTSC mode by default). Requires no external dependencies.
"""
__version__ = "0.1.1"
2023-05-30 18:08:52 +02:00
__author__ = "spicyjpeg"
from argparse import ArgumentParser, FileType, Namespace
from dataclasses import dataclass
from enum import IntEnum, IntFlag
from struct import Struct
2024-01-08 19:02:16 +01:00
from typing import BinaryIO, Generator
2023-05-30 18:08:52 +02:00
## Utilities
def alignToMultiple(data: bytearray, alignment: int):
padAmount: int = alignment - (len(data) % alignment)
if padAmount < alignment:
data.extend(b"\0" * padAmount)
def parseStructFromFile(_file: BinaryIO, _struct: Struct) -> tuple:
return _struct.unpack(_file.read(_struct.size))
def parseStructsFromFile(
_file: BinaryIO, _struct: Struct, count: int
) -> Generator[tuple, None, None]:
data: bytes = _file.read(_struct.size * count)
for offset in range(0, len(data), _struct.size):
yield _struct.unpack(data[offset:offset + _struct.size])
## ELF file parser
ELF_HEADER_STRUCT: Struct = Struct("< 4s 4B 8x 2H 5I 6H")
ELF_HEADER_MAGIC: bytes = b"\x7fELF"
PROG_HEADER_STRUCT: Struct = Struct("< 8I")
class ELFType(IntEnum):
RELOCATABLE = 1
EXECUTABLE = 2
SHARED = 3
CORE = 4
class ELFArchitecture(IntEnum):
MIPS = 8
class ELFEndianness(IntEnum):
LITTLE = 1
BIG = 2
class ProgHeaderType(IntEnum):
NONE = 0
LOAD = 1
DYNAMIC = 2
INTERPRETER = 3
NOTE = 4
class ProgHeaderFlag(IntFlag):
EXECUTE = 1 << 0
WRITE = 1 << 1
READ = 1 << 2
@dataclass
2023-05-30 18:08:52 +02:00
class Segment:
address: int
data: bytes
flags: ProgHeaderFlag
2023-05-30 18:08:52 +02:00
def isReadOnly(self) -> bool:
return not \
(self.flags & (ProgHeaderFlag.WRITE | ProgHeaderFlag.EXECUTE))
class ELF:
def __init__(self, _file: BinaryIO):
# Parse the file header and perform some minimal validation.
_file.seek(0)
(
magic,
wordSize,
endianness,
_,
abi,
elfType,
architecture,
_,
entryPoint,
progHeaderOffset,
secHeaderOffset,
flags,
elfHeaderSize,
progHeaderSize,
progHeaderCount,
secHeaderSize,
secHeaderCount,
_
) = \
2023-05-30 18:08:52 +02:00
parseStructFromFile(_file, ELF_HEADER_STRUCT)
if magic != ELF_HEADER_MAGIC:
raise RuntimeError("file is not a valid ELF")
if wordSize != 1 or endianness != ELFEndianness.LITTLE:
raise RuntimeError("ELF file must be 32-bit little-endian")
if (
elfHeaderSize != ELF_HEADER_STRUCT.size or
progHeaderSize != PROG_HEADER_STRUCT.size
2023-05-30 18:08:52 +02:00
):
raise RuntimeError("unsupported ELF format")
self.type: ELFType = ELFType(elfType)
self.architecture: int = architecture
self.abi: int = abi
self.entryPoint: int = entryPoint
self.flags: int = flags
2023-05-30 18:08:52 +02:00
# Parse the program headers and extract all loadable segments.
self.segments: list[Segment] = []
2023-05-30 18:08:52 +02:00
_file.seek(progHeaderOffset)
for (
headerType,
fileOffset,
address,
_,
fileLength,
length,
flags,
_
2023-05-30 18:08:52 +02:00
) in parseStructsFromFile(_file, PROG_HEADER_STRUCT, progHeaderCount):
if headerType != ProgHeaderType.LOAD:
continue
# Retrieve the segment and trim or pad it if necessary.
_file.seek(fileOffset)
data: bytes = _file.read(fileLength)
if length > len(data):
data = data.ljust(length, b"\0")
else:
data = data[0:length]
self.segments.append(Segment(address, data, flags))
#_file.close()
def flatten(self, stripReadOnly: bool = False) -> tuple[int, bytearray]:
# Find the lower and upper boundaries of the segments' address space.
startAddress: int = min(
seg.address for seg in self.segments
)
endAddress: int = max(
(seg.address + len(seg.data)) for seg in self.segments
)
# Copy all segments into a new byte array at their respective offsets.
data: bytearray = bytearray(endAddress - startAddress)
for seg in self.segments:
if stripReadOnly and seg.isReadOnly():
continue
offset: int = seg.address - startAddress
data[offset:offset + len(seg.data)] = seg.data
return startAddress, data
## Main
EXE_HEADER_STRUCT: Struct = Struct("< 8s 8x 4I 16x 2I 20x 1972s")
2023-05-30 18:08:52 +02:00
EXE_HEADER_MAGIC: bytes = b"PS-X EXE"
EXE_ALIGNMENT: int = 2048
def createParser() -> ArgumentParser:
parser = ArgumentParser(
description = \
"Converts an ELF executable into a PlayStation 1 .EXE file.",
add_help = False
)
group = parser.add_argument_group("Tool options")
group.add_argument(
"-h", "--help",
action = "help",
help = "Show this help message and exit"
)
group = parser.add_argument_group("Conversion options")
group.add_argument(
"-r", "--region-str",
type = str,
default = "",
help = "Add a custom region string to the header",
metavar = "string"
)
group.add_argument(
"-s", "--set-sp",
type = lambda value: int(value, 0),
default = 0,
help = "Add an initial value for the stack pointer to the header",
metavar = "value"
)
group.add_argument(
"-g", "--set-gp",
type = lambda value: int(value, 0),
default = 0,
help = "Add an initial value for the global pointer to the header",
metavar = "value"
)
group.add_argument(
"-S", "--strip-read-only",
action = "store_true",
help = \
"Remove all ELF segments not marked writable nor executable from "
"the output file"
)
group = parser.add_argument_group("File paths")
group.add_argument(
"input",
type = FileType("rb"),
help = "Path to ELF input executable",
)
group.add_argument(
"output",
type = FileType("wb"),
help = "Path to PS1 executable to generate"
)
return parser
def main():
parser: ArgumentParser = createParser()
args: Namespace = parser.parse_args()
with args.input as _file:
try:
elf: ELF = ELF(_file)
except RuntimeError as err:
parser.error(err.args[0])
if elf.type != ELFType.EXECUTABLE:
parser.error("ELF file must be an executable")
if elf.architecture != ELFArchitecture.MIPS:
parser.error("ELF architecture must be MIPS")
if not elf.segments:
parser.error("ELF file must contain at least one segment")
startAddress, data = elf.flatten(args.strip_read_only)
alignToMultiple(data, EXE_ALIGNMENT)
region: bytes = args.region_str.strip().encode("ascii")
header: bytes = EXE_HEADER_STRUCT.pack(
EXE_HEADER_MAGIC, # Magic
elf.entryPoint, # Entry point
args.set_gp, # Initial global pointer
startAddress, # Data load address
len(data), # Data size
args.set_sp, # Stack offset
0, # Stack size
region # Region string
)
with args.output as _file:
_file.write(header)
_file.write(data)
if __name__ == "__main__":
main()