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