mirror of
https://github.com/spicyjpeg/573in1.git
synced 2025-03-01 07:20:42 +01:00
Make launchers standalone, remove xorriso dependency
This commit is contained in:
parent
176eb07915
commit
b921d6e7b4
134
CMakeLists.txt
134
CMakeLists.txt
@ -4,28 +4,37 @@ cmake_minimum_required(VERSION 3.25)
|
|||||||
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
|
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/cmake/toolchain.cmake")
|
||||||
|
|
||||||
project(
|
project(
|
||||||
cart_tool_private
|
cart_tool
|
||||||
LANGUAGES C CXX ASM
|
LANGUAGES C CXX ASM
|
||||||
VERSION 0.4.2
|
VERSION 0.4.2
|
||||||
DESCRIPTION "Konami System 573 security cartridge tool"
|
DESCRIPTION "Konami System 573 security cartridge tool"
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
find_package(Python3 REQUIRED COMPONENTS Interpreter)
|
||||||
find_program(
|
|
||||||
XORRISO_PATH xorriso
|
|
||||||
PATHS "C:/msys64/usr/bin"
|
|
||||||
DOC "Path to xorriso tool (optional)"
|
|
||||||
)
|
|
||||||
find_program(
|
find_program(
|
||||||
CHDMAN_PATH chdman
|
CHDMAN_PATH chdman
|
||||||
PATHS "C:/Program Files/MAME" "C:/Program Files (x86)/MAME"
|
PATHS
|
||||||
DOC "Path to MAME chdman tool (optional)"
|
"C:/Program Files/MAME"
|
||||||
|
"C:/Program Files (x86)/MAME"
|
||||||
|
"/opt/mame"
|
||||||
|
DOC "Path to MAME chdman tool (optional)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
string(TOUPPER "${PROJECT_NAME}" _name)
|
||||||
string(REPLACE "." "_" _version "${PROJECT_VERSION}")
|
string(REPLACE "." "_" _version "${PROJECT_VERSION}")
|
||||||
set(releaseInfo "cart_tool build ${PROJECT_VERSION} - (C) 2022-2024 spicyjpeg")
|
|
||||||
set(releaseName "cart_tool_${PROJECT_VERSION}")
|
set(
|
||||||
set(cdVolumeName "CART_TOOL_${_version}")
|
RELEASE_INFO "${PROJECT_NAME} ${PROJECT_VERSION} - (C) 2022-2024 spicyjpeg"
|
||||||
|
CACHE STRING "Executable description and version string (optional)"
|
||||||
|
)
|
||||||
|
set(
|
||||||
|
RELEASE_NAME "${PROJECT_NAME}_${PROJECT_VERSION}"
|
||||||
|
CACHE STRING "CD-ROM image and release package file name"
|
||||||
|
)
|
||||||
|
set(
|
||||||
|
CD_VOLUME_NAME "${_name}_${_version}"
|
||||||
|
CACHE STRING "CD-ROM image volume label"
|
||||||
|
)
|
||||||
|
|
||||||
## Files common to all executables
|
## Files common to all executables
|
||||||
|
|
||||||
@ -61,7 +70,7 @@ function(addExecutable name address stackTop)
|
|||||||
COMMAND
|
COMMAND
|
||||||
"${Python3_EXECUTABLE}"
|
"${Python3_EXECUTABLE}"
|
||||||
"${PROJECT_SOURCE_DIR}/tools/convertExecutable.py"
|
"${PROJECT_SOURCE_DIR}/tools/convertExecutable.py"
|
||||||
-r "${releaseInfo}"
|
-r "${RELEASE_INFO}"
|
||||||
-s 0x${stackTop}
|
-s 0x${stackTop}
|
||||||
"$<TARGET_FILE:${name}>"
|
"$<TARGET_FILE:${name}>"
|
||||||
"${name}.psexe"
|
"${name}.psexe"
|
||||||
@ -157,8 +166,6 @@ addExecutable(
|
|||||||
)
|
)
|
||||||
target_link_libraries(boot PRIVATE bootFlags)
|
target_link_libraries(boot PRIVATE bootFlags)
|
||||||
|
|
||||||
list(APPEND packageContents boot.psexe)
|
|
||||||
|
|
||||||
function(addLauncher address stackTop)
|
function(addLauncher address stackTop)
|
||||||
addExecutable(
|
addExecutable(
|
||||||
launcher${address} ${address} ${stackTop}
|
launcher${address} ${address} ${stackTop}
|
||||||
@ -236,9 +243,9 @@ add_custom_command(
|
|||||||
resources.json
|
resources.json
|
||||||
assets/app.palette.json
|
assets/app.palette.json
|
||||||
assets/app.strings.json
|
assets/app.strings.json
|
||||||
main
|
main.psexe
|
||||||
launcher801f8000
|
launcher801f8000.psexe
|
||||||
launcher803f8000
|
launcher803f8000.psexe
|
||||||
COMMENT "Building resource archive"
|
COMMENT "Building resource archive"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
@ -249,80 +256,57 @@ addBinaryFile(
|
|||||||
|
|
||||||
## CD-ROM image
|
## CD-ROM image
|
||||||
|
|
||||||
|
configure_file(cd.json cd.json ESCAPE_QUOTES)
|
||||||
configure_file(assets/cdreadme.txt readme.txt NEWLINE_STYLE CRLF)
|
configure_file(assets/cdreadme.txt readme.txt NEWLINE_STYLE CRLF)
|
||||||
|
|
||||||
if(EXISTS "${XORRISO_PATH}")
|
add_custom_command(
|
||||||
|
COMMAND
|
||||||
|
"${Python3_EXECUTABLE}"
|
||||||
|
"${PROJECT_SOURCE_DIR}/tools/buildCDImage.py"
|
||||||
|
cd.json
|
||||||
|
"${RELEASE_NAME}.iso"
|
||||||
|
OUTPUT "${RELEASE_NAME}.iso"
|
||||||
|
DEPENDS
|
||||||
|
cd.json
|
||||||
|
boot.psexe
|
||||||
|
COMMENT "Building CD-ROM image"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
if(EXISTS "${CHDMAN_PATH}")
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
COMMAND
|
COMMAND
|
||||||
"${XORRISO_PATH}"
|
"${CHDMAN_PATH}" createcd -f
|
||||||
-outdev "${releaseName}.iso"
|
-i "${RELEASE_NAME}.iso"
|
||||||
-blank all
|
-o "${RELEASE_NAME}.chd"
|
||||||
-rockridge off
|
OUTPUT "${RELEASE_NAME}.chd"
|
||||||
-joliet off
|
DEPENDS "${RELEASE_NAME}.iso"
|
||||||
-padding 2M
|
COMMENT "Building MAME CHD image"
|
||||||
-volid "${cdVolumeName}"
|
|
||||||
-volset_id "${cdVolumeName}"
|
|
||||||
-publisher "SPICYJPEG"
|
|
||||||
-application_id "PLAYSTATION"
|
|
||||||
-system_id "PLAYSTATION"
|
|
||||||
-preparer_id "CART_TOOL BUILD SCRIPT"
|
|
||||||
-map readme.txt README.TXT
|
|
||||||
-map boot.psexe PSX.EXE
|
|
||||||
-clone PSX.EXE GSE.NXX
|
|
||||||
-clone PSX.EXE NSE.GXX
|
|
||||||
-clone PSX.EXE OSE.FXX
|
|
||||||
-clone PSX.EXE QSU.DXH
|
|
||||||
-clone PSX.EXE QSX.DXE
|
|
||||||
-clone PSX.EXE QSY.DXD
|
|
||||||
-clone PSX.EXE QSZ.DXC
|
|
||||||
-clone PSX.EXE RSU.CXH
|
|
||||||
-clone PSX.EXE RSV.CXG
|
|
||||||
-clone PSX.EXE RSW.CXF
|
|
||||||
-clone PSX.EXE RSZ.CXC
|
|
||||||
-clone PSX.EXE SSW.BXF
|
|
||||||
-clone PSX.EXE SSX.BXE
|
|
||||||
-clone PSX.EXE SSY.BXD
|
|
||||||
-clone PSX.EXE TSV.AXG
|
|
||||||
-clone PSX.EXE TSW.AXF
|
|
||||||
-clone PSX.EXE TSX.AXE
|
|
||||||
-clone PSX.EXE TSY.AXD
|
|
||||||
-clone PSX.EXE TSZ.AXC
|
|
||||||
OUTPUT "${releaseName}.iso"
|
|
||||||
DEPENDS boot
|
|
||||||
COMMENT "Building CD-ROM image"
|
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND packageContents "${releaseName}.iso")
|
list(APPEND packageFiles "${RELEASE_NAME}.chd")
|
||||||
|
|
||||||
if(EXISTS "${CHDMAN_PATH}")
|
|
||||||
add_custom_command(
|
|
||||||
COMMAND
|
|
||||||
"${CHDMAN_PATH}" createcd -f
|
|
||||||
-i "${releaseName}.iso"
|
|
||||||
-o "${releaseName}.chd"
|
|
||||||
OUTPUT "${releaseName}.chd"
|
|
||||||
DEPENDS "${releaseName}.iso"
|
|
||||||
COMMENT "Building MAME CHD image"
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
|
|
||||||
list(APPEND packageContents "${releaseName}.chd")
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
## Release package
|
## Release package
|
||||||
|
|
||||||
|
list(
|
||||||
|
APPEND packageFiles
|
||||||
|
readme.txt
|
||||||
|
boot.psexe
|
||||||
|
"${RELEASE_NAME}.iso"
|
||||||
|
)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
COMMAND
|
COMMAND
|
||||||
"${CMAKE_COMMAND}" -E tar cf
|
"${CMAKE_COMMAND}" -E tar cf
|
||||||
"${releaseName}.zip"
|
"${RELEASE_NAME}.zip"
|
||||||
--format=zip
|
--format=zip
|
||||||
${packageContents} readme.txt
|
${packageFiles}
|
||||||
OUTPUT "${releaseName}.zip"
|
OUTPUT "${RELEASE_NAME}.zip"
|
||||||
DEPENDS ${packageContents}
|
DEPENDS ${packageFiles}
|
||||||
COMMENT "Packaging built files"
|
COMMENT "Packaging built files"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(package ALL DEPENDS "${releaseName}.zip")
|
add_custom_target(package ALL DEPENDS "${RELEASE_NAME}.zip")
|
||||||
|
126
cd.json
Normal file
126
cd.json
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"identifiers": {
|
||||||
|
"system": "PLAYSTATION",
|
||||||
|
"volume": "${CD_VOLUME_NAME}",
|
||||||
|
"volumeSet": "${CD_VOLUME_NAME}",
|
||||||
|
"publisher": "SPICYJPEG",
|
||||||
|
"dataPreparer": "GCC ${CMAKE_CXX_COMPILER_VERSION}, CMAKE ${CMAKE_VERSION}, PYTHON ${Python3_VERSION}",
|
||||||
|
"application": "PLAYSTATION",
|
||||||
|
"copyright": "README.TXT;1"
|
||||||
|
},
|
||||||
|
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"name": "README.TXT",
|
||||||
|
"source": "${PROJECT_BINARY_DIR}/readme.txt"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"name": "PSX.EXE",
|
||||||
|
"source": "${PROJECT_BINARY_DIR}/boot.psexe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "GSE.NXX",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "NSE.GXX",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "OSE.FXX",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "QSU.DXH",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "QSX.DXE",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "QSY.DXD",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "QSZ.DXC",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "RSU.CXH",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "RSV.CXG",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "RSW.CXF",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "RSZ.CXC",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "SSW.BXF",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "SSX.BXE",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "SSY.BXD",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "TSV.AXG",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "TSW.AXF",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "TSX.AXE",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "TSY.AXD",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fileAlias",
|
||||||
|
"name": "TSZ.AXC",
|
||||||
|
"source": "PSX.EXE"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "padding",
|
||||||
|
"name": "PADDING.BIN",
|
||||||
|
"size": 1048576
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -2,18 +2,18 @@
|
|||||||
{
|
{
|
||||||
"type": "binary",
|
"type": "binary",
|
||||||
"name": "binaries/main.psexe.lz4",
|
"name": "binaries/main.psexe.lz4",
|
||||||
"source": "main.psexe",
|
"source": "${PROJECT_BINARY_DIR}/main.psexe",
|
||||||
"compression": "lz4"
|
"compression": "lz4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "binary",
|
"type": "binary",
|
||||||
"name": "binaries/launcher801f8000.psexe",
|
"name": "binaries/launcher801f8000.psexe",
|
||||||
"source": "launcher801f8000.psexe"
|
"source": "${PROJECT_BINARY_DIR}/launcher801f8000.psexe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "binary",
|
"type": "binary",
|
||||||
"name": "binaries/launcher803f8000.psexe",
|
"name": "binaries/launcher803f8000.psexe",
|
||||||
"source": "launcher803f8000.psexe"
|
"source": "${PROJECT_BINARY_DIR}/launcher803f8000.psexe"
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -87,7 +87,12 @@ bool ExecutableLauncherArgs::parseArgument(const char *arg) {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
#if 0
|
||||||
return CommonArgs::parseArgument(arg);
|
return CommonArgs::parseArgument(arg);
|
||||||
|
#else
|
||||||
|
// Avoid pulling in strtol().
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ static inline void setMiscOutput(MiscOutputPin pin, bool value) {
|
|||||||
static inline bool isDigitalIOPresent(void) {
|
static inline bool isDigitalIOPresent(void) {
|
||||||
return (
|
return (
|
||||||
(SYS573D_CPLD_STAT & (SYS573D_CPLD_STAT_ID1 | SYS573D_CPLD_STAT_ID2)) ==
|
(SYS573D_CPLD_STAT & (SYS573D_CPLD_STAT_ID1 | SYS573D_CPLD_STAT_ID2)) ==
|
||||||
SYS573D_CPLD_STAT_ID1
|
SYS573D_CPLD_STAT_ID2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,17 @@
|
|||||||
#include "vendor/ff.h"
|
#include "vendor/ff.h"
|
||||||
|
|
||||||
LauncherError ExecutableLauncher::openFile(void) {
|
LauncherError ExecutableLauncher::openFile(void) {
|
||||||
|
#if 0
|
||||||
if (!args.drive || !args.path) {
|
if (!args.drive || !args.path) {
|
||||||
LOG("required arguments missing");
|
LOG("required arguments missing");
|
||||||
return INVALID_ARGS;
|
return INVALID_ARGS;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!args.drive)
|
||||||
|
args.drive = "1:";
|
||||||
|
if (!args.path)
|
||||||
|
args.path = "psx.exe";
|
||||||
|
#endif
|
||||||
|
|
||||||
// The drive index is always a single digit, so there is no need to pull in
|
// The drive index is always a single digit, so there is no need to pull in
|
||||||
// strtol() here.
|
// strtol() here.
|
||||||
|
@ -302,7 +302,7 @@ void StorageActionsScreen::update(ui::Context &ctx) {
|
|||||||
ctx.show(APP->_storageInfoScreen, true, true);
|
ctx.show(APP->_storageInfoScreen, true, true);
|
||||||
} else {
|
} else {
|
||||||
if (action.region.isPresent()) {
|
if (action.region.isPresent()) {
|
||||||
this->selectedRegion = &(action.region);
|
selectedRegion = &(action.region);
|
||||||
|
|
||||||
if (action.region.regionLength > 0x1000000) {
|
if (action.region.regionLength > 0x1000000) {
|
||||||
APP->_cardSizeScreen.callback = action.target;
|
APP->_cardSizeScreen.callback = action.target;
|
||||||
|
280
tools/buildCDImage.py
Executable file
280
tools/buildCDImage.py
Executable file
@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""ISO9660 CD-ROM image generator
|
||||||
|
|
||||||
|
A simple tool for building ISO9660 CD-ROM images with 2048-byte sectors, as used
|
||||||
|
by the PlayStation 1 and other machines, from a JSON configuration file
|
||||||
|
describing their contents. Unlike most other ISO9660 authoring tools, files and
|
||||||
|
directories are placed in the image in the same order they are specified in the
|
||||||
|
JSON file, allowing for customization of the disc's layout. Requires pycdlib to
|
||||||
|
be installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__author__ = "spicyjpeg"
|
||||||
|
|
||||||
|
import json, re, sys
|
||||||
|
from argparse import ArgumentParser, FileType, Namespace
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, BinaryIO, Iterable
|
||||||
|
|
||||||
|
from pycdlib.dr import DirectoryRecord
|
||||||
|
from pycdlib.inode import Inode
|
||||||
|
from pycdlib.pycdlib import PyCdlib
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
PATH_IGNORE_REGEX: re.Pattern = re.compile(r";[0-9]+$")
|
||||||
|
PATH_INVALID_REGEX: re.Pattern = re.compile(r"[^0-9A-Z_./]")
|
||||||
|
|
||||||
|
def normalizePath(path: str, isDirectory: bool = False) -> str:
|
||||||
|
path = PATH_IGNORE_REGEX.sub("", path.upper())
|
||||||
|
path = PATH_INVALID_REGEX.sub("_", path)
|
||||||
|
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = f"/{path}"
|
||||||
|
|
||||||
|
if isDirectory:
|
||||||
|
path = path.replace(".", "_")
|
||||||
|
else:
|
||||||
|
# The ISO9660 specification requires file names to always contain
|
||||||
|
# exactly one period (even if no extension is present) and be terminated
|
||||||
|
# with a version number.
|
||||||
|
numPeriods: int = path.count(".")
|
||||||
|
|
||||||
|
if numPeriods:
|
||||||
|
path = path.replace(".", "_", numPeriods - 1)
|
||||||
|
else:
|
||||||
|
path += "."
|
||||||
|
|
||||||
|
path += ";1"
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def restoreISOFileOrder(
|
||||||
|
iso: PyCdlib, isoEntries: Iterable[DirectoryRecord | Inode],
|
||||||
|
printLayout: bool = False
|
||||||
|
):
|
||||||
|
# By default, when calling force_consistency(), pycdlib allocates files in
|
||||||
|
# the same order as their respective directory records, which are in turn
|
||||||
|
# sorted alphabetically as required by the ISO9660 specification. We're
|
||||||
|
# going to "undo" this sorting by iterating through the provided array and
|
||||||
|
# reassigning an LBA to each file and directory manually to restore the
|
||||||
|
# intended order.
|
||||||
|
iso.force_consistency()
|
||||||
|
|
||||||
|
sectorLength: int = iso.logical_block_size
|
||||||
|
sectorOffset: int = \
|
||||||
|
iso.pvd.path_table_location_be + iso.pvd.path_table_num_extents
|
||||||
|
|
||||||
|
if printLayout:
|
||||||
|
sys.stderr.write("CD-ROM image layout:\n")
|
||||||
|
|
||||||
|
# Allocate the indices (arrays of directory records) for the root directory
|
||||||
|
# and each subfolder. Note that these will always be placed before any file
|
||||||
|
# data, regardless of where they are listed in the configuration file.
|
||||||
|
for entry in isoEntries:
|
||||||
|
names: list[str] = []
|
||||||
|
length: int = \
|
||||||
|
(entry.data_length + sectorLength - 1) // sectorLength
|
||||||
|
|
||||||
|
if isinstance(entry, DirectoryRecord):
|
||||||
|
entry.set_data_location(sectorOffset, 0)
|
||||||
|
names.append(entry.file_identifier().decode("ascii"))
|
||||||
|
else:
|
||||||
|
entry.set_extent_location(sectorOffset)
|
||||||
|
|
||||||
|
for record, _ in entry.linked_records:
|
||||||
|
if not isinstance(record, DirectoryRecord):
|
||||||
|
continue
|
||||||
|
|
||||||
|
record.set_data_location(sectorOffset, 0)
|
||||||
|
names.append(record.file_identifier().decode("ascii"))
|
||||||
|
|
||||||
|
if printLayout:
|
||||||
|
nameList: str = ", ".join(names)
|
||||||
|
|
||||||
|
sys.stderr.write(
|
||||||
|
f" [{sectorOffset:6d}-{sectorOffset + length - 1:6d}] "
|
||||||
|
f"{nameList}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
sectorOffset += length
|
||||||
|
|
||||||
|
if printLayout:
|
||||||
|
sys.stderr.write(f"Total image size: {sectorOffset} sectors\n")
|
||||||
|
|
||||||
|
def showProgress(part: int, total: int, _: Any):
|
||||||
|
if part >= total:
|
||||||
|
sys.stderr.write("\rPacking finished.\n")
|
||||||
|
else:
|
||||||
|
sys.stderr.write(f"\rPacking: {part / total * 100.0:5.1f}%")
|
||||||
|
|
||||||
|
class PaddingFile(BinaryIO):
|
||||||
|
mode: str = "rb"
|
||||||
|
|
||||||
|
def read(self, length: int) -> bytes:
|
||||||
|
return bytes(length)
|
||||||
|
|
||||||
|
## Main
|
||||||
|
|
||||||
|
def createParser() -> ArgumentParser:
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description = \
|
||||||
|
"Parses a JSON file containing a list of ISO9660 identifiers, file "
|
||||||
|
"and directory entries and generates a CD-ROM image.",
|
||||||
|
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.add_argument(
|
||||||
|
"-q", "--quiet",
|
||||||
|
action = "store_true",
|
||||||
|
help = "Suppress all non-error output"
|
||||||
|
)
|
||||||
|
|
||||||
|
group = parser.add_argument_group("CD-ROM image options")
|
||||||
|
group.add_argument(
|
||||||
|
"-l", "--level",
|
||||||
|
type = lambda value: int(value, 0),
|
||||||
|
default = 1,
|
||||||
|
help = \
|
||||||
|
"Set ISO9660 interchange level and maximum file name length "
|
||||||
|
"(default 1)",
|
||||||
|
metavar = "1-4"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"-x", "--xa",
|
||||||
|
action = "store_true",
|
||||||
|
help = "Add CD-XA header and metadata tags"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"-S", "--system-area",
|
||||||
|
type = FileType("rb"),
|
||||||
|
help = \
|
||||||
|
"Insert specified file (in 2048-bytes-per-sector format, up to 32 "
|
||||||
|
"KB) into the image's system area",
|
||||||
|
metavar = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
group = parser.add_argument_group("File paths")
|
||||||
|
group.add_argument(
|
||||||
|
"-s", "--source-dir",
|
||||||
|
type = Path,
|
||||||
|
help = \
|
||||||
|
"Set path to directory containing source files (same directory as "
|
||||||
|
"configuration file by default)",
|
||||||
|
metavar = "dir"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"configFile",
|
||||||
|
type = FileType("rt"),
|
||||||
|
help = "Path to JSON configuration file",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"output",
|
||||||
|
type = FileType("wb"),
|
||||||
|
help = "Path to ISO9660 CD-ROM image to generate"
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser: ArgumentParser = createParser()
|
||||||
|
args: Namespace = parser.parse_args()
|
||||||
|
|
||||||
|
with args.configFile as _file:
|
||||||
|
configFile: dict[str, Any] = json.load(_file)
|
||||||
|
sourceDir: Path = \
|
||||||
|
args.source_dir or Path(_file.name).parent
|
||||||
|
|
||||||
|
iso: PyCdlib = PyCdlib()
|
||||||
|
paddingFile: PaddingFile = PaddingFile()
|
||||||
|
|
||||||
|
identifiers: dict[str, str] = configFile.get("identifiers", {})
|
||||||
|
|
||||||
|
iso.new(
|
||||||
|
interchange_level = args.level,
|
||||||
|
sys_ident = identifiers.get("system", ""),
|
||||||
|
vol_ident = identifiers.get("volume", ""),
|
||||||
|
vol_set_ident = identifiers.get("volumeSet", ""),
|
||||||
|
pub_ident_str = identifiers.get("publisher", ""),
|
||||||
|
preparer_ident_str = identifiers.get("dataPreparer", ""),
|
||||||
|
app_ident_str = identifiers.get("application", ""),
|
||||||
|
copyright_file = identifiers.get("copyright", ""),
|
||||||
|
abstract_file = identifiers.get("abstract", ""),
|
||||||
|
bibli_file = identifiers.get("bibliographic", ""),
|
||||||
|
joliet = None,
|
||||||
|
rock_ridge = None,
|
||||||
|
xa = args.xa,
|
||||||
|
udf = None
|
||||||
|
)
|
||||||
|
|
||||||
|
entryList: list[dict[str, Any]] = configFile["entries"]
|
||||||
|
isoEntries: list[DirectoryRecord | Inode] = [
|
||||||
|
iso.pvd.root_directory_record()
|
||||||
|
]
|
||||||
|
|
||||||
|
for entry in entryList:
|
||||||
|
match entry.get("type", "file").strip():
|
||||||
|
case "padding":
|
||||||
|
name: str = normalizePath(entry["name"])
|
||||||
|
|
||||||
|
iso.add_fp(
|
||||||
|
fp = paddingFile,
|
||||||
|
length = int(entry["size"]),
|
||||||
|
iso_path = name
|
||||||
|
)
|
||||||
|
iso.set_hidden(iso_path = name)
|
||||||
|
isoEntries.append(iso.inodes[-1])
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
iso.add_file(
|
||||||
|
filename = sourceDir / entry["source"],
|
||||||
|
iso_path = normalizePath(entry["name"])
|
||||||
|
)
|
||||||
|
isoEntries.append(iso.inodes[-1])
|
||||||
|
|
||||||
|
case "fileAlias":
|
||||||
|
iso.add_hard_link(
|
||||||
|
iso_old_path = normalizePath(entry["source"]),
|
||||||
|
iso_new_path = normalizePath(entry["name"])
|
||||||
|
)
|
||||||
|
|
||||||
|
case "directoryAlias":
|
||||||
|
iso.add_hard_link(
|
||||||
|
iso_old_path = normalizePath(entry["source"], True),
|
||||||
|
iso_new_path = normalizePath(entry["name"], True)
|
||||||
|
)
|
||||||
|
|
||||||
|
case "directory":
|
||||||
|
name: str = normalizePath(entry["name"], True)
|
||||||
|
|
||||||
|
iso.add_directory(iso_path = name)
|
||||||
|
isoEntries.append(iso.get_record(iso_path = name))
|
||||||
|
|
||||||
|
case _type:
|
||||||
|
raise KeyError(f"unsupported entry type '{_type}'")
|
||||||
|
|
||||||
|
restoreISOFileOrder(iso, isoEntries, not args.quiet)
|
||||||
|
|
||||||
|
with args.output as _file:
|
||||||
|
iso.write_fp(
|
||||||
|
outfp = _file,
|
||||||
|
progress_cb = None if args.quiet else showProgress
|
||||||
|
)
|
||||||
|
iso.close()
|
||||||
|
|
||||||
|
if args.system_area:
|
||||||
|
with args.system_area as inputFile:
|
||||||
|
_file.seek(0)
|
||||||
|
_file.write(inputFile.read(iso.logical_block_size * 16))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__version__ = "0.4.1"
|
__version__ = "0.4.2"
|
||||||
__author__ = "spicyjpeg"
|
__author__ = "spicyjpeg"
|
||||||
|
|
||||||
import json, logging, os, re
|
import json, logging, os, re
|
||||||
@ -14,6 +14,7 @@ from typing import Any, Mapping, Sequence, TextIO
|
|||||||
from common.cart import CartDump, DumpFlag
|
from common.cart import CartDump, DumpFlag
|
||||||
from common.cartdata import *
|
from common.cartdata import *
|
||||||
from common.games import GameDB, GameDBEntry
|
from common.games import GameDB, GameDBEntry
|
||||||
|
from common.util import setupLogger
|
||||||
|
|
||||||
## MAME NVRAM file parser
|
## MAME NVRAM file parser
|
||||||
|
|
||||||
@ -200,17 +201,6 @@ def createParser() -> ArgumentParser:
|
|||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def setupLogger(level: int | None):
|
|
||||||
logging.basicConfig(
|
|
||||||
format = "[{levelname:8s}] {message}",
|
|
||||||
style = "{",
|
|
||||||
level = (
|
|
||||||
logging.WARNING,
|
|
||||||
logging.INFO,
|
|
||||||
logging.DEBUG
|
|
||||||
)[min(level or 0, 2)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser: ArgumentParser = createParser()
|
parser: ArgumentParser = createParser()
|
||||||
args: Namespace = parser.parse_args()
|
args: Namespace = parser.parse_args()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__version__ = "0.4.1"
|
__version__ = "0.4.2"
|
||||||
__author__ = "spicyjpeg"
|
__author__ = "spicyjpeg"
|
||||||
|
|
||||||
import json, logging, os, re
|
import json, logging, os, re
|
||||||
@ -12,7 +12,7 @@ from typing import ByteString, Mapping, TextIO
|
|||||||
from common.cart import DumpFlag, ROMHeaderDump
|
from common.cart import DumpFlag, ROMHeaderDump
|
||||||
from common.cartdata import *
|
from common.cartdata import *
|
||||||
from common.games import GameDB, GameDBEntry
|
from common.games import GameDB, GameDBEntry
|
||||||
from common.util import InterleavedFile
|
from common.util import InterleavedFile, setupLogger
|
||||||
|
|
||||||
## Flash dump "parser"
|
## Flash dump "parser"
|
||||||
|
|
||||||
@ -143,17 +143,6 @@ def createParser() -> ArgumentParser:
|
|||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def setupLogger(level: int | None):
|
|
||||||
logging.basicConfig(
|
|
||||||
format = "[{levelname:8s}] {message}",
|
|
||||||
style = "{",
|
|
||||||
level = (
|
|
||||||
logging.WARNING,
|
|
||||||
logging.INFO,
|
|
||||||
logging.DEBUG
|
|
||||||
)[min(level or 0, 2)]
|
|
||||||
)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser: ArgumentParser = createParser()
|
parser: ArgumentParser = createParser()
|
||||||
args: Namespace = parser.parse_args()
|
args: Namespace = parser.parse_args()
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__version__ = "0.4.1"
|
__version__ = "0.4.2"
|
||||||
__author__ = "spicyjpeg"
|
__author__ = "spicyjpeg"
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from argparse import ArgumentParser, FileType, Namespace
|
from argparse import ArgumentParser, FileType, Namespace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import ByteString
|
from typing import Any, ByteString
|
||||||
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
||||||
|
|
||||||
import lz4.block
|
import lz4.block
|
||||||
@ -50,10 +50,11 @@ def createParser() -> ArgumentParser:
|
|||||||
group = parser.add_argument_group("File paths")
|
group = parser.add_argument_group("File paths")
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-s", "--source-dir",
|
"-s", "--source-dir",
|
||||||
type = Path,
|
type = Path,
|
||||||
help = \
|
help = \
|
||||||
"Set path to directory containing source files (same directory as "
|
"Set path to directory containing source files (same directory as "
|
||||||
"resource list by default)"
|
"resource list by default)",
|
||||||
|
metavar = "dir"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"resourceList",
|
"resourceList",
|
||||||
@ -73,8 +74,9 @@ def main():
|
|||||||
args: Namespace = parser.parse_args()
|
args: Namespace = parser.parse_args()
|
||||||
|
|
||||||
with args.resourceList as _file:
|
with args.resourceList as _file:
|
||||||
assetList: list = json.load(_file)
|
assetList: list[dict[str, Any]] = json.load(_file)
|
||||||
sourceDir: Path = args.source_dir or Path(_file.name).parent
|
sourceDir: Path = \
|
||||||
|
args.source_dir or Path(_file.name).parent
|
||||||
|
|
||||||
with ZipFile(args.output, "w", allowZip64 = False) as _zip:
|
with ZipFile(args.output, "w", allowZip64 = False) as _zip:
|
||||||
for asset in assetList:
|
for asset in assetList:
|
||||||
|
@ -9,7 +9,7 @@ from typing import Any, Generator, Mapping, Sequence
|
|||||||
import numpy
|
import numpy
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from .util import hashData
|
from .util import colorFromString, hashData
|
||||||
|
|
||||||
## .TIM image converter
|
## .TIM image converter
|
||||||
|
|
||||||
@ -155,8 +155,9 @@ def generateFontMetrics(metrics: Mapping[str, Any]) -> bytearray:
|
|||||||
|
|
||||||
## Color palette generator
|
## Color palette generator
|
||||||
|
|
||||||
_PALETTE_COLOR_REGEX: re.Pattern = re.compile(r"^#?([0-9A-Fa-f]{6})$")
|
_PALETTE_ENTRY_STRUCT: Struct = Struct("< 3B x")
|
||||||
_PALETTE_COLORS: Sequence[str] = (
|
|
||||||
|
_PALETTE_ENTRIES: Sequence[str] = (
|
||||||
"default",
|
"default",
|
||||||
"shadow",
|
"shadow",
|
||||||
"backdrop",
|
"backdrop",
|
||||||
@ -177,25 +178,22 @@ _PALETTE_COLORS: Sequence[str] = (
|
|||||||
"subtitle"
|
"subtitle"
|
||||||
)
|
)
|
||||||
|
|
||||||
_PALETTE_ENTRY_STRUCT: Struct = Struct("< 3s x")
|
def generateColorPalette(
|
||||||
|
palette: Mapping[str, str | Sequence[int]]
|
||||||
def generateColorPalette(palette: Mapping[str, str]) -> bytearray:
|
) -> bytearray:
|
||||||
data: bytearray = bytearray()
|
data: bytearray = bytearray()
|
||||||
|
|
||||||
for entry in _PALETTE_COLORS:
|
for entry in _PALETTE_ENTRIES:
|
||||||
color: str | None = palette.get(entry, None)
|
color: str | Sequence[int] | None = palette.get(entry, None)
|
||||||
|
|
||||||
if color is None:
|
if color is None:
|
||||||
raise ValueError(f"no entry found for {entry}")
|
raise ValueError(f"no entry found for {entry}")
|
||||||
|
if isinstance(color, str):
|
||||||
|
r, g, b = colorFromString(color)
|
||||||
|
else:
|
||||||
|
r, g, b = color
|
||||||
|
|
||||||
matched: re.Match | None = _PALETTE_COLOR_REGEX.match(color)
|
data.extend(_PALETTE_ENTRY_STRUCT.pack(r, g, b))
|
||||||
|
|
||||||
if matched is None:
|
|
||||||
raise ValueError(f"invalid color value: {color}")
|
|
||||||
|
|
||||||
colorValue: bytes = bytes.fromhex(matched.group(1))
|
|
||||||
|
|
||||||
data.extend(_PALETTE_ENTRY_STRUCT.pack(colorValue))
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -241,7 +239,7 @@ def _walkStringTree(
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
yield hashData(fullKey.encode("ascii")), None
|
yield hashData(fullKey.encode("ascii")), None
|
||||||
elif type(value) is str:
|
elif isinstance(value, str):
|
||||||
yield hashData(fullKey.encode("ascii")), _convertString(value)
|
yield hashData(fullKey.encode("ascii")), _convertString(value)
|
||||||
else:
|
else:
|
||||||
yield from _walkStringTree(value, f"{fullKey}.")
|
yield from _walkStringTree(value, f"{fullKey}.")
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from hashlib import md5
|
import logging, re
|
||||||
from io import SEEK_SET, SEEK_END
|
from collections import deque
|
||||||
from typing import BinaryIO, ByteString, Iterable, Iterator, Sequence, TextIO
|
from hashlib import md5
|
||||||
|
from io import SEEK_END, SEEK_SET
|
||||||
|
from typing import \
|
||||||
|
BinaryIO, ByteString, Iterable, Iterator, Mapping, MutableSequence, \
|
||||||
|
Sequence, TextIO
|
||||||
|
|
||||||
## Misc. utilities
|
import numpy
|
||||||
|
|
||||||
|
## Value and array manipulation
|
||||||
|
|
||||||
def signExtend(value: int, bitLength: int) -> int:
|
def signExtend(value: int, bitLength: int) -> int:
|
||||||
signMask: int = 1 << (bitLength - 1)
|
signMask: int = 1 << (bitLength - 1)
|
||||||
@ -12,12 +18,31 @@ def signExtend(value: int, bitLength: int) -> int:
|
|||||||
|
|
||||||
return (value & valueMask) - (value & signMask)
|
return (value & valueMask) - (value & signMask)
|
||||||
|
|
||||||
|
def blitArray(
|
||||||
|
source: numpy.ndarray, dest: numpy.ndarray, position: Sequence[int]
|
||||||
|
):
|
||||||
|
pos: map[int | None] = map(lambda x: x if x >= 0 else None, position)
|
||||||
|
neg: map[int | None] = map(lambda x: -x if x < 0 else None, position)
|
||||||
|
|
||||||
|
destView: numpy.ndarray = dest[tuple(
|
||||||
|
slice(start, None) for start in pos
|
||||||
|
)]
|
||||||
|
sourceView: numpy.ndarray = source[tuple(
|
||||||
|
slice(start, end) for start, end in zip(neg, destView.shape)
|
||||||
|
)]
|
||||||
|
|
||||||
|
destView[tuple(
|
||||||
|
slice(None, end) for end in source.shape
|
||||||
|
)] = sourceView
|
||||||
|
|
||||||
## String manipulation
|
## String manipulation
|
||||||
|
|
||||||
# This encoding is similar to standard base45, but with some problematic
|
# This encoding is similar to standard base45, but with some problematic
|
||||||
# characters (' ', '$', '%', '*') excluded.
|
# characters (' ', '$', '%', '*') excluded.
|
||||||
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:"
|
_BASE41_CHARSET: str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./:"
|
||||||
|
|
||||||
|
_COLOR_REGEX: re.Pattern = re.compile(r"^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")
|
||||||
|
|
||||||
def toPrintableChar(value: int) -> str:
|
def toPrintableChar(value: int) -> str:
|
||||||
if (value < 0x20) or (value > 0x7e):
|
if (value < 0x20) or (value > 0x7e):
|
||||||
return "."
|
return "."
|
||||||
@ -54,6 +79,27 @@ def decodeBase41(data: str) -> bytearray:
|
|||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def colorFromString(value: str) -> tuple[int, int, int]:
|
||||||
|
matched: re.Match | None = _COLOR_REGEX.match(value)
|
||||||
|
|
||||||
|
if matched is None:
|
||||||
|
raise ValueError(f"invalid color value '{value}'")
|
||||||
|
|
||||||
|
digits: str = matched.group(1)
|
||||||
|
|
||||||
|
if len(digits) == 3:
|
||||||
|
return (
|
||||||
|
int(digits[0], 16) * 0x11,
|
||||||
|
int(digits[1], 16) * 0x11,
|
||||||
|
int(digits[2], 16) * 0x11
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
int(digits[0:2], 16),
|
||||||
|
int(digits[2:4], 16),
|
||||||
|
int(digits[4:6], 16)
|
||||||
|
)
|
||||||
|
|
||||||
## Hashes and checksums
|
## Hashes and checksums
|
||||||
|
|
||||||
def hashData(data: Iterable[int]) -> int:
|
def hashData(data: Iterable[int]) -> int:
|
||||||
@ -87,6 +133,19 @@ def shortenedMD5(data: ByteString) -> bytearray:
|
|||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
def setupLogger(level: int | None):
|
||||||
|
logging.basicConfig(
|
||||||
|
format = "[{levelname:8s}] {message}",
|
||||||
|
style = "{",
|
||||||
|
level = (
|
||||||
|
logging.WARNING,
|
||||||
|
logging.INFO,
|
||||||
|
logging.DEBUG
|
||||||
|
)[min(level or 0, 2)]
|
||||||
|
)
|
||||||
|
|
||||||
## Odd/even interleaved file reader
|
## Odd/even interleaved file reader
|
||||||
|
|
||||||
class InterleavedFile(BinaryIO):
|
class InterleavedFile(BinaryIO):
|
||||||
@ -139,3 +198,158 @@ class InterleavedFile(BinaryIO):
|
|||||||
|
|
||||||
self._offset += _length
|
self._offset += _length
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
## Boolean algebra expression parser
|
||||||
|
|
||||||
|
class BooleanOperator:
|
||||||
|
precedence: int = 1
|
||||||
|
operands: int = 2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def execute(stack: MutableSequence[bool]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AndOperator(BooleanOperator):
|
||||||
|
precedence: int = 2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def execute(stack: MutableSequence[bool]):
|
||||||
|
a: bool = stack.pop()
|
||||||
|
b: bool = stack.pop()
|
||||||
|
|
||||||
|
stack.append(a and b)
|
||||||
|
|
||||||
|
class OrOperator(BooleanOperator):
|
||||||
|
@staticmethod
|
||||||
|
def execute(stack: MutableSequence[bool]):
|
||||||
|
a: bool = stack.pop()
|
||||||
|
b: bool = stack.pop()
|
||||||
|
|
||||||
|
stack.append(a or b)
|
||||||
|
|
||||||
|
class XorOperator(BooleanOperator):
|
||||||
|
@staticmethod
|
||||||
|
def execute(stack: MutableSequence[bool]):
|
||||||
|
a: bool = stack.pop()
|
||||||
|
b: bool = stack.pop()
|
||||||
|
|
||||||
|
stack.append(a != b)
|
||||||
|
|
||||||
|
class NotOperator(BooleanOperator):
|
||||||
|
precedence: int = 3
|
||||||
|
operands: int = 1
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def execute(stack: MutableSequence):
|
||||||
|
stack.append(not stack.pop())
|
||||||
|
|
||||||
|
_OPERATORS: Mapping[str, type[BooleanOperator]] = {
|
||||||
|
"*": AndOperator,
|
||||||
|
"+": OrOperator,
|
||||||
|
"@": XorOperator,
|
||||||
|
"~": NotOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanFunction:
|
||||||
|
def __init__(self, expression: str):
|
||||||
|
# "Compile" the expression to its respective RPN representation using
|
||||||
|
# the shunting yard algorithm.
|
||||||
|
self.expression: list[str | type[BooleanOperator]] = []
|
||||||
|
|
||||||
|
operators: deque[str] = deque()
|
||||||
|
tokenBuffer: str = ""
|
||||||
|
|
||||||
|
for char in expression:
|
||||||
|
if char not in "*+@~()":
|
||||||
|
tokenBuffer += char
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Flush the non-operator token buffer when an operator is
|
||||||
|
# encountered.
|
||||||
|
if tokenBuffer:
|
||||||
|
self.expression.append(tokenBuffer)
|
||||||
|
tokenBuffer = ""
|
||||||
|
|
||||||
|
match char:
|
||||||
|
case "(":
|
||||||
|
operators.append(char)
|
||||||
|
case ")":
|
||||||
|
if "(" not in operators:
|
||||||
|
raise RuntimeError("mismatched parentheses in expression")
|
||||||
|
while (op := operators.pop()) != "(":
|
||||||
|
self.expression.append(_OPERATORS[op])
|
||||||
|
case _:
|
||||||
|
precedence: int = _OPERATORS[char].precedence
|
||||||
|
|
||||||
|
while operators:
|
||||||
|
op: str = operators[-1]
|
||||||
|
|
||||||
|
if op == "(":
|
||||||
|
break
|
||||||
|
if _OPERATORS[op].precedence < precedence:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.expression.append(_OPERATORS[op])
|
||||||
|
operators.pop()
|
||||||
|
|
||||||
|
operators.append(char)
|
||||||
|
|
||||||
|
if tokenBuffer:
|
||||||
|
self.expression.append(tokenBuffer)
|
||||||
|
tokenBuffer = ""
|
||||||
|
|
||||||
|
if "(" in operators:
|
||||||
|
raise RuntimeError("mismatched parentheses in expression")
|
||||||
|
while operators:
|
||||||
|
self.expression.append(_OPERATORS[operators.pop()])
|
||||||
|
|
||||||
|
def evaluate(self, variables: Mapping[str, bool]) -> bool:
|
||||||
|
values: dict[str, bool] = { "0": False, "1": True, **variables }
|
||||||
|
stack: deque[bool] = deque()
|
||||||
|
|
||||||
|
for token in self.expression:
|
||||||
|
if isinstance(token, str):
|
||||||
|
value: bool | None = values.get(token)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
raise RuntimeError(f"unknown variable '{token}'")
|
||||||
|
|
||||||
|
stack.append(value)
|
||||||
|
else:
|
||||||
|
token.execute(stack)
|
||||||
|
|
||||||
|
if len(stack) != 1:
|
||||||
|
raise RuntimeError("invalid or malformed expression")
|
||||||
|
|
||||||
|
return stack[0]
|
||||||
|
|
||||||
|
## Logic lookup table conversion
|
||||||
|
|
||||||
|
def generateLUTFromExpression(expression: str, inputs: Sequence[str]) -> int:
|
||||||
|
lut: int = 0
|
||||||
|
|
||||||
|
function: BooleanFunction = BooleanFunction(expression)
|
||||||
|
variables: dict[str, bool] = {}
|
||||||
|
|
||||||
|
for index in range(1 << len(inputs)):
|
||||||
|
for bit, name in enumerate(inputs):
|
||||||
|
variables[name] = bool((index >> bit) & 1)
|
||||||
|
|
||||||
|
if function.evaluate(variables):
|
||||||
|
lut |= 1 << index # LSB-first
|
||||||
|
|
||||||
|
return lut
|
||||||
|
|
||||||
|
def generateExpressionFromLUT(lut: int, inputs: Sequence[str]) -> str:
|
||||||
|
products: list[str] = []
|
||||||
|
|
||||||
|
for index in range(1 << len(inputs)):
|
||||||
|
values: str = "*".join(
|
||||||
|
(value if (index >> bit) & 1 else f"~{value}")
|
||||||
|
for bit, value in enumerate(inputs)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (lut >> index) & 1:
|
||||||
|
products.append(f"({values})")
|
||||||
|
|
||||||
|
return "+".join(products) or "0"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# py -m pip install -r tools/requirements.txt (Windows)
|
# py -m pip install -r tools/requirements.txt (Windows)
|
||||||
# sudo pip install -r tools/requirements.txt (Linux/macOS)
|
# sudo pip install -r tools/requirements.txt (Linux/macOS)
|
||||||
|
|
||||||
lz4 >= 4.3.2
|
lz4 >= 4.3.2
|
||||||
numpy >= 1.19.4
|
numpy >= 1.19.4
|
||||||
Pillow >= 8.2.0
|
Pillow >= 8.2.0
|
||||||
|
pycdlib >= 1.14.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user