diff --git a/sauce.py b/sauce.py new file mode 100644 index 0000000..00a6408 --- /dev/null +++ b/sauce.py @@ -0,0 +1,510 @@ +#! /usr/bin/env python +# +# _______ +# ____________ _______ _\__ /_________ ___ _____ +# | _ _ \ _ | ____\ _ / | |/ _ \ +# | / / / / | | | /___/ _ | | / / +# |___/___/ /___/____|________|___ | |_| |___|_____/ +# \__/ |___| +# +# (c) 2006-2012 Wijnand Modderman-Lenstra - https://maze.io/ +# + +''' +Parser for SAUCE or Standard Architecture for Universal Comment Extensions. +''' + +__author__ = 'Wijnand Modderman-Lenstra ' +__copyright__ = '(C) 2006-2012 Wijnand Modderman-Lenstra' +__license__ = 'LGPL' +__version__ = '1.2' +__url__ = 'https://github.com/tehmaze/sauce' + +import datetime +import os +import struct +try: + from io import StringIO +except ImportError: + from io import StringIO + + +class SAUCE(object): + ''' + Parser for SAUCE or Standard Architecture for Universal Comment Extensions, + as defined in http://www.acid.org/info/sauce/s_spec.htm. + + :param filename: file name or file handle + :property author: Name or 'handle' of the creator of the file + :property datatype: Type of data + :property date: Date the file was created + :property filesize: Original filesize NOT including any information of + SAUCE + :property group: Name of the group/company the creator is employed by + :property title: Title of the file + + Example:: + + >>> art = open('31337.ANS', 'rb') + >>> nfo = sauce.SAUCE(art) + >>> nfo.author + 'maze' + ... + >>> nfo.group + '' + >>> nfo.group = 'mononoke' + >>> raw = str(nfo) + + Saving the new file:: + + >>> sav = open('31337.NEW', 'wb') + >>> nfo.write(sav) + >>> # OR you can do: + >>> sav = nfo.write('31337.NEW') + + ''' + + # template + template = ( + # name default size type + ('SAUCE', 'SAUCE', 5, '5s'), + ('SAUCEVersion', '00', 2, '2s'), + ('Title', '\x00' * 35, 35, '35s'), + ('Author', '\x00' * 20, 20, '20s'), + ('Group', '\x00' * 20, 20, '20s'), + ('Date', '\x00' * 8, 8, '8s'), + ('FileSize', [0], 4, 'I'), + ('DataType', [0], 1, 'B'), + ('FileType', [0], 1, 'B'), + ('TInfo1', [0], 2, 'H'), + ('TInfo2', [0], 2, 'H'), + ('TInfo3', [0], 2, 'H'), + ('TInfo4', [0], 2, 'H'), + ('Comments', [0], 1, 'B'), + ('Flags', [0], 1, 'B'), + ('Filler', ['\x00'] * 22, 22, '22c'), + ) + templates = [t[0] for t in template] + datatypes = ['None', 'Character', 'Graphics', 'Vector', 'Sound', + 'BinaryText', 'XBin', 'Archive', 'Executable'] + filetypes = { + 'None': { + 'filetype': ['Undefined'], + }, + 'Character': { + 'filetype': ['ASCII', 'ANSi', 'ANSiMation', 'RIP', 'PCBoard', + 'Avatar', 'HTML', 'Source'], + 'flags': {0: 'None', 1: 'iCE Color'}, + 'tinfo': ( + ('width', 'height', None, None), + ('width', 'height', None, None), + ('width', 'height', None, None), + ('width', 'height', 'colors', None), + ('width', 'height', None, None), + ('width', 'height', None, None), + (None, None, None, None), + ), + }, + 'Graphics': { + 'filetype': ['GIF', 'PCX', 'LBM/IFF', 'TGA', 'FLI', 'FLC', + 'BMP', 'GL', 'DL', 'WPG', 'PNG', 'JPG', 'MPG', + 'AVI'], + 'tinfo': (('width', 'height', 'bpp')) * 14, + }, + 'Vector': { + 'filetype': ['DX', 'DWG', 'WPG', '3DS'], + }, + 'Sound': { + 'filetype': ['MOD', '669', 'STM', 'S3M', 'MTM', 'FAR', 'ULT', + 'AMF', 'DMF', 'OKT', 'ROL', 'CMF', 'MIDI', 'SADT', + 'VOC', 'WAV', 'SMP8', 'SMP8S', 'SMP16', 'SMP16S', + 'PATCH8', 'PATCH16', 'XM', 'HSC', 'IT'], + 'tinfo': ((None,)) * 16 + (('Sampling Rate',)) * 4, + }, + 'BinaryText': { + 'flags': {0: 'None', 1: 'iCE Color'}, + }, + 'XBin': { + 'tinfo': (('width', 'height'),), + }, + 'Archive': { + 'filetype': ['ZIP', 'ARJ', 'LZH', 'ARC', 'TAR', 'ZOO', 'RAR', + 'UC2', 'PAK', 'SQZ'], + }, + } + + def __init__(self, filename='', data=''): + assert (filename or data), 'Need either filename or record' + + if filename: + # if type(filename) == file: + # self.filehand = filename + # else: + self.filehand = open(filename, 'rb') + self._size = os.path.getsize(self.filehand.name) + else: + self._size = len(data) + self.filehand = StringIO(data) + + self.record, self.data = self._read() + + def __str__(self): + return ''.join(list(self._read_file())) + + def _read_file(self): + # Buffered reader (generator), reads the original file without SAUCE + # record. + self.filehand.seek(0) + # Check if we have SAUCE data + if self.record: + reads, rest = divmod(self._size - 128, 1024) + else: + reads, rest = divmod(self._size, 1024) + for x in range(0, reads): + yield self.filehand.read(1024) + if rest: + yield self.filehand.read(rest) + + def _read(self): + if self._size >= 128: + self.filehand.seek(self._size - 128) + record = self.filehand.read(128) + if record.startswith(b'SAUCE'): + self.filehand.seek(0) + return record, self.filehand.read(self._size - 128) + + self.filehand.seek(0) + return None, self.filehand.read() + + def _gets(self, key): + if self.record is None: + return None + + name, default, offset, size, stype = self._template(key) + data = self.record[offset:offset + size] + data = struct.unpack(stype, data) + if stype[-1] in 'cs': + # return ''.join(data) + return data[0].decode() + elif stype[-1] in 'BI' and len(stype) == 1: + return data[0] + else: + return data + + def _puts(self, key, data): + name, default, offset, size, stype = self._template(key) + #print offset, size, data, repr(struct.pack(stype, data)) + if self.record is None: + self.record = self.sauce() + self.record = ''.join([ + self.record[:offset], + struct.pack(stype, data), + self.record[offset + size:] + ]) + return self.record + + def _template(self, key): + index = self.templates.index(key) + name, default, size, stype = self.template[index] + offset = sum([self.template[x][2] for x in range(0, index)]) + return name, default, offset, size, stype + + def sauce(self): + ''' + Get the raw SAUCE record. + ''' + if self.record: + return self.record + else: + data = 'SAUCE' + for name, default, size, stype in self.template[1:]: + #print stype, default + if stype[-1] in 's': + data += struct.pack(stype, default) + else: + data += struct.pack(stype, *default) + return data + + def write(self, filename): + ''' + Save the file including SAUCE data to the given file(handle). + ''' + filename = type(filename) == file and filename or open( + filename, 'wb') + for part in self._read_file(): + filename.write(part) + filename.write(self.sauce()) + return filename + + # SAUCE meta data + + def get_author(self): + astr = self._gets('Author') + if astr is not None: + return astr.strip() + else: + return '' + + def set_author(self, author): + self._puts('Author', author) + return self + + def get_comments(self): + return self._gets('Comments') + + def set_comments(self, comments): + self._puts('Comments', comments) + return self + + def get_datatype(self): + return self._gets('DataType') + + def get_datatype_str(self): + datatype = self.datatype + if datatype is None: + return None + if datatype < len(self.datatypes): + return self.datatypes[datatype] + else: + return None + + def set_datatype(self, datatype): + if type(datatype) == str: + datatype = datatype.lower().title() # fOoBAR -> Foobar + datatype = self.datatypes.index(datatype) + self._puts('DataType', datatype) + return self + + def get_date(self): + return self._gets('Date') + + def get_date_str(self, format='%Y%m%d'): + return datetime.datetime.strptime(self.date, format) + + def set_date(self, date=None, format='%Y%m%d'): + if date is None: + date = datetime.datetime.now().strftime(format) + elif type(date) in [datetime.date, datetime.datetime]: + date = date.strftime(format) + elif type(date) in [int, int, float]: + date = datetime.datetime.fromtimestamp(date).strftime(format) + self._puts('Date', date) + return self + + def get_filesize(self): + return self._gets('FileSize') + + def set_filesize(self, size): + self._puts('FileSize', size) + + def get_filler(self): + return self._gets('Filler') + + def get_filler_str(self): + filler = self._gets('Filler') + if filler is None: + return '' + else: + return filler.rstrip('\x00') + + def get_filetype(self): + return self._gets('FileType') + + def get_filetype_str(self): + datatype = self.datatype_str + filetype = self.filetype + + if datatype is None or filetype is None: + return None + + if datatype in self.filetypes and \ + 'filetype' in self.filetypes[datatype] and \ + filetype < len(self.filetypes[datatype]['filetype']): + return self.filetypes[datatype]['filetype'][filetype] + else: + return None + + def set_filetype(self, filetype): + datatype = self.datatype_str + if type(filetype) == str: + filetype = filetype.lower().title() # fOoBAR -> Foobar + filetype = [name.lower().title() + for name in self.filetypes[datatype]['filetype']].index(filetype) + self._puts('FileType', filetype) + return self + + def get_flags(self): + return self._gets('Flags') + + def set_flags(self, flags): + self._puts('Flags', flags) + return self + + def get_flags_str(self): + datatype = self.datatype_str + filetype = self.filetype + + if datatype is None or filetype is None: + return None + + if datatype in self.filetypes and \ + 'flags' in self.filetypes[datatype] and \ + filetype < len(self.filetypes[datatype]['filetype']): + return self.filetypes[datatype]['filetype'][filetype] + else: + return None + + def get_group(self): + gstr = self._gets('Group') + if gstr is not None: + return gstr.strip() + else: + return '' +# return self._gets('Group').strip() + + def set_group(self, group): + self._puts('Group', group) + return self + + def _get_tinfo_name(self, i): + datatype = self.datatype_str + filetype = self.filetype + + if datatype is None or filetype is None: + return None + + try: + return self.filetypes[datatype]['tinfo'][filetype][i - 1] + except (KeyError, IndexError): + return '' + + def get_tinfo1(self): + tinfo = self._gets('TInfo1') + if tinfo is not None: + return tinfo[0] + else: + return '' + + def get_tinfo1_name(self): + return self._get_tinfo_name(1) + + def set_tinfo1(self, tinfo): + self._puts('TInfo1', tinfo) + return self + + def get_tinfo2(self): + tinfo = self._gets('TInfo2') + if tinfo is not None: + return tinfo[0] + else: + return '' + + def get_tinfo2_name(self): + return self._get_tinfo_name(2) + + def set_tinfo2(self, tinfo): + self._puts('TInfo2', tinfo) + return self + + def get_tinfo3(self): + tinfo = self._gets('TInfo3') + if tinfo is not None: + return tinfo[0] + return '' + + def get_tinfo3_name(self): + return self._get_tinfo_name(3) + + def set_tinfo3(self, tinfo): + self._puts('TInfo3', tinfo) + return self + + def get_tinfo4(self): + tinfo = self._gets('TInfo4') + if tinfo is not None: + return tinfo[0] + return '' + + def get_tinfo4_name(self): + return self._get_tinfo_name(4) + + def set_tinfo4(self, tinfo): + self._puts('TInfo4', tinfo) + return self + + def get_title(self): + tstr = self._gets('Title') + if tstr is not None: + return tstr.strip() + else: + return '' +# return self._gets('Title').strip() + + def set_title(self, title): + self._puts('Title', title) + return self + + def get_version(self): + return self._gets('SAUCEVersion') + + def set_version(self, version): + self._puts('SAUCEVersion', version) + return self + + # properties + author = property(get_author, set_author) + comments = property(get_comments, set_comments) + datatype = property(get_datatype, set_datatype) + datatype_str = property(get_datatype_str) + date = property(get_date, set_date) + filesize = property(get_filesize, set_filesize) + filetype = property(get_filetype, set_filetype) + filetype_str = property(get_filetype_str) + filler = property(get_filler) + filler_str = property(get_filler_str) + flags = property(get_flags, set_flags) + flags_str = property(get_flags_str) + group = property(get_group, set_group) + tinfo1 = property(get_tinfo1, set_tinfo1) + tinfo1_name = property(get_tinfo1_name) + tinfo2 = property(get_tinfo2, set_tinfo2) + tinfo2_name = property(get_tinfo2_name) + tinfo3 = property(get_tinfo3, set_tinfo3) + tinfo3_name = property(get_tinfo3_name) + tinfo4 = property(get_tinfo4, set_tinfo4) + tinfo4_name = property(get_tinfo4_name) + title = property(get_title, set_title) + version = property(get_version) + + +if __name__ == '__main__': + import sys + if len(sys.argv) != 2: + print('%s ' % (sys.argv[0],), file=sys.stderr) + sys.exit(1) + else: + test = SAUCE(sys.argv[1]) + + def show(sauce): + print('Version.:', sauce.version) + print('Title...:', sauce.title) + print('Author..:', sauce.author) + print('Group...:', sauce.group) + print('Date....:', sauce.date) + print('FileSize:', sauce.filesize) + print('DataType:', sauce.datatype, sauce.datatype_str) + print('FileType:', sauce.filetype, sauce.filetype_str) + print('TInfo1..:', sauce.tinfo1) + print('TInfo2..:', sauce.tinfo2) + print('TInfo3..:', sauce.tinfo3) + print('TInfo4..:', sauce.tinfo4) + print('Flags...:', sauce.flags, sauce.flags_str) + print('Record..:', len(sauce.record), repr(sauce.record)) + print('Filler..:', sauce.filler_str) + + if test.record: + show(test) + else: + print('No SAUCE record found') + test = SAUCE(data=test.sauce()) + show(test)