python: Improve and test Context.wait.
[gpgme.git] / lang / python / pyme / core.py
1 # $Id$
2 # Copyright (C) 2004,2008 Igor Belyi <belyi@users.sourceforge.net>
3 # Copyright (C) 2002 John Goerzen <jgoerzen@complete.org>
4 #
5 #    This library is free software; you can redistribute it and/or
6 #    modify it under the terms of the GNU Lesser General Public
7 #    License as published by the Free Software Foundation; either
8 #    version 2.1 of the License, or (at your option) any later version.
9 #
10 #    This library is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #    Lesser General Public License for more details.
14 #
15 #    You should have received a copy of the GNU Lesser General Public
16 #    License along with this library; if not, write to the Free Software
17 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
18
19 # import generators for portability with python2.2
20
21
22 from . import pygpgme
23 from .errors import errorcheck, GPGMEError
24 from . import errors
25
26 class GpgmeWrapper(object):
27     """Base class all Pyme wrappers for GPGME functionality.  Not to be
28     instantiated directly."""
29
30     def __init__(self, wrapped):
31         self._callback_excinfo = None
32         self.wrapped = wrapped
33
34     def __repr__(self):
35         return '<instance of %s.%s with GPG object at %s>' % \
36                (__name__, self.__class__.__name__,
37                 self.wrapped)
38
39     def __str__(self):
40         return repr(self)
41
42     def __hash__(self):
43         return hash(repr(self.wrapped))
44
45     def __eq__(self, other):
46         if other == None:
47             return False
48         else:
49             return repr(self.wrapped) == repr(other.wrapped)
50
51     def _getctype(self):
52         """Must be implemented by child classes.
53
54         Must return the name of the c type."""
55         raise NotImplementedError()
56
57     def _getnameprepend(self):
58         """Must be implemented by child classes.
59
60         Must return the prefix of all c functions mapped to methods of
61         this class."""
62         raise NotImplementedError()
63
64     def _errorcheck(self, name):
65         """Must be implemented by child classes.
66
67         This function must return a trueish value for all c functions
68         returning gpgme_error_t."""
69         raise NotImplementedError()
70
71     def __getattr__(self, key):
72         """On-the-fly function generation."""
73         if key[0] == '_' or self._getnameprepend() == None:
74             return None
75         name = self._getnameprepend() + key
76         func = getattr(pygpgme, name)
77
78         if self._errorcheck(name):
79             def _funcwrap(slf, *args, **kwargs):
80                 result = func(slf.wrapped, *args, **kwargs)
81                 if slf._callback_excinfo:
82                     pygpgme.pygpgme_raise_callback_exception(slf)
83                 return errorcheck(result, "Invocation of " + name)
84         else:
85             def _funcwrap(slf, *args, **kwargs):
86                 result = func(slf.wrapped, *args, **kwargs)
87                 if slf._callback_excinfo:
88                     pygpgme.pygpgme_raise_callback_exception(slf)
89                 return result
90
91         _funcwrap.__doc__ = getattr(func, "__doc__")
92
93         # Monkey-patch the class.
94         setattr(self.__class__, key, _funcwrap)
95
96         # Bind the method to 'self'.
97         def wrapper(*args, **kwargs):
98             return _funcwrap(self, *args, **kwargs)
99         _funcwrap.__doc__ = getattr(func, "__doc__")
100
101         return wrapper
102
103 class Context(GpgmeWrapper):
104     """From the GPGME C documentation:
105
106     * All cryptographic operations in GPGME are performed within a
107     * context, which contains the internal state of the operation as well as
108     * configuration parameters.  By using several contexts you can run
109     * several cryptographic operations in parallel, with different
110     * configuration.
111
112     Thus, this is the place that you will usually start."""
113
114     def _getctype(self):
115         return 'gpgme_ctx_t'
116
117     def _getnameprepend(self):
118         return 'gpgme_'
119
120     def _errorcheck(self, name):
121         """This function should list all functions returning gpgme_error_t"""
122         if (name.startswith('gpgme_op_') and \
123             not name.endswith('_result')) or \
124             name == 'gpgme_signers_add' or \
125             name == 'gpgme_set_locale' or \
126             name == 'gpgme_set_keylist_mode' or \
127             name == 'gpgme_set_protocol':
128             return 1
129         return 0
130
131     def __init__(self, wrapped=None):
132         if wrapped:
133             self.own = False
134         else:
135             tmp = pygpgme.new_gpgme_ctx_t_p()
136             errorcheck(pygpgme.gpgme_new(tmp))
137             wrapped = pygpgme.gpgme_ctx_t_p_value(tmp)
138             pygpgme.delete_gpgme_ctx_t_p(tmp)
139             self.own = True
140         super().__init__(wrapped)
141         self.last_passcb = None
142         self.last_progresscb = None
143         self.last_statuscb = None
144
145     def __del__(self):
146         if not pygpgme:
147             # At interpreter shutdown, pygpgme is set to NONE.
148             return
149
150         self._free_passcb()
151         self._free_progresscb()
152         if self.own and pygpgme.gpgme_release:
153             pygpgme.gpgme_release(self.wrapped)
154
155     def _free_passcb(self):
156         if self.last_passcb != None:
157             if pygpgme.pygpgme_clear_generic_cb:
158                 pygpgme.pygpgme_clear_generic_cb(self.last_passcb)
159             if pygpgme.delete_PyObject_p_p:
160                 pygpgme.delete_PyObject_p_p(self.last_passcb)
161             self.last_passcb = None
162
163     def _free_progresscb(self):
164         if self.last_progresscb != None:
165             if pygpgme.pygpgme_clear_generic_cb:
166                 pygpgme.pygpgme_clear_generic_cb(self.last_progresscb)
167             if pygpgme.delete_PyObject_p_p:
168                 pygpgme.delete_PyObject_p_p(self.last_progresscb)
169             self.last_progresscb = None
170
171     def _free_statuscb(self):
172         if self.last_statuscb != None:
173             if pygpgme.pygpgme_clear_generic_cb:
174                 pygpgme.pygpgme_clear_generic_cb(self.last_statuscb)
175             if pygpgme.delete_PyObject_p_p:
176                 pygpgme.delete_PyObject_p_p(self.last_statuscb)
177             self.last_statuscb = None
178
179     def op_keylist_all(self, *args, **kwargs):
180         self.op_keylist_start(*args, **kwargs)
181         key = self.op_keylist_next()
182         while key:
183             yield key
184             key = self.op_keylist_next()
185
186     def op_keylist_next(self):
187         """Returns the next key in the list created
188         by a call to op_keylist_start().  The object returned
189         is of type Key."""
190         ptr = pygpgme.new_gpgme_key_t_p()
191         try:
192             errorcheck(pygpgme.gpgme_op_keylist_next(self.wrapped, ptr))
193             key = pygpgme.gpgme_key_t_p_value(ptr)
194         except errors.GPGMEError as excp:
195             key = None
196             if excp.getcode() != errors.EOF:
197                 raise excp
198         pygpgme.delete_gpgme_key_t_p(ptr)
199         if key:
200             key.__del__ = lambda self: pygpgme.gpgme_key_unref(self)
201             return key
202
203     def get_key(self, fpr, secret):
204         """Return the key corresponding to the fingerprint 'fpr'"""
205         ptr = pygpgme.new_gpgme_key_t_p()
206         errorcheck(pygpgme.gpgme_get_key(self.wrapped, fpr, ptr, secret))
207         key = pygpgme.gpgme_key_t_p_value(ptr)
208         pygpgme.delete_gpgme_key_t_p(ptr)
209         if key:
210             key.__del__ = lambda self: pygpgme.gpgme_key_unref(self)
211             return key
212
213     def op_trustlist_all(self, *args, **kwargs):
214         self.op_trustlist_start(*args, **kwargs)
215         trust = self.ctx.op_trustlist_next()
216         while trust:
217             yield trust
218             trust = self.ctx.op_trustlist_next()
219
220     def op_trustlist_next(self):
221         """Returns the next trust item in the list created
222         by a call to op_trustlist_start().  The object returned
223         is of type TrustItem."""
224         ptr = pygpgme.new_gpgme_trust_item_t_p()
225         try:
226             errorcheck(pygpgme.gpgme_op_trustlist_next(self.wrapped, ptr))
227             trust = pygpgme.gpgme_trust_item_t_p_value(ptr)
228         except errors.GPGMEError as excp:
229             trust = None
230             if excp.getcode() != errors.EOF:
231                 raise
232         pygpgme.delete_gpgme_trust_item_t_p(ptr)
233         return trust
234
235     def set_passphrase_cb(self, func, hook=None):
236         """Sets the passphrase callback to the function specified by func.
237
238         When the system needs a passphrase, it will call func with three args:
239         hint, a string describing the key it needs the passphrase for;
240         desc, a string describing the passphrase it needs;
241         prev_bad, a boolean equal True if this is a call made after
242         unsuccessful previous attempt.
243
244         If hook has a value other than None it will be passed into the func
245         as a forth argument.
246
247         Please see the GPGME manual for more information.
248         """
249         self._free_passcb()
250         if func == None:
251             hookdata = None
252         else:
253             self.last_passcb = pygpgme.new_PyObject_p_p()
254             if hook == None:
255                 hookdata = (self, func)
256             else:
257                 hookdata = (self, func, hook)
258         pygpgme.pygpgme_set_passphrase_cb(self.wrapped, hookdata, self.last_passcb)
259
260     def set_progress_cb(self, func, hook=None):
261         """Sets the progress meter callback to the function specified by FUNC.
262         If FUNC is None, the callback will be cleared.
263
264         This function will be called to provide an interactive update
265         of the system's progress.  The function will be called with
266         three arguments, type, total, and current.  If HOOK is not
267         None, it will be supplied as fourth argument.
268
269         Please see the GPGME manual for more information.
270
271         """
272         self._free_progresscb()
273         if func == None:
274             hookdata = None
275         else:
276             self.last_progresscb = pygpgme.new_PyObject_p_p()
277             if hook == None:
278                 hookdata = (self, func)
279             else:
280                 hookdata = (self, func, hook)
281         pygpgme.pygpgme_set_progress_cb(self.wrapped, hookdata, self.last_progresscb)
282
283     def set_status_cb(self, func, hook=None):
284         """Sets the status callback to the function specified by FUNC.  If
285         FUNC is None, the callback will be cleared.
286
287         The function will be called with two arguments, keyword and
288         args.  If HOOK is not None, it will be supplied as third
289         argument.
290
291         Please see the GPGME manual for more information.
292
293         """
294         self._free_statuscb()
295         if func == None:
296             hookdata = None
297         else:
298             self.last_statuscb = pygpgme.new_PyObject_p_p()
299             if hook == None:
300                 hookdata = (self, func)
301             else:
302                 hookdata = (self, func, hook)
303         pygpgme.pygpgme_set_status_cb(self.wrapped, hookdata,
304                                       self.last_statuscb)
305
306     def get_engine_info(self):
307         """Returns this context specific engine info"""
308         return pygpgme.gpgme_ctx_get_engine_info(self.wrapped)
309
310     def set_engine_info(self, proto, file_name, home_dir=None):
311         """Changes the configuration of the crypto engine implementing the
312     protocol 'proto' for the context. 'file_name' is the file name of
313     the executable program implementing this protocol. 'home_dir' is the
314     directory name of the configuration directory (engine's default is
315     used if omitted)."""
316         errorcheck(pygpgme.gpgme_ctx_set_engine_info(self.wrapped, proto, file_name, home_dir))
317
318     def wait(self, hang):
319         """Wait for asynchronous call to finish. Wait forever if hang is True.
320         Raises an exception on errors.
321
322         Please read the GPGME manual for more information.
323
324         """
325         ptr = pygpgme.new_gpgme_error_t_p()
326         pygpgme.gpgme_wait(self.wrapped, ptr, hang)
327         status = pygpgme.gpgme_error_t_p_value(ptr)
328         pygpgme.delete_gpgme_error_t_p(ptr)
329         errorcheck(status)
330
331     def op_edit(self, key, func, fnc_value, out):
332         """Start key editing using supplied callback function"""
333         if key == None:
334             raise ValueError("op_edit: First argument cannot be None")
335         if fnc_value:
336             opaquedata = (self, func, fnc_value)
337         else:
338             opaquedata = (self, func)
339
340         result = pygpgme.gpgme_op_edit(self.wrapped, key, opaquedata, out)
341         if self._callback_excinfo:
342             pygpgme.pygpgme_raise_callback_exception(self)
343         errorcheck(result)
344
345 class Data(GpgmeWrapper):
346     """From the GPGME C manual:
347
348 * A lot of data has to be exchanged between the user and the crypto
349 * engine, like plaintext messages, ciphertext, signatures and information
350 * about the keys.  The technical details about exchanging the data
351 * information are completely abstracted by GPGME.  The user provides and
352 * receives the data via `gpgme_data_t' objects, regardless of the
353 * communication protocol between GPGME and the crypto engine in use.
354
355         This Data class is the implementation of the GpgmeData objects.
356
357         Please see the information about __init__ for instantiation."""
358
359     def _getctype(self):
360         return 'gpgme_data_t'
361
362     def _getnameprepend(self):
363         return 'gpgme_data_'
364
365     def _errorcheck(self, name):
366         """This function should list all functions returning gpgme_error_t"""
367         if name == 'gpgme_data_release_and_get_mem' or \
368                name == 'gpgme_data_get_encoding' or \
369                name == 'gpgme_data_seek':
370             return 0
371         return 1
372
373     def __init__(self, string = None, file = None, offset = None,
374                  length = None, cbs = None):
375         """Initialize a new gpgme_data_t object.
376
377         If no args are specified, make it an empty object.
378
379         If string alone is specified, initialize it with the data
380         contained there.
381
382         If file, offset, and length are all specified, file must
383         be either a filename or a file-like object, and the object
384         will be initialized by reading the specified chunk from the file.
385
386         If cbs is specified, it MUST be a tuple of the form:
387
388         ((read_cb, write_cb, seek_cb, release_cb), hook)
389
390         where func is a callback function taking two arguments (count,
391         hook) and returning a string of read data, or None on EOF.
392         This will supply the read() method for the system.
393
394         If file is specified without any other arguments, then
395         it must be a filename, and the object will be initialized from
396         that file.
397
398         Any other use will result in undefined or erroneous behavior."""
399         super().__init__(None)
400         self.last_readcb = None
401
402         if cbs != None:
403             self.new_from_cbs(*cbs)
404         elif string != None:
405             self.new_from_mem(string)
406         elif file != None and offset != None and length != None:
407             self.new_from_filepart(file, offset, length)
408         elif file != None:
409             if type(file) == type("x"):
410                 self.new_from_file(file)
411             else:
412                 self.new_from_fd(file)
413         else:
414             self.new()
415
416     def __del__(self):
417         if not pygpgme:
418             # At interpreter shutdown, pygpgme is set to NONE.
419             return
420
421         if self.wrapped != None and pygpgme.gpgme_data_release:
422             pygpgme.gpgme_data_release(self.wrapped)
423         self._free_readcb()
424
425     def _free_readcb(self):
426         if self.last_readcb != None:
427             if pygpgme.pygpgme_clear_generic_cb:
428                 pygpgme.pygpgme_clear_generic_cb(self.last_readcb)
429             if pygpgme.delete_PyObject_p_p:
430                 pygpgme.delete_PyObject_p_p(self.last_readcb)
431             self.last_readcb = None
432
433     def new(self):
434         tmp = pygpgme.new_gpgme_data_t_p()
435         errorcheck(pygpgme.gpgme_data_new(tmp))
436         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
437         pygpgme.delete_gpgme_data_t_p(tmp)
438
439     def new_from_mem(self, string, copy = 1):
440         tmp = pygpgme.new_gpgme_data_t_p()
441         errorcheck(pygpgme.gpgme_data_new_from_mem(tmp,string,len(string),copy))
442         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
443         pygpgme.delete_gpgme_data_t_p(tmp)
444
445     def new_from_file(self, filename, copy = 1):
446         tmp = pygpgme.new_gpgme_data_t_p()
447         errorcheck(pygpgme.gpgme_data_new_from_file(tmp, filename, copy))
448         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
449         pygpgme.delete_gpgme_data_t_p(tmp)
450
451     def new_from_cbs(self, funcs, hook):
452         """Argument funcs must be a 4 element tuple with callbacks:
453         (read_cb, write_cb, seek_cb, release_cb)"""
454         tmp = pygpgme.new_gpgme_data_t_p()
455         self._free_readcb()
456         self.last_readcb = pygpgme.new_PyObject_p_p()
457         hookdata = (funcs, hook)
458         pygpgme.pygpgme_data_new_from_cbs(tmp, hookdata, self.last_readcb)
459         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
460         pygpgme.delete_gpgme_data_t_p(tmp)
461
462     def new_from_filepart(self, file, offset, length):
463         """This wraps the GPGME gpgme_data_new_from_filepart() function.
464         The argument "file" may be:
465
466         1. a string specifying a file name, or
467         3. a a file-like object. supporting the fileno() call and the mode
468            attribute."""
469
470         tmp = pygpgme.new_gpgme_data_t_p()
471         filename = None
472         fp = None
473
474         if type(file) == type("x"):
475             filename = file
476         else:
477             fp = pygpgme.fdopen(file.fileno(), file.mode)
478             if fp == None:
479                 raise ValueError("Failed to open file from %s arg %s" % \
480                       (str(type(file)), str(file)))
481
482         errorcheck(pygpgme.gpgme_data_new_from_filepart(tmp, filename, fp,
483                                                       offset, length))
484         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
485         pygpgme.delete_gpgme_data_t_p(tmp)
486
487     def new_from_fd(self, file):
488         """This wraps the GPGME gpgme_data_new_from_fd() function.
489         The argument "file" may be a file-like object, supporting the fileno()
490         call and the mode attribute."""
491
492         tmp = pygpgme.new_gpgme_data_t_p()
493         fp = pygpgme.fdopen(file.fileno(), file.mode)
494         if fp == None:
495             raise ValueError("Failed to open file from %s arg %s" % \
496                   (str(type(file)), str(file)))
497         errorcheck(pygpgme.gpgme_data_new_from_fd(tmp, fp))
498         self.wrapped = pygpgme.gpgme_data_t_p_value(tmp)
499         pygpgme.delete_gpgme_data_t_p(tmp)
500
501     def new_from_stream(self, file):
502         """This wrap around gpgme_data_new_from_stream is an alias for
503         new_from_fd() method since in python there's not difference
504         between file stream and file descriptor"""
505         self.new_from_fd(file)
506
507     def write(self, buffer):
508         """Write buffer given as string or bytes.
509
510         If a string is given, it is implicitly encoded using UTF-8."""
511         written = pygpgme.gpgme_data_write(self.wrapped, buffer)
512         if written < 0:
513             raise GPGMEError.fromSyserror()
514         return written
515
516     def read(self, size = -1):
517         """Read at most size bytes, returned as bytes.
518
519         If the size argument is negative or omitted, read until EOF is reached.
520
521         Returns the data read, or the empty string if there was no data
522         to read before EOF was reached."""
523
524         if size == 0:
525             return ''
526
527         if size > 0:
528             return pygpgme.gpgme_data_read(self.wrapped, size)
529         else:
530             chunks = []
531             while 1:
532                 result = pygpgme.gpgme_data_read(self.wrapped, 4096)
533                 if len(result) == 0:
534                     break
535                 chunks.append(result)
536             return b''.join(chunks)
537
538 def pubkey_algo_name(algo):
539     return pygpgme.gpgme_pubkey_algo_name(algo)
540
541 def hash_algo_name(algo):
542     return pygpgme.gpgme_hash_algo_name(algo)
543
544 def get_protocol_name(proto):
545     return pygpgme.gpgme_get_protocol_name(proto)
546
547 def check_version(version=None):
548     return pygpgme.gpgme_check_version(version)
549
550 def engine_check_version (proto):
551     try:
552         errorcheck(pygpgme.gpgme_engine_check_version(proto))
553         return True
554     except errors.GPGMEError:
555         return False
556
557 def get_engine_info():
558     ptr = pygpgme.new_gpgme_engine_info_t_p()
559     try:
560         errorcheck(pygpgme.gpgme_get_engine_info(ptr))
561         info = pygpgme.gpgme_engine_info_t_p_value(ptr)
562     except errors.GPGMEError:
563         info = None
564     pygpgme.delete_gpgme_engine_info_t_p(ptr)
565     return info
566
567 def set_engine_info(proto, file_name, home_dir=None):
568     """Changes the default configuration of the crypto engine implementing
569     the protocol 'proto'. 'file_name' is the file name of
570     the executable program implementing this protocol. 'home_dir' is the
571     directory name of the configuration directory (engine's default is
572     used if omitted)."""
573     errorcheck(pygpgme.gpgme_set_engine_info(proto, file_name, home_dir))
574
575 def set_locale(category, value):
576     """Sets the default locale used by contexts"""
577     errorcheck(pygpgme.gpgme_set_locale(None, category, value))
578
579 def wait(hang):
580     """Wait for asynchronous call on any Context  to finish.
581     Wait forever if hang is True.
582
583     For finished anynch calls it returns a tuple (status, context):
584         status  - status return by asnynchronous call.
585         context - context which caused this call to return.
586
587     Please read the GPGME manual of more information."""
588     ptr = pygpgme.new_gpgme_error_t_p()
589     context = pygpgme.gpgme_wait(None, ptr, hang)
590     status = pygpgme.gpgme_error_t_p_value(ptr)
591     pygpgme.delete_gpgme_error_t_p(ptr)
592     if context == None:
593         errorcheck(status)
594     else:
595         context = Context(context)
596     return (status, context)