-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathciso.py
executable file
·229 lines (181 loc) · 5.68 KB
/
ciso.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
#!/usr/bin/python3
# Copyright 2018 David O'Rourke <[email protected]>
# Copyright 2022 MakeMHz LLC <[email protected]>
# Based on ciso from https://github.com/jamie/ciso
import os
import struct
import sys
import lz4.frame
CISO_MAGIC = 0x4F534943 # CISO
CISO_HEADER_SIZE = 0x18 # 24
CISO_BLOCK_SIZE = 0x800 # 2048
CISO_HEADER_FMT = '<LLQLBBxx' # Little endian
CISO_PLAIN_BLOCK = 0x80000000
#assert(struct.calcsize(CISO_HEADER_FMT) == CISO_HEADER_SIZE)
image_offset = 0
def get_terminal_size(fd=sys.stdout.fileno()):
try:
import fcntl, termios
hw = struct.unpack("hh", fcntl.ioctl(
fd, termios.TIOCGWINSZ, '1234'))
except:
try:
hw = (os.environ['LINES'], os.environ['COLUMNS'])
except:
hw = (25, 80)
return hw
(console_height, console_width) = get_terminal_size()
def update_progress(progress):
barLength = console_width - len("Progress: 100% []") - 1
block = int(round(barLength*progress)) + 1
text = "\rProgress: [{blocks}] {percent:.0f}%".format(
blocks="#" * block + "-" * (barLength - block),
percent=progress * 100)
sys.stdout.write(text)
sys.stdout.flush()
def check_file_size(f):
global image_offset
f.seek(0, os.SEEK_END)
file_size = f.tell() - image_offset
ciso = {
'magic': CISO_MAGIC,
'ver': 2,
'block_size': CISO_BLOCK_SIZE,
'total_bytes': file_size,
'total_blocks': int(file_size / CISO_BLOCK_SIZE),
'align': 2,
}
f.seek(image_offset, os.SEEK_SET)
return ciso
def write_cso_header(f, ciso):
f.write(struct.pack(CISO_HEADER_FMT,
ciso['magic'],
CISO_HEADER_SIZE,
ciso['total_bytes'],
ciso['block_size'],
ciso['ver'],
ciso['align']
))
def write_block_index(f, block_index):
for index, block in enumerate(block_index):
try:
f.write(struct.pack('<I', block))
except Exception as e:
print("Writing block={} with data={} failed.".format(
index, block))
print(e)
sys.exit(1)
def detect_iso_type(f):
global image_offset
# Detect if the image is a REDUMP image
f.seek(0x18310000)
buffer = f.read(20)
if buffer == b"MICROSOFT*XBOX*MEDIA":
print("REDUMP image detected")
image_offset = 0x18300000
return
# Detect if the image is a raw XDVDFS image
f.seek(0x10000)
buffer = f.read(20)
if buffer == b"MICROSOFT*XBOX*MEDIA":
image_offset = 0
return
# Print error and exit
print("ERROR: Could not detect ISO type.")
sys.exit(1)
# Pad file size to ATA block size * 2
def pad_file_size(f):
f.seek(0, os.SEEK_END)
size = f.tell()
f.write(struct.pack('<B', 0x00) * (0x400 - (size & 0x3FF)))
def compress_iso(infile):
lz4_context = lz4.frame.create_compression_context()
# Replace file extension with .cso
fout_1 = open(os.path.splitext(infile)[0] + '.1.cso', 'wb')
fout_2 = None
with open(infile, 'rb') as fin:
print("Compressing '{}'".format(infile))
# Detect and validate the ISO
detect_iso_type(fin)
ciso = check_file_size(fin)
for k, v in ciso.items():
print("{}: {}".format(k, v))
write_cso_header(fout_1, ciso)
block_index = [0x00] * (ciso['total_blocks'] + 1)
# Write the dummy block index for now.
write_block_index(fout_1, block_index)
write_pos = fout_1.tell()
align_b = 1 << ciso['align']
align_m = align_b - 1
# Alignment buffer is unsigned char.
alignment_buffer = struct.pack('<B', 0x00) * 64
# Progress counters
percent_period = ciso['total_blocks'] / 100
percent_cnt = 0
split_fout = fout_1
for block in range(0, ciso['total_blocks']):
# Check if we need to split the ISO (due to FATX limitations)
# TODO: Determine a better value for this.
if write_pos > 0xFFBF6000:
# Create new file for the split
fout_2 = open(os.path.splitext(infile)[0] + '.2.cso', 'wb')
split_fout = fout_2
# Reset write position
write_pos = 0
# Write alignment
align = int(write_pos & align_m)
if align:
align = align_b - align
size = split_fout.write(alignment_buffer[:align])
write_pos += align
# Mark offset index
block_index[block] = write_pos >> ciso['align']
# Read raw data
raw_data = fin.read(ciso['block_size'])
raw_data_size = len(raw_data)
# Compress block
# Compressed data will have the gzip header on it, we strip that.
lz4.frame.compress_begin(lz4_context, compression_level=lz4.frame.COMPRESSIONLEVEL_MAX,
auto_flush=True, content_checksum=False, block_checksum=False, block_linked=False, source_size=False)
compressed_data = lz4.frame.compress_chunk(lz4_context, raw_data, return_bytearray=True)
compressed_size = len(compressed_data)
lz4.frame.compress_flush(lz4_context)
# Ensure compressed data is smaller than raw data
# TODO: Find optimal block size to avoid fragmentation
if (compressed_size + 12) >= raw_data_size:
writable_data = raw_data
# Next index
write_pos += raw_data_size
else:
writable_data = compressed_data
# LZ4 block marker
block_index[block] |= 0x80000000
# Next index
write_pos += compressed_size
# Write data
split_fout.write(writable_data)
# Progress bar
percent = int(round((block / (ciso['total_blocks'] + 1)) * 100))
if percent > percent_cnt:
update_progress((block / (ciso['total_blocks'] + 1)))
percent_cnt = percent
# TODO: Pad file to ATA block size
# end for block
# last position (total size)
# NOTE: We don't actually need this, but we're keeping it for legacy reasons.
block_index[-1] = write_pos >> ciso['align']
# write header and index block
print("\nWriting block index")
fout_1.seek(CISO_HEADER_SIZE, os.SEEK_SET)
write_block_index(fout_1, block_index)
# end open(infile)
pad_file_size(fout_1)
fout_1.close()
if fout_2:
pad_file_size(fout_2)
fout_2.close()
def main(argv):
infile = argv[1]
compress_iso(infile)
if __name__ == '__main__':
sys.exit(main(sys.argv))