-
-
Save mmozeiko/4f868c6d48795cb6193879b5a80da952 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import os | |
import re | |
import csv | |
import sys | |
import hmac | |
import hashlib | |
import struct | |
import urllib.request | |
import urllib.error | |
import xml.etree.ElementTree as ET | |
import xml.dom.minidom as minidom | |
# http://wololo.net/talk/viewtopic.php?f=54&t=44091 | |
KEY = bytes.fromhex("E5E278AA1EE34082A088279C83F9BBC806821C52F2AB5D2B4ABD995450355114") | |
def title2info(title): | |
h = hmac.new(KEY, ("np_" + title).encode("ascii"), hashlib.sha256) | |
url = "http://gs-sec.ww.np.dl.playstation.net/pl/np/%s/%s/%s-ver.xml" % (title, h.hexdigest(), title) | |
try: | |
with urllib.request.urlopen(url) as f: | |
data = f.read() | |
except urllib.error.HTTPError as e: | |
if e.code == 404: | |
return None | |
raise e | |
if data: | |
return data.decode("utf-8") | |
return None | |
def parse_sfo(sfo): | |
header, version, key_table, data_table, count = struct.unpack_from("<IIIII", sfo) | |
assert header == 0x46535000 | |
assert version == 0x00000101 | |
ret = {} | |
for i in range(count): | |
key_offset, param_type, param_len, param_maxlen, data_offset = struct.unpack_from("<HHIII", sfo, 0x14 + 0x10 * i) | |
key_end = sfo.find(b"\0", key_table + key_offset) | |
key = struct.unpack_from("%ds" % (key_end - (key_table + key_offset)), sfo, key_table + key_offset) | |
key = key[0].decode("ascii") | |
if param_type == 0x0204: | |
value = struct.unpack_from("%ds" % (param_len - 1), sfo, data_table + data_offset) | |
ret[key] = value[0].decode("utf-8") | |
elif param_type == 0x0404: | |
assert param_len == 4 | |
value = struct.unpack_from("<I", sfo, data_table + data_offset) | |
ret[key] = value[0] | |
else: | |
assert not "unknown type" | |
return ret | |
def get_pkg_start(pkg, size): | |
req = urllib.request.Request(pkg, headers={"Range": "bytes=0-%d" % size}) | |
with urllib.request.urlopen(req) as f: | |
return f.read() | |
def get_info(patchurl): | |
MaxVersion = "99.99" | |
data = get_pkg_start(patchurl, 16*1024) | |
magic1 = struct.unpack_from(">I", data)[0] | |
magic2 = struct.unpack_from(">I", data, 192)[0] | |
if magic1 != 0x7f504b47 or magic2 != 0x7f657874: | |
print(patchurl) | |
print("corrupted pkg?") | |
return MaxVersion | |
meta_offset, meta_count = struct.unpack_from(">II", data, 8) | |
sfo_offset = None | |
for i in range(meta_count): | |
meta_type, meta_size = struct.unpack_from(">II", data, meta_offset) | |
if meta_type == 14: | |
sfo_offset, sfo_size = struct.unpack_from(">II", data, meta_offset + 8) | |
break | |
meta_offset += 8 + meta_size | |
if sfo_offset is None: | |
print(patchurl) | |
print("cannot find sfo in pkg") | |
return MaxVersion | |
if sfo_offset + sfo_size > len(data): | |
print(patchurl) | |
print("sfo is not in beginning of pkg [%d..%d]" % (sfo_offset, sfo_offset + sfo_size-1)) | |
return MaxVersion | |
sfo = parse_sfo(data[sfo_offset:sfo_offset+sfo_size]) | |
fw = sfo["PSP2_DISP_VER"] | |
name = sfo["STITLE"] if "STITLE" in sfo else sfo["TITLE"] | |
name = name.strip() | |
m = re.match(r"(\d+\.\d\d).*", fw) | |
return m.group(1), name | |
def get_supported(xml, supported = "03.60"): | |
packages = list(ET.fromstring(xml).findall("./tag/package")) | |
for p in reversed(packages): | |
patchurl = p.attrib["url"] | |
size = p.attrib["size"] | |
version = p.attrib["version"] | |
fw, name = get_info(patchurl) | |
if fw <= supported: | |
return patchurl, size, version.lstrip("0"), fw.lstrip("0"), name | |
return None | |
def info(title): | |
xml = title2info(title) | |
xml = minidom.parseString(xml) | |
xml = xml.toprettyxml(indent=" ") | |
print(xml) | |
def patch(title): | |
xml = title2info(title) | |
if xml is None: | |
print(None) | |
else: | |
supported = get_supported(xml) | |
if supported is None: | |
print(None) | |
else: | |
patchurl, size, version, fw, name = supported | |
print("Name:", name) | |
print("Patch:", patchurl) | |
print("Version:", version) | |
print("FW:", fw) | |
print("Size:", size) | |
def latest(title): | |
xml = title2info(title) | |
if xml is None: | |
print("None") | |
else: | |
supported = get_supported(xml, "99.99") | |
if supported is None: | |
print(None) | |
else: | |
patchurl, size, version, fw, name = supported | |
print("Name:", name) | |
print("Patch:", patchurl) | |
print("Version:", version) | |
print("FW:", fw) | |
print("Size:", size) | |
if __name__ == "__main__": | |
if len(sys.argv) == 1: | |
print("Usage: %s COMMAND TITLEID" % (sys.argv[0])) | |
print() | |
print("Available commands:") | |
print(" info TITLEID - retrieve title patch information xml file, prints to stdout") | |
print(" patch TITLEID - retrieve latest usable (on 3.60) patch for title, prints to stdout") | |
print(" latest TITLEID - retrieve latest patch (may not work on 3.60) for title, prints to stdout") | |
exit(0) | |
if sys.argv[1] == "info": | |
info(sys.argv[2]) | |
elif sys.argv[1] == "patch": | |
patch(sys.argv[2]) | |
elif sys.argv[1] == "latest": | |
latest(sys.argv[2]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment