blob: a7f906d695cbaca6ae1dc5c75ea23be447265bd5 [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
Jack Jansen479c1b31995-08-14 12:41:20 +000029DEBUG=0
30if DEBUG:
31 testf=open('@binhex.dbg.out', 'w')
32
Jack Jansenfcdffea1995-08-07 14:36:51 +000033Error = '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):
Jack Jansenac7c0df1995-08-17 14:17:39 +0000147 if DEBUG:
Jack Jansen479c1b31995-08-14 12:41:20 +0000148 testf.write(data) # XXXX
Jack Jansenfcdffea1995-08-07 14:36:51 +0000149 self.data = self.data + data
150 if len(self.data) < REASONABLY_LARGE:
151 return
152 rledata = binascii.rlecode_hqx(self.data)
153 self.ofp.write(rledata)
154 self.data = ''
155
156 def close(self):
157 if self.data:
158 rledata = binascii.rlecode_hqx(self.data)
159 self.ofp.write(rledata)
160 self.ofp.close()
161
162class BinHex:
163 def __init__(self, (name, finfo, dlen, rlen), ofp):
164 if type(ofp) == type(''):
165 ofname = ofp
166 ofp = open(ofname, 'w')
167 if os.name == 'mac':
168 fss = macfs.FSSpec(ofname)
169 fss.SetCreatorType('BnHq', 'TEXT')
170 ofp.write('(This file may be decompressed with BinHex 4.0)\n\n:')
171 hqxer = _Hqxcoderengine(ofp)
172 self.ofp = _Rlecoderengine(hqxer)
173 self.crc = 0
174 if finfo == None:
175 finfo = FInfo()
176 self.dlen = dlen
177 self.rlen = rlen
178 self._writeinfo(name, finfo)
179 self.state = _DID_HEADER
180
181 def _writeinfo(self, name, finfo):
182 if DEBUG:
183 print 'binhex info:', name, finfo.Type, finfo.Creator, self.dlen, self.rlen
184 name = name
185 nl = len(name)
186 if nl > 63:
187 raise Error, 'Filename too long'
188 d = chr(nl) + name + '\0'
189 d2 = finfo.Type + finfo.Creator
190 d3 = struct.pack('h', finfo.Flags)
191 d4 = struct.pack('ii', self.dlen, self.rlen)
192 info = d + d2 + d3 + d4
193 self._write(info)
194 self._writecrc()
195
196 def _write(self, data):
197 self.crc = binascii.crc_hqx(data, self.crc)
198 self.ofp.write(data)
199
200 def _writecrc(self):
201 self.ofp.write(struct.pack('h', self.crc))
202 self.crc = 0
203
204 def write(self, data):
205 if self.state != _DID_HEADER:
206 raise Error, 'Writing data at the wrong time'
207 self.dlen = self.dlen - len(data)
208 self._write(data)
209
210 def close_data(self):
211 if self.dlen <> 0:
212 raise Error, 'Incorrect data size, diff='+`self.rlen`
213 self._writecrc()
214 self.state = _DID_DATA
215
216 def write_rsrc(self, data):
217 if self.state < _DID_DATA:
218 self.close_data()
219 if self.state != _DID_DATA:
220 raise Error, 'Writing resource data at the wrong time'
221 self.rlen = self.rlen - len(data)
222 self._write(data)
223
224 def close(self):
225 if self.state < _DID_DATA:
226 self.close_data()
227 if self.state != _DID_DATA:
228 raise Error, 'Close at the wrong time'
229 if self.rlen <> 0:
230 raise Error, "Incorrect resource-datasize, diff="+`self.rlen`
231 self._writecrc()
232 self.ofp.close()
233 self.state = None
234
235def binhex(inp, out):
236 """(infilename, outfilename) - Create binhex-encoded copy of a file"""
237 finfo = getfileinfo(inp)
238 ofp = BinHex(finfo, out)
239
240 ifp = open(inp, 'rb')
241 # XXXX Do textfile translation on non-mac systems
242 d = ifp.read()
243 ofp.write(d)
244 ofp.close_data()
245 ifp.close()
246
247 ifp = openrsrc(inp, 'rb')
248 d = ifp.read()
249 ofp.write_rsrc(d)
250 ofp.close()
251 ifp.close()
252
253class _Hqxdecoderengine:
254 """Read data via the decoder in 4-byte chunks"""
255
256 def __init__(self, ifp):
257 self.ifp = ifp
258 self.eof = 0
259
260 def read(self, wtd):
261 """Read at least wtd bytes (or until EOF)"""
262 decdata = ''
263 #
264 # The loop here is convoluted, since we don't really now how much
265 # to decode: there may be newlines in the incoming data.
266 while wtd > 0:
267 if self.eof: return decdata
268 wtd = ((wtd+2)/3)*4
269 data = self.ifp.read(wtd)
270 #
271 # Next problem: there may not be a complete number of bytes in what we
272 # pass to a2b. Solve by yet another loop.
273 #
274 while 1:
275 try:
276 decdatacur, self.eof = binascii.a2b_hqx(data)
277 if self.eof: print 'EOF'
278 break
279 except binascii.Incomplete:
280 pass
281 newdata = self.ifp.read(1)
282 if not newdata:
283 raise Error, 'Premature EOF on binhex file'
284 data = data + newdata
285 decdata = decdata + decdatacur
286 wtd = wtd - len(decdata)
287 if not decdata and not self.eof:
288 raise Error, 'Premature EOF on binhex file'
289 return decdata
290
291 def close(self):
292 self.ifp.close()
293
294class _Rledecoderengine:
295 """Read data via the RLE-coder"""
296
297 def __init__(self, ifp):
298 self.ifp = ifp
299 self.pre_buffer = ''
300 self.post_buffer = ''
301 self.eof = 0
302
303 def read(self, wtd):
304 if wtd > len(self.post_buffer):
305 self._fill(wtd-len(self.post_buffer))
306 rv = self.post_buffer[:wtd]
307 self.post_buffer = self.post_buffer[wtd:]
308 print 'WTD', wtd, 'GOT', len(rv)
309 return rv
310
311 def _fill(self, wtd):
312 #
313 # Obfuscated code ahead. We keep at least one byte in the pre_buffer,
314 # so we don't stumble over an orphaned RUNCHAR later on. If the
315 # last or second-last char is a RUNCHAR we keep more bytes.
316 #
317 self.pre_buffer = self.pre_buffer + self.ifp.read(wtd+2)
318 if self.ifp.eof:
319 self.post_buffer = self.post_buffer + \
320 binascii.rledecode_hqx(self.pre_buffer)
321 self.pre_buffer = ''
322 return
323
324 lastrle = string.rfind(self.pre_buffer, RUNCHAR)
325 if lastrle > 0 and lastrle == len(self.pre_buffer)-1:
326 # Last byte is an RLE, keep two bytes
327 mark = len(self.pre_buffer)-2
328 elif lastrle > 0 and lastrle == len(self.pre_buffer)-2:
329 # second-last byte is an RLE. Decode all.
330 mark = len(self.pre_buffer)
331 else:
332 mark = len(self.pre_buffer)-1
333 self.post_buffer = self.post_buffer + \
334 binascii.rledecode_hqx(self.pre_buffer[:mark])
335 self.pre_buffer = self.pre_buffer[mark:]
336
337 def close(self):
338 self.ifp.close()
339
340class HexBin:
341 def __init__(self, ifp):
342 if type(ifp) == type(''):
343 ifp = open(ifp)
344 #
345 # Find initial colon.
346 #
347 while 1:
348 ch = ifp.read(1)
349 if not ch:
350 raise Error, "No binhex data found"
351 if ch == ':':
352 break
353 if ch != '\n':
354 dummy = ifp.readline()
355 if DEBUG:
356 print 'SKIP:', ch+dummy
357
358 hqxifp = _Hqxdecoderengine(ifp)
359 self.ifp = _Rledecoderengine(hqxifp)
360 self.crc = 0
361 self._readheader()
362
363 def _read(self, len):
364 data = self.ifp.read(len)
365 self.crc = binascii.crc_hqx(data, self.crc)
366 return data
367
368 def _checkcrc(self):
369 filecrc = struct.unpack('h', self.ifp.read(2))[0] & 0xffff
370 self.crc = self.crc & 0xffff
371 if DEBUG:
372 print 'DBG CRC %x %x'%(self.crc, filecrc)
373 #if filecrc != self.crc:
374 # raise Error, 'CRC error, computed %x, read %x'%(self.crc, filecrc)
375 self.crc = 0
376
377 def _readheader(self):
378 len = self._read(1)
379 fname = self._read(ord(len))
380 rest = self._read(1+4+4+2+4+4)
381 self._checkcrc()
382
383 type = rest[1:5]
384 creator = rest[5:9]
385 flags = struct.unpack('h', rest[9:11])[0]
386 self.dlen = struct.unpack('l', rest[11:15])[0]
387 self.rlen = struct.unpack('l', rest[15:19])[0]
388
389 if DEBUG:
390 print 'DATA, RLEN', self.dlen, self.rlen
391
392 self.FName = fname
393 self.FInfo = FInfo()
394 self.FInfo.Creator = creator
395 self.FInfo.Type = type
396 self.FInfo.Flags = flags
397
398 self.state = _DID_HEADER
399
400 def read(self, *n):
401 if self.state != _DID_HEADER:
402 raise Error, 'Read data at wrong time'
403 if n:
404 n = n[0]
405 n = min(n, self.dlen)
406 else:
407 n = self.dlen
408 self.dlen = self.dlen - n
409 return self._read(n)
410
411 def close_data(self):
412 if self.state != _DID_HEADER:
413 raise Error, 'close_data at wrong time'
414 if self.dlen:
415 dummy = self._read(self.dlen)
416 self._checkcrc()
417 self.state = _DID_DATA
418
419 def read_rsrc(self, *n):
420 if self.state == _DID_HEADER:
421 self.close_data()
422 if self.state != _DID_DATA:
423 raise Error, 'Read resource data at wrong time'
424 if n:
425 n = n[0]
426 n = min(n, self.rlen)
427 else:
428 n = self.rlen
429 self.rlen = self.rlen - n
430 return self._read(n)
431
432 def close(self):
433 if self.rlen:
434 dummy = self.read_rsrc(self.rlen)
435 self._checkcrc()
436 self.state = _DID_RSRC
437 self.ifp.close()
438
439def hexbin(inp, out):
440 """(infilename, outfilename) - Decode binhexed file"""
441 ifp = HexBin(inp)
442 finfo = ifp.FInfo
443 if not out:
444 out = ifp.FName
445 if os.name == 'mac':
446 ofss = macfs.FSSpec(out)
447 out = ofss.as_pathname()
448
449 ofp = open(out, 'wb')
450 # XXXX Do translation on non-mac systems
451 d = ifp.read()
452 ofp.write(d)
453 ofp.close()
454 ifp.close_data()
455
456 d = ifp.read_rsrc()
457 if d:
458 ofp = openrsrc(out, 'wb')
459 ofp.write(d)
460 ofp.close()
461
462 if os.name == 'mac':
463 nfinfo = ofss.GetFInfo()
464 nfinfo.Creator = finfo.Creator
465 nfinfo.Type = finfo.Type
466 nfinfo.Flags = finfo.Flags
467 ofss.SetFInfo(nfinfo)
468
469 ifp.close()
470
471def _test():
472 if os.name == 'mac':
Jack Jansen479c1b31995-08-14 12:41:20 +0000473 fss, ok = macfs.PromptGetFile('File to convert:')
Jack Jansenfcdffea1995-08-07 14:36:51 +0000474 if not ok:
475 sys.exit(0)
476 fname = fss.as_pathname()
477 else:
478 fname = sys.argv[1]
479 #binhex(fname, fname+'.hqx')
480 #hexbin(fname+'.hqx', fname+'.viahqx')
481 hexbin(fname, fname+'.unpacked')
482 sys.exit(1)
483
484if __name__ == '__main__':
485 _test()
486