Skip to content

Instantly share code, notes, and snippets.

@mmozeiko
Created October 18, 2017 20:31
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mmozeiko/4f868c6d48795cb6193879b5a80da952 to your computer and use it in GitHub Desktop.
Save mmozeiko/4f868c6d48795cb6193879b5a80da952 to your computer and use it in GitHub Desktop.
#!/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