lyrical

ipf.py

Jan 14th, 2017
112
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.24 KB | None | 0 0
  1. #! /usr/bin/env python
  2.  
  3. import struct
  4. import sys
  5. import os
  6. import zlib
  7. import argparse
  8.  
  9. from binascii import crc32
  10.  
  11. SUPPORTED_FORMATS = (bytearray('\x50\x4b\x05\x06'),)
  12. UNCOMPRESSED_EXT = (".jpg", ".JPG", ".fsb", ".mp3")
  13.  
  14. class IpfInfo(object):
  15.     """
  16.    This class encapsulates information about a file entry in an IPF archive.
  17.  
  18.    Attributes:
  19.        filename: A string representing the path and name of the file.
  20.        archivename: The name of the originating IPF archive.
  21.        filename_length: Length of the filename.
  22.        archivename_length: Length of the archive name.
  23.        compressed_length: The length of the compressed file data.
  24.        uncompressed_length: The length of the uncompressed file data.
  25.        data_offset: Offset in the archive file where data for this file begins.
  26.    """
  27.  
  28.     def __init__(self, filename=None, archivename=None, datafile=None):
  29.         """
  30.        Inits IpfInfo class.
  31.        """
  32.         self._filename_length = 0
  33.         self._unknown1 = None
  34.         self._compressed_length = 0
  35.         self._uncompressed_length = 0
  36.         self._data_offset = 0
  37.         self._archivename_length = 0
  38.  
  39.         self._filename = filename
  40.         self._archivename = archivename
  41.         self.datafile = datafile
  42.  
  43.         if filename:
  44.             self._filename_length = len(filename)
  45.         if archivename:
  46.             self._archivename_length = len(archivename)
  47.  
  48.     @classmethod
  49.     def from_buffer(self, buf):
  50.         """
  51.        Creates IpfInfo instance from a data buffer.
  52.        """
  53.         info = IpfInfo()
  54.         data = struct.unpack('<HIIIIH', buf)
  55.  
  56.         info._filename_length = data[0]
  57.         info._crc = data[1]
  58.         info._compressed_length = data[2]
  59.         info._uncompressed_length = data[3]
  60.         info._data_offset = data[4]
  61.         info._archivename_length = data[5]
  62.         return info
  63.  
  64.     def to_buffer(self):
  65.         """
  66.        Creates a data buffer that represents this instance.
  67.        """
  68.         data = struct.pack('<HIIIIH', self.filename_length, self.crc, self.compressed_length, self.uncompressed_length, self.data_offset, self.archivename_length)
  69.         data += self.archivename
  70.         data += self.filename
  71.         return data
  72.  
  73.     @property
  74.     def filename(self):
  75.         return self._filename
  76.  
  77.     @property
  78.     def archivename(self):
  79.         return self._archivename
  80.  
  81.     @property
  82.     def filename_length(self):
  83.         return self._filename_length
  84.  
  85.     @property
  86.     def archivename_length(self):
  87.         return self._archivename_length
  88.  
  89.     @property
  90.     def compressed_length(self):
  91.         return self._compressed_length
  92.  
  93.     @property
  94.     def uncompressed_length(self):
  95.         return self._uncompressed_length
  96.  
  97.     @property
  98.     def data_offset(self):
  99.         return self._data_offset
  100.  
  101.     @property
  102.     def crc(self):
  103.         return self._crc
  104.  
  105.     @property
  106.     def key(self):
  107.         return '%s_%s' % (self.archivename.lower(), self.filename.lower())
  108.  
  109. class IpfArchive(object):
  110.     """
  111.    Class that represents an IPF archive file.
  112.    """
  113.  
  114.     def __init__(self, name, verbose=False, revision=0, base_revision=0):
  115.         """
  116.        Inits IpfArchive with a file `name`.
  117.  
  118.        Note: IpfArchive will immediately try to open the file. If it does not exist, an exception will be raised.
  119.        """
  120.         self.name = name
  121.         self.verbose = verbose
  122.         self.revision = revision
  123.         self.base_revision = base_revision
  124.         self.fullname = os.path.abspath(name)
  125.         _, self.archivename = os.path.split(self.name)
  126.        
  127.         self.file_handle = None
  128.         self.closed = True
  129.  
  130.         self._files = None
  131.  
  132.     @property
  133.     def files(self):
  134.         if self._files is None:
  135.             raise Exception('File has not been opened yet!')
  136.         return self._files    
  137.  
  138.     def close(self):
  139.         """
  140.        Closes all file handles if they are not already closed.
  141.        """
  142.         if self.closed:
  143.             return
  144.  
  145.         if self.file_handle.mode.startswith('w'):
  146.             self._write()
  147.  
  148.         if self.file_handle:
  149.             self.file_handle.close()
  150.         self.closed = True
  151.  
  152.     def open(self, mode='rb'):
  153.         if not self.closed:
  154.             return
  155.  
  156.         self.file_handle = open(self.name, mode)
  157.         self.closed = False
  158.         self._files = {}
  159.  
  160.         if mode.startswith('r'):
  161.             self._read()
  162.  
  163.     def _read(self):
  164.         self.file_handle.seek(-24, 2)
  165.         self._archive_header = self.file_handle.read(24)
  166.         self._file_size = self.file_handle.tell()
  167.  
  168.         self._archive_header_data = struct.unpack('<HIHI4sII', self._archive_header)
  169.         self.file_count = self._archive_header_data[0]
  170.         self._filetable_offset = self._archive_header_data[1]
  171.  
  172.         self._filefooter_offset = self._archive_header_data[3]
  173.         self._format = self._archive_header_data[4]
  174.         self.base_revision = self._archive_header_data[5]
  175.         self.revision = self._archive_header_data[6]
  176.  
  177.         if self._format not in SUPPORTED_FORMATS:
  178.             raise Exception('Unknown archive format: %s' % repr(self._format))
  179.  
  180.         # start reading file list
  181.         self.file_handle.seek(self._filetable_offset, 0)
  182.         for i in range(self.file_count):
  183.             buf = self.file_handle.read(20)
  184.             info = IpfInfo.from_buffer(buf)
  185.             info._archivename = self.file_handle.read(info.archivename_length)
  186.             info._filename = self.file_handle.read(info.filename_length)
  187.  
  188.             if info.key in self.files:
  189.                 # duplicate file name?!
  190.                 raise Exception('Duplicate file name: %s' % info.filename)
  191.  
  192.             self.files[info.key] = info
  193.  
  194.     def _write(self):
  195.         pos = 0
  196.         # write data entries first
  197.         for key in self.files:
  198.             fi = self.files[key]
  199.  
  200.             # read data
  201.             f = open(fi.datafile, 'rb')
  202.             data = f.read()
  203.             f.close()
  204.  
  205.             fi._crc = crc32(data) & 0xffffffff
  206.             fi._uncompressed_length = len(data)
  207.  
  208.             # check for extension
  209.             _, extension = os.path.splitext(fi.filename)
  210.             if extension in UNCOMPRESSED_EXT:
  211.                 # write data uncompressed
  212.                 self.file_handle.write(data)
  213.                 fi._compressed_length = fi.uncompressed_length
  214.             else:
  215.                 # compress data
  216.                 deflater = zlib.compressobj(6, zlib.DEFLATED, -15)
  217.                 compressed = deflater.compress(data)
  218.                 compressed += deflater.flush()
  219.                 self.file_handle.write(compressed)
  220.                 fi._compressed_length = len(compressed)
  221.                 deflater = None
  222.  
  223.             # update file info
  224.             fi._data_offset = pos
  225.             pos += fi.compressed_length
  226.  
  227.         self._filetable_offset = pos
  228.  
  229.         # write the file table
  230.         for key in self.files:
  231.             fi = self.files[key]
  232.             buf = fi.to_buffer()
  233.             self.file_handle.write(buf)
  234.             pos += len(buf)
  235.  
  236.         # write archive footer
  237.         buf = struct.pack('<HIHI4sII', len(self.files), self._filetable_offset, 0, pos, str(SUPPORTED_FORMATS[0]), self.base_revision, self.revision)
  238.         self.file_handle.write(buf)
  239.  
  240.     def get(self, filename, archive=None):
  241.         """
  242.        Retrieves the `IpfInfo` object for `filename`.
  243.  
  244.        Args:
  245.            filename: The name of the file.
  246.            archive: The name of the archive. Defaults to the current archive
  247.  
  248.        Returns:
  249.            An `IpfInfo` instance that describes the file entry.
  250.            If the file could not be found, None is returned.
  251.        """
  252.         if archive is None:
  253.             archive = self.archivename
  254.         key = '%s_%s' % (archive.lower(), filename.lower())
  255.         if key not in self.files:
  256.             return None
  257.         return self.files[key]
  258.  
  259.     def get_data(self, filename, archive=None):
  260.         """
  261.        Returns the uncompressed data of `filename` in the archive.
  262.  
  263.        Args:
  264.            filename: The name of the file.
  265.            archive: The name of the archive. Defaults to the current archive
  266.  
  267.        Returns:
  268.            A string of uncompressed data.
  269.            If the file could not be found, None is returned.
  270.        """
  271.         info = self.get(filename, archive)
  272.         if info is None:
  273.             return None
  274.         self.file_handle.seek(info.data_offset)
  275.         data = self.file_handle.read(info.compressed_length)
  276.         if info.compressed_length == info.uncompressed_length:
  277.             return data
  278.         return zlib.decompress(data, -15)
  279.  
  280.     def extract_all(self, output_dir, overwrite=False):
  281.         """
  282.        Extracts all files into a directory.
  283.  
  284.        Args:
  285.            output_dir: A string describing the output directory.
  286.        """
  287.         for filename in self.files:
  288.             info = self.files[filename]
  289.             output_file = os.path.join(output_dir, info.archivename, info.filename)
  290.  
  291.             if self.verbose:
  292.                 print('%s: %s' % (info.archivename, info.filename))
  293.  
  294.             # print output_file
  295.             # print info.__dict__
  296.             if not overwrite and os.path.isfile(output_file):
  297.                 continue
  298.             head, tail = os.path.split(output_file)
  299.             try:
  300.                 os.makedirs(head)
  301.             except os.error:
  302.                 pass
  303.  
  304.             f = open(output_file, 'wb')
  305.             try:
  306.                 data = self.get_data(info.filename, info.archivename)
  307.                 f.write(data)
  308.             except Exception, e:
  309.                 print('Could not unpack %s' % info.filename)
  310.                 print(info.__dict__)
  311.                 print(e)
  312.                 print(data)
  313.             f.close()
  314.  
  315.     def add(self, name, archive=None, newname=None):
  316.         if archive is None:
  317.             archive = self.archivename
  318.  
  319.         mode = 'Adding'
  320.         fi = IpfInfo(newname or name, archive, datafile=name)
  321.         if fi.key in self.files:
  322.             mode = 'Replacing'
  323.         if self.verbose:
  324.             print('%s %s: %s' % (mode, fi.archivename, fi.filename))
  325.         self.files[fi.key] = fi
  326.  
  327.  
  328. def print_meta(ipf, args):
  329.     print('{:<15}: {:}'.format('File count', ipf.file_count))
  330.     print('{:<15}: {:}'.format('First file', ipf._filetable_offset))
  331.     print('{:<15}: {:}'.format('Unknown', ipf._archive_header_data[2]))
  332.     print('{:<15}: {:}'.format('Archive header', ipf._filefooter_offset))
  333.     print('{:<15}: {:}'.format('Format', repr(ipf._format)))
  334.     print('{:<15}: {:}'.format('Base revision', ipf.base_revision))
  335.     print('{:<15}: {:}'.format('Revision', ipf.revision))
  336.  
  337. def print_list(ipf, args):
  338.     for k in ipf.files:
  339.         f = ipf.files[k]
  340.         print('%s _ %s' % (f.archivename, f.filename))
  341.  
  342.         # crc check
  343.         # data = ipf.get_data(k)
  344.         # print('%s / %s / %s' % (f.crc, crc32(data) & 0xffffffff, ''))
  345.  
  346. def get_norm_relpath(path, start):
  347.     newpath = os.path.normpath(os.path.relpath(path, args.target))
  348.     if newpath == '.':
  349.         return ''
  350.     return newpath
  351.  
  352. def create_archive(ipf, args):
  353.     if not args.target:
  354.         raise Exception('No target for --create specified')
  355.  
  356.     _, filename = os.path.split(ipf.name)
  357.  
  358.     if os.path.isdir(args.target):
  359.         for root, dirs, files in os.walk(args.target):
  360.             # strip target path
  361.             path = get_norm_relpath(root, args.target)
  362.  
  363.             # get archivename
  364.             archive = filename
  365.             components = path.split(os.path.sep)
  366.             if components[0].endswith('.ipf'):
  367.                 archive = components[0]
  368.  
  369.             if path.startswith(archive):
  370.                 path = path[len(archive) + 1:]
  371.            
  372.             for f in files:
  373.                 newname = '/'.join(path.replace('\\', '/').split('/')) + '/' + f
  374.                 ipf.add(os.path.join(root, f), archive=archive, newname=newname.strip('/'))
  375.  
  376.     elif os.path.isfile(args.target):
  377.         # TODO: Calculate relative path and stuff
  378.         ipf.add(args.target)
  379.     else:
  380.         raise Exception('Target for --create not found')
  381.  
  382. if __name__ == '__main__':
  383.     parser = argparse.ArgumentParser()
  384.  
  385.     # functions
  386.     parser.add_argument('-t', '--list', action='store_true', help='list the contents of an archive')
  387.     parser.add_argument('-x', '--extract', action='store_true', help='extract files from an archive')
  388.     parser.add_argument('-m', '--meta', action='store_true', help='show meta information of an archive')
  389.     parser.add_argument('-c', '--create', action='store_true', help='create archive from target')
  390.     # options
  391.     parser.add_argument('-f', '--file', help='use archive file')
  392.     parser.add_argument('-v', '--verbose', action='store_true', help='verbosely list files processed')
  393.     parser.add_argument('-C', '--directory', metavar='DIR', help='change directory to DIR')
  394.     parser.add_argument('-r', '--revision', type=int, help='revision number for the archive')
  395.     parser.add_argument('-b', '--base-revision', type=int, help='base revision number for the archive')
  396.  
  397.     parser.add_argument('target', nargs='?', help='target file/directory to be extracted or packed')
  398.  
  399.     args = parser.parse_args()
  400.  
  401.     if args.list and args.extract:
  402.         parser.print_help()
  403.         print('You can only use one function!')
  404.     elif not any([args.list, args.extract, args.meta, args.create]):
  405.         parser.print_help()
  406.         print('Please specify a function!')
  407.     else:
  408.         if not args.file:
  409.             parser.print_help()
  410.             print('Please specify a file!')
  411.         else:
  412.             ipf = IpfArchive(args.file, verbose=args.verbose)
  413.  
  414.             if not args.create:
  415.                 ipf.open()
  416.             else:
  417.                 ipf.open('wb')
  418.  
  419.             if args.revision:
  420.                 ipf.revision = args.revision
  421.             if args.base_revision:
  422.                 ipf.base_revision = args.base_revision
  423.  
  424.             if args.meta:
  425.                 print_meta(ipf, args)
  426.  
  427.             if args.list:
  428.                 print_list(ipf, args)
  429.             elif args.extract:
  430.                 ipf.extract_all(args.directory or '.')
  431.             elif args.create:
  432.                 create_archive(ipf, args)
  433.  
  434.             ipf.close()
Advertisement
Add Comment
Please, Sign In to add comment