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