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 an 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
51import warnings
52
53warnings._deprecated(__name__, remove=(3, 13))
54
55class Chunk:
56    def __init__(self, file, align=True, bigendian=True, inclheader=False):
57        import struct
58        self.closed = False
59        self.align = align      # whether to align to word (2-byte) boundaries
60        if bigendian:
61            strflag = '>'
62        else:
63            strflag = '<'
64        self.file = file
65        self.chunkname = file.read(4)
66        if len(self.chunkname) < 4:
67            raise EOFError
68        try:
69            self.chunksize = struct.unpack_from(strflag+'L', file.read(4))[0]
70        except struct.error:
71            raise EOFError from None
72        if inclheader:
73            self.chunksize = self.chunksize - 8 # subtract header
74        self.size_read = 0
75        try:
76            self.offset = self.file.tell()
77        except (AttributeError, OSError):
78            self.seekable = False
79        else:
80            self.seekable = True
81
82    def getname(self):
83        """Return the name (ID) of the current chunk."""
84        return self.chunkname
85
86    def getsize(self):
87        """Return the size of the current chunk."""
88        return self.chunksize
89
90    def close(self):
91        if not self.closed:
92            try:
93                self.skip()
94            finally:
95                self.closed = True
96
97    def isatty(self):
98        if self.closed:
99            raise ValueError("I/O operation on closed file")
100        return False
101
102    def seek(self, pos, whence=0):
103        """Seek to specified position into the chunk.
104        Default position is 0 (start of chunk).
105        If the file is not seekable, this will result in an error.
106        """
107
108        if self.closed:
109            raise ValueError("I/O operation on closed file")
110        if not self.seekable:
111            raise OSError("cannot seek")
112        if whence == 1:
113            pos = pos + self.size_read
114        elif whence == 2:
115            pos = pos + self.chunksize
116        if pos < 0 or pos > self.chunksize:
117            raise RuntimeError
118        self.file.seek(self.offset + pos, 0)
119        self.size_read = pos
120
121    def tell(self):
122        if self.closed:
123            raise ValueError("I/O operation on closed file")
124        return self.size_read
125
126    def read(self, size=-1):
127        """Read at most size bytes from the chunk.
128        If size is omitted or negative, read until the end
129        of the chunk.
130        """
131
132        if self.closed:
133            raise ValueError("I/O operation on closed file")
134        if self.size_read >= self.chunksize:
135            return b''
136        if size < 0:
137            size = self.chunksize - self.size_read
138        if size > self.chunksize - self.size_read:
139            size = self.chunksize - self.size_read
140        data = self.file.read(size)
141        self.size_read = self.size_read + len(data)
142        if self.size_read == self.chunksize and \
143           self.align and \
144           (self.chunksize & 1):
145            dummy = self.file.read(1)
146            self.size_read = self.size_read + len(dummy)
147        return data
148
149    def skip(self):
150        """Skip the rest of the chunk.
151        If you are not interested in the contents of the chunk,
152        this method should be called so that the file points to
153        the start of the next chunk.
154        """
155
156        if self.closed:
157            raise ValueError("I/O operation on closed file")
158        if self.seekable:
159            try:
160                n = self.chunksize - self.size_read
161                # maybe fix alignment
162                if self.align and (self.chunksize & 1):
163                    n = n + 1
164                self.file.seek(n, 1)
165                self.size_read = self.size_read + n
166                return
167            except OSError:
168                pass
169        while self.size_read < self.chunksize:
170            n = min(8192, self.chunksize - self.size_read)
171            dummy = self.read(n)
172            if not dummy:
173                raise EOFError
174