-
Notifications
You must be signed in to change notification settings - Fork 3
/
sgex.py
189 lines (142 loc) · 5.24 KB
/
sgex.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#
# Save Game Extractor (GPL3)
# https://github.com/slinga-homebrew/Save-Game-Extractor
#
# This script parses an encoded transmission from the Sega Saturn, validates it
# and writes it out to disk.
#
# The transmission consists of a TRANSMISSION_HEADER and a BUP_HEADER followed
# by a variable number of bytes of data. The transmission is zipped, Reed
# Solomon encoded, and then escaped. This Python script undoes all of that.
#
import sys
import binascii
import hashlib
import reedsolo
import zlib
'''
Taken from main.h
typedef struct _TRANSMISSION_HEADER
{
char magic[TRANSMISSION_MAGIC_SIZE]; // magic bytes be SGEX
unsigned char md5Hash[MD5_HASH_SIZE];
char saveFilename[MAX_SAVE_FILENAME]; // save filename
unsigned int saveFileSize; // size of the file in bytes
unsigned char saveFileData[0]; // saveFileSize number of bytes of save data
} TRANSMISSION_HEADER, *PTRANSMISSION_HEADER;
Taken from bup_header.h
'''
MAGIC = "SGEX"
TRANSMISSION_HEADER_SIZE = 36
BUP_HEADER_SIZE = 64
ESCAPE_BYTE = 0x54
SYNC_REPLACE = 0x9F
SYNC_BYTE = 0xAB
# Change two ESCAPE_BYTEs in a row to a single ESCAPE_BYTE
# Change an ESCAPE_BYTE followed by SYNC_REPLACE byte to a single SYNC_BYTE
def unescape(message):
escapedMessage = b''
i = 0
while(i < len(message)):
if message[i] == ESCAPE_BYTE:
if message[i + 1] == ESCAPE_BYTE:
# two ESCAPE_BYTES, replace with a single ESCAPE_BYTE
escapedMessage = escapedMessage + ESCAPE_BYTE.to_bytes(1, 'big')
i = i + 1
elif message[i + 1] == SYNC_REPLACE:
# ESCAPE_BYTE followed by a SYNC_REPLACE
# replace with a SYNC_BYTE
escapedMessage = escapedMessage + SYNC_BYTE.to_bytes(1, 'big')
i = i + 1
else:
# invalid escape sequence data, the data is corrupted
return ""
else:
# not an escape byte, continue as normal
escapedMessage = escapedMessage + message[i].to_bytes(1, 'big')
i += 1
return escapedMessage
def main():
print("Save Game Extractor");
print("(github.com/slinga-homebrew/Save-Game-Extractor)\n")
if len(sys.argv) != 2:
print("Error: Input filename required")
return -1
filename = sys.argv[1]
try:
inFile = open(filename, "rb")
except:
print("Error: Could not open " + filename + " for reading")
return -1
#
# Unescape the buffer
#
escapedBuf = inFile.read()
# unescape the buffer
unescapedBuf = unescape(escapedBuf)
if unescapedBuf == "":
print("Failed to unescape data, something is corrupt.");
return -1
#
# Reed Solomon decode
#
# Reed Solomon parameters must match settings used by libcorrect
rsc = reedsolo.RSCodec(nsym=32, nsize=255, fcr=1, prim=0x187)
try:
decodedBuf = rsc.decode(unescapedBuf)
except:
print("Reed Solomon couldn't decode buffer, too many errors.")
print(sys.exc_info()[0])
return -1
print("Errors Corrected: " + str(len(decodedBuf[2])))
compressedBuf = decodedBuf[0]
#
# Decompress the data
#
decompressedBuf = zlib.decompress(compressedBuf);
#
# TRANSMISSION_HEADER + variable length save data
#
# sanity check the buffer
if len(decompressedBuf) < TRANSMISSION_HEADER_SIZE:
print("Error: " + filename + " is too small. Must be at least TRANSMISSION_HEADER_SIZE")
return -1
# SGEX magic bytes
magic = decompressedBuf[0:4].decode("utf-8")
if magic != MAGIC:
print("Error: The magic bytes are invalid")
return -1
saveSize = binascii.b2a_hex(decompressedBuf[32:36])
saveSize = int(saveSize, 16)
# validate length, shouldn't fail here because of the Reed Solomon check
if TRANSMISSION_HEADER_SIZE + BUP_HEADER_SIZE + saveSize != len(decompressedBuf):
print("Error: Received incorrect number of bytes. Expected " + str(TRANSMISSION_HEADER_SIZE + BUP_HEADER_SIZE + saveSize) + ", got " + str(len(decompressedBuf)))
return -1
saveName = decompressedBuf[20:31].decode("utf-8")
md5Hash = binascii.b2a_hex(decompressedBuf[4:20]).decode("utf-8")
# verify the MD5 hash. Again shouldn't ever fail here due to the Reed Solomon check
computedHashResult = hashlib.md5(decompressedBuf[TRANSMISSION_HEADER_SIZE + BUP_HEADER_SIZE:])
computedHash = computedHashResult.hexdigest()
print("Transmitted Filename: " + saveName)
print("Transmitted Save Size: " + str(saveSize))
print("Transmitted MD5: " + str(md5Hash))
print("Computed MD5: " + computedHash)
print("")
if md5Hash != computedHash:
print("MD5 hashes don't match, save is corrupt.")
else:
print("MD5 hashes validate, save is correct.")
# create the output .BUP file
try:
outFile = open(saveName + ".BUP", "wb")
outFile.write(decompressedBuf[TRANSMISSION_HEADER_SIZE:])
outFile.close()
except:
print("Error writing save " + saveName + ".BUP to disk")
return -1
print("Wrote save game " + saveName + ".BUP to disk")
if __name__ == "__main__":
if sys.version_info.major != 3:
print("Python 3 required")
sys.exit(-1)
main()