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