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