Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 # that can be found in the LICENSE file. | |
| 4 | |
| 5 import os | |
| 6 import doctest | |
| 7 import shutil | |
| 8 import stat | |
| 9 import struct | |
| 10 | |
| 11 from collections import namedtuple | |
|
M-A Ruel
2016/06/16 14:57:05
we don't do that according to the style guide
mithro
2016/06/17 06:44:50
Don't do what? I checked the CODING_STYLE.md and t
| |
| 12 from cStringIO import StringIO | |
| 13 | |
| 14 AR_MAGIC_START = '!<arch>\n' | |
| 15 AR_MAGIC_BIT = '\x60\n' | |
| 16 AR_PADDING = '\n' | |
| 17 | |
| 18 AR_FORMAT_SIMPLE = 'Simple Format' | |
| 19 AR_FORMAT_BSD = '4.4BSD Format' | |
| 20 AR_FORMAT_SYSV = 'System V / GNU Format' | |
| 21 | |
| 22 AR_DEFAULT_MTIME = 1447140471 | |
| 23 AR_DEFAULT_UID = 1000 | |
| 24 AR_DEFAULT_GID = 1000 | |
| 25 AR_DEFAULT_MODE = 0100640 # 100640 -- Octal | |
| 26 | |
| 27 _ArInfoStruct = struct.Struct('16s 12s 6s 6s 8s 10s 2s') | |
| 28 _ArFormatSimpleForbidden = " " | |
| 29 | |
| 30 _ArInfoBase = namedtuple('ArInfo', [ | |
| 31 'format', 'name', 'size', 'mtime', 'uid', 'gid', 'mode']) | |
|
M-A Ruel
2016/06/16 14:57:05
use +4
mithro
2016/06/17 06:44:50
Done.
| |
| 32 class ArInfo(_ArInfoBase): | |
| 33 """A ArInfo object represents one member in an ArFile. | |
| 34 | |
| 35 It does *not* contain the file's data. | |
| 36 """ | |
| 37 | |
| 38 @staticmethod | |
| 39 def _format(path, arformat=None): | |
| 40 """ | |
| 41 Allow forcing the format to a given type | |
| 42 >>> assert ArInfo._format('a') == AR_FORMAT_SIMPLE | |
| 43 >>> assert ArInfo._format('a', AR_FORMAT_BSD) == AR_FORMAT_BSD | |
| 44 | |
| 45 Certain file paths require the BSD format | |
| 46 >>> assert ArInfo._format('f f') == AR_FORMAT_BSD | |
| 47 >>> assert ArInfo._format('123456789abcdef..') == AR_FORMAT_BSD | |
| 48 | |
| 49 >>> ArInfo._format('123456789abcdef..', AR_FORMAT_SIMPLE) | |
| 50 Traceback (most recent call last): | |
| 51 ... | |
| 52 IOError: File name too long for format! | |
| 53 | |
| 54 >>> ArInfo._format('f f', AR_FORMAT_SIMPLE) | |
| 55 Traceback (most recent call last): | |
| 56 ... | |
| 57 IOError: File name contains forbidden character for format! | |
| 58 """ | |
| 59 if path.startswith("#1/"): | |
| 60 if arformat is None: | |
| 61 arformat = AR_FORMAT_BSD | |
| 62 elif arformat == AR_FORMAT_SIMPLE: | |
| 63 raise IOError('File name starts with special for format!') | |
| 64 | |
| 65 if len(path) >= 16: | |
| 66 if arformat is None: | |
| 67 arformat = AR_FORMAT_BSD | |
| 68 elif arformat == AR_FORMAT_SIMPLE: | |
| 69 raise IOError('File name too long for format!') | |
| 70 | |
| 71 if _ArFormatSimpleForbidden in path: | |
| 72 if arformat is None: | |
| 73 arformat = AR_FORMAT_BSD | |
| 74 elif arformat == AR_FORMAT_SIMPLE: | |
| 75 raise IOError('File name contains forbidden character for format!') | |
| 76 | |
| 77 if arformat is None: | |
| 78 arformat = AR_FORMAT_SIMPLE | |
| 79 | |
| 80 return arformat | |
| 81 | |
| 82 @property | |
| 83 def needspadding(self): | |
| 84 """ | |
| 85 >>> ArInfo(AR_FORMAT_SIMPLE, '', 10, 0, 0, 0, 0).needspadding | |
| 86 False | |
| 87 >>> ArInfo(AR_FORMAT_SIMPLE, '', 11, 0, 0, 0, 0).needspadding | |
| 88 True | |
| 89 >>> ArInfo(AR_FORMAT_BSD, 'a', 10, 0, 0, 0, 0).needspadding | |
| 90 True | |
| 91 >>> ArInfo(AR_FORMAT_BSD, 'ab', 10, 0, 0, 0, 0).needspadding | |
| 92 False | |
| 93 >>> ArInfo(AR_FORMAT_BSD, 'ab', 11, 0, 0, 0, 0).needspadding | |
| 94 True | |
| 95 >>> ArInfo(AR_FORMAT_BSD, 'ab', 12, 0, 0, 0, 0).needspadding | |
| 96 False | |
| 97 """ | |
| 98 return self.datasize % 2 != 0 | |
| 99 | |
| 100 @property | |
| 101 def datasize(self): | |
| 102 """ | |
| 103 >>> ArInfo(AR_FORMAT_SIMPLE, '', 1, 0, 0, 0, 0).datasize | |
| 104 1 | |
| 105 >>> ArInfo(AR_FORMAT_SIMPLE, '', 10, 0, 0, 0, 0).datasize | |
| 106 10 | |
| 107 >>> ArInfo(AR_FORMAT_BSD, '', 1, 0, 0, 0, 0).datasize | |
| 108 1 | |
| 109 >>> ArInfo(AR_FORMAT_BSD, 'a', 1, 0, 0, 0, 0).datasize | |
| 110 2 | |
| 111 >>> ArInfo(AR_FORMAT_BSD, '', 10, 0, 0, 0, 0).datasize | |
| 112 10 | |
| 113 >>> ArInfo(AR_FORMAT_BSD, 'abc', 10, 0, 0, 0, 0).datasize | |
| 114 13 | |
| 115 """ | |
| 116 if self.format is AR_FORMAT_SIMPLE: | |
| 117 return self.size | |
| 118 elif self.format is AR_FORMAT_BSD: | |
| 119 return len(self.name)+self.size | |
| 120 else: | |
|
M-A Ruel
2016/06/16 14:57:05
no need for else:
mithro
2016/06/17 06:44:50
Done.
| |
| 121 assert False, 'Unknown format %r' % self.format | |
| 122 | |
| 123 @classmethod | |
| 124 def fromfileobj(cls, fileobj, fullparse=True): | |
| 125 """Create and return a ArInfo object from fileobj. | |
| 126 | |
| 127 Raises IOError if the buffer is invalid. | |
| 128 """ | |
| 129 buf = fileobj.read(_ArInfoStruct.size) | |
| 130 if not buf: | |
| 131 return None | |
| 132 | |
| 133 if len(buf) < _ArInfoStruct.size: | |
| 134 raise IOError( | |
| 135 'not enough data for header, got %r, needed %r' % ( | |
| 136 len(buf), _ArInfoStruct.size)) | |
| 137 | |
| 138 name, mtime, uid, gid, mode, datasize, magic = _ArInfoStruct.unpack(buf) | |
| 139 | |
| 140 datasize = int(datasize) | |
| 141 if fullparse: | |
| 142 mtime = int(mtime) | |
| 143 uid = int(uid) | |
| 144 gid = int(gid) | |
| 145 mode = int(mode, 8) | |
| 146 | |
| 147 if name.startswith('#1/'): | |
| 148 arformat = AR_FORMAT_BSD | |
| 149 | |
| 150 try: | |
| 151 filenamesize = int(name[3:]) | |
| 152 except ValueError: | |
| 153 raise IOError('invalid file name length: %r' % name[3:]) | |
| 154 | |
| 155 filename = fileobj.read(filenamesize) | |
| 156 if len(filename) != filenamesize: | |
| 157 raise IOError( | |
| 158 'not enough data for filename, got %r, needed %r' % ( | |
| 159 len(name), filenamesize)) | |
| 160 | |
| 161 filesize = datasize - filenamesize | |
| 162 | |
| 163 elif name.startswith('/'): | |
| 164 arformat = AR_FORMAT_SYSV | |
| 165 raise SystemError('%s format is not supported.' % arformat) | |
| 166 | |
| 167 else: | |
| 168 arformat = AR_FORMAT_SIMPLE | |
| 169 filename = name.strip() | |
| 170 filesize = datasize | |
| 171 | |
| 172 if magic != AR_MAGIC_BIT: | |
| 173 raise IOError('file magic invalid, got %r, needed %r' % ( | |
| 174 magic, AR_MAGIC_BIT)) | |
| 175 | |
| 176 return cls(arformat, filename, filesize, mtime, uid, gid, mode) | |
| 177 | |
| 178 @classmethod | |
| 179 def frompath(cls, path, arformat=None): | |
| 180 """Return an ArInfo object from a file path for information.""" | |
| 181 st = os.stat(path) | |
| 182 | |
| 183 if not stat.S_ISREG(st): | |
| 184 raise IOError('Only work on regular files.') | |
| 185 | |
| 186 return cls( | |
| 187 cls._format(path, arformat), path, | |
| 188 st.st_size, st.st_mtime, st.st_uid, st.st_gid, st.st_mode) | |
| 189 | |
| 190 @classmethod | |
| 191 def fromdefault(cls, path, size, arformat=None): | |
| 192 """Return an ArInfo object using name and size (with defaults elsewhere). | |
| 193 | |
| 194 Only a file's name and content are needed to create the ArInfo, all of the | |
| 195 modification time, user, group and mode information will be set to default | |
| 196 values. This means that you don't need to perform an expensive stat the | |
| 197 file. | |
| 198 | |
| 199 >>> ai = ArInfo.fromdefault('abc123', 10) | |
| 200 >>> ai.name | |
| 201 'abc123' | |
| 202 >>> ai.size | |
| 203 10 | |
| 204 >>> assert ai.mtime == AR_DEFAULT_MTIME | |
| 205 >>> assert ai.uid == AR_DEFAULT_UID | |
| 206 >>> assert ai.gid == AR_DEFAULT_GID | |
| 207 >>> assert ai.mode == AR_DEFAULT_MODE | |
| 208 """ | |
| 209 return cls( | |
| 210 cls._format(path, arformat), path, size, | |
|
M-A Ruel
2016/06/16 14:57:05
align arguments at +4 everywhere
mithro
2016/06/17 06:44:50
I think I've gotten them all.
Any idea why your p
| |
| 211 AR_DEFAULT_MTIME, AR_DEFAULT_UID, AR_DEFAULT_GID, AR_DEFAULT_MODE) | |
| 212 | |
| 213 def tofileobj(self, fileobj): | |
| 214 """Create a string buffer from a TarInfo object.""" | |
| 215 # File name, 16 bytes | |
| 216 if self.format is AR_FORMAT_SIMPLE: | |
| 217 assert len(self.name) < 16 | |
| 218 fileobj.write('%-16s' % self.name) | |
| 219 datasize = self.size | |
| 220 elif self.format is AR_FORMAT_BSD: | |
| 221 fileobj.write('#1/%-13s' % str(len(self.name))) | |
| 222 datasize = self.size + len(self.name) | |
| 223 | |
| 224 # Modtime, 12 bytes | |
| 225 fileobj.write('%-12i' % self.mtime) | |
| 226 # Owner ID, 6 bytes | |
| 227 fileobj.write('%-6i' % self.uid) | |
| 228 # Group ID, 6 bytes | |
| 229 fileobj.write('%-6i' % self.gid) | |
| 230 # File mode, 8 bytes | |
| 231 fileobj.write('%-8o' % self.mode) | |
| 232 # File size, 10 bytes | |
| 233 fileobj.write('%-10s' % datasize) | |
| 234 # File magic, 2 bytes | |
| 235 fileobj.write(AR_MAGIC_BIT) | |
| 236 | |
| 237 # Filename - BSD variant | |
| 238 if self.format is AR_FORMAT_BSD: | |
| 239 fileobj.write(self.name) | |
| 240 | |
| 241 | |
| 242 class ArFileReader(object): | |
| 243 """Read an ar archive from the given input buffer.""" | |
| 244 | |
| 245 def __init__(self, fileobj, fullparse=True): | |
| 246 self.fullparse = fullparse | |
| 247 self.fileobj = fileobj | |
| 248 | |
| 249 magic = self.fileobj.read(len(AR_MAGIC_START)) | |
| 250 if magic != AR_MAGIC_START: | |
| 251 raise IOError( | |
| 252 'Not an ar file, invalid magic, got %r, wanted %r.' % ( | |
| 253 magic, AR_MAGIC_START)) | |
| 254 | |
| 255 def __iter__(self): | |
| 256 while True: | |
| 257 if self.fileobj.closed: | |
| 258 raise IOError('Tried to read after the file closed.') | |
| 259 ai = ArInfo.fromfileobj(self.fileobj, self.fullparse) | |
| 260 if ai is None: | |
| 261 return | |
| 262 | |
| 263 start = self.fileobj.tell() | |
| 264 yield ai, self.fileobj | |
| 265 end = self.fileobj.tell() | |
| 266 | |
| 267 read = end - start | |
| 268 # If the reader didn't touch the input buffer, seek past the file. | |
| 269 if read == 0: | |
| 270 self.fileobj.seek(ai.size, os.SEEK_CUR) | |
| 271 elif read != ai.size: | |
| 272 raise IOError( | |
| 273 'Wrong amount of data read from fileobj! got %i, wanted %i' % ( | |
| 274 read, ai.size)) | |
| 275 | |
| 276 if ai.needspadding: | |
| 277 padding = self.fileobj.read(len(AR_PADDING)) | |
| 278 if padding != AR_PADDING: | |
| 279 raise IOError( | |
| 280 'incorrect padding, got %r, wanted %r' % (padding, AR_PADDING)) | |
| 281 | |
| 282 def close(self): | |
| 283 """Close the archive. | |
| 284 | |
| 285 Will close the output buffer.""" | |
| 286 self.fileobj.close() | |
| 287 | |
| 288 | |
| 289 class ArFileWriter(object): | |
| 290 """Write an ar archive from the given output buffer.""" | |
| 291 | |
| 292 def __init__(self, fileobj): | |
| 293 self.fileobj = fileobj | |
| 294 self.fileobj.write(AR_MAGIC_START) | |
| 295 | |
| 296 def addfile(self, arinfo, fileobj=None): | |
| 297 if not fileobj and arinfo.size != 0: | |
| 298 raise ValueError('Need to supply fileobj if file is non-zero in side.') | |
| 299 | |
| 300 arinfo.tofileobj(self.fileobj) | |
| 301 if fileobj: | |
| 302 shutil.copyfileobj(fileobj, self.fileobj, arinfo.size) | |
| 303 | |
| 304 if arinfo.needspadding: | |
| 305 self.fileobj.write(AR_PADDING) | |
| 306 | |
| 307 def flush(self): | |
| 308 """Flush the output buffer.""" | |
| 309 self.fileobj.flush() | |
| 310 | |
| 311 def close(self): | |
| 312 """Close the archive. | |
| 313 | |
| 314 Will close the output buffer.""" | |
| 315 self.fileobj.close() | |
| 316 | |
| 317 | |
| 318 def is_arfile(name): | |
| 319 return file(name, 'rb').read(len(AR_MAGIC_START)) == AR_MAGIC_START | |
|
M-A Ruel
2016/06/16 14:57:05
with a with statement
mithro
2016/06/17 06:44:50
Done.
| |
| 320 | |
| 321 | |
| 322 # pylint: disable=redefined-builtin | |
| 323 def open(name=None, mode='r', fileobj=None): | |
| 324 if name is None and fileobj is None: | |
| 325 raise ValueError('Nothing to open!') | |
| 326 | |
| 327 if name is not None: | |
| 328 if fileobj is not None: | |
| 329 raise ValueError('Provided both a file name and file object!') | |
| 330 fileobj = file(name, mode+'b') | |
| 331 | |
| 332 if 'b' not in fileobj.mode: | |
| 333 raise ValueError('File object not open in binary mode.') | |
| 334 | |
| 335 if mode == 'rb': | |
| 336 return ArFileReader(fileobj) | |
| 337 elif mode == 'wb': | |
| 338 return ArFileWriter(fileobj) | |
| 339 else: | |
|
M-A Ruel
2016/06/16 14:57:05
no need for else
mithro
2016/06/17 06:44:50
Done.
| |
| 340 raise ValueError('Unknown file mode.') | |
| 341 | |
| 342 | |
| 343 if __name__ == '__main__': | |
| 344 doctest.testmod() | |
| OLD | NEW |