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

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: Reworking API to better match zipfile/tarfile API. 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
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 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()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698