-
Notifications
You must be signed in to change notification settings - Fork 6
/
crp_extract.py
161 lines (140 loc) · 5.63 KB
/
crp_extract.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
import argparse
import errno
import os
import json
from formatter import get_formatted_data
from formatter import get_raw
from formatter import unpack
# Safely make directories necessary to save something at the given path
def make_directories_for(path):
path = os.path.dirname(path)
try:
os.makedirs(path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
def argparse_valid_directory_type(value):
abs_path = os.path.abspath(value)
if not os.path.isdir(abs_path):
raise argparse.ArgumentTypeError("{0} is not a valid directory".format(abs_path))
return abs_path
# Return a string from the file
def string_at(file, offset, size):
file.seek(offset)
return unpack(file, "s", size).decode("utf-8", "ignore")
# Slice part of the given file and write it to file at the given path
def slice_and_write_file(file, offset, size, path):
make_directories_for(path)
write_file = open(path, "wb")
write_file.truncate()
file.seek(offset)
write_file.write(file.read(size))
write_file.close()
# Return the relative offset from the offset in the file of the first sequence
# of bytes that match. Return None if there is no matching sequence.
def first_sequence(file, offset, sequence, exactly_first=False):
file.seek(offset)
index = 0
while(True):
value = unpack(file, "B")
# Next byte matches
if value == sequence[index]:
index += 1
# We read the whole sequence so return the offset from the offset
if index == len(sequence):
return file.tell() - offset - len(sequence)
# Next byte does not match
else:
# Try this byte one more time from the start of the sequence if
# we were in the middle before.
if index != 0:
index = 0
file.seek(-1, os.SEEK_CUR)
# If we are searching for the sequence exactly at the start and it
# is not at the exact start then return None.
if exactly_first and file.tell() - offset > len(sequence):
return None
# TODO: handle hitting the end of the file and returning None
def main():
parser = argparse.ArgumentParser(description="Unpack Colossal Raw Package (.crp) files")
parser.add_argument("file", type=argparse.FileType("rb"),
help="The file to unpack")
parser.add_argument("--output-dir", type=argparse_valid_directory_type, default=".",
help="The directory to put the unpacked files into (Default: current working directory)")
args = parser.parse_args()
file_name = args.file.name
file = args.file
data = get_formatted_data(file, "crp", "crp")
name_of_mod = get_raw(data.get("name_of_mod", ""), file)
# If there is no mod name default to the file name
if name_of_mod == "":
name_of_mod = file_name[:-4]
output_path = os.path.join(args.output_dir, name_of_mod.decode('utf-8'))
end_header_offset = get_raw(data["end_header_offset"], file)
file_metadata = {}
# Go through each file from the header
for file_header in data["file_headers"]:
file_name = get_raw(file_header["file_name"], file).decode('utf-8')
offset_from_header = get_raw(file_header["offset_from_header"], file)
file_size = get_raw(file_header["file_size"], file)
file_offset = offset_from_header + end_header_offset
# Try to read a special descriptive string at the start of the file
file.seek(file_offset)
try:
id_string = str(unpack(file, "s", 48)).lower()
except:
id_string = ""
# Check for PNG header at start of file
is_png = first_sequence(
file,
file_offset,
[137, 80, 78, 71, 13, 10, 26, 10],
exactly_first=True
)
# DDS
if "unityengine.texture2d" in id_string:
file_path = os.path.join(output_path, file_name + '.dds')
relative_offset = first_sequence(file, file_offset, [68, 68, 83, 32])
# Slice starting at "DDS "
slice_and_write_file(
file,
file_offset + relative_offset,
file_size - relative_offset,
file_path
)
file_metadata[file_path] = string_at(file, file_offset, relative_offset)
# PNG
elif "icolossalframework.importers.image" in id_string or is_png:
file_path = os.path.join(output_path, file_name + '.png')
relative_offset = first_sequence(file, file_offset, [137, 80, 78, 71])
# Slice starting at PNG header
slice_and_write_file(
file,
file_offset + relative_offset,
file_size - relative_offset,
file_path
)
file_metadata[file_path] = string_at(file, file_offset, relative_offset)
# Other
else:
slice_and_write_file(
file,
file_offset,
file_size,
os.path.join(output_path, file_name)
)
metadata = {
"version": get_raw(data["version"], file),
"steam_id": get_raw(data["steam_id"], file).decode("utf-8"),
"number_of_files": get_raw(data["number_of_files"], file),
"file_metadata": file_metadata
}
# Save the metadata as json
meta_path = os.path.join(output_path, "metadata.json")
make_directories_for(meta_path)
with open(meta_path, "w") as f:
json.dump(metadata, f, indent=4, sort_keys=True)
if __name__ == '__main__':
main()