| Index: xarfile.py | 
| =================================================================== | 
| new file mode 100644 | 
| --- /dev/null | 
| +++ b/xarfile.py | 
| @@ -0,0 +1,150 @@ | 
| +# This Source Code Form is subject to the terms of the Mozilla Public | 
| +# License, v. 2.0. If a copy of the MPL was not distributed with this | 
| +# file, You can obtain one at http://mozilla.org/MPL/2.0/. | 
| + | 
| +import base64 | 
| +import re | 
| +import struct | 
| +import time | 
| +from xml.etree import ElementTree | 
| +import zlib | 
| + | 
| +from Crypto.Hash import SHA | 
| +from Crypto.PublicKey import RSA | 
| +from Crypto.Signature import PKCS1_v1_5 | 
| + | 
| +XAR_HEADER_MAGIC = 0x78617221 | 
| +XAR_HEADER_SIZE = 28 | 
| +XAR_VERSION = 1 | 
| +XAR_CKSUM_SHA1 = 1 | 
| + | 
| +def read_key(keyfile): | 
| + with open(keyfile, 'r') as file: | 
| + data = file.read() | 
| + data = re.sub(r'(-+END PRIVATE KEY-+).*', r'\1', data, flags=re.S) | 
| + return RSA.importKey(data) | 
| + | 
| +def read_certificates(keyfile): | 
| + certs = [] | 
| + with open(keyfile, 'r') as file: | 
| + data = file.read() | 
| + for match in re.finditer(r'-+BEGIN CERTIFICATE-+(.*?)-+END CERTIFICATE-+', data, re.S): | 
| + certs.append(base64.b64decode(match.group(1))) | 
| + return certs | 
| + | 
| +def get_checksum(data): | 
| + return SHA.new(data).digest() | 
| + | 
| +def get_hexchecksum(data): | 
| + return SHA.new(data).hexdigest() | 
| + | 
| +def get_signature(key, data): | 
| + return PKCS1_v1_5.new(key).sign(SHA.new(data)) | 
| + | 
| +def compress_files(filedata, root, offset): | 
| + files = [] | 
| + filedata = sorted(filedata) | 
| + directory_stack = [{'path': '', 'element': root}] | 
| + file_id = 1 | 
| + for path, data in filedata: | 
| + # Remove directories that are done | 
| + while True: | 
| + directory = directory_stack[-1] | 
| + directory_path = directory['path'] | 
| + if path.startswith(directory_path): | 
| + break | 
| + directory_stack.pop() | 
| + | 
| + # Add new directories | 
| + relpath = path[len(directory_path):] | 
| + while '/' in relpath: | 
| + directory_name, relpath = relpath.split('/', 1) | 
| + directory_path += directory_name + '/' | 
| + element = ElementTree.SubElement(directory['element'], 'file') | 
| + directory = { | 
| + 'path': directory_path, | 
| + 'element': element, | 
| + } | 
| + element.set('id', str(file_id)) | 
| + file_id += 1 | 
| + ElementTree.SubElement(element, 'name').text = directory_name | 
| + ElementTree.SubElement(element, 'type').text = 'directory' | 
| + ElementTree.SubElement(element, 'mode').text = '0755' | 
| + directory_stack.append(directory) | 
| + | 
| + # Add the actual file | 
| + element = ElementTree.SubElement(directory['element'], 'file') | 
| + element.set('id', str(file_id)) | 
| + file_id += 1 | 
| + ElementTree.SubElement(element, 'name').text = relpath | 
| + ElementTree.SubElement(element, 'type').text = 'file' | 
| + ElementTree.SubElement(element, 'mode').text = '0644' | 
| + | 
| + datatag = ElementTree.SubElement(element, 'data') | 
| + ElementTree.SubElement(datatag, 'extracted-checksum', {'style': 'sha1'}).text = get_hexchecksum(data) | 
| + ElementTree.SubElement(datatag, 'size').text = str(len(data)) | 
| + | 
| + compressed = zlib.compress(data, 9) | 
| + ElementTree.SubElement(datatag, 'encoding', {'style': 'application/x-gzip'}) | 
| + ElementTree.SubElement(datatag, 'archived-checksum', {'style': 'sha1'}).text = get_hexchecksum(compressed) | 
| + ElementTree.SubElement(datatag, 'offset').text = str(offset) | 
| + ElementTree.SubElement(datatag, 'length').text = str(len(compressed)) | 
| + offset += len(compressed) | 
| + | 
| + files.append(compressed) | 
| + return files | 
| + | 
| +def create(archivepath, contents, keyfile): | 
| + key = read_key(keyfile) | 
| + certs = read_certificates(keyfile) | 
| + | 
| + root = ElementTree.Element('xar') | 
| + toc = ElementTree.SubElement(root, 'toc') | 
| + | 
| + creation_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) | 
| + ElementTree.SubElement(toc, 'creation-time').text = creation_time | 
| + | 
| + # Timestamp epoch starts at 2001-01-01T00:00:00.000Z | 
| + sign_time = str(time.time() - 978307200) | 
| + ElementTree.SubElement(toc, 'signature-creation-time').text = sign_time | 
| + | 
| + offset = 0 | 
| + | 
| + checksum_size = len(get_checksum('')) | 
| + checksum = ElementTree.SubElement(toc, 'checksum', {'style': 'sha1'}) | 
| + ElementTree.SubElement(checksum, 'offset').text = str(offset) | 
| + ElementTree.SubElement(checksum, 'size').text = str(checksum_size) | 
| + offset += checksum_size | 
| + | 
| + signature_size = len(get_signature(key, '')) | 
| + signature = ElementTree.SubElement(toc, 'signature', {'style': 'RSA'}) | 
| + ElementTree.SubElement(signature, 'offset').text = str(offset) | 
| + ElementTree.SubElement(signature, 'size').text = str(signature_size) | 
| + offset += signature_size | 
| + | 
| + keyinfo = ElementTree.SubElement(signature, 'KeyInfo') | 
| + keyinfo.set('xmlns', 'http://www.w3.org/2000/09/xmldsig#') | 
| + x509data = ElementTree.SubElement(keyinfo, 'X509Data') | 
| + for cert in certs: | 
| + ElementTree.SubElement(x509data, 'X509Certificate').text = base64.b64encode(cert) | 
| + | 
| + files = compress_files(contents.iteritems(), toc, offset) | 
| + | 
| + toc_uncompressed = ElementTree.tostring(root).encode('utf-8') | 
| + toc_compressed = zlib.compress(toc_uncompressed, 9) | 
| + | 
| + with open(archivepath, 'wb') as file: | 
| + # The file starts with a minimalistic header | 
| + header = struct.pack('>IHHQQI', XAR_HEADER_MAGIC, XAR_HEADER_SIZE, | 
| + XAR_VERSION, len(toc_compressed), len(toc_uncompressed), | 
| + XAR_CKSUM_SHA1) | 
| + file.write(header) | 
| + | 
| + # It's followed up with a compressed XML table of contents | 
| + file.write(toc_compressed) | 
| + | 
| + # Now the actual data, all the offsets are in the table of contents | 
| + file.write(get_checksum(toc_compressed)) | 
| + file.write(get_signature(key, toc_compressed)) | 
| + for compressed in files: | 
| + file.write(compressed) |