52e45d3886cd464ab9244d385fc8c8fb1ce251a3
[portage.git] / pym / portage / checksum.py
1 # checksum.py -- core Portage functionality
2 # Copyright 1998-2011 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 import portage
6 from portage.const import PRELINK_BINARY,HASHING_BLOCKSIZE
7 from portage.localization import _
8 from portage import os
9 from portage import _encodings
10 from portage import _unicode_encode
11 import errno
12 import stat
13 import tempfile
14
15 #dict of all available hash functions
16 hashfunc_map = {}
17 hashorigin_map = {}
18
19 class _generate_hash_function(object):
20
21         __slots__ = ("_hashobject",)
22
23         def __init__(self, hashtype, hashobject, origin="unknown"):
24                 self._hashobject = hashobject
25                 hashfunc_map[hashtype] = self
26                 hashorigin_map[hashtype] = origin
27
28         def __call__(self, filename):
29                 """
30                 Run a checksum against a file.
31         
32                 @param filename: File to run the checksum against
33                 @type filename: String
34                 @return: The hash and size of the data
35                 """
36                 try:
37                         f = open(_unicode_encode(filename,
38                                 encoding=_encodings['fs'], errors='strict'), 'rb')
39                 except IOError as e:
40                         func_call = "open('%s')" % filename
41                         if e.errno == errno.EPERM:
42                                 raise portage.exception.OperationNotPermitted(func_call)
43                         elif e.errno == errno.EACCES:
44                                 raise portage.exception.PermissionDenied(func_call)
45                         elif e.errno == errno.ENOENT:
46                                 raise portage.exception.FileNotFound(filename)
47                         else:
48                                 raise
49                 blocksize = HASHING_BLOCKSIZE
50                 data = f.read(blocksize)
51                 size = 0
52                 checksum = self._hashobject()
53                 while data:
54                         checksum.update(data)
55                         size = size + len(data)
56                         data = f.read(blocksize)
57                 f.close()
58
59                 return (checksum.hexdigest(), size)
60
61 # Define hash functions, try to use the best module available. Later definitions
62 # override earlier ones
63
64 # Use the internal modules as last fallback
65 try:
66         from hashlib import md5 as _new_md5
67 except ImportError:
68         from md5 import new as _new_md5
69
70 md5hash = _generate_hash_function("MD5", _new_md5, origin="internal")
71
72 try:
73         from hashlib import sha1 as _new_sha1
74 except ImportError:
75         from sha import new as _new_sha1
76
77 sha1hash = _generate_hash_function("SHA1", _new_sha1, origin="internal")
78
79 # Use pycrypto when available, prefer it over the internal fallbacks
80 try:
81         from Crypto.Hash import SHA256, RIPEMD
82         sha256hash = _generate_hash_function("SHA256", SHA256.new, origin="pycrypto")
83         rmd160hash = _generate_hash_function("RMD160", RIPEMD.new, origin="pycrypto")
84 except ImportError as e:
85         pass
86
87 # Use hashlib from python-2.5 if available and prefer it over pycrypto and internal fallbacks.
88 # Need special handling for RMD160 as it may not always be provided by hashlib.
89 try:
90         import hashlib
91         
92         md5hash = _generate_hash_function("MD5", hashlib.md5, origin="hashlib")
93         sha1hash = _generate_hash_function("SHA1", hashlib.sha1, origin="hashlib")
94         sha256hash = _generate_hash_function("SHA256", hashlib.sha256, origin="hashlib")
95         try:
96                 hashlib.new('ripemd160')
97         except ValueError:
98                 pass
99         else:
100                 def rmd160():
101                         return hashlib.new('ripemd160')
102                 rmd160hash = _generate_hash_function("RMD160", rmd160, origin="hashlib")
103 except ImportError as e:
104         pass
105         
106
107 # Use python-fchksum if available, prefer it over all other MD5 implementations
108 try:
109         import fchksum
110         
111         def md5hash(filename):
112                 return fchksum.fmd5t(filename)
113         hashfunc_map["MD5"] = md5hash
114         hashorigin_map["MD5"] = "python-fchksum"
115
116 except ImportError:
117         pass
118
119 # There is only one implementation for size
120 def getsize(filename):
121         size = os.stat(filename).st_size
122         return (size, size)
123 hashfunc_map["size"] = getsize
124
125 # end actual hash functions
126
127 prelink_capable = False
128 if os.path.exists(PRELINK_BINARY):
129         results = portage.subprocess_getstatusoutput(
130                 "%s --version > /dev/null 2>&1" % (PRELINK_BINARY,))
131         if (results[0] >> 8) == 0:
132                 prelink_capable=1
133         del results
134
135 def perform_md5(x, calc_prelink=0):
136         return perform_checksum(x, "MD5", calc_prelink)[0]
137
138 def _perform_md5_merge(x, **kwargs):
139         return perform_md5(_unicode_encode(x,
140                 encoding=_encodings['merge'], errors='strict'), **kwargs)
141
142 def perform_all(x, calc_prelink=0):
143         mydict = {}
144         for k in hashfunc_map:
145                 mydict[k] = perform_checksum(x, hashfunc_map[k], calc_prelink)[0]
146         return mydict
147
148 def get_valid_checksum_keys():
149         return list(hashfunc_map)
150
151 def get_hash_origin(hashtype):
152         if hashtype not in hashfunc_map:
153                 raise KeyError(hashtype)
154         return hashorigin_map.get(hashtype, "unknown")
155
156 def verify_all(filename, mydict, calc_prelink=0, strict=0):
157         """
158         Verify all checksums against a file.
159
160         @param filename: File to run the checksums against
161         @type filename: String
162         @param calc_prelink: Whether or not to reverse prelink before running the checksum
163         @type calc_prelink: Integer
164         @param strict: Enable/Disable strict checking (which stops exactly at a checksum failure and throws an exception)
165         @type strict: Integer
166         @rtype: Tuple
167         @return: Result of the checks and possible message:
168                 1) If size fails, False, and a tuple containing a message, the given size, and the actual size
169                 2) If there is an os error, False, and a tuple containing the system error followed by 2 nulls
170                 3) If a checksum fails, False and a tuple containing a message, the given hash, and the actual hash
171                 4) If all checks succeed, return True and a fake reason
172         """
173         # Dict relates to single file only.
174         # returns: (passed,reason)
175         file_is_ok = True
176         reason     = "Reason unknown"
177         try:
178                 mysize = os.stat(filename)[stat.ST_SIZE]
179                 if mydict["size"] != mysize:
180                         return False,(_("Filesize does not match recorded size"), mysize, mydict["size"])
181         except OSError as e:
182                 if e.errno == errno.ENOENT:
183                         raise portage.exception.FileNotFound(filename)
184                 return False, (str(e), None, None)
185
186         verifiable_hash_types = set(mydict).intersection(hashfunc_map)
187         verifiable_hash_types.discard("size")
188         if not verifiable_hash_types:
189                 expected = set(hashfunc_map)
190                 expected.discard("size")
191                 expected = list(expected)
192                 expected.sort()
193                 expected = " ".join(expected)
194                 got = set(mydict)
195                 got.discard("size")
196                 got = list(got)
197                 got.sort()
198                 got = " ".join(got)
199                 return False, (_("Insufficient data for checksum verification"), got, expected)
200
201         for x in sorted(mydict):
202                 if   x == "size":
203                         continue
204                 elif x in hashfunc_map:
205                         myhash = perform_checksum(filename, x, calc_prelink=calc_prelink)[0]
206                         if mydict[x] != myhash:
207                                 if strict:
208                                         raise portage.exception.DigestException(
209                                                 ("Failed to verify '$(file)s' on " + \
210                                                 "checksum type '%(type)s'") % \
211                                                 {"file" : filename, "type" : x})
212                                 else:
213                                         file_is_ok = False
214                                         reason     = (("Failed on %s verification" % x), myhash,mydict[x])
215                                         break
216         return file_is_ok,reason
217
218 def perform_checksum(filename, hashname="MD5", calc_prelink=0):
219         """
220         Run a specific checksum against a file. The filename can
221         be either unicode or an encoded byte string. If filename
222         is unicode then a UnicodeDecodeError will be raised if
223         necessary.
224
225         @param filename: File to run the checksum against
226         @type filename: String
227         @param hashname: The type of hash function to run
228         @type hashname: String
229         @param calc_prelink: Whether or not to reverse prelink before running the checksum
230         @type calc_prelink: Integer
231         @rtype: Tuple
232         @return: The hash and size of the data
233         """
234         global prelink_capable
235         # Make sure filename is encoded with the correct encoding before
236         # it is passed to spawn (for prelink) and/or the hash function.
237         filename = _unicode_encode(filename,
238                 encoding=_encodings['fs'], errors='strict')
239         myfilename = filename
240         prelink_tmpfile = None
241         try:
242                 if calc_prelink and prelink_capable:
243                         # Create non-prelinked temporary file to checksum.
244                         # Files rejected by prelink are summed in place.
245                         try:
246                                 tmpfile_fd, prelink_tmpfile = tempfile.mkstemp()
247                                 try:
248                                         retval = portage.process.spawn([PRELINK_BINARY,
249                                                 "--verify", filename], fd_pipes={1:tmpfile_fd})
250                                 finally:
251                                         os.close(tmpfile_fd)
252                                 if retval == os.EX_OK:
253                                         myfilename = prelink_tmpfile
254                         except portage.exception.CommandNotFound:
255                                 # This happens during uninstallation of prelink.
256                                 prelink_capable = False
257                 try:
258                         if hashname not in hashfunc_map:
259                                 raise portage.exception.DigestException(hashname + \
260                                         " hash function not available (needs dev-python/pycrypto)")
261                         myhash, mysize = hashfunc_map[hashname](myfilename)
262                 except (OSError, IOError) as e:
263                         if e.errno == errno.ENOENT:
264                                 raise portage.exception.FileNotFound(myfilename)
265                         raise
266                 return myhash, mysize
267         finally:
268                 if prelink_tmpfile:
269                         try:
270                                 os.unlink(prelink_tmpfile)
271                         except OSError as e:
272                                 if e.errno != errno.ENOENT:
273                                         raise
274                                 del e
275
276 def perform_multiple_checksums(filename, hashes=["MD5"], calc_prelink=0):
277         """
278         Run a group of checksums against a file.
279
280         @param filename: File to run the checksums against
281         @type filename: String
282         @param hashes: A list of checksum functions to run against the file
283         @type hashname: List
284         @param calc_prelink: Whether or not to reverse prelink before running the checksum
285         @type calc_prelink: Integer
286         @rtype: Tuple
287         @return: A dictionary in the form:
288                 return_value[hash_name] = (hash_result,size)
289                 for each given checksum
290         """
291         rVal = {}
292         for x in hashes:
293                 if x not in hashfunc_map:
294                         raise portage.exception.DigestException(x+" hash function not available (needs dev-python/pycrypto or >=dev-lang/python-2.5)")
295                 rVal[x] = perform_checksum(filename, x, calc_prelink)[0]
296         return rVal