| """binhex - Macintosh binhex compression/decompression |
| |
| easy interface: |
| binhex(inputfilename, outputfilename) |
| hexbin(inputfilename, outputfilename) |
| """ |
| |
| # |
| # Jack Jansen, CWI, August 1995. |
| # |
| # The module is supposed to be as compatible as possible. Especially the |
| # easy interface should work "as expected" on any platform. |
| # XXXX Note: currently, textfiles appear in mac-form on all platforms. |
| # We seem to lack a simple character-translate in python. |
| # (we should probably use ISO-Latin-1 on all but the mac platform). |
| # XXXX The simeple routines are too simple: they expect to hold the complete |
| # files in-core. Should be fixed. |
| # XXXX It would be nice to handle AppleDouble format on unix |
| # (for servers serving macs). |
| # XXXX I don't understand what happens when you get 0x90 times the same byte on |
| # input. The resulting code (xx 90 90) would appear to be interpreted as an |
| # escaped *value* of 0x90. All coders I've seen appear to ignore this nicety... |
| # |
| import sys |
| import os |
| import struct |
| import string |
| import binascii |
| |
| Error = 'binhex.Error' |
| |
| # States (what have we written) |
| [_DID_HEADER, _DID_DATA, _DID_RSRC] = range(3) |
| |
| # Various constants |
| REASONABLY_LARGE=32768 # Minimal amount we pass the rle-coder |
| LINELEN=64 |
| RUNCHAR=chr(0x90) # run-length introducer |
| |
| # |
| # This code is no longer byte-order dependent |
| |
| # |
| # Workarounds for non-mac machines. |
| if os.name == 'mac': |
| import macfs |
| import MacOS |
| try: |
| openrf = MacOS.openrf |
| except AttributeError: |
| # Backward compatability |
| openrf = open |
| |
| def FInfo(): |
| return macfs.FInfo() |
| |
| def getfileinfo(name): |
| finfo = macfs.FSSpec(name).GetFInfo() |
| dir, file = os.path.split(name) |
| # XXXX Get resource/data sizes |
| fp = open(name, 'rb') |
| fp.seek(0, 2) |
| dlen = fp.tell() |
| fp = openrf(name, '*rb') |
| fp.seek(0, 2) |
| rlen = fp.tell() |
| return file, finfo, dlen, rlen |
| |
| def openrsrc(name, *mode): |
| if not mode: |
| mode = '*rb' |
| else: |
| mode = '*' + mode[0] |
| return openrf(name, mode) |
| |
| else: |
| # |
| # Glue code for non-macintosh useage |
| # |
| |
| class FInfo: |
| def __init__(self): |
| self.Type = '????' |
| self.Creator = '????' |
| self.Flags = 0 |
| |
| def getfileinfo(name): |
| finfo = FInfo() |
| # Quick check for textfile |
| fp = open(name) |
| data = open(name).read(256) |
| for c in data: |
| if not c in string.whitespace \ |
| and (c<' ' or ord(c) > 0177): |
| break |
| else: |
| finfo.Type = 'TEXT' |
| fp.seek(0, 2) |
| dsize = fp.tell() |
| fp.close() |
| dir, file = os.path.split(name) |
| file = string.replace(file, ':', '-', 1) |
| return file, finfo, dsize, 0 |
| |
| class openrsrc: |
| def __init__(self, *args): |
| pass |
| |
| def read(self, *args): |
| return '' |
| |
| def write(self, *args): |
| pass |
| |
| def close(self): |
| pass |
| |
| class _Hqxcoderengine: |
| """Write data to the coder in 3-byte chunks""" |
| |
| def __init__(self, ofp): |
| self.ofp = ofp |
| self.data = '' |
| self.hqxdata = '' |
| self.linelen = LINELEN-1 |
| |
| def write(self, data): |
| self.data = self.data + data |
| datalen = len(self.data) |
| todo = (datalen/3)*3 |
| data = self.data[:todo] |
| self.data = self.data[todo:] |
| if not data: |
| return |
| self.hqxdata = self.hqxdata + binascii.b2a_hqx(data) |
| self._flush(0) |
| |
| def _flush(self, force): |
| first = 0 |
| while first <= len(self.hqxdata)-self.linelen: |
| last = first + self.linelen |
| self.ofp.write(self.hqxdata[first:last]+'\n') |
| self.linelen = LINELEN |
| first = last |
| self.hqxdata = self.hqxdata[first:] |
| if force: |
| self.ofp.write(self.hqxdata + ':\n') |
| |
| def close(self): |
| if self.data: |
| self.hqxdata = \ |
| self.hqxdata + binascii.b2a_hqx(self.data) |
| self._flush(1) |
| self.ofp.close() |
| del self.ofp |
| |
| class _Rlecoderengine: |
| """Write data to the RLE-coder in suitably large chunks""" |
| |
| def __init__(self, ofp): |
| self.ofp = ofp |
| self.data = '' |
| |
| def write(self, data): |
| self.data = self.data + data |
| if len(self.data) < REASONABLY_LARGE: |
| return |
| rledata = binascii.rlecode_hqx(self.data) |
| self.ofp.write(rledata) |
| self.data = '' |
| |
| def close(self): |
| if self.data: |
| rledata = binascii.rlecode_hqx(self.data) |
| self.ofp.write(rledata) |
| self.ofp.close() |
| del self.ofp |
| |
| class BinHex: |
| def __init__(self, (name, finfo, dlen, rlen), ofp): |
| if type(ofp) == type(''): |
| ofname = ofp |
| ofp = open(ofname, 'w') |
| if os.name == 'mac': |
| fss = macfs.FSSpec(ofname) |
| fss.SetCreatorType('BnHq', 'TEXT') |
| ofp.write('(This file must be converted with BinHex 4.0)\n\n:') |
| hqxer = _Hqxcoderengine(ofp) |
| self.ofp = _Rlecoderengine(hqxer) |
| self.crc = 0 |
| if finfo == None: |
| finfo = FInfo() |
| self.dlen = dlen |
| self.rlen = rlen |
| self._writeinfo(name, finfo) |
| self.state = _DID_HEADER |
| |
| def _writeinfo(self, name, finfo): |
| name = name |
| nl = len(name) |
| if nl > 63: |
| raise Error, 'Filename too long' |
| d = chr(nl) + name + '\0' |
| d2 = finfo.Type + finfo.Creator |
| |
| # Force all structs to be packed with big-endian |
| d3 = struct.pack('>h', finfo.Flags) |
| d4 = struct.pack('>ii', self.dlen, self.rlen) |
| info = d + d2 + d3 + d4 |
| self._write(info) |
| self._writecrc() |
| |
| def _write(self, data): |
| self.crc = binascii.crc_hqx(data, self.crc) |
| self.ofp.write(data) |
| |
| def _writecrc(self): |
| # XXXX Should this be here?? |
| # self.crc = binascii.crc_hqx('\0\0', self.crc) |
| self.ofp.write(struct.pack('>h', self.crc)) |
| self.crc = 0 |
| |
| def write(self, data): |
| if self.state != _DID_HEADER: |
| raise Error, 'Writing data at the wrong time' |
| self.dlen = self.dlen - len(data) |
| self._write(data) |
| |
| def close_data(self): |
| if self.dlen <> 0: |
| raise Error, 'Incorrect data size, diff='+`self.rlen` |
| self._writecrc() |
| self.state = _DID_DATA |
| |
| def write_rsrc(self, data): |
| if self.state < _DID_DATA: |
| self.close_data() |
| if self.state != _DID_DATA: |
| raise Error, 'Writing resource data at the wrong time' |
| self.rlen = self.rlen - len(data) |
| self._write(data) |
| |
| def close(self): |
| if self.state < _DID_DATA: |
| self.close_data() |
| if self.state != _DID_DATA: |
| raise Error, 'Close at the wrong time' |
| if self.rlen <> 0: |
| raise Error, \ |
| "Incorrect resource-datasize, diff="+`self.rlen` |
| self._writecrc() |
| self.ofp.close() |
| self.state = None |
| del self.ofp |
| |
| def binhex(inp, out): |
| """(infilename, outfilename) - Create binhex-encoded copy of a file""" |
| finfo = getfileinfo(inp) |
| ofp = BinHex(finfo, out) |
| |
| ifp = open(inp, 'rb') |
| # XXXX Do textfile translation on non-mac systems |
| while 1: |
| d = ifp.read(128000) |
| if not d: break |
| ofp.write(d) |
| ofp.close_data() |
| ifp.close() |
| |
| ifp = openrsrc(inp, 'rb') |
| while 1: |
| d = ifp.read(128000) |
| if not d: break |
| ofp.write_rsrc(d) |
| ofp.close() |
| ifp.close() |
| |
| class _Hqxdecoderengine: |
| """Read data via the decoder in 4-byte chunks""" |
| |
| def __init__(self, ifp): |
| self.ifp = ifp |
| self.eof = 0 |
| |
| def read(self, totalwtd): |
| """Read at least wtd bytes (or until EOF)""" |
| decdata = '' |
| wtd = totalwtd |
| # |
| # The loop here is convoluted, since we don't really now how |
| # much to decode: there may be newlines in the incoming data. |
| while wtd > 0: |
| if self.eof: return decdata |
| wtd = ((wtd+2)/3)*4 |
| data = self.ifp.read(wtd) |
| # |
| # Next problem: there may not be a complete number of |
| # bytes in what we pass to a2b. Solve by yet another |
| # loop. |
| # |
| while 1: |
| try: |
| decdatacur, self.eof = \ |
| binascii.a2b_hqx(data) |
| break |
| except binascii.Incomplete: |
| pass |
| newdata = self.ifp.read(1) |
| if not newdata: |
| raise Error, \ |
| 'Premature EOF on binhex file' |
| data = data + newdata |
| decdata = decdata + decdatacur |
| wtd = totalwtd - len(decdata) |
| if not decdata and not self.eof: |
| raise Error, 'Premature EOF on binhex file' |
| return decdata |
| |
| def close(self): |
| self.ifp.close() |
| |
| class _Rledecoderengine: |
| """Read data via the RLE-coder""" |
| |
| def __init__(self, ifp): |
| self.ifp = ifp |
| self.pre_buffer = '' |
| self.post_buffer = '' |
| self.eof = 0 |
| |
| def read(self, wtd): |
| if wtd > len(self.post_buffer): |
| self._fill(wtd-len(self.post_buffer)) |
| rv = self.post_buffer[:wtd] |
| self.post_buffer = self.post_buffer[wtd:] |
| return rv |
| |
| def _fill(self, wtd): |
| self.pre_buffer = self.pre_buffer + self.ifp.read(wtd+4) |
| if self.ifp.eof: |
| self.post_buffer = self.post_buffer + \ |
| binascii.rledecode_hqx(self.pre_buffer) |
| self.pre_buffer = '' |
| return |
| |
| # |
| # Obfuscated code ahead. We have to take care that we don't |
| # end up with an orphaned RUNCHAR later on. So, we keep a couple |
| # of bytes in the buffer, depending on what the end of |
| # the buffer looks like: |
| # '\220\0\220' - Keep 3 bytes: repeated \220 (escaped as \220\0) |
| # '?\220' - Keep 2 bytes: repeated something-else |
| # '\220\0' - Escaped \220: Keep 2 bytes. |
| # '?\220?' - Complete repeat sequence: decode all |
| # otherwise: keep 1 byte. |
| # |
| mark = len(self.pre_buffer) |
| if self.pre_buffer[-3:] == RUNCHAR + '\0' + RUNCHAR: |
| mark = mark - 3 |
| elif self.pre_buffer[-1] == RUNCHAR: |
| mark = mark - 2 |
| elif self.pre_buffer[-2:] == RUNCHAR + '\0': |
| mark = mark - 2 |
| elif self.pre_buffer[-2] == RUNCHAR: |
| pass # Decode all |
| else: |
| mark = mark - 1 |
| |
| self.post_buffer = self.post_buffer + \ |
| binascii.rledecode_hqx(self.pre_buffer[:mark]) |
| self.pre_buffer = self.pre_buffer[mark:] |
| |
| def close(self): |
| self.ifp.close() |
| |
| class HexBin: |
| def __init__(self, ifp): |
| if type(ifp) == type(''): |
| ifp = open(ifp) |
| # |
| # Find initial colon. |
| # |
| while 1: |
| ch = ifp.read(1) |
| if not ch: |
| raise Error, "No binhex data found" |
| # Cater for \r\n terminated lines (which show up as \n\r, hence |
| # all lines start with \r) |
| if ch == '\r': |
| continue |
| if ch == ':': |
| break |
| if ch != '\n': |
| dummy = ifp.readline() |
| |
| hqxifp = _Hqxdecoderengine(ifp) |
| self.ifp = _Rledecoderengine(hqxifp) |
| self.crc = 0 |
| self._readheader() |
| |
| def _read(self, len): |
| data = self.ifp.read(len) |
| self.crc = binascii.crc_hqx(data, self.crc) |
| return data |
| |
| def _checkcrc(self): |
| filecrc = struct.unpack('>h', self.ifp.read(2))[0] & 0xffff |
| #self.crc = binascii.crc_hqx('\0\0', self.crc) |
| # XXXX Is this needed?? |
| self.crc = self.crc & 0xffff |
| if filecrc != self.crc: |
| raise Error, 'CRC error, computed %x, read %x' \ |
| %(self.crc, filecrc) |
| self.crc = 0 |
| |
| def _readheader(self): |
| len = self._read(1) |
| fname = self._read(ord(len)) |
| rest = self._read(1+4+4+2+4+4) |
| self._checkcrc() |
| |
| type = rest[1:5] |
| creator = rest[5:9] |
| flags = struct.unpack('>h', rest[9:11])[0] |
| self.dlen = struct.unpack('>l', rest[11:15])[0] |
| self.rlen = struct.unpack('>l', rest[15:19])[0] |
| |
| self.FName = fname |
| self.FInfo = FInfo() |
| self.FInfo.Creator = creator |
| self.FInfo.Type = type |
| self.FInfo.Flags = flags |
| |
| self.state = _DID_HEADER |
| |
| def read(self, *n): |
| if self.state != _DID_HEADER: |
| raise Error, 'Read data at wrong time' |
| if n: |
| n = n[0] |
| n = min(n, self.dlen) |
| else: |
| n = self.dlen |
| rv = '' |
| while len(rv) < n: |
| rv = rv + self._read(n-len(rv)) |
| self.dlen = self.dlen - n |
| return rv |
| |
| def close_data(self): |
| if self.state != _DID_HEADER: |
| raise Error, 'close_data at wrong time' |
| if self.dlen: |
| dummy = self._read(self.dlen) |
| self._checkcrc() |
| self.state = _DID_DATA |
| |
| def read_rsrc(self, *n): |
| if self.state == _DID_HEADER: |
| self.close_data() |
| if self.state != _DID_DATA: |
| raise Error, 'Read resource data at wrong time' |
| if n: |
| n = n[0] |
| n = min(n, self.rlen) |
| else: |
| n = self.rlen |
| self.rlen = self.rlen - n |
| return self._read(n) |
| |
| def close(self): |
| if self.rlen: |
| dummy = self.read_rsrc(self.rlen) |
| self._checkcrc() |
| self.state = _DID_RSRC |
| self.ifp.close() |
| |
| def hexbin(inp, out): |
| """(infilename, outfilename) - Decode binhexed file""" |
| ifp = HexBin(inp) |
| finfo = ifp.FInfo |
| if not out: |
| out = ifp.FName |
| if os.name == 'mac': |
| ofss = macfs.FSSpec(out) |
| out = ofss.as_pathname() |
| |
| ofp = open(out, 'wb') |
| # XXXX Do translation on non-mac systems |
| while 1: |
| d = ifp.read(128000) |
| if not d: break |
| ofp.write(d) |
| ofp.close() |
| ifp.close_data() |
| |
| d = ifp.read_rsrc(128000) |
| if d: |
| ofp = openrsrc(out, 'wb') |
| ofp.write(d) |
| while 1: |
| d = ifp.read_rsrc(128000) |
| if not d: break |
| ofp.write(d) |
| ofp.close() |
| |
| if os.name == 'mac': |
| nfinfo = ofss.GetFInfo() |
| nfinfo.Creator = finfo.Creator |
| nfinfo.Type = finfo.Type |
| nfinfo.Flags = finfo.Flags |
| ofss.SetFInfo(nfinfo) |
| |
| ifp.close() |
| |
| def _test(): |
| if os.name == 'mac': |
| fss, ok = macfs.PromptGetFile('File to convert:') |
| if not ok: |
| sys.exit(0) |
| fname = fss.as_pathname() |
| else: |
| fname = sys.argv[1] |
| binhex(fname, fname+'.hqx') |
| hexbin(fname+'.hqx', fname+'.viahqx') |
| #hexbin(fname, fname+'.unpacked') |
| sys.exit(1) |
| |
| if __name__ == '__main__': |
| _test() |