2018-07-07 00:38:10 +02:00
|
|
|
# -*- coding: utf8 -*-
|
|
|
|
|
|
|
|
|
|
# libray - Libre Blu-Ray PS3 ISO Tool
|
2026-05-18 16:31:58 +02:00
|
|
|
# Copyright © 2018 - 2024 Nichlas Severinsen
|
2019-05-16 10:37:28 +02:00
|
|
|
#
|
2018-07-07 00:38:10 +02:00
|
|
|
# This file is part of libray.
|
2019-05-16 10:37:28 +02:00
|
|
|
#
|
2018-07-07 00:38:10 +02:00
|
|
|
# libray is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
2019-05-16 10:37:28 +02:00
|
|
|
#
|
2018-07-07 00:38:10 +02:00
|
|
|
# libray is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU General Public License for more details.
|
2019-05-16 10:37:28 +02:00
|
|
|
#
|
2018-07-07 00:38:10 +02:00
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
|
# along with libray. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
2019-06-07 09:00:03 +02:00
|
|
|
|
2018-07-07 00:38:10 +02:00
|
|
|
import os
|
|
|
|
|
import sys
|
2019-07-07 21:18:34 +02:00
|
|
|
import stat
|
2021-06-05 22:03:31 +02:00
|
|
|
import zlib
|
2018-07-07 19:03:47 +02:00
|
|
|
import shutil
|
2018-07-09 07:40:41 +02:00
|
|
|
import requests
|
|
|
|
|
from bs4 import BeautifulSoup
|
2026-05-19 17:41:17 +02:00
|
|
|
from typing import Any, Optional
|
2018-07-07 11:12:05 +02:00
|
|
|
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
try:
|
2026-05-18 16:31:58 +02:00
|
|
|
from libray import iso
|
|
|
|
|
from libray import ird
|
2018-07-07 00:38:10 +02:00
|
|
|
except ImportError:
|
2026-05-18 16:31:58 +02:00
|
|
|
import iso
|
|
|
|
|
import ird
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
# Magic numbers / Constant variables
|
|
|
|
|
|
|
|
|
|
SECTOR = 2048
|
2018-07-09 07:40:41 +02:00
|
|
|
ALL_IRD_NET_LOC = 'http://jonnysp.bplaced.net/data.php'
|
|
|
|
|
GET_IRD_NET_LOC = 'http://jonnysp.bplaced.net/ird/'
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
# Utility functions
|
|
|
|
|
|
2019-06-07 09:00:03 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def to_int(data: bytes, byteorder: str = 'big') -> int:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Convert bytes to integer"""
|
|
|
|
|
if isinstance(data, bytes):
|
|
|
|
|
return int.from_bytes(data, byteorder)
|
2026-05-19 17:41:17 +02:00
|
|
|
return 0
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def to_bytes(data: str) -> Optional[bytes]:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Convert a string of HEX to bytes"""
|
|
|
|
|
if isinstance(data, str):
|
|
|
|
|
return bytes(bytearray.fromhex(data))
|
2026-05-19 17:41:17 +02:00
|
|
|
return None
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
ISO_SECRET = to_bytes("380bcf0b53455b3c7817ab4fa3ba90ed")
|
|
|
|
|
ISO_IV = to_bytes("69474772af6fdab342743aefaa186287")
|
|
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def size(path: str) -> int:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Get size of a file or block device in bytes"""
|
|
|
|
|
pathstat = os.stat(path)
|
2019-07-07 21:18:34 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
# Check if it's a block device
|
2019-07-07 21:18:34 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
if stat.S_ISBLK(pathstat.st_mode):
|
|
|
|
|
return open(path, 'rb').seek(0, os.SEEK_END)
|
2019-07-07 21:18:34 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
# Otherwise, it's hopefully a file
|
2019-07-07 21:18:34 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
return pathstat.st_size
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def read_seven_bit_encoded_int(fileobj, order: str) -> int:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Read an Int32, 7 bits at a time."""
|
|
|
|
|
# The highest bit of the byte, when on, means to continue reading more bytes.
|
|
|
|
|
count = 0
|
|
|
|
|
shift = 0
|
|
|
|
|
byte = -1
|
|
|
|
|
while (byte & 0x80) != 0 or byte == -1:
|
|
|
|
|
# Check for a corrupted stream. Read a max of 5 bytes.
|
|
|
|
|
if shift == (5 * 7):
|
|
|
|
|
raise ValueError
|
|
|
|
|
byte = to_int(fileobj.read(1), order)
|
|
|
|
|
count |= (byte & 0x7F) << shift
|
|
|
|
|
shift += 7
|
|
|
|
|
return count
|
2018-07-07 00:38:10 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def error(msg: str) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Print fatal error message and terminate"""
|
2026-05-19 17:41:17 +02:00
|
|
|
print(f'[ERROR] {msg}', file=sys.stderr)
|
2026-05-18 16:31:58 +02:00
|
|
|
sys.exit(1)
|
2018-07-07 00:38:10 +02:00
|
|
|
|
2018-07-07 19:03:47 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def warning(msg: str, args) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Print a warning message. Warning messages can be silenced with --quiet"""
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
if not args.quiet:
|
2026-05-19 17:41:17 +02:00
|
|
|
print(f'[WARNING] {msg}. Continuing regardless.', file=sys.stderr)
|
2021-06-05 22:03:31 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def vprint(msg: str, args) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Vprint, verbose print, can be silenced with --quiet"""
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
if not args.quiet:
|
2026-05-19 17:41:17 +02:00
|
|
|
print(f'[*] {msg}')
|
2018-07-07 19:03:47 +02:00
|
|
|
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def download_ird(ird_name: str) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Download an .ird from GET_IRD_NET_LOC"""
|
2020-08-03 19:52:46 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
# Check if file already exists and skip if it does
|
|
|
|
|
if os.path.exists(ird_name): # TODO: might want to check that the file is valid first, could do a HEAD agains the url
|
|
|
|
|
return
|
2020-08-03 19:52:46 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
ird_link = GET_IRD_NET_LOC + ird_name
|
2026-05-19 17:41:17 +02:00
|
|
|
r = requests.get(ird_link, stream=True, timeout=30)
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
with open(ird_name, 'wb') as ird_file:
|
|
|
|
|
r.raw.decode_content = True
|
|
|
|
|
shutil.copyfileobj(r.raw, ird_file)
|
2018-07-09 07:40:41 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def ird_by_game_id(game_id: str) -> Optional[str]:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Using a game_id, download the responding .ird from ALL_IRD_NET_LOC"""
|
|
|
|
|
gameid = game_id.replace('-', '')
|
|
|
|
|
try:
|
|
|
|
|
r = requests.get(ALL_IRD_NET_LOC, headers={'User-Agent': 'Anonymous (You)'}, timeout=5)
|
|
|
|
|
except requests.exceptions.ReadTimeout:
|
|
|
|
|
error('Server timed out, fix your connection or manually specify a key/ird.')
|
|
|
|
|
soup = BeautifulSoup(r.text, "html.parser")
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
ird_name = False
|
|
|
|
|
for elem in soup.find_all("a"):
|
|
|
|
|
url = elem.get('href').split('/')[-1].replace('\\"', '')
|
|
|
|
|
if gameid in url:
|
|
|
|
|
ird_name = url
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
if not ird_name:
|
|
|
|
|
error("Unable to download IRD, couldn't find link. You could specify the decryption key with -d if you have it.")
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
download_ird(ird_name)
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
return (ird_name)
|
2019-05-16 10:37:28 +02:00
|
|
|
|
2018-07-09 07:40:41 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def crc32(filename: str, keep_going: Optional[list] = None) -> Optional[str]:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Calculate crc32 for file"""
|
2026-05-19 17:41:17 +02:00
|
|
|
if keep_going is None:
|
|
|
|
|
keep_going = [True]
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
with open(filename, 'rb') as infile:
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
crc_val = 0
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
while keep_going[0] is True:
|
2026-05-18 16:31:58 +02:00
|
|
|
data = infile.read(65536)
|
|
|
|
|
if not data:
|
|
|
|
|
break
|
2026-05-19 17:41:17 +02:00
|
|
|
crc_val = zlib.crc32(data, crc_val)
|
2021-06-05 22:03:31 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
if keep_going[0] is False:
|
2026-05-18 16:31:58 +02:00
|
|
|
return None
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
return f"{crc_val & 0xFFFFFFFF:08X}"
|
2021-06-05 22:03:31 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def serial_country(title: str) -> str:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Get country from disc serial / productcode / title_id"""
|
|
|
|
|
|
|
|
|
|
if title[2] == 'A':
|
|
|
|
|
return 'Asia'
|
|
|
|
|
if title[2] == 'C':
|
|
|
|
|
return 'China'
|
|
|
|
|
if title[2] == 'E':
|
|
|
|
|
return 'Europe'
|
|
|
|
|
if title[2] == 'H':
|
|
|
|
|
return 'Hong Kong'
|
|
|
|
|
if title[2] == 'J' or title[2] == 'P':
|
|
|
|
|
return 'Japan'
|
|
|
|
|
if title[2] == 'K':
|
|
|
|
|
return 'Korea'
|
|
|
|
|
if title[2] == 'U' or title[2] == 'T':
|
|
|
|
|
return 'USA'
|
|
|
|
|
|
|
|
|
|
raise ValueError('Unknown country?!')
|
2021-06-06 23:09:33 +02:00
|
|
|
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def multiman_title(title: str) -> str:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Fix special characters in title for Multiman style"""
|
2021-06-06 23:09:33 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
replace = {
|
|
|
|
|
':': ' -',
|
|
|
|
|
'/': '-',
|
|
|
|
|
'™': '',
|
|
|
|
|
'®': '',
|
|
|
|
|
}
|
2021-06-06 23:09:33 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
for key, val in replace.items():
|
|
|
|
|
title = title.replace(key, val)
|
2021-06-06 23:09:33 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
return title
|
2021-06-06 23:09:33 +02:00
|
|
|
|
|
|
|
|
|
2018-07-07 00:38:10 +02:00
|
|
|
# Main functions
|
|
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def info(args: Any) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Print information about .iso and then quit."""
|
|
|
|
|
|
|
|
|
|
if args.iso:
|
|
|
|
|
input_iso = iso.ISO(args)
|
|
|
|
|
input_iso.print_info()
|
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
|
|
if args.ird:
|
|
|
|
|
input_ird = ird.IRD(args.ird)
|
|
|
|
|
input_ird.print_info(regions=True)
|
|
|
|
|
sys.exit()
|
|
|
|
|
|
2018-07-07 19:03:47 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def decrypt(args: Any) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Try to decrypt a given .iso using relevant .ird or encryption key from argparse
|
2019-07-07 21:18:34 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
|
|
|
|
"""
|
2018-07-07 00:38:10 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
input_iso = iso.ISO(args)
|
2018-07-07 00:38:10 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
# TODO: some of the logic should probably be moved up here instead of residing in the decrypt function
|
|
|
|
|
input_iso.decrypt(args)
|
2018-07-07 00:38:10 +02:00
|
|
|
|
2019-05-16 10:37:28 +02:00
|
|
|
|
2026-05-19 17:41:17 +02:00
|
|
|
def encrypt(args: Any) -> None:
|
2026-05-18 16:31:58 +02:00
|
|
|
"""Try to re-encrypt a decrypted .iso using relevant .ird or encryption key from argparse
|
2019-05-16 10:37:28 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
If no .ird is given this will try to automatically download an .ird file with the encryption/decryption key for the given game .iso
|
|
|
|
|
"""
|
2021-06-02 20:25:49 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
input_iso = iso.ISO(args)
|
2021-06-02 20:25:49 +02:00
|
|
|
|
2026-05-18 16:31:58 +02:00
|
|
|
input_iso.encrypt(args)
|