Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(217)

Side by Side Diff: client/libs/arfile/arfile.py

Issue 2049523004: luci-py: Tools for working with BSD style ar archives. (Closed) Base URL: https://github.com/luci/luci-py.git@master
Patch Set: Fixing for review. Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « client/libs/arfile/__init__.py ('k') | client/libs/arfile/arfile_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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']
M-A Ruel 2016/06/23 13:22:07 IMHO it's worse because now it is mutable. Please
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()
OLDNEW
« no previous file with comments | « client/libs/arfile/__init__.py ('k') | client/libs/arfile/arfile_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698