1"""Simple class to read IFF chunks.
2
3An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
4Format)) has the following structure:
5
6+----------------+
7| ID (4 bytes)   |
8+----------------+
9| size (4 bytes) |
10+----------------+
11| data           |
12| ...            |
13+----------------+
14
15The ID is a 4-byte string which identifies the type of chunk.
16
17The size field (a 32-bit value, encoded using big-endian byte order)
18gives the size of the whole chunk, including the 8-byte header.
19
20Usually an IFF-type file consists of one or more chunks.  The proposed
21usage of the Chunk class defined here is to instantiate an instance at
22the start of each chunk and read from the instance until it reaches
23the end, after which a new instance can be instantiated.  At the end
24of the file, creating a new instance will fail with a EOFError
25exception.
26
27Usage:
28while True:
29    try:
30        chunk = Chunk(file)
31    except EOFError:
32        break
33    chunktype = chunk.getname()
34    while True:
35        data = chunk.read(nbytes)
36        if not data:
37            pass
38        # do something with data
39
40The interface is file-like.  The implemented methods are:
41read, close, seek, tell, isatty.
42Extra methods are: skip() (called by close, skips to the end of the chunk),
43getname() (returns the name (ID) of the chunk)
44
45The __init__ method has one required argument, a file-like object
46(including a chunk instance), and one optional argument, a flag which
47specifies whether or not chunks are aligned on 2-byte boundaries.  The
48default is 1, i.e. aligned.
49"""
50
51class Chunk:
52    def __init__(self, file, align=True, bigendian=True, inclheader=False):
53        import struct
54        self.closed = False
55        self.align = align      # whether to align to word (2-byte) boundaries
56        if bigendian:
57            strflag = '>'
58        else:
59            strflag = '<'
60        self.file = file
61        self.chunkname = file.read(4)
62        if len(self.chunkname) < 4:
63            raise EOFError
64        try:
65            self.chunksize = struct.unpack(strflag+'L', file.read(4))[0]
66        except struct.error:
67            raise EOFError
68        if inclheader:
69            self.chunksize = self.chunksize - 8 # subtract header
70        self.size_read = 0
71        try:
72            self.offset = self.file.tell()
73        except (AttributeError, IOError):
74            self.seekable = False
75        else:
76            self.seekable = True
77
78    def getname(self):
79        """Return the name (ID) of the current chunk."""
80        return self.chunkname
81
82    def getsize(self):
83        """Return the size of the current chunk."""
84        return self.chunksize
85
86    def close(self):
87        if not self.closed:
88            self.skip()
89            self.closed = True
90
91    def isatty(self):
92        if self.closed:
93            raise ValueError, "I/O operation on closed file"
94        return False
95
96    def seek(self, pos, whence=0):
97        """Seek to specified position into the chunk.
98        Default position is 0 (start of chunk).
99        If the file is not seekable, this will result in an error.
100        """
101
102        if self.closed:
103            raise ValueError, "I/O operation on closed file"
104        if not self.seekable:
105            raise IOError, "cannot seek"
106        if whence == 1:
107            pos = pos + self.size_read
108        elif whence == 2:
109            pos = pos + self.chunksize
110        if pos < 0 or pos > self.chunksize:
111            raise RuntimeError
112        self.file.seek(self.offset + pos, 0)
113        self.size_read = pos
114
115    def tell(self):
116        if self.closed:
117            raise ValueError, "I/O operation on closed file"
118        return self.size_read
119
120    def read(self, size=-1):
121        """Read at most size bytes from the chunk.
122        If size is omitted or negative, read until the end
123        of the chunk.
124        """
125
126        if self.closed:
127            raise ValueError, "I/O operation on closed file"
128        if self.size_read >= self.chunksize:
129            return ''
130        if size < 0:
131            size = self.chunksize - self.size_read
132        if size > self.chunksize - self.size_read:
133            size = self.chunksize - self.size_read
134        data = self.file.read(size)
135        self.size_read = self.size_read + len(data)
136        if self.size_read == self.chunksize and \
137           self.align and \
138           (self.chunksize & 1):
139            dummy = self.file.read(1)
140            self.size_read = self.size_read + len(dummy)
141        return data
142
143    def skip(self):
144        """Skip the rest of the chunk.
145        If you are not interested in the contents of the chunk,
146        this method should be called so that the file points to
147        the start of the next chunk.
148        """
149
150        if self.closed:
151            raise ValueError, "I/O operation on closed file"
152        if self.seekable:
153            try:
154                n = self.chunksize - self.size_read
155                # maybe fix alignment
156                if self.align and (self.chunksize & 1):
157                    n = n + 1
158                self.file.seek(n, 1)
159                self.size_read = self.size_read + n
160                return
161            except IOError:
162                pass
163        while self.size_read < self.chunksize:
164            n = min(8192, self.chunksize - self.size_read)
165            dummy = self.read(n)
166            if not dummy:
167                raise EOFError
168