1 """Simple class to read IFF chunks.
2 
3 An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
4 Format)) has the following structure:
5 
6 +----------------+
7 | ID (4 bytes)   |
8 +----------------+
9 | size (4 bytes) |
10 +----------------+
11 | data           |
12 | ...            |
13 +----------------+
14 
15 The ID is a 4-byte string which identifies the type of chunk.
16 
17 The size field (a 32-bit value, encoded using big-endian byte order)
18 gives the size of the whole chunk, including the 8-byte header.
19 
20 Usually an IFF-type file consists of one or more chunks.  The proposed
21 usage of the Chunk class defined here is to instantiate an instance at
22 the start of each chunk and read from the instance until it reaches
23 the end, after which a new instance can be instantiated.  At the end
24 of the file, creating a new instance will fail with an EOFError
25 exception.
26 
27 Usage:
28 while 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 
40 The interface is file-like.  The implemented methods are:
41 read, close, seek, tell, isatty.
42 Extra methods are: skip() (called by close, skips to the end of the chunk),
43 getname() (returns the name (ID) of the chunk)
44 
45 The __init__ method has one required argument, a file-like object
46 (including a chunk instance), and one optional argument, a flag which
47 specifies whether or not chunks are aligned on 2-byte boundaries.  The
48 default is 1, i.e. aligned.
49 """
50 
51 import warnings
52 
53 warnings._deprecated(__name__, remove=(3, 13))
54 
55 class 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