Files
Nazky dab6fad2ff little update
- Add a GUI (beta for now, expect some bugs)
- Add archive support (same beta, bugs, etc.)
- Update README
- Add batch and bash script to use the GUI
- More i forgot...
2026-04-08 13:35:10 +02:00

2670 lines
110 KiB
Python

"""
PS5 Backport Library
A library for processing PS5 ELF/SELF files including SDK downgrade, fake signing, and decryption.
"""
import os
import sys
import shutil
import argparse
import subprocess
import tempfile
import json
import ctypes
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Union, Any
import os
import sys
import shutil
import argparse
import subprocess
import tempfile
import json
import ctypes
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Union, Any
# =====================================================================
# CROSS-PLATFORM ENVIRONMENT FIXER (Windows PATH quirk)
# =====================================================================
def _fix_windows_environment_paths():
"""
On Windows, standard installers for 7-Zip and WinRAR often DO NOT add
their directories to the system PATH. This silently adds them to the
current Python process PATH so py7zr, rarfile, and subprocess can locate them.
"""
if sys.platform != "win32":
return
current_path = os.environ.get("PATH", "")
dirs_to_add = []
seven_zip_dir = Path(r"C:\Program Files\7-Zip")
if seven_zip_dir.exists() and str(seven_zip_dir) not in current_path:
dirs_to_add.append(str(seven_zip_dir))
winrar_dir = Path(r"C:\Program Files\WinRAR")
if winrar_dir.exists() and str(winrar_dir) not in current_path:
dirs_to_add.append(str(winrar_dir))
if dirs_to_add:
os.environ["PATH"] = current_path + os.pathsep + os.pathsep.join(dirs_to_add)
_fix_windows_environment_paths()
from src.Archive import ArchiveHandler, extract_archive_to_temp, cleanup_temp_dir
# ==========================================================================
# CROSS-PLATFORM ANSI COLOR SUPPORT
# Enables VT100 escape sequences on Windows 10+ consoles automatically.
# ==========================================================================
def _enable_windows_ansi():
"""Enable Windows Virtual Terminal Processing for ANSI escape sequences."""
if sys.platform == 'nt':
try:
kernel32 = ctypes.windll.kernel32
# STD_OUTPUT_HANDLE = -11
h_out = kernel32.GetStdHandle(-11)
mode = ctypes.c_ulong()
kernel32.GetConsoleMode(h_out, ctypes.byref(mode))
# ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
kernel32.SetConsoleMode(h_out, mode.value | 0x0004)
# STD_ERROR_HANDLE = -12
h_err = kernel32.GetStdHandle(-12)
kernel32.GetConsoleMode(h_err, ctypes.byref(mode))
kernel32.SetConsoleMode(h_err, mode.value | 0x0004)
except Exception:
# Silently fail if we can't enable VT mode (e.g., redirected output, older Windows)
pass
_enable_windows_ansi()
# ==========================================================================
try:
from src.ps5_sdk_version_patcher import SDKVersionPatcher
from src.make_fself import FakeSignedELFConverter
from src.decrypt_fself import UnsignedELFConverter
except ImportError:
try:
from .src.ps5_sdk_version_patcher import SDKVersionPatcher
from .src.make_fself import FakeSignedELFConverter
from .src.decrypt_fself import UnsignedELFConverter
except ImportError:
raise ImportError(
"Could not import required modules. "
"Please ensure ps5_sdk_version_patcher.py, make_fself.py, "
"and decrypt_fself.py are available in the src folder."
)
# ANSI color codes
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
CYAN = '\033[96m'
BLUE = '\033[94m'
RESET = '\033[0m'
BOLD = '\033[1m'
# Configuration file path
CONFIG_FILE = "ps5_backport_config.json"
class PS5ELFProcessor:
"""Main class for PS5 ELF processing operations."""
# Constants for libc.prx patching
LIBC_PATCH_PATTERN = b'4h6F1LLbTiw#A#B'
LIBC_PATCH_REPLACEMENT = b'IWIBBdTHit4#A#B'
def __init__(self, use_colors: bool = True, project_root: Optional[Union[str, Path]] = None):
"""
Initialize the PS5 ELF processor.
Args:
use_colors: Whether to use colored output in console
project_root: Root directory of the project (for finding fakelib).
If None, uses directory of this file.
"""
# Disable colors automatically if output is redirected to a file
if use_colors and not sys.stdout.isatty():
self.use_colors = False
else:
self.use_colors = use_colors
self.project_root = Path(project_root) if project_root else Path(__file__).parent
def _color(self, text: str, color_code: str) -> str:
"""Apply color to text if colors are enabled."""
return color_code + text + RESET if self.use_colors else text
def _print(self, message: str, color: Optional[str] = None, bold: bool = False):
"""Print a message with optional color and bold."""
if color:
message = self._color(message, color)
if bold and self.use_colors:
message = BOLD + message
print(message)
def _is_elf_file(self, file_path: Path) -> bool:
"""Check if a file is an ELF file by checking its magic bytes."""
if file_path.name.endswith('.bak'):
return False
try:
with open(file_path, 'rb') as f:
magic = f.read(4)
return magic == b'\x7FELF'
except:
return False
def _is_self_file(self, file_path: Path) -> bool:
"""Check if a file is a SELF file by checking its magic bytes."""
if file_path.name.endswith('.bak'):
return False
try:
with open(file_path, 'rb') as f:
magic = f.read(4)
return magic in [b'\x4F\x15\x3D\x1D', b'\x54\x14\xF5\xEE']
except:
return False
def _should_skip_dir(self, dirs: List[str], skip_name: str = 'decrypted') -> None:
"""Remove directories with specific names (case-insensitive) from the dirs list to skip them."""
dirs_to_remove = [d for d in dirs if d.lower() == skip_name.lower()]
for dir_name in dirs_to_remove:
dirs.remove(dir_name)
def get_supported_sdk_pairs(self) -> Dict[int, Tuple[int, int]]:
"""Get all supported SDK version pairs."""
return SDKVersionPatcher.get_supported_pairs()
def get_sdk_pair_info(self, sdk_pair: int) -> Optional[Tuple[int, int]]:
"""Get PS5 and PS4 SDK versions for a specific pair."""
pairs = self.get_supported_sdk_pairs()
return pairs.get(sdk_pair)
def parse_ptype(self, ptype_str: str) -> int:
"""Parse program type from string (e.g., 'fake', 'npdrm_exec')."""
return FakeSignedELFConverter.parse_ptype(ptype_str.lower())
def decrypt_files(
self,
input_dir: Union[str, Path],
output_dir: Union[str, Path],
overwrite: bool = False,
verbose: bool = True,
save_to_config: bool = True
) -> Dict[str, Any]:
"""
Decrypt SELF files back to ELF files.
Args:
input_dir: Directory containing SELF files
output_dir: Directory for output ELF files
overwrite: Overwrite existing files
verbose: Print progress information
save_to_config: Whether to save directories to config file (default: True)
Returns:
Dictionary with processing results
"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
# Save directories to config only if requested
if save_to_config:
self._save_directories_to_config(str(input_dir), str(output_dir))
results = {
'operation': 'decrypt',
'input_dir': str(input_dir),
'output_dir': str(output_dir),
'successful': 0,
'failed': 0,
'files': {},
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Step 1/1] Decrypting SELF Files", BLUE, bold=True)
self._print(f"Input: {input_dir}", CYAN)
self._print(f"Output: {output_dir}", CYAN)
# Find all SELF files in input directory
self_files = []
for root, dirs, files in os.walk(input_dir):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_self_file(file_path):
self_files.append(file_path)
if not self_files:
if verbose:
self._print(f"No SELF files found in input directory", YELLOW)
return results
if verbose:
self._print(f"Found {len(self_files)} SELF file(s) to decrypt", CYAN)
# Create output directory structure
output_dir.mkdir(parents=True, exist_ok=True)
# Initialize converter
converter = UnsignedELFConverter(verbose=verbose)
for self_file in self_files:
relative_path = self_file.relative_to(input_dir)
# Output file keeps same name and extension
output_file = output_dir / relative_path
# Create parent directories if they don't exist
output_file.parent.mkdir(parents=True, exist_ok=True)
# Check if file exists and skip if not overwriting
if output_file.exists() and not overwrite:
if verbose:
self._print(f"Skipping (exists): {relative_path}", YELLOW)
continue
if verbose:
self._print(f"Decrypting: {relative_path}", None)
# Decrypt the file
try:
success = converter.convert_file(str(self_file), str(output_file))
file_result = {
'success': success,
'output': str(output_file),
'message': 'Success' if success else 'Failed'
}
results['files'][str(self_file)] = file_result
if success:
results['successful'] += 1
if verbose:
self._print(f" ✓ Success", GREEN)
else:
results['failed'] += 1
if verbose:
self._print(f" ✗ Failed", RED)
except Exception as e:
results['failed'] += 1
error_msg = f"Error: {str(e)}"
file_result = {
'success': False,
'output': str(output_file),
'message': error_msg
}
results['files'][str(self_file)] = file_result
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nDecryption complete: {results['successful']} successful, "
f"{results['failed']} failed", CYAN)
return results
def apply_libc_patch(
self,
input_dir: Union[str, Path],
search_pattern: bytes = None,
replacement_pattern: bytes = None,
create_backup: bool = True,
verbose: bool = True
) -> Dict[str, Any]:
"""
Apply the libc.prx patch to SELF files in the directory or a single file.
Args:
input_dir: Directory containing SELF files to patch OR a single file path
search_pattern: Bytes pattern to search for (defaults to LIBC_PATCH_PATTERN)
replacement_pattern: Bytes pattern to replace with (defaults to LIBC_PATCH_REPLACEMENT)
create_backup: Create backup files before patching
verbose: Print progress information
Returns:
Dictionary with patching results
"""
input_path = Path(input_dir)
if search_pattern is None:
search_pattern = self.LIBC_PATCH_PATTERN
if replacement_pattern is None:
replacement_pattern = self.LIBC_PATCH_REPLACEMENT
results = {
'operation': 'apply_libc_patch',
'input_path': str(input_path),
'is_file': input_path.is_file(),
'search_pattern': search_pattern.hex(),
'replacement_pattern': replacement_pattern.hex(),
'applied': 0,
'already_patched': 0,
'pattern_not_found': 0,
'failed': 0,
'files': {},
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Libc Patch] Applying libc.prx patch", BLUE, bold=True)
if input_path.is_file():
self._print(f"Input file: {input_path}", CYAN)
else:
self._print(f"Input directory: {input_path}", CYAN)
self._print(f"Search pattern: {search_pattern}", CYAN)
self._print(f"Replacement pattern: {replacement_pattern}", CYAN)
# Collect files to patch
files_to_patch = []
if input_path.is_file():
# Single file input
if self._is_self_file(input_path) or 'libc' in input_path.name.lower():
files_to_patch.append(input_path)
elif verbose:
self._print(f"Warning: Input file may not be a SELF or libc file", YELLOW)
files_to_patch.append(input_path) # Try anyway
else:
# Directory input - search recursively
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_self_file(file_path):
files_to_patch.append(file_path)
# Also search for libc.prx files specifically
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.lower() == 'libc.prx' and file_path not in files_to_patch:
files_to_patch.append(file_path)
if not files_to_patch:
if verbose:
if input_path.is_file():
self._print(f"Input file is not a recognized SELF or libc file", YELLOW)
else:
self._print(f"No SELF files found in input directory", YELLOW)
return results
if verbose:
if input_path.is_file():
self._print(f"Checking 1 file", CYAN)
else:
self._print(f"Found {len(files_to_patch)} file(s) to check for libc patch", CYAN)
for file_path in files_to_patch:
# For single file input, show just filename. For directory input, show relative path
if input_path.is_file():
display_path = file_path.name
else:
relative_path = file_path.relative_to(input_path)
display_path = str(relative_path)
if verbose:
self._print(f"Checking: {display_path}", None)
try:
# Read file content
with open(file_path, 'rb') as f:
content = f.read()
# Check if pattern exists
if search_pattern in content:
# Check if already has replacement pattern
if replacement_pattern in content:
results['already_patched'] += 1
results['files'][str(file_path)] = {
'status': 'already_patched',
'message': 'File already contains replacement pattern'
}
if verbose:
self._print(f" ⚠ Already patched", YELLOW)
continue
# Create backup if requested
backup_path = None
if create_backup:
backup_path = file_path.with_suffix('.bak')
shutil.copy2(file_path, backup_path)
try:
# Apply patch
patched_content = content.replace(search_pattern, replacement_pattern)
# Write patched content
with open(file_path, 'wb') as f:
f.write(patched_content)
# Verify patch
with open(file_path, 'rb') as f:
new_content = f.read()
if search_pattern not in new_content and replacement_pattern in new_content:
results['applied'] += 1
file_result = {
'status': 'applied',
'backup': str(backup_path) if backup_path else None,
'message': 'Patch applied successfully'
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✓ Patch applied", GREEN)
else:
# Restore from backup if exists
if backup_path and backup_path.exists():
shutil.copy2(backup_path, file_path)
results['failed'] += 1
file_result = {
'status': 'failed',
'message': 'Patch verification failed'
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Patch verification failed", RED)
# Clean up backup if successful
if backup_path and backup_path.exists():
try:
os.remove(backup_path)
if 'backup' in file_result:
file_result['backup_cleaned'] = True
except:
pass
except Exception as e:
# Restore from backup on error
if backup_path and backup_path.exists():
shutil.copy2(backup_path, file_path)
results['failed'] += 1
file_result = {
'status': 'error',
'message': f"Error during patching: {str(e)}"
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Error: {str(e)[:50]}", RED)
else:
# Pattern not found in this file
results['pattern_not_found'] += 1
results['files'][str(file_path)] = {
'status': 'pattern_not_found',
'message': 'Search pattern not found in file'
}
if 'libc' in file_path.name.lower():
if verbose:
self._print(f" ⚠ Pattern not found in libc file", YELLOW)
elif verbose:
self._print(f" Pattern not found", CYAN)
except Exception as e:
results['failed'] += 1
file_result = {
'status': 'error',
'message': f"Error reading file: {str(e)}"
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Error reading file: {str(e)[:50]}", RED)
if verbose:
self._print(f"\nLibc patch complete:", CYAN)
self._print(f" Applied: {results['applied']}", GREEN)
self._print(f" Already patched: {results['already_patched']}", YELLOW)
self._print(f" Pattern not found: {results['pattern_not_found']}")
self._print(f" Failed: {results['failed']}", RED if results['failed'] > 0 else "")
return results
def revert_libc_patch(
self,
input_dir: Union[str, Path],
search_pattern: bytes = None,
original_pattern: bytes = None,
create_backup: bool = True,
verbose: bool = True
) -> Dict[str, Any]:
"""
Revert the libc.prx patch from SELF files in the directory or a single file.
This restores files patched by apply_libc_patch() back to their original state.
Args:
input_dir: Directory containing SELF files to revert OR a single file path
search_pattern: Bytes pattern to search for (defaults to LIBC_PATCH_REPLACEMENT)
original_pattern: Bytes pattern to restore (defaults to LIBC_PATCH_PATTERN)
create_backup: Create backup files before reverting
verbose: Print progress information
Returns:
Dictionary with reversion results
"""
input_path = Path(input_dir)
if search_pattern is None:
search_pattern = self.LIBC_PATCH_REPLACEMENT
if original_pattern is None:
original_pattern = self.LIBC_PATCH_PATTERN
results = {
'operation': 'revert_libc_patch',
'input_path': str(input_path),
'is_file': input_path.is_file(),
'search_pattern': search_pattern.hex(),
'original_pattern': original_pattern.hex(),
'reverted': 0,
'already_original': 0,
'patch_not_found': 0,
'failed': 0,
'files': {},
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Libc Patch] Reverting libc.prx patch", BLUE, bold=True)
if input_path.is_file():
self._print(f"Input file: {input_path}", CYAN)
else:
self._print(f"Input directory: {input_path}", CYAN)
self._print(f"Search pattern: {search_pattern}", CYAN)
self._print(f"Restore pattern: {original_pattern}", CYAN)
self._print(f"This will restore files to original state for SDK > 6", CYAN)
# Collect files to revert
files_to_revert = []
if input_path.is_file():
# Single file input
if self._is_self_file(input_path) or 'libc' in input_path.name.lower():
files_to_revert.append(input_path)
elif verbose:
self._print(f"Warning: Input file may not be a SELF or libc file", YELLOW)
files_to_revert.append(input_path) # Try anyway
else:
# Directory input - search recursively
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_self_file(file_path):
files_to_revert.append(file_path)
# Also search for libc.prx files specifically
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.lower() == 'libc.prx' and file_path not in files_to_revert:
files_to_revert.append(file_path)
if not files_to_revert:
if verbose:
if input_path.is_file():
self._print(f"Input file is not a recognized SELF or libc file", YELLOW)
else:
self._print(f"No SELF files found in input directory", YELLOW)
return results
if verbose:
if input_path.is_file():
self._print(f"Checking 1 file", CYAN)
else:
self._print(f"Found {len(files_to_revert)} file(s) to check for libc patch reversion", CYAN)
for file_path in files_to_revert:
# For single file input, show just filename. For directory input, show relative path
if input_path.is_file():
display_path = file_path.name
else:
relative_path = file_path.relative_to(input_path)
display_path = str(relative_path)
if verbose:
self._print(f"Checking: {display_path}", None)
try:
# Read file content
with open(file_path, 'rb') as f:
content = f.read()
# Check if patch pattern exists
if search_pattern in content:
# Check if already has original pattern (already reverted)
if original_pattern in content:
results['already_original'] += 1
results['files'][str(file_path)] = {
'status': 'already_original',
'message': 'File already contains original pattern'
}
if verbose:
self._print(f" ⚠ Already original", YELLOW)
continue
# Create backup if requested
backup_path = None
if create_backup:
backup_path = file_path.with_suffix('.revert_bak')
shutil.copy2(file_path, backup_path)
try:
# Revert patch
reverted_content = content.replace(search_pattern, original_pattern)
# Write reverted content
with open(file_path, 'wb') as f:
f.write(reverted_content)
# Verify reversion
with open(file_path, 'rb') as f:
new_content = f.read()
if original_pattern in new_content and search_pattern not in new_content:
results['reverted'] += 1
file_result = {
'status': 'reverted',
'backup': str(backup_path) if backup_path else None,
'message': 'Patch reverted successfully'
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✓ Patch reverted", GREEN)
else:
# Restore from backup if exists
if backup_path and backup_path.exists():
shutil.copy2(backup_path, file_path)
results['failed'] += 1
file_result = {
'status': 'failed',
'message': 'Reversion verification failed'
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Reversion verification failed", RED)
# Clean up backup if successful
if backup_path and backup_path.exists():
try:
os.remove(backup_path)
if 'backup' in file_result:
file_result['backup_cleaned'] = True
except:
pass
except Exception as e:
# Restore from backup on error
if backup_path and backup_path.exists():
shutil.copy2(backup_path, file_path)
results['failed'] += 1
file_result = {
'status': 'error',
'message': f"Error during reversion: {str(e)}"
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Error: {str(e)[:50]}", RED)
else:
# Patch pattern not found in this file
results['patch_not_found'] += 1
results['files'][str(file_path)] = {
'status': 'patch_not_found',
'message': 'Patch pattern not found in file'
}
if verbose and 'libc' in file_path.name.lower():
self._print(f" ⚠ Patch pattern not found in libc file", YELLOW)
elif verbose:
self._print(f" Patch pattern not found", CYAN)
except Exception as e:
results['failed'] += 1
file_result = {
'status': 'error',
'message': f"Error reading file: {str(e)}"
}
results['files'][str(file_path)] = file_result
if verbose:
self._print(f" ✗ Error reading file: {str(e)[:50]}", RED)
if verbose:
self._print(f"\nLibc patch reversion complete:", CYAN)
self._print(f" Reverted: {results['reverted']}", GREEN)
self._print(f" Already original: {results['already_original']}", YELLOW)
self._print(f" Patch not found: {results['patch_not_found']}")
self._print(f" Failed: {results['failed']}", RED if results['failed'] > 0 else "")
return results
def check_libc_patch_status(
self,
input_path: Union[str, Path],
search_pattern: bytes = None,
patch_pattern: bytes = None,
verbose: bool = True
) -> Dict[str, Any]:
"""
Check the status of libc.prx patches in SELF files or a single file.
Args:
input_path: Directory containing SELF files OR a single file to check
search_pattern: Original bytes pattern (defaults to LIBC_PATCH_PATTERN)
patch_pattern: Patch bytes pattern (defaults to LIBC_PATCH_REPLACEMENT)
verbose: Print progress information
Returns:
Dictionary with patch status information
"""
input_path = Path(input_path)
if search_pattern is None:
search_pattern = self.LIBC_PATCH_PATTERN
if patch_pattern is None:
patch_pattern = self.LIBC_PATCH_REPLACEMENT
results = {
'operation': 'check_libc_patch_status',
'input_path': str(input_path),
'is_file_input': input_path.is_file(),
'original_pattern': search_pattern.hex(),
'patch_pattern': patch_pattern.hex(),
'original_files': [],
'patched_files': [],
'both_patterns_files': [],
'no_pattern_files': [],
'error_files': [],
'total_files': 0,
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Libc Patch] Checking libc.prx patch status", BLUE, bold=True)
if input_path.is_file():
self._print(f"Input: Single file: {input_path}", CYAN)
else:
self._print(f"Input: Directory: {input_path}", CYAN)
# Collect files to check
self_files = []
if input_path.is_file():
# Single file input
if self._is_self_file(input_path) or 'libc' in input_path.name.lower():
self_files.append(input_path)
elif verbose:
self._print(f"Input file may not be a recognized SELF or libc file", YELLOW)
self_files.append(input_path) # Try to check anyway
else:
# Directory input - search recursively
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_self_file(file_path):
self_files.append(file_path)
# Also search for libc.prx files specifically
for root, dirs, files in os.walk(input_path):
# Skip folders named "decrypted"
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.lower() == 'libc.prx' and file_path not in self_files:
self_files.append(file_path)
if not self_files:
if verbose:
if input_path.is_file():
self._print(f"Input file is not a valid SELF or libc file", YELLOW)
else:
self._print(f"No SELF files found in input directory", YELLOW)
return results
results['total_files'] = len(self_files)
if verbose:
if input_path.is_file():
self._print(f"Checking 1 file", CYAN)
else:
self._print(f"Found {len(self_files)} file(s) to check", CYAN)
for file_path in self_files:
# For single file input, show just filename. For directory input, show relative path
if input_path.is_file():
display_path = file_path.name
relative_path = file_path # Use full path for single file
else:
relative_path = file_path.relative_to(input_path)
display_path = str(relative_path)
try:
# Read file content
with open(file_path, 'rb') as f:
content = f.read()
has_original = search_pattern in content
has_patch = patch_pattern in content
file_info = {
'path': str(file_path),
'has_original': has_original,
'has_patch': has_patch,
'is_libc_file': 'libc' in file_path.name.lower()
}
# Add relative path only for directory inputs
if not input_path.is_file():
file_info['relative_path'] = str(relative_path)
if has_original and not has_patch:
results['original_files'].append(file_info)
status = "Original (not patched)"
color = GREEN
elif has_patch and not has_original:
results['patched_files'].append(file_info)
status = "Patched"
color = YELLOW
elif has_original and has_patch:
results['both_patterns_files'].append(file_info)
status = "Both patterns found (error)"
color = RED
else:
results['no_pattern_files'].append(file_info)
status = "No patterns found"
color = CYAN
if verbose:
status_display = f"{display_path}"
if 'libc' in file_path.name.lower():
status_display += " [libc]"
self._print(f"{status_display}: {status}", color)
except Exception as e:
error_info = {
'path': str(file_path),
'error': str(e)
}
# Add relative path only for directory inputs
if not input_path.is_file():
error_info['relative_path'] = str(relative_path)
results['error_files'].append(error_info)
if verbose:
self._print(f"{display_path}: Error reading file", RED)
if verbose:
self._print(f"\nPatch status summary:", BLUE, bold=True)
self._print(f" Original files (not patched): {len(results['original_files'])}", GREEN)
self._print(f" Patched files: {len(results['patched_files'])}", YELLOW)
self._print(f" Both patterns (error): {len(results['both_patterns_files'])}", RED)
self._print(f" No patterns: {len(results['no_pattern_files'])}", CYAN)
self._print(f" Error reading: {len(results['error_files'])}", RED)
self._print(f" Total files: {results['total_files']}")
return results
def check_libc_patch_in_file(
self,
input_file: Union[str, Path],
search_pattern: bytes = None,
patch_pattern: bytes = None,
verbose: bool = True
) -> Dict[str, Any]:
"""
Check the status of libc.prx patch in a single file.
Args:
input_file: Single file to check
search_pattern: Original bytes pattern (defaults to LIBC_PATCH_PATTERN)
patch_pattern: Patch bytes pattern (defaults to LIBC_PATCH_REPLACEMENT)
verbose: Print progress information
Returns:
Dictionary with patch status information
"""
input_path = Path(input_file)
if not input_path.exists():
return {
'operation': 'check_libc_patch_in_file',
'input_file': str(input_file),
'error': 'File does not exist',
'timestamp': self._get_timestamp()
}
if not input_path.is_file():
return {
'operation': 'check_libc_patch_in_file',
'input_file': str(input_file),
'error': 'Path is not a file',
'timestamp': self._get_timestamp()
}
# Use the existing method but for single file
return self.check_libc_patch_status(
input_path=input_file,
search_pattern=search_pattern,
patch_pattern=patch_pattern,
verbose=verbose
)
def downgrade_and_sign(
self,
input_dir: Union[str, Path],
output_dir: Union[str, Path],
sdk_pair: int,
paid: int,
ptype: int,
fakelib_source: Optional[Union[str, Path]] = None,
create_backup: bool = True,
overwrite: bool = False,
apply_libc_patch: bool = True,
auto_revert_for_high_sdk: bool = True,
verbose: bool = True,
save_to_config: bool = True
) -> Dict[str, Any]:
"""
Process files through downgrade and signing pipeline.
IMPORTANT: libc.prx patch is applied AFTER signing to the SELF files.
"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
if save_to_config:
self._save_directories_to_config(str(input_dir), str(output_dir))
results = {
'operation': 'downgrade_and_sign',
'input_dir': str(input_dir),
'output_dir': str(output_dir),
'sdk_pair': sdk_pair,
'paid': paid,
'ptype': ptype,
'downgrade': {'successful': 0, 'failed': 0, 'files': {}},
'signing': {'successful': 0, 'failed': 0, 'files': {}},
'fakelib': {'success': False, 'message': ''},
'libc_patch': {'applied': 0, 'reverted': 0, 'results': {}},
'fakelib_copies': {'created': 0, 'locations': []},
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Step 1/4] Downgrading SDK Versions", BLUE, bold=True)
self._print(f"Input: {input_dir}", CYAN)
self._print(f"Output: {output_dir}", CYAN)
sdk_patcher = SDKVersionPatcher(
create_backup=create_backup,
use_colors=self.use_colors
)
sdk_patcher.set_versions_by_pair(sdk_pair)
ps5_ver, ps4_ver = sdk_patcher.get_current_versions()
results['ps5_sdk_version'] = ps5_ver
results['ps4_sdk_version'] = ps4_ver
if verbose:
self._print(f"Using PS5 SDK: 0x{ps5_ver:08X}, PS4 Version: 0x{ps4_ver:08X}", CYAN)
if sdk_pair <= 6:
self._print(f"SDK pair {sdk_pair} selected - will apply libc.prx patch AFTER signing", YELLOW)
elif auto_revert_for_high_sdk:
self._print(f"SDK pair {sdk_pair} > 6 - will revert libc.prx patch AFTER signing if found", YELLOW)
elf_files = []
for root, dirs, files in os.walk(input_dir):
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_elf_file(file_path):
elf_files.append(file_path)
if not elf_files:
if verbose:
self._print(f"No ELF files found in input directory", YELLOW)
return results
if verbose:
self._print(f"Found {len(elf_files)} ELF file(s) to process", CYAN)
for elf_file in elf_files:
relative_path = elf_file.relative_to(input_dir)
if verbose:
self._print(f"Downgrading: {relative_path}", None)
try:
success, message = sdk_patcher.patch_file(str(elf_file))
results['downgrade']['files'][str(elf_file)] = {
'success': success,
'message': message
}
if success:
results['downgrade']['successful'] += 1
if verbose:
self._print(f" ✓ Success", GREEN)
else:
results['downgrade']['failed'] += 1
if verbose:
self._print(f" ✗ {message[:50]}", RED)
except Exception as e:
results['downgrade']['failed'] += 1
error_msg = f"Error: {str(e)}"
results['downgrade']['files'][str(elf_file)] = {
'success': False,
'message': error_msg
}
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nDowngrade complete: {results['downgrade']['successful']} successful, "
f"{results['downgrade']['failed']} failed", CYAN)
if verbose:
self._print(f"\n[Step 2/4] Fake Signing Files (ELF → SELF)", BLUE, bold=True)
self._print(f"Using PAID: 0x{paid:016X}, PType: 0x{ptype:08X}", CYAN)
output_dir.mkdir(parents=True, exist_ok=True)
converter = FakeSignedELFConverter(
paid=paid,
ptype=ptype,
app_version=0,
fw_version=0,
auth_info=None
)
for elf_file in elf_files:
relative_path = elf_file.relative_to(input_dir)
input_file_str = str(elf_file)
if not results['downgrade']['files'].get(input_file_str, {}).get('success', False):
if verbose:
self._print(f"Skipping (downgrade failed): {relative_path}", YELLOW)
results['signing']['files'][str(elf_file)] = {
'success': False,
'output': '',
'message': 'Skipped due to downgrade failure'
}
results['signing']['failed'] += 1
continue
output_file = output_dir / relative_path
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists() and not overwrite:
if verbose:
self._print(f"Skipping (exists): {relative_path}", YELLOW)
continue
if verbose:
self._print(f"Signing: {relative_path}", None)
try:
success = converter.sign_file(input_file_str, str(output_file))
results['signing']['files'][str(elf_file)] = {
'success': success,
'output': str(output_file),
'message': 'Success' if success else 'Failed'
}
if success:
results['signing']['successful'] += 1
if verbose:
self._print(f" ✓ Success (converted to SELF)", GREEN)
else:
results['signing']['failed'] += 1
if verbose:
self._print(f" ✗ Failed", RED)
except Exception as e:
results['signing']['failed'] += 1
error_msg = f"Error: {str(e)}"
results['signing']['files'][str(elf_file)] = {
'success': False,
'output': str(output_file),
'message': error_msg
}
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nSigning complete: {results['signing']['successful']} successful, "
f"{results['signing']['failed']} failed", CYAN)
self._print(f"All ELF files have been converted to SELF format", CYAN)
if apply_libc_patch:
if sdk_pair <= 6:
if verbose:
self._print(f"\n[Step 3/4] Applying libc.prx patch to SELF files (SDK ≤ 6)", BLUE, bold=True)
self._print(f"Patching SELF files in output directory", CYAN)
patch_results = self.apply_libc_patch(
output_dir,
create_backup=False,
verbose=verbose
)
results['libc_patch']['applied'] = patch_results['applied']
results['libc_patch']['results'] = patch_results
elif auto_revert_for_high_sdk:
if verbose:
self._print(f"\n[Step 3/4] Reverting libc.prx patch from SELF files (SDK > 6)", BLUE, bold=True)
self._print(f"Reverting patches in SELF files in output directory", CYAN)
revert_results = self.revert_libc_patch(
output_dir,
create_backup=False,
verbose=verbose
)
results['libc_patch']['reverted'] = revert_results['reverted']
results['libc_patch']['results'] = revert_results
if fakelib_source:
fakelib_source_path = Path(fakelib_source)
if verbose:
self._print(f"\n[Step 4/4] Copying Fakelib Directory", BLUE, bold=True)
success, message = self._copy_fakelib(fakelib_source_path, output_dir)
results['fakelib']['success'] = success
results['fakelib']['message'] = message
if verbose:
if success:
self._print(f"✓ {message}", GREEN)
else:
self._print(f"⚠ {message}", YELLOW)
if success:
fakelib_copies = self._copy_fakelib_to_eboot_dirs(fakelib_source_path, output_dir, verbose)
results['fakelib_copies']['created'] = fakelib_copies['created']
results['fakelib_copies']['locations'] = fakelib_copies['locations']
if verbose and fakelib_copies['created'] > 0:
self._print(f"✓ Created {fakelib_copies['created']} fakelib copy(ies) in eboot.bin directories", GREEN)
return results
def decrypt_and_sign_pipeline(
self,
input_dir: Union[str, Path],
output_dir: Union[str, Path],
sdk_pair: int,
paid: int,
ptype: int,
fakelib_source: Optional[Union[str, Path]] = None,
create_backup: bool = True,
overwrite: bool = False,
apply_libc_patch: bool = True,
auto_revert_for_high_sdk: bool = True,
verbose: bool = True,
save_to_config: bool = True
) -> Dict[str, Any]:
"""
Process files through a combined pipeline that automatically detects file types.
If files are SELF: decrypt → save to decrypted folder → downgrade → sign
If files are ELF: downgrade → sign directly
libc.prx patch is applied AFTER signing to the SELF files.
"""
input_dir = Path(input_dir)
output_dir = Path(output_dir)
if save_to_config:
self._save_directories_to_config(str(input_dir), str(output_dir))
results = {
'operation': 'decrypt_and_sign_pipeline',
'input_dir': str(input_dir),
'output_dir': str(output_dir),
'sdk_pair': sdk_pair,
'paid': paid,
'ptype': ptype,
'detection': {'self_files': 0, 'elf_files': 0, 'other_files': 0},
'decrypt': {'successful': 0, 'failed': 0, 'files': {}},
'downgrade': {'successful': 0, 'failed': 0, 'files': {}},
'signing': {'successful': 0, 'failed': 0, 'files': {}},
'fakelib': {'success': False, 'message': ''},
'libc_patch': {'applied': 0, 'reverted': 0, 'results': {}},
'fakelib_copies': {'created': 0, 'locations': []},
'decrypted_folder': None,
'timestamp': self._get_timestamp()
}
if verbose:
self._print(f"\n[Step 1/5] Detecting File Types", BLUE, bold=True)
self._print(f"Input: {input_dir}", CYAN)
self._print(f"Output: {output_dir}", CYAN)
self_files = []
elf_files = []
for root, dirs, files in os.walk(input_dir):
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_self_file(file_path):
self_files.append(file_path)
results['detection']['self_files'] += 1
elif self._is_elf_file(file_path):
elf_files.append(file_path)
results['detection']['elf_files'] += 1
else:
results['detection']['other_files'] += 1
if verbose:
self._print(f"Found: {len(self_files)} SELF file(s), {len(elf_files)} ELF file(s), "
f"{results['detection']['other_files']} other file(s)", CYAN)
if not self_files and not elf_files:
if verbose:
self._print(f"No SELF or ELF files found in input directory", YELLOW)
return results
output_dir.mkdir(parents=True, exist_ok=True)
working_dir = output_dir / "working"
decrypt_output_dir = output_dir / "decrypted"
if self_files:
if verbose:
self._print(f"\n[Step 2/5] Decrypting SELF Files", BLUE, bold=True)
self._print(f"Decrypting {len(self_files)} SELF file(s) to: {decrypt_output_dir}", CYAN)
decrypt_output_dir.mkdir(parents=True, exist_ok=True)
results['decrypted_folder'] = str(decrypt_output_dir)
converter = UnsignedELFConverter(verbose=verbose)
for self_file in self_files:
relative_path = self_file.relative_to(input_dir)
output_file = decrypt_output_dir / relative_path
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists() and not overwrite:
if verbose:
self._print(f"Skipping (exists): {relative_path}", YELLOW)
continue
if verbose:
self._print(f"Decrypting: {relative_path}", None)
try:
success = converter.convert_file(str(self_file), str(output_file))
file_result = {
'success': success,
'output': str(output_file),
'message': 'Success' if success else 'Failed'
}
results['decrypt']['files'][str(self_file)] = file_result
if success:
results['decrypt']['successful'] += 1
if verbose:
self._print(f" ✓ Success", GREEN)
else:
results['decrypt']['failed'] += 1
if verbose:
self._print(f" ✗ Failed", RED)
except Exception as e:
results['decrypt']['failed'] += 1
error_msg = f"Error: {str(e)}"
file_result = {
'success': False,
'output': str(output_file),
'message': error_msg
}
results['decrypt']['files'][str(self_file)] = file_result
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nDecryption complete: {results['decrypt']['successful']} successful, "
f"{results['decrypt']['failed']} failed", CYAN)
working_dir = decrypt_output_dir
else:
working_dir = input_dir
if elf_files and self_files:
if verbose:
self._print(f"\n[Step 3/5] Copying existing ELF files to working directory", BLUE, bold=True)
for elf_file in elf_files:
relative_path = elf_file.relative_to(input_dir)
dest_file = working_dir / relative_path
dest_file.parent.mkdir(parents=True, exist_ok=True)
if not dest_file.exists() or overwrite:
shutil.copy2(elf_file, dest_file)
if verbose:
self._print(f"Copied: {relative_path}", CYAN)
if verbose:
self._print(f"\n[Step 4/5] Downgrading SDK Versions", BLUE, bold=True)
self._print(f"Processing files from: {working_dir}", CYAN)
sdk_patcher = SDKVersionPatcher(
create_backup=create_backup,
use_colors=self.use_colors
)
sdk_patcher.set_versions_by_pair(sdk_pair)
ps5_ver, ps4_ver = sdk_patcher.get_current_versions()
results['ps5_sdk_version'] = ps5_ver
results['ps4_sdk_version'] = ps4_ver
if verbose:
self._print(f"Using PS5 SDK: 0x{ps5_ver:08X}, PS4 Version: 0x{ps4_ver:08X}", CYAN)
if sdk_pair <= 6:
self._print(f"SDK pair {sdk_pair} selected - will apply libc.prx patch AFTER signing", YELLOW)
elif auto_revert_for_high_sdk:
self._print(f"SDK pair {sdk_pair} > 6 - will revert libc.prx patch AFTER signing if found", YELLOW)
elf_files_to_process = []
for root, dirs, files in os.walk(working_dir):
self._should_skip_dir(dirs, 'decrypted')
for filename in files:
file_path = Path(root) / filename
if filename.endswith('.bak'):
continue
if self._is_elf_file(file_path):
elf_files_to_process.append(file_path)
if not elf_files_to_process:
if verbose:
self._print(f"No ELF files found to downgrade", YELLOW)
return results
if verbose:
self._print(f"Found {len(elf_files_to_process)} ELF file(s) to downgrade", CYAN)
for elf_file in elf_files_to_process:
relative_path = elf_file.relative_to(working_dir)
if verbose:
self._print(f"Downgrading: {relative_path}", None)
try:
success, message = sdk_patcher.patch_file(str(elf_file))
results['downgrade']['files'][str(elf_file)] = {
'success': success,
'message': message
}
if success:
results['downgrade']['successful'] += 1
if verbose:
self._print(f" ✓ Success", GREEN)
else:
results['downgrade']['failed'] += 1
if verbose:
self._print(f" ✗ {message[:50]}", RED)
except Exception as e:
results['downgrade']['failed'] += 1
error_msg = f"Error: {str(e)}"
results['downgrade']['files'][str(elf_file)] = {
'success': False,
'message': error_msg
}
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nDowngrade complete: {results['downgrade']['successful']} successful, "
f"{results['downgrade']['failed']} failed", CYAN)
if verbose:
self._print(f"\n[Step 5/5] Fake Signing Files (ELF → SELF)", BLUE, bold=True)
self._print(f"Using PAID: 0x{paid:016X}, PType: 0x{ptype:08X}", CYAN)
converter = FakeSignedELFConverter(
paid=paid,
ptype=ptype,
app_version=0,
fw_version=0,
auth_info=None
)
for elf_file in elf_files_to_process:
relative_path = elf_file.relative_to(working_dir)
input_file_str = str(elf_file)
if not results['downgrade']['files'].get(input_file_str, {}).get('success', False):
if verbose:
self._print(f"Skipping (downgrade failed): {relative_path}", YELLOW)
results['signing']['files'][str(elf_file)] = {
'success': False,
'output': '',
'message': 'Skipped due to downgrade failure'
}
results['signing']['failed'] += 1
continue
output_file = output_dir / relative_path
output_file.parent.mkdir(parents=True, exist_ok=True)
if output_file.exists() and not overwrite:
if verbose:
self._print(f"Skipping (exists): {relative_path}", YELLOW)
continue
if verbose:
self._print(f"Signing: {relative_path}", None)
try:
success = converter.sign_file(input_file_str, str(output_file))
results['signing']['files'][str(elf_file)] = {
'success': success,
'output': str(output_file),
'message': 'Success' if success else 'Failed'
}
if success:
results['signing']['successful'] += 1
if verbose:
self._print(f" ✓ Success (converted to SELF)", GREEN)
else:
results['signing']['failed'] += 1
if verbose:
self._print(f" ✗ Failed", RED)
except Exception as e:
results['signing']['failed'] += 1
error_msg = f"Error: {str(e)}"
results['signing']['files'][str(elf_file)] = {
'success': False,
'output': str(output_file),
'message': error_msg
}
if verbose:
self._print(f" ✗ {error_msg[:50]}", RED)
if verbose:
self._print(f"\nSigning complete: {results['signing']['successful']} successful, "
f"{results['signing']['failed']} failed", CYAN)
self._print(f"All ELF files have been converted to SELF format", CYAN)
if apply_libc_patch:
if sdk_pair <= 6:
if verbose:
self._print(f"\n[Step 6/6] Applying libc.prx patch to SELF files (SDK ≤ 6)", BLUE, bold=True)
self._print(f"Patching SELF files in output directory", CYAN)
patch_results = self.apply_libc_patch(
output_dir,
create_backup=False,
verbose=verbose
)
results['libc_patch']['applied'] = patch_results['applied']
results['libc_patch']['results'] = patch_results
elif auto_revert_for_high_sdk:
if verbose:
self._print(f"\n[Step 6/6] Reverting libc.prx patch from SELF files (SDK > 6)", BLUE, bold=True)
self._print(f"Reverting patches in SELF files in output directory", CYAN)
revert_results = self.revert_libc_patch(
output_dir,
create_backup=False,
verbose=verbose
)
results['libc_patch']['reverted'] = revert_results['reverted']
results['libc_patch']['results'] = revert_results
if fakelib_source:
fakelib_source_path = Path(fakelib_source)
if verbose:
self._print(f"\n[Step 7/7] Copying Fakelib Directory", BLUE, bold=True)
success, message = self._copy_fakelib(fakelib_source_path, output_dir)
results['fakelib']['success'] = success
results['fakelib']['message'] = message
if verbose:
if success:
self._print(f"✓ {message}", GREEN)
else:
self._print(f"⚠ {message}", YELLOW)
if success:
fakelib_copies = self._copy_fakelib_to_eboot_dirs(fakelib_source_path, output_dir, verbose)
results['fakelib_copies']['created'] = fakelib_copies['created']
results['fakelib_copies']['locations'] = fakelib_copies['locations']
if verbose and fakelib_copies['created'] > 0:
self._print(f"✓ Created {fakelib_copies['created']} fakelib copy(ies) in eboot.bin directories", GREEN)
return results
def _copy_fakelib(self, fakelib_source: Path, output_dir: Path) -> Tuple[bool, str]:
"""Copy the fakelib directory to the output directory."""
if not fakelib_source.exists():
return True, f"fakelib directory not found at {fakelib_source} (skipping)"
if not fakelib_source.is_dir():
return False, f"fakelib path exists but is not a directory: {fakelib_source}"
fakelib_dest = output_dir / "fakelib"
try:
if fakelib_dest.exists():
shutil.rmtree(fakelib_dest)
shutil.copytree(fakelib_source, fakelib_dest)
file_count = sum(1 for _ in fakelib_dest.rglob('*') if _.is_file())
return True, f"Copied fakelib directory from {fakelib_source} ({file_count} files)"
except Exception as e:
return False, f"Failed to copy fakelib: {str(e)}"
def _copy_fakelib_to_eboot_dirs(self, fakelib_source: Path, output_dir: Path, verbose: bool = True) -> Dict[str, Any]:
"""Copy fakelib directory to directories containing eboot.bin files."""
if not fakelib_source.exists() or not fakelib_source.is_dir():
return {'created': 0, 'locations': []}
results = {
'created': 0,
'locations': []
}
eboot_files = []
for root, dirs, files in os.walk(output_dir):
for filename in files:
if filename.lower() == 'eboot.bin':
file_path = Path(root) / filename
eboot_files.append(file_path)
if not eboot_files:
if verbose:
self._print(f"No eboot.bin files found in output directory", CYAN)
return results
if verbose:
self._print(f"Found {len(eboot_files)} eboot.bin file(s)", CYAN)
for eboot_file in eboot_files:
eboot_dir = eboot_file.parent
if eboot_dir == output_dir:
continue
fakelib_dest = eboot_dir / "fakelib"
try:
if fakelib_dest.exists():
shutil.rmtree(fakelib_dest)
shutil.copytree(fakelib_source, fakelib_dest)
results['created'] += 1
results['locations'].append(str(fakelib_dest))
if verbose:
self._print(f" ✓ Copied fakelib to: {fakelib_dest.relative_to(output_dir)}", GREEN)
except Exception as e:
if verbose:
self._print(f" ✗ Failed to copy fakelib to {eboot_dir.relative_to(output_dir)}: {str(e)[:50]}", RED)
return results
def _save_directories_to_config(self, input_dir: str, output_dir: str):
"""Save input and output directories to configuration file."""
config = self._load_config()
config['directories'] = {
'last_input': input_dir,
'last_output': output_dir,
'last_used': self._get_timestamp()
}
self._save_config(config)
def _load_config(self) -> Dict[str, Any]:
"""Load configuration from file."""
config_path = self.project_root / CONFIG_FILE
if config_path.exists():
try:
with open(config_path, 'r', encoding='utf-8') as f:
return json.load(f)
except:
pass
return {
'directories': {
'last_input': '',
'last_output': '',
'last_used': ''
}
}
def _save_config(self, config: Dict[str, Any]):
"""Save configuration to file."""
config_path = self.project_root / CONFIG_FILE
try:
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2)
except:
pass
def get_last_directories(self) -> Tuple[str, str]:
"""Get last used input and output directories from config."""
config = self._load_config()
dirs = config.get('directories', {})
return dirs.get('last_input', ''), dirs.get('last_output', '')
def _get_timestamp(self) -> str:
"""Get current timestamp for results."""
import time
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
# Interactive CLI functions
def print_banner():
"""Print a banner for the tool."""
banner = f"""
{CYAN}{BOLD}╔══════════════════════════════════════════════════════════════════════╗
║ PS5 Backport Tool ║
║ SDK Downgrade + Fake Sign + Decrypt Functions ║
╚══════════════════════════════════════════════════════════════════════╝{RESET}
"""
print(banner)
def get_sdk_version_choice() -> int:
"""Prompt user to select an SDK version pair."""
pairs = SDKVersionPatcher.get_supported_pairs()
print(f"{CYAN}Available SDK Version Pairs:{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f"{BOLD}{'Pair':<6} {'PS5 SDK Version':<20} {'PS4 Version':<20}{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
for pair_num, (ps5_ver, ps4_ver) in pairs.items():
print(f" {pair_num:<4} 0x{ps5_ver:08X}{' ' * 10}0x{ps4_ver:08X}")
print(f"{YELLOW}{'─' * 60}{RESET}")
while True:
try:
choice = input(f"\n{CYAN}Enter SDK version pair number (1-{len(pairs)}): {RESET}").strip()
if not choice:
print(f"{YELLOW}Using default: Pair 4 (0x04000031, 0x09040001){RESET}")
return 4
choice_num = int(choice)
if choice_num in pairs:
return choice_num
else:
print(f"{RED}Invalid choice. Please select a number between 1 and {len(pairs)}.{RESET}")
except ValueError:
print(f"{RED}Invalid input. Please enter a number.{RESET}")
def get_paid_choice() -> int:
"""Prompt user to select PAID (Program Authentication ID)."""
paid_options = {
1: ("Fake Paid (Default)", 0x3100000000000002),
2: ("System Paid", 0x3200000000000001),
3: ("NPDRM Paid", 0x3300000000000003),
4: ("Custom Paid", None)
}
print(f"\n{CYAN}Available PAID (Program Authentication ID) Options:{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f"{BOLD}{'Option':<8} {'Description':<30} {'Value':<20}{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
for option_num, (desc, value) in paid_options.items():
if value:
print(f" {option_num:<6} {desc:<30} 0x{value:016X}")
else:
print(f" {option_num:<6} {desc:<30} (custom input)")
print(f"{YELLOW}{'─' * 60}{RESET}")
while True:
try:
choice = input(f"\n{CYAN}Select PAID option (1-4, default=1): {RESET}").strip()
if not choice:
print(f"{YELLOW}Using default: Fake Paid (0x3100000000000002){RESET}")
return paid_options[1][1]
choice_num = int(choice)
if choice_num in paid_options:
if choice_num == 4:
while True:
try:
custom_paid = input(f"{CYAN}Enter custom PAID (hex, e.g., 0x3200000000000001): {RESET}").strip()
if custom_paid.startswith('0x'):
custom_paid = int(custom_paid, 16)
else:
custom_paid = int(custom_paid, 0)
if 0 <= custom_paid <= 0xFFFFFFFFFFFFFFFF:
return custom_paid
else:
print(f"{RED}PAID must be a 64-bit value (0-0xFFFFFFFFFFFFFFFF){RESET}")
except ValueError:
print(f"{RED}Invalid hex value. Try again.{RESET}")
else:
return paid_options[choice_num][1]
else:
print(f"{RED}Invalid choice. Please select 1-4.{RESET}")
except ValueError:
print(f"{RED}Invalid input. Please enter a number.{RESET}")
def get_ptype_choice() -> int:
"""Prompt user to select program type."""
ptype_options = {
1: ("Fake (Default)", 1),
2: ("NPDRM Executable", 4),
3: ("NPDRM Dynamic Library", 5),
4: ("System Executable", 8),
5: ("System Dynamic Library", 9),
6: ("Custom PType", None)
}
print(f"\n{CYAN}Available Program Type Options:{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f"{BOLD}{'Option':<8} {'Description':<30} {'Value':<20}{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
for option_num, (desc, value) in ptype_options.items():
if value is not None:
print(f" {option_num:<6} {desc:<30} 0x{value:08X}")
else:
print(f" {option_num:<6} {desc:<30} (custom input)")
print(f"{YELLOW}{'─' * 60}{RESET}")
while True:
try:
choice = input(f"\n{CYAN}Select program type (1-6, default=1): {RESET}").strip()
if not choice:
print(f"{YELLOW}Using default: Fake (0x1){RESET}")
return ptype_options[1][1]
choice_num = int(choice)
if choice_num in ptype_options:
if choice_num == 6:
while True:
try:
custom_ptype = input(f"{CYAN}Enter custom PType (hex or name): {RESET}").strip()
try:
if custom_ptype.startswith('0x'):
ptype_value = int(custom_ptype, 16)
else:
ptype_value = int(custom_ptype, 0)
except ValueError:
try:
ptype_value = FakeSignedELFConverter.parse_ptype(custom_ptype.lower())
except Exception:
raise ValueError(f"Unknown program type: {custom_ptype}")
if 0 <= ptype_value <= 0xFFFFFFFF:
return ptype_value
else:
print(f"{RED}PType must be a 32-bit value (0-0xFFFFFFFF){RESET}")
except Exception as e:
print(f"{RED}{str(e)}. Try again or use 'fake', 'npdrm_exec', etc.{RESET}")
else:
return ptype_options[choice_num][1]
else:
print(f"{RED}Invalid choice. Please select 1-6.{RESET}")
except ValueError:
print(f"{RED}Invalid input. Please enter a number.{RESET}")
def get_operation_choice() -> str:
"""Prompt user to select operation mode."""
operations = {
'1': 'decrypt_and_sign_pipeline',
'2': 'downgrade_and_sign',
'3': 'decrypt_only',
'4': 'downgrade_and_sign'
}
print(f"\n{CYAN}Available Operations:{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f"{BOLD}{'Option':<8} {'Description':<40}{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f" 1 Auto-detect pipeline (decrypt SELF, sign everything) - DEFAULT")
print(f" 2 Downgrade and sign only (convert ELF to SELF)")
print(f" 3 Decrypt only (convert SELF to ELF)")
print(f" 4 Legacy pipeline (for backward compatibility)")
print(f"{YELLOW}{'─' * 60}{RESET}")
while True:
choice = input(f"\n{CYAN}Select operation (1-4, default=1): {RESET}").strip()
if not choice:
print(f"{YELLOW}Using default: Auto-detect pipeline{RESET}")
return 'decrypt_and_sign_pipeline'
if choice in operations:
return operations[choice]
else:
print(f"{RED}Invalid choice. Please select 1-4.{RESET}")
def get_fakelib_choice(project_root: Path, args_fakelib: Optional[str] = None) -> Optional[Path]:
"""Prompt user to select or specify fakelib directory."""
if args_fakelib:
fakelib_path = Path(args_fakelib)
if fakelib_path.exists() and fakelib_path.is_dir():
return fakelib_path
else:
print(f"{YELLOW}Warning: Custom fakelib directory does not exist: {fakelib_path}{RESET}")
print(f"{YELLOW}Falling back to interactive selection{RESET}")
default_fakelib = project_root / "fakelib"
print(f"\n{CYAN}Fakelib Directory Options:{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f"{BOLD}{'Option':<8} {'Description':<40}{RESET}")
print(f"{YELLOW}{'─' * 60}{RESET}")
print(f" 1 Use default fakelib ({default_fakelib})")
print(f" 2 Use custom fakelib directory")
print(f" 3 Skip fakelib copy")
print(f"{YELLOW}{'─' * 60}{RESET}")
while True:
choice = input(f"\n{CYAN}Select fakelib option (1-3, default=1): {RESET}").strip()
if not choice:
print(f"{YELLOW}Using default: Use default fakelib{RESET}")
return default_fakelib if default_fakelib.exists() else None
if choice == '1':
if default_fakelib.exists() and default_fakelib.is_dir():
return default_fakelib
else:
print(f"{YELLOW}Default fakelib not found at {default_fakelib}{RESET}")
return None
elif choice == '2':
while True:
custom_path = input(f"{CYAN}Enter custom fakelib directory: {RESET}").strip()
if not custom_path:
print(f"{YELLOW}Using default fakelib instead{RESET}")
return default_fakelib if default_fakelib.exists() else None
fakelib_path = Path(custom_path)
if fakelib_path.exists() and fakelib_path.is_dir():
return fakelib_path
else:
print(f"{RED}Error: Directory does not exist or is not a directory: {fakelib_path}{RESET}")
print(f"{YELLOW}Please enter a valid directory path{RESET}")
elif choice == '3':
print(f"{YELLOW}Skipping fakelib copy{RESET}")
return None
else:
print(f"{RED}Invalid choice. Please select 1-3.{RESET}")
def get_input_directory_with_memory(processor: PS5ELFProcessor) -> Path:
"""Get input directory with memory of last used directory."""
last_input, last_output = processor.get_last_directories()
if last_input and Path(last_input).exists():
print(f"{CYAN}Last used input directory: {last_input}{RESET}")
use_last = input(f"{CYAN}Use this directory? (Y/n): {RESET}").strip().lower()
if use_last in ['', 'y', 'yes']:
return Path(last_input)
while True:
input_path = input(f"{CYAN}Enter input directory: {RESET}").strip()
if not input_path:
print(f"{RED}Error: Input directory is required{RESET}")
continue
input_dir = Path(input_path)
if not input_dir.exists():
print(f"{RED}Error: Input directory does not exist: {input_dir}{RESET}")
continue
if not input_dir.is_dir():
print(f"{RED}Error: Input path is not a directory: {input_dir}{RESET}")
continue
return input_dir
def get_output_directory_with_memory(processor: PS5ELFProcessor) -> Path:
"""Get output directory with memory of last used directory."""
last_input, last_output = processor.get_last_directories()
if last_output and Path(last_output).exists():
print(f"{CYAN}Last used output directory: {last_output}{RESET}")
use_last = input(f"{CYAN}Use this directory? (Y/n): {RESET}").strip().lower()
if use_last in ['', 'y', 'yes']:
return Path(last_output)
while True:
output_path = input(f"{CYAN}Enter output directory: {RESET}").strip()
if not output_path:
print(f"{RED}Error: Output directory is required{RESET}")
continue
output_dir = Path(output_path)
try:
output_dir.mkdir(parents=True, exist_ok=True)
return output_dir
except Exception as e:
print(f"{RED}Error creating output directory: {str(e)}{RESET}")
def print_summary(results: Dict[str, Dict[str, any]], output_dir: Path, operation: str):
"""Print a summary of the processing results."""
print(f"\n{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
print(f"{CYAN}{BOLD} PROCESSING SUMMARY {RESET}")
print(f"{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
operation_display = {
'decrypt_and_sign_pipeline': 'Auto-detect Pipeline',
'downgrade_and_sign': 'Downgrade & Sign',
'decrypt_only': 'Decrypt Only'
}
print(f"\n{BOLD}Operation:{RESET} {operation_display.get(operation, operation)}")
if 'detection' in results:
detection = results['detection']
print(f"\n{BOLD}File Detection:{RESET}")
print(f" {CYAN}SELF files: {detection['self_files']}{RESET}")
print(f" {CYAN}ELF files: {detection['elf_files']}{RESET}")
print(f" {CYAN}Other files: {detection['other_files']}{RESET}")
if 'decrypt' in results:
decrypt = results['decrypt']
print(f"\n{BOLD}Decryption Results:{RESET}")
print(f" {GREEN}Successful: {decrypt['successful']}{RESET}")
print(f" {RED if decrypt['failed'] > 0 else YELLOW}Failed: {decrypt['failed']}{RESET}")
print(f" {CYAN}Total: {decrypt['successful'] + decrypt['failed']}{RESET}")
if 'error' in decrypt:
print(f" {RED}Error: {decrypt['error']}{RESET}")
if 'downgrade' in results:
downgrade = results['downgrade']
print(f"\n{BOLD}Downgrade Results:{RESET}")
print(f" {GREEN}Successful: {downgrade['successful']}{RESET}")
print(f" {RED if downgrade['failed'] > 0 else YELLOW}Failed: {downgrade['failed']}{RESET}")
print(f" {CYAN}Total: {downgrade['successful'] + downgrade['failed']}{RESET}")
if 'signing' in results:
signing = results['signing']
print(f"\n{BOLD}Signing Results:{RESET}")
print(f" {GREEN}Successful: {signing['successful']}{RESET}")
print(f" {RED if signing['failed'] > 0 else YELLOW}Failed: {signing['failed']}{RESET}")
print(f" {CYAN}Total: {signing['successful'] + signing['failed']}{RESET}")
if 'libc_patch' in results:
patch = results['libc_patch']
if patch.get('applied', 0) > 0:
print(f"\n{BOLD}libc.prx Patch Results:{RESET}")
print(f" {GREEN}Files patched: {patch['applied']}{RESET}")
if patch.get('results'):
print(f" {CYAN}Details: {len(patch['results'])} file(s) processed{RESET}")
elif patch.get('reverted', 0) > 0:
print(f"\n{BOLD}libc.prx Patch Results:{RESET}")
print(f" {GREEN}Files reverted: {patch['reverted']}{RESET}")
if patch.get('results'):
print(f" {CYAN}Details: {len(patch['results'])} file(s) processed{RESET}")
elif 'results' in patch and patch['results']:
print(f"\n{BOLD}libc.prx Patch Results:{RESET}")
print(f" {YELLOW}No files were patched or reverted{RESET}")
if 'fakelib' in results:
fakelib = results['fakelib']
if fakelib.get('message'):
print(f"\n{BOLD}Fakelib Copy:{RESET}")
if fakelib.get('success', False):
print(f" {GREEN}✓ {fakelib['message']}{RESET}")
else:
print(f" {YELLOW}⚠ {fakelib['message']}{RESET}")
if 'fakelib_copies' in results:
fakelib_copies = results['fakelib_copies']
if fakelib_copies.get('created', 0) > 0:
print(f"\n{BOLD}Fakelib Copies to eboot.bin directories:{RESET}")
print(f" {GREEN}Created: {fakelib_copies['created']} copy(ies){RESET}")
for i, location in enumerate(fakelib_copies.get('locations', [])[:3]):
print(f" {CYAN} • {Path(location).relative_to(Path(results.get('output_dir', '')))}{RESET}")
if len(fakelib_copies.get('locations', [])) > 3:
print(f" {CYAN} ... and {len(fakelib_copies['locations']) - 3} more{RESET}")
if 'decrypted_folder' in results and results['decrypted_folder']:
print(f"\n{BOLD}Decrypted Files:{RESET}")
print(f" {CYAN}Saved to: {results['decrypted_folder']}{RESET}")
failed_files = []
if 'decrypt' in results:
failed_files.extend([(f, 'Decryption', data.get('message', 'Unknown error'))
for f, data in results['decrypt']['files'].items()
if not data.get('success', False)])
if 'downgrade' in results:
failed_files.extend([(f, 'Downgrade', data.get('message', 'Unknown error'))
for f, data in results['downgrade']['files'].items()
if not data.get('success', False)])
if 'signing' in results:
failed_files.extend([(f, 'Signing', data.get('message', 'Unknown error'))
for f, data in results['signing']['files'].items()
if not data.get('success', False)])
if failed_files:
print(f"\n{BOLD}Failed Files (first 5 shown):{RESET}")
for f, op, msg in failed_files[:5]:
filename = Path(f).name
print(f" {RED}• [{op}] {filename}: {msg[:100]}{'...' if len(msg) > 100 else ''}{RESET}")
if len(failed_files) > 5:
print(f" {YELLOW}... and {len(failed_files) - 5} more{RESET}")
print(f"\n{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
print(f"{GREEN}{BOLD}Processing complete! Output directory: {output_dir}{RESET}")
print(f"{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
def run_interactive_cli():
"""Run the full interactive CLI with prompts."""
print_banner()
processor = PS5ELFProcessor(use_colors=True)
operation = get_operation_choice()
input_dir = get_input_directory_with_memory(processor)
output_dir = get_output_directory_with_memory(processor)
project_root = Path(__file__).parent
fakelib_source = None
if operation in ['decrypt_and_sign_pipeline', 'downgrade_and_sign']:
fakelib_source = get_fakelib_choice(project_root)
sdk_pair = None
paid = None
ptype = None
if operation in ['decrypt_and_sign_pipeline', 'downgrade_and_sign']:
sdk_pair = get_sdk_version_choice()
paid = get_paid_choice()
ptype = get_ptype_choice()
print(f"\n{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
print(f"{CYAN}{BOLD} CONFIGURATION {RESET}")
print(f"{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
print(f" {BOLD}Operation:{RESET} {operation.replace('_', ' ').title()}")
print(f" {BOLD}Input Directory:{RESET} {input_dir}")
print(f" {BOLD}Output Directory:{RESET} {output_dir}")
if fakelib_source:
print(f" {BOLD}Fakelib Source:{RESET} {fakelib_source}")
else:
print(f" {BOLD}Fakelib Source:{RESET} None (will skip)")
if operation in ['decrypt_and_sign_pipeline', 'downgrade_and_sign']:
pairs = SDKVersionPatcher.get_supported_pairs()
ps5_sdk_version, ps4_version = pairs[sdk_pair]
print(f" {BOLD}SDK Version Pair:{RESET} {sdk_pair} (PS5: 0x{ps5_sdk_version:08X}, PS4: 0x{ps4_version:08X})")
print(f" {BOLD}PAID:{RESET} 0x{paid:016X}")
print(f" {BOLD}PType:{RESET} 0x{ptype:08X}")
if sdk_pair <= 6:
print(f" {YELLOW}{BOLD}Note:{RESET} SDK pair {sdk_pair} selected - will apply libc.prx patch after signing{RESET}")
print(f"{BLUE}{BOLD}══════════════════════════════════════════════════════════{RESET}")
confirm = input(f"\n{CYAN}Proceed with processing? (y/N): {RESET}").strip().lower()
if confirm not in ['y', 'yes']:
print(f"{YELLOW}Processing cancelled.{RESET}")
sys.exit(0)
try:
if operation == 'decrypt_only':
results = processor.decrypt_files(
input_dir=input_dir,
output_dir=output_dir,
overwrite=False,
verbose=True
)
elif operation == 'downgrade_and_sign':
results = processor.downgrade_and_sign(
input_dir=input_dir,
output_dir=output_dir,
sdk_pair=sdk_pair,
paid=paid,
ptype=ptype,
fakelib_source=fakelib_source,
create_backup=True,
overwrite=False,
apply_libc_patch=True,
auto_revert_for_high_sdk=True,
verbose=True
)
elif operation == 'decrypt_and_sign_pipeline':
results = processor.decrypt_and_sign_pipeline(
input_dir=input_dir,
output_dir=output_dir,
sdk_pair=sdk_pair,
paid=paid,
ptype=ptype,
fakelib_source=fakelib_source,
create_backup=True,
overwrite=False,
apply_libc_patch=True,
auto_revert_for_high_sdk=True,
verbose=True
)
else:
print(f"{RED}Error: Unknown operation: {operation}{RESET}")
sys.exit(1)
print_summary(results, output_dir, operation)
has_failures = any(
results.get(key, {}).get('failed', 0) > 0
for key in ['decrypt', 'downgrade', 'signing']
if key in results
)
if has_failures:
print(f"\n{YELLOW}Warning: Some files failed to process{RESET}")
sys.exit(1)
else:
print(f"\n{GREEN}All files processed successfully!{RESET}")
sys.exit(0)
except KeyboardInterrupt:
print(f"\n{YELLOW}Processing interrupted by user.{RESET}")
sys.exit(1)
except Exception as e:
print(f"\n{RED}Unexpected error: {str(e)}{RESET}")
import traceback
traceback.print_exc()
sys.exit(1)
# Utility functions for common operations
def decrypt_file(
input_file: Union[str, Path],
output_file: Union[str, Path],
verbose: bool = False
) -> bool:
"""Convenience function to decrypt a single SELF file."""
processor = PS5ELFProcessor(use_colors=False)
converter = UnsignedELFConverter(verbose=verbose)
return converter.convert_file(str(input_file), str(output_file))
def sign_file(
input_file: Union[str, Path],
output_file: Union[str, Path],
sdk_pair: int = 4,
paid: int = 0x3100000000000002,
ptype: int = 1,
verbose: bool = False
) -> bool:
"""Convenience function to sign a single ELF file."""
temp_dir = Path(tempfile.mkdtemp(prefix="ps5_elf_"))
try:
processor = PS5ELFProcessor(use_colors=False)
temp_input = temp_dir / Path(input_file).name
shutil.copy2(input_file, temp_input)
temp_output = temp_dir / "output"
results = processor.downgrade_and_sign(
input_dir=temp_dir,
output_dir=temp_output,
sdk_pair=sdk_pair,
paid=paid,
ptype=ptype,
fakelib_source=None,
create_backup=False,
overwrite=True,
apply_libc_patch=False,
auto_revert_for_high_sdk=True,
verbose=verbose,
save_to_config=False
)
if results['signing']['successful'] > 0:
for file_info in results['signing']['files'].values():
if file_info.get('success', False):
output_path = file_info.get('output', '')
if output_path:
output_dir = Path(output_file).parent
output_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(output_path, output_file)
return True
return False
finally:
try:
shutil.rmtree(temp_dir)
except:
pass
def get_sdk_version_info() -> Dict[int, Tuple[int, int]]:
"""Get all supported SDK version pairs."""
processor = PS5ELFProcessor(use_colors=False)
return processor.get_supported_sdk_pairs()
def get_default_fakelib_path(project_root: Optional[Union[str, Path]] = None) -> Optional[Path]:
"""Get default fakelib path if it exists."""
if project_root is None:
project_root = Path(__file__).parent
project_root = Path(project_root)
fakelib_path = project_root / "fakelib"
if fakelib_path.exists() and fakelib_path.is_dir():
return fakelib_path
return None
# CLI Interface
def run_cli():
"""Command-line interface for the PS5 Backport Tool."""
parser = argparse.ArgumentParser(
description='PS5 Backport Tool - Downgrade, fake sign, and decrypt ELF/SELF files',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
Interactive mode with prompts (full CLI):
python Backport.py -c
Direct command mode (AUTO is default when --input is provided):
python Backport.py --mode auto --input input/ --output output/ --sdk-pair 4
python Backport.py --mode decrypt --input encrypted/ --output decrypted/
python Backport.py --mode downgrade --input decrypted/ --output signed/ --sdk-pair 4
Auto mode from a compressed archive (ZIP, TAR, TAR.GZ, etc.):
python Backport.py --input game.zip --output output/ --sdk-pair 4
python Backport.py --input game.tar.gz --output output/ --sdk-pair 4
Libc patch operations (on SELF files OR single files):
python Backport.py --mode libc-patch --input libc.prx --action check
python Backport.py --mode libc-patch --input signed/ --action apply
python Backport.py --mode libc-patch --input signed/ --action revert
With custom fakelib:
python Backport.py --input in/ --output out/ --sdk-pair 4 --fakelib /path/to/fakelib
List available SDK pairs:
python Backport.py --list-sdk-pairs
"""
)
parser.add_argument('-c', '--cli', action='store_true', help='Run in interactive CLI mode (with prompts)')
parser.add_argument('--mode', '-m', type=str, choices=['auto', 'decrypt', 'downgrade', 'libc-patch'], default=None, help='Operation mode')
parser.add_argument('--action', type=str, choices=['apply', 'revert', 'check'], help='Action for libc-patch mode')
parser.add_argument('--input', '-i', type=str, help='Input directory, archive file, or single file')
parser.add_argument('--output', '-o', type=str, help='Output directory for processed files')
parser.add_argument('--password', type=str, default=None, help='Password for encrypted archives')
parser.add_argument('--sdk-pair', '-s', type=int, default=4, help='SDK version pair number (1-10, default: 4)')
parser.add_argument('--paid', type=str, default='0x3100000000000002', help='Program Authentication ID (hex, default: 0x3100000000000002)')
parser.add_argument('--ptype', type=str, default='fake', help='Program type (name or hex, default: "fake")')
parser.add_argument('--no-libc-patch', action='store_true', help='Skip libc.prx patch entirely')
parser.add_argument('--no-auto-revert', action='store_true', help='Do not automatically revert libc patch for SDK > 6')
parser.add_argument('--fakelib', '-f', type=str, help='Custom fakelib directory path')
parser.add_argument('--no-backup', action='store_true', help='Do not create backup files during downgrade')
parser.add_argument('--no-colors', action='store_true', help='Disable colored output')
parser.add_argument('--overwrite', action='store_true', help='Overwrite existing files')
parser.add_argument('--quiet', action='store_true', help='Suppress output')
parser.add_argument('--list-sdk-pairs', action='store_true', help='List available SDK version pairs and exit')
args = parser.parse_args()
# ==========================================================================
# ARCHIVE AUTO-DETECTION
# ==========================================================================
_archive_temp_dir = None
if args.input and args.mode not in ['libc-patch']:
input_path = Path(args.input)
if input_path.exists() and input_path.is_file() and ArchiveHandler.is_archive(input_path):
verbose = not args.quiet
ext, handler_type, tool = ArchiveHandler.get_archive_info(input_path)
if handler_type == 'multipart_wrong_part':
if verbose:
print(f"Error: You provided '{input_path.name}', which is not the first part of the archive.")
print(f"Please provide the first part (e.g., .part01.rar, .part1.rar, .7z.001).")
sys.exit(1)
temp_dir, extract_result = extract_archive_to_temp(
archive_path=input_path,
verbose=verbose,
provided_password=args.password
)
if temp_dir is None:
errors = extract_result.get('errors', ['Unknown error'])
if verbose:
print("Error: Failed to extract archive")
for err in errors:
print(f" • {err}")
sys.exit(1)
_archive_temp_dir = temp_dir
args.input = str(temp_dir)
if verbose:
print("[Archive] Using extracted files as input\n")
# ==========================================================================
if args.cli:
run_interactive_cli()
return
if args.list_sdk_pairs:
sdk_pairs = get_sdk_version_info()
print("Available SDK Version Pairs:")
print("┌──────┬──────────────────────┬──────────────────────┐")
print("│ Pair │ PS5 SDK Version │ PS4 Version │")
print("├──────┼──────────────────────┼──────────────────────┤")
for pair_num, (ps5_ver, ps4_ver) in sdk_pairs.items():
print(f"│ {pair_num:<4} │ 0x{ps5_ver:08X} │ 0x{ps4_ver:08X} │")
print("└──────┴──────────────────────┴──────────────────────┘")
sys.exit(0)
mode_was_specified = '--mode' in sys.argv or '-m' in sys.argv
if args.mode is None:
args.mode = 'auto'
if not mode_was_specified and not args.input:
run_interactive_cli()
return
if args.mode == 'libc-patch':
if not args.action:
print("Error: --action is required for libc-patch mode")
sys.exit(1)
if not args.input:
print("Error: --input is required for libc-patch mode")
sys.exit(1)
elif args.mode in ['auto', 'decrypt', 'downgrade']:
if not args.input:
print(f"Error: --input is required for {args.mode} mode")
sys.exit(1)
if not args.output:
print(f"Error: --output is required for {args.mode} mode")
sys.exit(1)
else:
print("Error: Invalid mode specified")
sys.exit(1)
processor = PS5ELFProcessor(use_colors=not args.no_colors)
try:
if args.mode == 'decrypt':
results = processor.decrypt_files(
input_dir=args.input,
output_dir=args.output,
overwrite=args.overwrite,
verbose=not args.quiet
)
elif args.mode == 'downgrade':
try:
if args.ptype.startswith('0x'):
ptype = int(args.ptype, 16)
else:
try:
ptype = int(args.ptype, 0)
except ValueError:
ptype = processor.parse_ptype(args.ptype.lower())
except Exception as e:
print(f"Error: Invalid ptype '{args.ptype}': {str(e)}")
sys.exit(1)
try:
if args.paid.startswith('0x'):
paid = int(args.paid, 16)
else:
paid = int(args.paid, 0)
except ValueError:
print("Error: Invalid PAID format. Use hex (0x...) or decimal")
sys.exit(1)
fakelib_source = None
if args.fakelib:
fakelib_source = args.fakelib
else:
default_fakelib = get_default_fakelib_path()
if default_fakelib:
fakelib_source = str(default_fakelib)
results = processor.downgrade_and_sign(
input_dir=args.input,
output_dir=args.output,
sdk_pair=args.sdk_pair,
paid=paid,
ptype=ptype,
fakelib_source=fakelib_source,
create_backup=not args.no_backup,
overwrite=args.overwrite,
apply_libc_patch=not args.no_libc_patch,
auto_revert_for_high_sdk=not args.no_auto_revert,
verbose=not args.quiet
)
elif args.mode == 'auto':
try:
if args.ptype.startswith('0x'):
ptype = int(args.ptype, 16)
else:
try:
ptype = int(args.ptype, 0)
except ValueError:
ptype = processor.parse_ptype(args.ptype.lower())
except Exception as e:
print(f"Error: Invalid ptype '{args.ptype}': {str(e)}")
sys.exit(1)
try:
if args.paid.startswith('0x'):
paid = int(args.paid, 16)
else:
paid = int(args.paid, 0)
except ValueError:
print("Error: Invalid PAID format. Use hex (0x...) or decimal")
sys.exit(1)
fakelib_source = None
if args.fakelib:
fakelib_source = args.fakelib
else:
default_fakelib = get_default_fakelib_path()
if default_fakelib:
fakelib_source = str(default_fakelib)
results = processor.decrypt_and_sign_pipeline(
input_dir=args.input,
output_dir=args.output,
sdk_pair=args.sdk_pair,
paid=paid,
ptype=ptype,
fakelib_source=fakelib_source,
create_backup=not args.no_backup,
overwrite=args.overwrite,
apply_libc_patch=not args.no_libc_patch,
auto_revert_for_high_sdk=not args.no_auto_revert,
verbose=not args.quiet,
save_to_config=True
)
elif args.mode == 'libc-patch':
if not args.action:
print("Error: --action is required for libc-patch mode")
sys.exit(1)
if not args.input:
print("Error: --input is required for libc-patch mode")
sys.exit(1)
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: Input path does not exist: {args.input}")
sys.exit(1)
if input_path.is_file():
if not (processor._is_self_file(input_path) or 'libc' in input_path.name.lower()):
print("Warning: Input file may not be a SELF or libc file")
if not args.quiet:
confirm = input("Continue anyway? (y/N): ").strip().lower()
if confirm not in ['y', 'yes']:
print("Aborted")
sys.exit(0)
if args.action == 'apply':
results = processor.apply_libc_patch(
input_dir=args.input,
create_backup=True,
verbose=not args.quiet
)
elif args.action == 'revert':
results = processor.revert_libc_patch(
input_dir=args.input,
create_backup=True,
verbose=not args.quiet
)
elif args.action == 'check':
results = processor.check_libc_patch_status(
input_path=args.input,
verbose=not args.quiet
)
if not args.quiet:
print(f"\nProcessing complete!")
if 'operation' in results:
print(f"Operation: {results['operation'].replace('_', ' ').title()}")
if 'detection' in results:
detection = results['detection']
print(f"File detection: {detection['self_files']} SELF, {detection['elf_files']} ELF")
if 'decrypted_folder' in results and results['decrypted_folder']:
print(f"Decrypted files saved to: {results['decrypted_folder']}")
if 'successful' in results:
print(f"Successful: {results['successful']}")
if 'failed' in results:
print(f"Failed: {results['failed']}")
if 'applied' in results and results['applied'] > 0:
print(f"Libc patches applied: {results['applied']}")
if 'reverted' in results and results['reverted'] > 0:
print(f"Libc patches reverted: {results['reverted']}")
if 'fakelib_copies' in results and results['fakelib_copies'].get('created', 0) > 0:
print(f"Fakelib copies to eboot.bin dirs: {results['fakelib_copies']['created']}")
if 'output_dir' in results:
print(f"Output: {results['output_dir']}")
if args.mode in ['auto', 'downgrade']:
print(f"\nNote: All output files are in SELF format")
if not args.no_libc_patch:
if args.sdk_pair <= 6:
print(f"Libc.prx patch was applied to SELF files (SDK ≤ 6)")
elif not args.no_auto_revert:
print(f"Libc.prx patch was reverted if found (SDK > 6)")
if results.get('failed', 0) > 0:
sys.exit(1)
else:
sys.exit(0)
except KeyboardInterrupt:
print("\nProcessing interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\nError: {str(e)}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
if '_archive_temp_dir' in locals() and _archive_temp_dir:
cleanup_temp_dir(_archive_temp_dir, verbose=not args.quiet)
if __name__ == "__main__":
run_cli()