1"""Macintosh binhex compression/decompression. 2 3easy interface: 4binhex(inputfilename, outputfilename) 5hexbin(inputfilename, outputfilename) 6""" 7 8# 9# Jack Jansen, CWI, August 1995. 10# 11# The module is supposed to be as compatible as possible. Especially the 12# easy interface should work "as expected" on any platform. 13# XXXX Note: currently, textfiles appear in mac-form on all platforms. 14# We seem to lack a simple character-translate in python. 15# (we should probably use ISO-Latin-1 on all but the mac platform). 16# XXXX The simple routines are too simple: they expect to hold the complete 17# files in-core. Should be fixed. 18# XXXX It would be nice to handle AppleDouble format on unix 19# (for servers serving macs). 20# XXXX I don't understand what happens when you get 0x90 times the same byte on 21# input. The resulting code (xx 90 90) would appear to be interpreted as an 22# escaped *value* of 0x90. All coders I've seen appear to ignore this nicety... 23# 24import sys 25import os 26import struct 27import binascii 28 29__all__ = ["binhex","hexbin","Error"] 30 31class Error(Exception): 32 pass 33 34# States (what have we written) 35_DID_HEADER = 0 36_DID_DATA = 1 37 38# Various constants 39REASONABLY_LARGE=32768 # Minimal amount we pass the rle-coder 40LINELEN=64 41RUNCHAR=chr(0x90) # run-length introducer 42 43# 44# This code is no longer byte-order dependent 45 46# 47# Workarounds for non-mac machines. 48try: 49 from Carbon.File import FSSpec, FInfo 50 from MacOS import openrf 51 52 def getfileinfo(name): 53 finfo = FSSpec(name).FSpGetFInfo() 54 dir, file = os.path.split(name) 55 # XXX Get resource/data sizes 56 fp = open(name, 'rb') 57 fp.seek(0, 2) 58 dlen = fp.tell() 59 fp = openrf(name, '*rb') 60 fp.seek(0, 2) 61 rlen = fp.tell() 62 return file, finfo, dlen, rlen 63 64 def openrsrc(name, *mode): 65 if not mode: 66 mode = '*rb' 67 else: 68 mode = '*' + mode[0] 69 return openrf(name, mode) 70 71except ImportError: 72 # 73 # Glue code for non-macintosh usage 74 # 75 76 class FInfo: 77 def __init__(self): 78 self.Type = '????' 79 self.Creator = '????' 80 self.Flags = 0 81 82 def getfileinfo(name): 83 finfo = FInfo() 84 # Quick check for textfile 85 fp = open(name) 86 data = open(name).read(256) 87 for c in data: 88 if not c.isspace() and (c<' ' or ord(c) > 0x7f): 89 break 90 else: 91 finfo.Type = 'TEXT' 92 fp.seek(0, 2) 93 dsize = fp.tell() 94 fp.close() 95 dir, file = os.path.split(name) 96 file = file.replace(':', '-', 1) 97 return file, finfo, dsize, 0 98 99 class openrsrc: 100 def __init__(self, *args): 101 pass 102 103 def read(self, *args): 104 return '' 105 106 def write(self, *args): 107 pass 108 109 def close(self): 110 pass 111 112class _Hqxcoderengine: 113 """Write data to the coder in 3-byte chunks""" 114 115 def __init__(self, ofp): 116 self.ofp = ofp 117 self.data = '' 118 self.hqxdata = '' 119 self.linelen = LINELEN-1 120 121 def write(self, data): 122 self.data = self.data + data 123 datalen = len(self.data) 124 todo = (datalen//3)*3 125 data = self.data[:todo] 126 self.data = self.data[todo:] 127 if not data: 128 return 129 self.hqxdata = self.hqxdata + binascii.b2a_hqx(data) 130 self._flush(0) 131 132 def _flush(self, force): 133 first = 0 134 while first <= len(self.hqxdata)-self.linelen: 135 last = first + self.linelen 136 self.ofp.write(self.hqxdata[first:last]+'\n') 137 self.linelen = LINELEN 138 first = last 139 self.hqxdata = self.hqxdata[first:] 140 if force: 141 self.ofp.write(self.hqxdata + ':\n') 142 143 def close(self): 144 if self.data: 145 self.hqxdata = \ 146 self.hqxdata + binascii.b2a_hqx(self.data) 147 self._flush(1) 148 self.ofp.close() 149 del self.ofp 150 151class _Rlecoderengine: 152 """Write data to the RLE-coder in suitably large chunks""" 153 154 def __init__(self, ofp): 155 self.ofp = ofp 156 self.data = '' 157 158 def write(self, data): 159 self.data = self.data + data 160 if len(self.data) < REASONABLY_LARGE: 161 return 162 rledata = binascii.rlecode_hqx(self.data) 163 self.ofp.write(rledata) 164 self.data = '' 165 166 def close(self): 167 if self.data: 168 rledata = binascii.rlecode_hqx(self.data) 169 self.ofp.write(rledata) 170 self.ofp.close() 171 del self.ofp 172 173class BinHex: 174 def __init__(self, name_finfo_dlen_rlen, ofp): 175 name, finfo, dlen, rlen = name_finfo_dlen_rlen 176 if type(ofp) == type(''): 177 ofname = ofp 178 ofp = open(ofname, 'w') 179 ofp.write('(This file must be converted with BinHex 4.0)\n\n:') 180 hqxer = _Hqxcoderengine(ofp) 181 self.ofp = _Rlecoderengine(hqxer) 182 self.crc = 0 183 if finfo is None: 184 finfo = FInfo() 185 self.dlen = dlen 186 self.rlen = rlen 187 self._writeinfo(name, finfo) 188 self.state = _DID_HEADER 189 190 def _writeinfo(self, name, finfo): 191 nl = len(name) 192 if nl > 63: 193 raise Error, 'Filename too long' 194 d = chr(nl) + name + '\0' 195 d2 = finfo.Type + finfo.Creator 196 197 # Force all structs to be packed with big-endian 198 d3 = struct.pack('>h', finfo.Flags) 199 d4 = struct.pack('>ii', self.dlen, self.rlen) 200 info = d + d2 + d3 + d4 201 self._write(info) 202 self._writecrc() 203 204 def _write(self, data): 205 self.crc = binascii.crc_hqx(data, self.crc) 206 self.ofp.write(data) 207 208 def _writecrc(self): 209 # XXXX Should this be here?? 210 # self.crc = binascii.crc_hqx('\0\0', self.crc) 211 if self.crc < 0: 212 fmt = '>h' 213 else: 214 fmt = '>H' 215 self.ofp.write(struct.pack(fmt, self.crc)) 216 self.crc = 0 217 218 def write(self, data): 219 if self.state != _DID_HEADER: 220 raise Error, 'Writing data at the wrong time' 221 self.dlen = self.dlen - len(data) 222 self._write(data) 223 224 def close_data(self): 225 if self.dlen != 0: 226 raise Error, 'Incorrect data size, diff=%r' % (self.rlen,) 227 self._writecrc() 228 self.state = _DID_DATA 229 230 def write_rsrc(self, data): 231 if self.state < _DID_DATA: 232 self.close_data() 233 if self.state != _DID_DATA: 234 raise Error, 'Writing resource data at the wrong time' 235 self.rlen = self.rlen - len(data) 236 self._write(data) 237 238 def close(self): 239 if self.state is None: 240 return 241 try: 242 if self.state < _DID_DATA: 243 self.close_data() 244 if self.state != _DID_DATA: 245 raise Error, 'Close at the wrong time' 246 if self.rlen != 0: 247 raise Error, \ 248 "Incorrect resource-datasize, diff=%r" % (self.rlen,) 249 self._writecrc() 250 finally: 251 self.state = None 252 ofp = self.ofp 253 del self.ofp 254 ofp.close() 255 256def binhex(inp, out): 257 """(infilename, outfilename) - Create binhex-encoded copy of a file""" 258 finfo = getfileinfo(inp) 259 ofp = BinHex(finfo, out) 260 261 ifp = open(inp, 'rb') 262 # XXXX Do textfile translation on non-mac systems 263 while 1: 264 d = ifp.read(128000) 265 if not d: break 266 ofp.write(d) 267 ofp.close_data() 268 ifp.close() 269 270 ifp = openrsrc(inp, 'rb') 271 while 1: 272 d = ifp.read(128000) 273 if not d: break 274 ofp.write_rsrc(d) 275 ofp.close() 276 ifp.close() 277 278class _Hqxdecoderengine: 279 """Read data via the decoder in 4-byte chunks""" 280 281 def __init__(self, ifp): 282 self.ifp = ifp 283 self.eof = 0 284 285 def read(self, totalwtd): 286 """Read at least wtd bytes (or until EOF)""" 287 decdata = '' 288 wtd = totalwtd 289 # 290 # The loop here is convoluted, since we don't really now how 291 # much to decode: there may be newlines in the incoming data. 292 while wtd > 0: 293 if self.eof: return decdata 294 wtd = ((wtd+2)//3)*4 295 data = self.ifp.read(wtd) 296 # 297 # Next problem: there may not be a complete number of 298 # bytes in what we pass to a2b. Solve by yet another 299 # loop. 300 # 301 while 1: 302 try: 303 decdatacur, self.eof = \ 304 binascii.a2b_hqx(data) 305 break 306 except binascii.Incomplete: 307 pass 308 newdata = self.ifp.read(1) 309 if not newdata: 310 raise Error, \ 311 'Premature EOF on binhex file' 312 data = data + newdata 313 decdata = decdata + decdatacur 314 wtd = totalwtd - len(decdata) 315 if not decdata and not self.eof: 316 raise Error, 'Premature EOF on binhex file' 317 return decdata 318 319 def close(self): 320 self.ifp.close() 321 322class _Rledecoderengine: 323 """Read data via the RLE-coder""" 324 325 def __init__(self, ifp): 326 self.ifp = ifp 327 self.pre_buffer = '' 328 self.post_buffer = '' 329 self.eof = 0 330 331 def read(self, wtd): 332 if wtd > len(self.post_buffer): 333 self._fill(wtd-len(self.post_buffer)) 334 rv = self.post_buffer[:wtd] 335 self.post_buffer = self.post_buffer[wtd:] 336 return rv 337 338 def _fill(self, wtd): 339 self.pre_buffer = self.pre_buffer + self.ifp.read(wtd+4) 340 if self.ifp.eof: 341 self.post_buffer = self.post_buffer + \ 342 binascii.rledecode_hqx(self.pre_buffer) 343 self.pre_buffer = '' 344 return 345 346 # 347 # Obfuscated code ahead. We have to take care that we don't 348 # end up with an orphaned RUNCHAR later on. So, we keep a couple 349 # of bytes in the buffer, depending on what the end of 350 # the buffer looks like: 351 # '\220\0\220' - Keep 3 bytes: repeated \220 (escaped as \220\0) 352 # '?\220' - Keep 2 bytes: repeated something-else 353 # '\220\0' - Escaped \220: Keep 2 bytes. 354 # '?\220?' - Complete repeat sequence: decode all 355 # otherwise: keep 1 byte. 356 # 357 mark = len(self.pre_buffer) 358 if self.pre_buffer[-3:] == RUNCHAR + '\0' + RUNCHAR: 359 mark = mark - 3 360 elif self.pre_buffer[-1] == RUNCHAR: 361 mark = mark - 2 362 elif self.pre_buffer[-2:] == RUNCHAR + '\0': 363 mark = mark - 2 364 elif self.pre_buffer[-2] == RUNCHAR: 365 pass # Decode all 366 else: 367 mark = mark - 1 368 369 self.post_buffer = self.post_buffer + \ 370 binascii.rledecode_hqx(self.pre_buffer[:mark]) 371 self.pre_buffer = self.pre_buffer[mark:] 372 373 def close(self): 374 self.ifp.close() 375 376class HexBin: 377 def __init__(self, ifp): 378 if type(ifp) == type(''): 379 ifp = open(ifp) 380 # 381 # Find initial colon. 382 # 383 while 1: 384 ch = ifp.read(1) 385 if not ch: 386 raise Error, "No binhex data found" 387 # Cater for \r\n terminated lines (which show up as \n\r, hence 388 # all lines start with \r) 389 if ch == '\r': 390 continue 391 if ch == ':': 392 break 393 if ch != '\n': 394 dummy = ifp.readline() 395 396 hqxifp = _Hqxdecoderengine(ifp) 397 self.ifp = _Rledecoderengine(hqxifp) 398 self.crc = 0 399 self._readheader() 400 401 def _read(self, len): 402 data = self.ifp.read(len) 403 self.crc = binascii.crc_hqx(data, self.crc) 404 return data 405 406 def _checkcrc(self): 407 filecrc = struct.unpack('>h', self.ifp.read(2))[0] & 0xffff 408 #self.crc = binascii.crc_hqx('\0\0', self.crc) 409 # XXXX Is this needed?? 410 self.crc = self.crc & 0xffff 411 if filecrc != self.crc: 412 raise Error, 'CRC error, computed %x, read %x' \ 413 %(self.crc, filecrc) 414 self.crc = 0 415 416 def _readheader(self): 417 len = self._read(1) 418 fname = self._read(ord(len)) 419 rest = self._read(1+4+4+2+4+4) 420 self._checkcrc() 421 422 type = rest[1:5] 423 creator = rest[5:9] 424 flags = struct.unpack('>h', rest[9:11])[0] 425 self.dlen = struct.unpack('>l', rest[11:15])[0] 426 self.rlen = struct.unpack('>l', rest[15:19])[0] 427 428 self.FName = fname 429 self.FInfo = FInfo() 430 self.FInfo.Creator = creator 431 self.FInfo.Type = type 432 self.FInfo.Flags = flags 433 434 self.state = _DID_HEADER 435 436 def read(self, *n): 437 if self.state != _DID_HEADER: 438 raise Error, 'Read data at wrong time' 439 if n: 440 n = n[0] 441 n = min(n, self.dlen) 442 else: 443 n = self.dlen 444 rv = '' 445 while len(rv) < n: 446 rv = rv + self._read(n-len(rv)) 447 self.dlen = self.dlen - n 448 return rv 449 450 def close_data(self): 451 if self.state != _DID_HEADER: 452 raise Error, 'close_data at wrong time' 453 if self.dlen: 454 dummy = self._read(self.dlen) 455 self._checkcrc() 456 self.state = _DID_DATA 457 458 def read_rsrc(self, *n): 459 if self.state == _DID_HEADER: 460 self.close_data() 461 if self.state != _DID_DATA: 462 raise Error, 'Read resource data at wrong time' 463 if n: 464 n = n[0] 465 n = min(n, self.rlen) 466 else: 467 n = self.rlen 468 self.rlen = self.rlen - n 469 return self._read(n) 470 471 def close(self): 472 if self.state is None: 473 return 474 try: 475 if self.rlen: 476 dummy = self.read_rsrc(self.rlen) 477 self._checkcrc() 478 finally: 479 self.state = None 480 self.ifp.close() 481 482def hexbin(inp, out): 483 """(infilename, outfilename) - Decode binhexed file""" 484 ifp = HexBin(inp) 485 finfo = ifp.FInfo 486 if not out: 487 out = ifp.FName 488 489 ofp = open(out, 'wb') 490 # XXXX Do translation on non-mac systems 491 while 1: 492 d = ifp.read(128000) 493 if not d: break 494 ofp.write(d) 495 ofp.close() 496 ifp.close_data() 497 498 d = ifp.read_rsrc(128000) 499 if d: 500 ofp = openrsrc(out, 'wb') 501 ofp.write(d) 502 while 1: 503 d = ifp.read_rsrc(128000) 504 if not d: break 505 ofp.write(d) 506 ofp.close() 507 508 ifp.close() 509 510def _test(): 511 fname = sys.argv[1] 512 binhex(fname, fname+'.hqx') 513 hexbin(fname+'.hqx', fname+'.viahqx') 514 #hexbin(fname, fname+'.unpacked') 515 sys.exit(1) 516 517if __name__ == '__main__': 518 _test() 519