applesingle.py 4.57 KB
Newer Older
1 2
r"""Routines to decode AppleSingle files
"""
3 4
import struct
import sys
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
try:
    import MacOS
    import Carbon.File
except:
    class MacOS:
        def openrf(path, mode):
            return open(path + '.rsrc', mode)
        openrf = classmethod(openrf)
    class Carbon:
        class File:
            class FSSpec:
                pass
            class FSRef:
                pass
            class Alias:
                pass
21

22 23 24 25
# all of the errors in this module are really errors in the input
# so I think it should test positive against ValueError.
class Error(ValueError):
    pass
26 27

# File header format: magic, version, unused, number of entries
28
AS_HEADER_FORMAT="LL16sh"
29 30 31 32 33 34 35 36 37 38 39 40 41 42
AS_HEADER_LENGTH=26
# The flag words for AppleSingle
AS_MAGIC=0x00051600
AS_VERSION=0x00020000

# Entry header format: id, offset, length
AS_ENTRY_FORMAT="lll"
AS_ENTRY_LENGTH=12

# The id values
AS_DATAFORK=1
AS_RESOURCEFORK=2
AS_IGNORE=(3,4,5,6,8,9,10,11,12,13,14,15)

43 44 45 46 47 48
class AppleSingle(object):
    datafork = None
    resourcefork = None

    def __init__(self, fileobj, verbose=False):
        header = fileobj.read(AS_HEADER_LENGTH)
Jack Jansen's avatar
Jack Jansen committed
49
        try:
50
            magic, version, ig, nentry = struct.unpack(AS_HEADER_FORMAT, header)
Jack Jansen's avatar
Jack Jansen committed
51
        except ValueError, arg:
52
            raise Error, "Unpack header error: %s" % (arg,)
Jack Jansen's avatar
Jack Jansen committed
53
        if verbose:
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
            print 'Magic:   0x%8.8x' % (magic,)
            print 'Version: 0x%8.8x' % (version,)
            print 'Entries: %d' % (nentry,)
        if magic != AS_MAGIC:
            raise Error, "Unknown AppleSingle magic number 0x%8.8x" % (magic,)
        if version != AS_VERSION:
            raise Error, "Unknown AppleSingle version number 0x%8.8x" % (version,)
        if nentry <= 0:
            raise Error, "AppleSingle file contains no forks"
        headers = [fileobj.read(AS_ENTRY_LENGTH) for i in xrange(nentry)]
        self.forks = []
        for hdr in headers:
            try:
                restype, offset, length = struct.unpack(AS_ENTRY_FORMAT, hdr)
            except ValueError, arg:
                raise Error, "Unpack entry error: %s" % (arg,)
Jack Jansen's avatar
Jack Jansen committed
70
            if verbose:
71 72 73 74 75 76 77 78 79 80
                print "Fork %d, offset %d, length %d" % (restype, offset, length)
            fileobj.seek(offset)
            data = fileobj.read(length)
            if len(data) != length:
                raise Error, "Short read: expected %d bytes got %d" % (length, len(data))
            self.forks.append((restype, data))
            if restype == AS_DATAFORK:
                self.datafork = data
            elif restype == AS_RESOURCEFORK:
                self.resourcefork = data
81

82 83 84 85 86 87 88 89
    def tofile(self, path, resonly=False):
        outfile = open(path, 'wb')
        data = False
        if resonly:
            if self.resourcefork is None:
                raise Error, "No resource fork found"
            fp = open(path, 'wb')
            fp.write(self.resourcefork)
Jack Jansen's avatar
Jack Jansen committed
90
            fp.close()
91 92
        elif (self.resourcefork is None and self.datafork is None):
            raise Error, "No useful forks found"
Jack Jansen's avatar
Jack Jansen committed
93
        else:
94 95 96 97 98 99 100 101
            if self.datafork is not None:
                fp = open(path, 'wb')
                fp.write(self.datafork)
                fp.close()
            if self.resourcefork is not None:
                fp = MacOS.openrf(path, '*wb')
                fp.write(self.resourcefork)
                fp.close()
102

103 104
def decode(infile, outpath, resonly=False, verbose=False):
    """decode(infile, outpath [, resonly=False, verbose=False])
105

106
    Creates a decoded file from an AppleSingle encoded file.
107
    If resonly is True, then it will create a regular file at
108 109
    outpath containing only the resource fork from infile.
    Otherwise it will create an AppleDouble file at outpath
110
    with the data and resource forks from infile.  On platforms
111 112 113 114 115 116 117 118 119 120 121 122 123
    without the MacOS module, it will create inpath and inpath+'.rsrc'
    with the data and resource forks respectively.

    """
    if not hasattr(infile, 'read'):
        if isinstance(infile, Carbon.File.Alias):
            infile = infile.ResolveAlias()[0]
        if isinstance(infile, (Carbon.File.FSSpec, Carbon.File.FSRef)):
            infile = infile.as_pathname()
        infile = open(infile, 'rb')

    as = AppleSingle(infile, verbose=verbose)
    as.tofile(outpath, resonly=resonly)
124

125
def _test():
Jack Jansen's avatar
Jack Jansen committed
126 127 128 129
    if len(sys.argv) < 3 or sys.argv[1] == '-r' and len(sys.argv) != 4:
        print 'Usage: applesingle.py [-r] applesinglefile decodedfile'
        sys.exit(1)
    if sys.argv[1] == '-r':
130
        resonly = True
Jack Jansen's avatar
Jack Jansen committed
131 132
        del sys.argv[1]
    else:
133
        resonly = False
Jack Jansen's avatar
Jack Jansen committed
134
    decode(sys.argv[1], sys.argv[2], resonly=resonly)
135

136
if __name__ == '__main__':
Jack Jansen's avatar
Jack Jansen committed
137
    _test()