1ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehr"""Routines to decode AppleSingle files
2ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh"""
3ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
4ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehfrom warnings import warnpy3k
5ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehwarnpy3k("In 3.x, the applesingle module is removed.", stacklevel=2)
6ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
7ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport struct
8ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehimport sys
9ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehtry:
10ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    import MacOS
11ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    import Carbon.File
12ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehexcept:
13ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    class MacOS:
14ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        def openrf(path, mode):
15ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            return open(path + '.rsrc', mode)
16ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        openrf = classmethod(openrf)
17ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    class Carbon:
18ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        class File:
19ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            class FSSpec:
20ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
21ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            class FSRef:
22ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
23ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            class Alias:
24ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                pass
25ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
26ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# all of the errors in this module are really errors in the input
27ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# so I think it should test positive against ValueError.
28ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass Error(ValueError):
29ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    pass
30ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
31ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# File header format: magic, version, unused, number of entries
32ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_HEADER_FORMAT=">LL16sh"
33ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_HEADER_LENGTH=26
34ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# The flag words for AppleSingle
35ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_MAGIC=0x00051600
36ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_VERSION=0x00020000
37ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
38ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# Entry header format: id, offset, length
39ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_ENTRY_FORMAT=">lll"
40ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_ENTRY_LENGTH=12
41ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
42ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh# The id values
43ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_DATAFORK=1
44ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_RESOURCEFORK=2
45ffab958fd8d42ed7227d83007350e61555a1fa36Andrew HsiehAS_IGNORE=(3,4,5,6,8,9,10,11,12,13,14,15)
46ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
47ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehclass AppleSingle(object):
48ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    datafork = None
49ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    resourcefork = None
50ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
51ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def __init__(self, fileobj, verbose=False):
52ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        header = fileobj.read(AS_HEADER_LENGTH)
53ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        try:
54ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            magic, version, ig, nentry = struct.unpack(AS_HEADER_FORMAT, header)
55ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        except ValueError, arg:
56ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            raise Error, "Unpack header error: %s" % (arg,)
57ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if verbose:
58ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            print 'Magic:   0x%8.8x' % (magic,)
59ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            print 'Version: 0x%8.8x' % (version,)
60ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            print 'Entries: %d' % (nentry,)
61ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if magic != AS_MAGIC:
62ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            raise Error, "Unknown AppleSingle magic number 0x%8.8x" % (magic,)
63ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if version != AS_VERSION:
64ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            raise Error, "Unknown AppleSingle version number 0x%8.8x" % (version,)
65ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if nentry <= 0:
66ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            raise Error, "AppleSingle file contains no forks"
67ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        headers = [fileobj.read(AS_ENTRY_LENGTH) for i in xrange(nentry)]
68ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        self.forks = []
69ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        for hdr in headers:
70ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            try:
71ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                restype, offset, length = struct.unpack(AS_ENTRY_FORMAT, hdr)
72ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            except ValueError, arg:
73ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                raise Error, "Unpack entry error: %s" % (arg,)
74ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if verbose:
75ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                print "Fork %d, offset %d, length %d" % (restype, offset, length)
76ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            fileobj.seek(offset)
77ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            data = fileobj.read(length)
78ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if len(data) != length:
79ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                raise Error, "Short read: expected %d bytes got %d" % (length, len(data))
80ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            self.forks.append((restype, data))
81ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if restype == AS_DATAFORK:
82ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.datafork = data
83ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            elif restype == AS_RESOURCEFORK:
84ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                self.resourcefork = data
85ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
86ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    def tofile(self, path, resonly=False):
87ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        outfile = open(path, 'wb')
88ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        data = False
89ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if resonly:
90ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.resourcefork is None:
91ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                raise Error, "No resource fork found"
92ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            fp = open(path, 'wb')
93ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            fp.write(self.resourcefork)
94ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            fp.close()
95ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        elif (self.resourcefork is None and self.datafork is None):
96ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            raise Error, "No useful forks found"
97ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        else:
98ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.datafork is not None:
99ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp = open(path, 'wb')
100ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp.write(self.datafork)
101ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp.close()
102ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if self.resourcefork is not None:
103ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp = MacOS.openrf(path, '*wb')
104ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp.write(self.resourcefork)
105ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                fp.close()
106ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
107ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef decode(infile, outpath, resonly=False, verbose=False):
108ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """decode(infile, outpath [, resonly=False, verbose=False])
109ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
110ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Creates a decoded file from an AppleSingle encoded file.
111ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    If resonly is True, then it will create a regular file at
112ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    outpath containing only the resource fork from infile.
113ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    Otherwise it will create an AppleDouble file at outpath
114ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    with the data and resource forks from infile.  On platforms
115ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    without the MacOS module, it will create inpath and inpath+'.rsrc'
116ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    with the data and resource forks respectively.
117ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
118ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    """
119ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    if not hasattr(infile, 'read'):
120ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if isinstance(infile, Carbon.File.Alias):
121ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            infile = infile.ResolveAlias()[0]
122ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
123ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        if hasattr(Carbon.File, "FSSpec"):
124ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if isinstance(infile, (Carbon.File.FSSpec, Carbon.File.FSRef)):
125ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                infile = infile.as_pathname()
126ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        else:
127ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh            if isinstance(infile, Carbon.File.FSRef):
128ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh                infile = infile.as_pathname()
129ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        infile = open(infile, 'rb')
130ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
131ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    asfile = AppleSingle(infile, verbose=verbose)
132ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    asfile.tofile(outpath, resonly=resonly)
133ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
134ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehdef _test():
135ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    if len(sys.argv) < 3 or sys.argv[1] == '-r' and len(sys.argv) != 4:
136ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        print 'Usage: applesingle.py [-r] applesinglefile decodedfile'
137ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        sys.exit(1)
138ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    if sys.argv[1] == '-r':
139ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        resonly = True
140ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        del sys.argv[1]
141ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    else:
142ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh        resonly = False
143ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    decode(sys.argv[1], sys.argv[2], resonly=resonly)
144ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh
145ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsiehif __name__ == '__main__':
146ffab958fd8d42ed7227d83007350e61555a1fa36Andrew Hsieh    _test()
147