#!python2 import sys, os, struct from io import BytesIO from pprint import pprint def read_cstring(f): bytes = [] while True: byte = f.read(1) if byte == b'\x00': break elif byte == '': raise EOFError() else: bytes.append(byte) return b''.join(bytes) def check_file_magic(f, expected_magic): old_offset = f.tell() try: magic = f.read(len(expected_magic)) except: return False finally: f.seek(old_offset) return magic == expected_magic script_file_name = os.path.split(sys.argv[0])[1] script_file_base = os.path.splitext(script_file_name)[0] if len(sys.argv) < 2: print('CXML decompiler (c) flatz') print('Usage: {0} '.format(script_file_name)) sys.exit() ENDIANNESS = '>' def write_raw(f, data): if type(data) == str: f.write(data) elif type(data) == unicode: f.write(data.decode('utf-8')) else: f.write(data) def write_indent(f, depth): write_raw(f, '\t' * depth) def write_line(f, data): write_raw(f, data) write_raw(f, '\n') INT_FMT = ENDIANNESS + 'i' FLOAT_FMT = ENDIANNESS + 'f' STRING_FMT = ENDIANNESS + 'ii' INT_ARRAY_FMT = ENDIANNESS + 'ii' FLOAT_ARRAY_FMT = ENDIANNESS + 'ii' FILE_FMT = ENDIANNESS + 'ii' ID_FMT = ENDIANNESS + 'i' ID_REF_FMT = ENDIANNESS + 'i' class Attribute(object): HEADER_FMT = ENDIANNESS + 'ii' HEADER_SIZE = struct.calcsize(HEADER_FMT) SIZE = HEADER_SIZE + max(struct.calcsize(INT_FMT), struct.calcsize(FLOAT_FMT), struct.calcsize(STRING_FMT), struct.calcsize(INT_ARRAY_FMT), struct.calcsize(FLOAT_ARRAY_FMT), struct.calcsize(FILE_FMT), struct.calcsize(ID_FMT), struct.calcsize(ID_REF_FMT)) TYPE_NONE = 0 TYPE_INT = 1 TYPE_FLOAT = 2 TYPE_STRING = 3 TYPE_INT_ARRAY = 4 TYPE_FLOAT_ARRAY = 5 TYPE_FILE = 6 TYPE_ID = 7 TYPE_ID_REF = 8 def __init__(self, element): self.element = element self.start = None self.name = None self.type = None self.offset = None self.length = None self.value = None def load(self, f): self.start = f.tell() data = f.read(self.HEADER_SIZE) self.name, self.type = struct.unpack(self.HEADER_FMT, data) data = f.read(self.SIZE - self.HEADER_SIZE) if self.type == self.TYPE_NONE: pass elif self.type == self.TYPE_INT: self.value, = struct.unpack(INT_FMT, data[:struct.calcsize(INT_FMT)]) elif self.type == self.TYPE_FLOAT: self.value, = struct.unpack(FLOAT_FMT, data[:struct.calcsize(FLOAT_FMT)]) elif self.type == self.TYPE_STRING: self.offset, self.length = struct.unpack(STRING_FMT, data[:struct.calcsize(STRING_FMT)]) elif self.type == self.TYPE_INT_ARRAY: self.offset, self.length = struct.unpack(INT_ARRAY_FMT, data[:struct.calcsize(INT_ARRAY_FMT)]) elif self.type == self.TYPE_FLOAT_ARRAY: self.offset, self.length = struct.unpack(FLOAT_ARRAY_FMT, data[:struct.calcsize(FLOAT_ARRAY_FMT)]) elif self.type == self.TYPE_FILE: self.offset, self.length = struct.unpack(FILE_FMT, data[:struct.calcsize(FILE_FMT)]) elif self.type == self.TYPE_ID: self.offset, = struct.unpack(ID_FMT, data[:struct.calcsize(ID_FMT)]) elif self.type == self.TYPE_ID_REF: self.offset, = struct.unpack(ID_REF_FMT, data[:struct.calcsize(ID_REF_FMT)]) return True def get_name(self): return self.element.document.get_string(self.name) def get_int(self): if self.type != self.TYPE_INT: return None return self.value def get_float(self): if self.type != self.TYPE_FLOAT: return None return self.value def get_string(self): if self.type != self.TYPE_STRING: return None value = self.element.document.get_string(self.offset) if len(value) != self.length: return None return value def get_int_array(self): if self.type != self.TYPE_INT_ARRAY: return None value = self.element.document.get_int_array(self.offset, self.length) if len(value) != self.length: return None return value def get_float_array(self): if self.type != self.TYPE_FLOAT_ARRAY: return None value = self.element.document.get_float_array(self.offset, self.length) if len(value) != self.length: return None return value def get_file(self): if self.type != self.TYPE_FILE: return None value = self.element.document.get_file(self.offset, self.length) return value def get_id(self): if self.type != self.TYPE_ID: return None id = self.element.document.get_id_string(self.offset) return id def get_id_ref(self): if self.type != self.TYPE_ID_REF: return None id = self.element.document.get_id_string(self.offset) element = Element(self.element.document) return [id, element] def dump(self, f, depth): pass #print(' ' * depth + 'Attribute:' + 'name:{0} type:{1}'.format(self.name, self.type), end='\n', file=f) class Element(object): HEADER_FMT = ENDIANNESS + 'iiiiiii' SIZE = struct.calcsize(HEADER_FMT) TAG_NAME = 0 ATTR_NUM = 1 PARENT = 2 PREV = 3 NEXT = 4 FIRST_CHILD = 5 LAST_CHILD = 6 def __init__(self, document): self.document = document self.start = None self.name = None self.num_attributes = None self.parent = None self.prev = None self.next = None self.first_child = None self.last_child = None def load(self, f): self.start = f.tell() self.name, self.num_attributes, self.parent, self.prev, self.next, self.first_child, self.last_child = struct.unpack(self.HEADER_FMT, f.read(self.SIZE)) return True def get_name(self): return self.document.get_string(self.name) def get_attribute(self, index): if index < 0 or index >= self.num_attributes: return None offset = self.start + Element.SIZE + index * Attribute.SIZE if not is_valid_attribute(self.document, offset): return None attribute = Attribute(self) f = BytesIO(self.document.tree_bin) f.seek(offset) attribute.load(f) return attribute def get_parent(self): if not is_valid_element(self.document, self.parent): return None element = Element(self.document) f = BytesIO(self.document.tree_bin) f.seek(parent) element.load(f) return element def get_first_child(self): if not is_valid_element(self.document, self.first_child): return None element = Element(self.document) f = BytesIO(self.document.tree_bin) f.seek(self.first_child) element.load(f) return element def get_last_child(self): if not is_valid_element(self.document, self.last_child): return None element = Element(self.document) f = BytesIO(self.document.tree_bin) f.seek(self.last_child) element.load(f) return element def get_prev_sibling(self): if not is_valid_element(self.document, self.prev): return None element = Element(self.document) f = BytesIO(self.document.tree_bin) f.seek(self.prev) element.load(f) return element def get_next_sibling(self): if not is_valid_element(self.document, self.next): return None element = Element(self.document) f = BytesIO(self.document.tree_bin) f.seek(self.next) element.load(f) return element def dump(self, f, depth): write_indent(f, depth) name = self.get_name() write_raw(f, '<' + name) for i in range(self.num_attributes): attribute = self.get_attribute(i) if attribute is None: return False write_raw(f, ' {0}='.format(attribute.get_name())) if attribute.type == Attribute.TYPE_NONE: write_raw(f, '\"null\"') elif attribute.type == Attribute.TYPE_INT: write_raw(f, '\"{0}\"'.format(attribute.get_int())) elif attribute.type == Attribute.TYPE_FLOAT: write_raw(f, '\"{0:3.7}\"'.format(attribute.get_float())) elif attribute.type == Attribute.TYPE_STRING: write_raw(f, '\"{0}\"'.format(attribute.get_string())) elif attribute.type == Attribute.TYPE_INT_ARRAY: write_raw(f, '\"') array = attribute.get_int_array() array_length = len(array) for j in range(array_length): write_raw(f, '{0}'.format(array[j])) if j + 1 < array_length: write_raw(f, ',') write_raw(f, '\"') elif attribute.type == Attribute.TYPE_FLOAT_ARRAY: write_raw(f, '\"') array = attribute.get_float_array() array_length = len(array) for j in range(array_length): write_raw(f, '{0:3.7}'.format(array[j])) if j + 1 < array_length: write_raw(f, ',') write_raw(f, '\"') elif attribute.type == Attribute.TYPE_FILE: file_name = '{0}_{1:08X}_{2:08X}.bin'.format(self.document.file_prefix, attribute.offset, attribute.length) file_data = attribute.get_file() with open(file_name, 'wb') as of: of.write(file_data) write_raw(f, '\"{0}\"'.format(file_name)) elif attribute.type == Attribute.TYPE_ID: write_raw(f, '\"{0}\"'.format(attribute.get_id())) elif attribute.type == Attribute.TYPE_ID_REF: id_entity = attribute.get_id_ref() write_raw(f, '\"{0}\"'.format(id_entity[0])) child_element = self.get_first_child() if not child_element is None: write_raw(f, '>\n') while not child_element is None: child_element.dump(f, depth + 1) child_element = child_element.get_next_sibling() write_indent(f, depth) write_raw(f, '\n') else: write_raw(f, ' />\n') def is_valid_element(document, offset): if offset < 0 or offset + Element.SIZE > document.tree_size: return False element = Element(document) f = BytesIO(document.tree_bin) f.seek(offset) element.load(f) if element.num_attributes < 0 or offset + Element.SIZE + element.num_attributes * Attribute.SIZE > document.tree_size: return False return True def is_valid_attribute(document, offset): if offset < 0 or offset + Attribute.SIZE > document.tree_size: return False return True class Document(object): HEADER_FMT = ENDIANNESS + '4siiiiiiiiiiiii8x' HEADER_SIZE = struct.calcsize(HEADER_FMT) def __init__(self, file_prefix=''): self.file_prefix = file_prefix self.magic = None self.version = None self.tree_offset = None self.tree_size = None self.id_table_offset = None self.id_table_size = None self.string_table_offset = None self.string_table_size = None self.int_array_table_offset = None self.int_array_table_size = None self.float_array_table_offset = None self.float_array_table_size = None self.file_table_offset = None self.file_table_size = None self.tree_bin = None self.id_table_bin = None self.string_table_bin = None self.int_array_table_bin = None self.float_array_table_bin = None self.file_table_bin = None self.root = None def get_document_element(self): if not is_valid_element(self, 0): return None element = Element(self) f = BytesIO(self.tree_bin) element.load(f) return element def get_id_string(self, offset): if offset < 0 or offset >= self.id_table_size: return None f = BytesIO(self.id_table_bin) f.seek(offset) entity_offset, = struct.unpack(INT_FMT, f.read(struct.calcsize(INT_FMT))) return read_cstring(f) def get_string(self, offset): if offset < 0 or offset >= self.string_table_size: return None f = BytesIO(self.string_table_bin) f.seek(offset) return read_cstring(f) def get_int_array(self, offset, length): if offset < 0 or (offset + length) * struct.calcsize(INT_FMT) > self.int_array_table_size: return None f = BytesIO(self.int_array_table_bin) f.seek(offset * struct.calcsize(INT_FMT)) array = [] for i in range(length): value, = struct.unpack(INT_FMT, f.read(struct.calcsize(INT_FMT))) array.append(value) return array def get_float_array(self, offset, length): if offset < 0 or (offset + length) * struct.calcsize(FLOAT_FMT) > self.float_array_table_size: return None f = BytesIO(self.float_array_table_bin) f.seek(offset * struct.calcsize(FLOAT_FMT)) array = [] for i in range(length): value, = struct.unpack(FLOAT_FMT, f.read(struct.calcsize(FLOAT_FMT))) array.append(value) return array def get_file(self, offset, length): if offset < 0 or offset + length > self.file_table_size: return None return self.file_table_bin[offset:offset + length] def load(self, f): self.magic, self.version, self.tree_offset, self.tree_size, self.id_table_offset, self.id_table_size, self.string_table_offset, self.string_table_size, self.int_array_table_offset, self.int_array_table_size, self.float_array_table_offset, self.float_array_table_size, self.file_table_offset, self.file_table_size = struct.unpack(self.HEADER_FMT, f.read(self.HEADER_SIZE)) f.seek(self.tree_offset) self.tree_bin = f.read(self.tree_size) f.seek(self.id_table_offset) self.id_table_bin = f.read(self.id_table_size) f.seek(self.string_table_offset) self.string_table_bin = f.read(self.string_table_size) f.seek(self.int_array_table_offset) self.int_array_table_bin = f.read(self.int_array_table_size) f.seek(self.float_array_table_offset) self.float_array_table_bin = f.read(self.float_array_table_size) f.seek(self.file_table_offset) self.file_table_bin = f.read(self.file_table_size) self.root = self.get_document_element() return True def check(self, f): return check_file_magic(f, 'CXML') def dump(self, f=sys.stdout, depth=0): if self.root is None: return self.root.dump(f, depth) if len(sys.argv) < 3: print('error: insufficient options specified') sys.exit() cxml_file_path = sys.argv[1] if not os.path.isfile(cxml_file_path): print('error: invalid cxml file specified') sys.exit() xml_file_path = sys.argv[2] if os.path.exists(xml_file_path) and not os.path.isfile(xml_file_path): print('error: invalid xml file specified') sys.exit() cxml_file_base = os.path.splitext(cxml_file_path)[0] document = Document(cxml_file_base) with open(cxml_file_path, 'rb') as f: #if not document.check(f): # print 'error: invalid CXML file format' # sys.exit() document.load(f) with open(xml_file_path, 'wb') as f: write_raw(f, '\n') document.dump(f)