-
Notifications
You must be signed in to change notification settings - Fork 0
/
tcx_rle_decoder.py
261 lines (190 loc) · 7.38 KB
/
tcx_rle_decoder.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# Turbo Copy 3/Turbo Copy 4 data stream analyzer & decompressor
#
# done by Seban/Slight
#
# This is very simple & lame python code (I never programmed in Python
# so this is an experiment, maybe I learn something new? ;)
#
# This simple script was used to extract files from old tape images
#
# The data structure is "guessed" from loader code,
# I'm not sure this will be correct for all cases
# I have only a few example files, and because those files
# was generated by Turbo Copy 3 or 4, the file extension TCX was selected
# for those files.
#
# >>> Python 3.8.2 was used to test & run this code <<<
#
# .O. released at 2020.05.09
# ..O
# OOO >>> Public Domain <<<
import os
import sys
# example cmd-line parameters, needed only when testing inside the IDLE Python IDE
#sys.argv = [sys.argv[0], 'test_files/tcx/super_cobra.tcx']
#sys.argv = [sys.argv[0], 'test_files/tcx/ixion2.tcx','0x61']
#sys.argv = [sys.argv[0], 'test_files/tcx/spy_vs_spy.tcx','0xaf']
#sys.argv = [sys.argv[0], 'test_files/tcx/universal_hero.tcx','0xb4']
# if want do see stream debug info set to "True"
stream_debug_mode = False
if (len(sys.argv) < 2):
print("No input file specified! Exiting.")
exit(-1)
input_file_name = sys.argv[1]
# check if file exists
if (os.path.exists(input_file_name) == False):
print("Input file not found! Exiting!")
exit(-1)
input_file_size = os.path.getsize(input_file_name)
print("\nInput file is",input_file_name,"and the file size is", input_file_size, "bytes.")
in_data = bytearray( open(input_file_name,"rb").read() )
print("TCX data loaded, checking data...\n")
if (in_data[0] != in_data[1]):
print("Wrong file header! Bad file or corrupted? Exiting.")
exit(-1)
else:
xor_key = (in_data[0] ^ int(0xff))
if (xor_key != 0):
print(">>> Data encrypted! Trying to use calculated XOR key 0x%02X <<<\n" % xor_key)
# XOR input data with passed XOR key
for x in range(0,len(in_data)):
in_data[x] ^= xor_key
# for debug only! xor-ed data dump to file.
if (stream_debug_mode):
xor_filename = input_file_name + ".xor"
open(xor_filename,"wb").write(in_data)
print("Header is: $%02x%02x" %(in_data[0],in_data[1]))
# check if the input file have correct Atari-DOS file header ($ff,$ff)
if ((in_data[0] != 0xff) or (in_data[1] != 0xff)):
print("\nNot a Atari DOS file header! Wrong file type? Data encrypted?")
print("Maybe the XOR key for data decoding is wrong or not given?")
exit(-1)
# segment offset list
seg_offsets = []
# at the begining put Atari-DOS header on the output data array
out_data = bytearray(b'\xFF\xFF')
# zeroize block counter
blk = 0
# skip atari header file ($ff,$ff)
i = 2
# init segments cleanup flag
cleanup_needed = False
# main data processing loop
while (i < len(in_data)):
# check if any sensible amount of data is present
if (len(in_data) - i) < 4:
break
# get the block addresses & calculate block size
blk_start = in_data[i+0]+256*in_data[i+1]
blk_end = in_data[i+2]+256*in_data[i+3]
blk_len = (blk_end - blk_start) + 1
if stream_debug_mode:
print()
print("block %03d: $%04x-$%04x ($%04x)" %(blk,blk_start,blk_end,blk_len))
# check block validity
if (blk_len > 0) and (blk_start != (xor_key+256*xor_key)):
# add current segment offset to list
seg_offsets.append(len(out_data))
# copy header block data to output
out_data.append(in_data[i+0])
out_data.append(in_data[i+1])
out_data.append(in_data[i+2])
out_data.append(in_data[i+3])
# move input data pointer to next valid data
i += 4
# increment block counter
blk += 1
else:
# if no valid data block...
print("^^^^^^^^^: Invalid block header detected! Processing stopped.")
# chceck block length
if (blk_len < 0):
print("\n>>> Something is really wrong! File corupted!?! Sorry, I can't do anything more. Exiting.")
exit(-1)
# abort processing
break
# remember actual offset @ out_data
actual_out_data_offset = (len(out_data)-4)
# block processing loop
while (blk_len):
if (i > len(in_data)-1):
print("^^^^^^^^^: unexpected end of data at file segment! [missing $%04x byte(s)]" %(blk_len))
print(" : block throwed away, output file may be corrupted.\n")
# cut out bytes added from bad block
out_data = out_data[0:actual_out_data_offset]
# remove bad segment from list
seg_offsets.pop()
# decrement valid blocks count
blk -= 1
cleanup_needed = True
break
# check for RLE zero escape code
if (in_data[i] == 0xbf):
# get number of zeros from stream
cnt = in_data[i+1]
# send the zeros to output
for r in range(0,cnt):
out_data.append(0x00)
# move input data pointer to next valid data
i += 2
# decremant block length
blk_len -= cnt
# print deg-info message
if stream_debug_mode:
print("\t@ $%04x RLE zero block, len =%5i" %(i,r))
# check for RLE byte escape code
elif (in_data[i] == 0xcf):
# get number of repetition from input stream
cnt = in_data[i+1]
# get the value of repeated byte
val = in_data[i+2]
# send the repeated byte to output
for r in range(0,cnt):
out_data.append(val)
# move input data pointer to next valid data
i += 3
# decrement block length
blk_len -= cnt
# print dbg-info message
if stream_debug_mode:
print("\t@ $%04x RLE fill block, len =%5i" %(i , cnt), "(",format(val,"02x"),")")
# if plain byte on input stream then copy to output
else:
out_data.append(in_data[i])
# move input data pointer to next byte
i +=1
# decrement block length
blk_len -=1
# chceck cleanup flag
if (cleanup_needed):
print("Trying to cleanup data segments, removing blocks: ",end = '')
# process all segments
while len(seg_offsets):
# get actual block offset
offset = seg_offsets.pop()
# calculate block adresses, length
blk_start = out_data[offset+0]+256*out_data[offset+1]
blk_end = out_data[offset+2]+256*out_data[offset+3]
blk_len = (blk_end - blk_start)
# if correct block found, stop cleanup.
if (blk_len > 0):
break
# print block number
print("%03d, " %(blk-1), end='')
# cut output data stream
out_data = out_data[0:offset]
# decrement block count
blk -= 1
print("done.")
# processing done message
print("\nInput data processing done,", blk, "correct block(s) processed, generating output file...")
# generate output filename
output_filename = input_file_name + ".xex"
# calculate the size diff
size_diff = len(out_data) - input_file_size
# print info messagaes about output file/data
print("Output file is", output_filename, "and the file size is", len(out_data), "bytes.")
# write out decompressed data
open(output_filename,"wb").write(out_data)
# print out last message and exit!
print("Processing done, output file written, IN/OUT file size diff is", size_diff, "byte(s).")