diff --git a/lang/python/gpgme.i b/lang/python/gpgme.i index 84bc6e98..5b3c193b 100644 --- a/lang/python/gpgme.i +++ b/lang/python/gpgme.i @@ -180,10 +180,13 @@ PyObject* object_to_gpgme_t(PyObject* input, const char* objtype, int argnum) { gpgme_data_t pubkey, gpgme_data_t seckey, gpgme_data_t out}; -// SWIG has problem interpreting ssize_t, off_t or gpgme_error_t in gpgme.h +/* SWIG has problems interpreting ssize_t, off_t or gpgme_error_t in + gpgme.h. */ +/* XXX: This is wrong at least for off_t if compiled with LFS. */ %typemap(out) ssize_t, off_t, gpgme_error_t, gpgme_err_code_t, gpgme_err_source_t, gpg_error_t { $result = PyLong_FromLong($1); } +/* XXX: This is wrong at least for off_t if compiled with LFS. */ %typemap(in) ssize_t, off_t, gpgme_error_t, gpgme_err_code_t, gpgme_err_source_t, gpg_error_t { $1 = PyLong_AsLong($input); } @@ -201,7 +204,7 @@ PyObject* object_to_gpgme_t(PyObject* input, const char* objtype, int argnum) { Py_XDECREF($result); /* Blow away any previous result */ if (result < 0) { /* Check for I/O error */ free($1); - return NULL; + return PyErr_SetFromErrno(PyExc_RuntimeError); } $result = PyBytes_FromStringAndSize($1,result); free($1); diff --git a/lang/python/helpers.c b/lang/python/helpers.c index 9fe81c9f..4792c87d 100644 --- a/lang/python/helpers.c +++ b/lang/python/helpers.c @@ -402,3 +402,245 @@ gpgme_error_t pyEditCb(void *opaque, gpgme_status_code_t status, Py_XDECREF(retval); return err_status; } + +/* Data callbacks. */ + +/* Read up to SIZE bytes into buffer BUFFER from the data object with + the handle HOOK. Return the number of characters read, 0 on EOF + and -1 on error. If an error occurs, errno is set. */ +static ssize_t pyDataReadCb(void *hook, void *buffer, size_t size) +{ + ssize_t result; + PyObject *pyhook = (PyObject *) hook; + PyObject *self = NULL; + PyObject *func = NULL; + PyObject *dataarg = NULL; + PyObject *pyargs = NULL; + PyObject *retval = NULL; + + assert (PyTuple_Check(pyhook)); + assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6); + + self = PyTuple_GetItem(pyhook, 0); + func = PyTuple_GetItem(pyhook, 1); + if (PyTuple_Size(pyhook) == 6) { + dataarg = PyTuple_GetItem(pyhook, 5); + pyargs = PyTuple_New(2); + } else { + pyargs = PyTuple_New(1); + } + + PyTuple_SetItem(pyargs, 0, PyLong_FromSize_t(size)); + if (dataarg) { + Py_INCREF(dataarg); + PyTuple_SetItem(pyargs, 1, dataarg); + } + + retval = PyObject_CallObject(func, pyargs); + Py_DECREF(pyargs); + if (PyErr_Occurred()) { + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + if (! PyBytes_Check(retval)) { + PyErr_Format(PyExc_TypeError, + "expected bytes from read callback, got %s", + retval->ob_type->tp_name); + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + if (PyBytes_Size(retval) > size) { + PyErr_Format(PyExc_TypeError, + "expected %zu bytes from read callback, got %zu", + size, PyBytes_Size(retval)); + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + memcpy(buffer, PyBytes_AsString(retval), PyBytes_Size(retval)); + result = PyBytes_Size(retval); + + leave: + Py_XDECREF(retval); + return result; +} + +/* Write up to SIZE bytes from buffer BUFFER to the data object with + the handle HOOK. Return the number of characters written, or -1 + on error. If an error occurs, errno is set. */ +static ssize_t pyDataWriteCb(void *hook, const void *buffer, size_t size) +{ + ssize_t result; + PyObject *pyhook = (PyObject *) hook; + PyObject *self = NULL; + PyObject *func = NULL; + PyObject *dataarg = NULL; + PyObject *pyargs = NULL; + PyObject *retval = NULL; + + assert (PyTuple_Check(pyhook)); + assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6); + + self = PyTuple_GetItem(pyhook, 0); + func = PyTuple_GetItem(pyhook, 2); + if (PyTuple_Size(pyhook) == 6) { + dataarg = PyTuple_GetItem(pyhook, 5); + pyargs = PyTuple_New(2); + } else { + pyargs = PyTuple_New(1); + } + + PyTuple_SetItem(pyargs, 0, PyBytes_FromStringAndSize(buffer, size)); + if (dataarg) { + Py_INCREF(dataarg); + PyTuple_SetItem(pyargs, 1, dataarg); + } + + retval = PyObject_CallObject(func, pyargs); + Py_DECREF(pyargs); + if (PyErr_Occurred()) { + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + if (! PyLong_Check(retval)) { + PyErr_Format(PyExc_TypeError, + "expected int from read callback, got %s", + retval->ob_type->tp_name); + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + result = PyLong_AsSsize_t(retval); + + leave: + Py_XDECREF(retval); + return result; +} + +/* Set the current position from where the next read or write starts + in the data object with the handle HOOK to OFFSET, relativ to + WHENCE. Returns the new offset in bytes from the beginning of the + data object. */ +static off_t pyDataSeekCb(void *hook, off_t offset, int whence) +{ + off_t result; + PyObject *pyhook = (PyObject *) hook; + PyObject *self = NULL; + PyObject *func = NULL; + PyObject *dataarg = NULL; + PyObject *pyargs = NULL; + PyObject *retval = NULL; + + assert (PyTuple_Check(pyhook)); + assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6); + + self = PyTuple_GetItem(pyhook, 0); + func = PyTuple_GetItem(pyhook, 3); + if (PyTuple_Size(pyhook) == 6) { + dataarg = PyTuple_GetItem(pyhook, 5); + pyargs = PyTuple_New(3); + } else { + pyargs = PyTuple_New(2); + } + +#if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 + PyTuple_SetItem(pyargs, 0, PyLong_FromLongLong((long long) offset)); +#else + PyTuple_SetItem(pyargs, 0, PyLong_FromLong((long) offset)); +#endif + PyTuple_SetItem(pyargs, 1, PyLong_FromLong((long) whence)); + if (dataarg) { + Py_INCREF(dataarg); + PyTuple_SetItem(pyargs, 2, dataarg); + } + + retval = PyObject_CallObject(func, pyargs); + Py_DECREF(pyargs); + if (PyErr_Occurred()) { + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + + if (! PyLong_Check(retval)) { + PyErr_Format(PyExc_TypeError, + "expected int from read callback, got %s", + retval->ob_type->tp_name); + pygpgme_stash_callback_exception(self); + result = -1; + goto leave; + } + +#if defined(_FILE_OFFSET_BITS) && _FILE_OFFSET_BITS == 64 + result = PyLong_AsLongLong(retval); +#else + result = PyLong_AsLong(retval); +#endif + + leave: + Py_XDECREF(retval); + return result; +} + +/* Close the data object with the handle HOOK. */ +static void pyDataReleaseCb(void *hook) +{ + PyObject *pyhook = (PyObject *) hook; + PyObject *self = NULL; + PyObject *func = NULL; + PyObject *dataarg = NULL; + PyObject *pyargs = NULL; + PyObject *retval = NULL; + + assert (PyTuple_Check(pyhook)); + assert (PyTuple_Size(pyhook) == 5 || PyTuple_Size(pyhook) == 6); + + self = PyTuple_GetItem(pyhook, 0); + func = PyTuple_GetItem(pyhook, 4); + if (PyTuple_Size(pyhook) == 6) { + dataarg = PyTuple_GetItem(pyhook, 5); + pyargs = PyTuple_New(1); + } else { + pyargs = PyTuple_New(0); + } + + if (dataarg) { + Py_INCREF(dataarg); + PyTuple_SetItem(pyargs, 0, dataarg); + } + + retval = PyObject_CallObject(func, pyargs); + Py_XDECREF(retval); + Py_DECREF(pyargs); + if (PyErr_Occurred()) + pygpgme_stash_callback_exception(self); +} + +gpgme_error_t pygpgme_data_new_from_cbs(gpgme_data_t *r_data, + PyObject *pycbs, + PyObject **freelater) +{ + static struct gpgme_data_cbs cbs = { + pyDataReadCb, + pyDataWriteCb, + pyDataSeekCb, + pyDataReleaseCb, + }; + PyObject *dataarg = NULL; + + assert (PyTuple_Check(pycbs)); + assert (PyTuple_Size(pycbs) == 5 || PyTuple_Size(pycbs) == 6); + + Py_INCREF(pycbs); + *freelater = pycbs; + + return gpgme_data_new_from_cbs(r_data, &cbs, (void *) pycbs); +} diff --git a/lang/python/helpers.h b/lang/python/helpers.h index 5dd88a06..8b900085 100644 --- a/lang/python/helpers.h +++ b/lang/python/helpers.h @@ -40,3 +40,7 @@ void pygpgme_set_status_cb(gpgme_ctx_t ctx, PyObject *cb, gpgme_error_t pyEditCb(void *opaque, gpgme_status_code_t status, const char *args, int fd); + +gpgme_error_t pygpgme_data_new_from_cbs(gpgme_data_t *r_data, + PyObject *pycbs, + PyObject **freelater); diff --git a/lang/python/pyme/core.py b/lang/python/pyme/core.py index 6ef2dabe..e89c1812 100644 --- a/lang/python/pyme/core.py +++ b/lang/python/pyme/core.py @@ -384,7 +384,7 @@ class Data(GpgmeWrapper): If cbs is specified, it MUST be a tuple of the form: - ((read_cb, write_cb, seek_cb, release_cb), hook) + (read_cb, write_cb, seek_cb, release_cb[, hook]) where func is a callback function taking two arguments (count, hook) and returning a string of read data, or None on EOF. @@ -396,7 +396,7 @@ class Data(GpgmeWrapper): Any other use will result in undefined or erroneous behavior.""" super().__init__(None) - self.last_readcb = None + self.data_cbs = None if cbs != None: self.new_from_cbs(*cbs) @@ -419,15 +419,18 @@ class Data(GpgmeWrapper): if self.wrapped != None and pygpgme.gpgme_data_release: pygpgme.gpgme_data_release(self.wrapped) - self._free_readcb() + if self._callback_excinfo: + print(self._callback_excinfo) + pygpgme.pygpgme_raise_callback_exception(self) + self._free_datacbs() - def _free_readcb(self): - if self.last_readcb != None: + def _free_datacbs(self): + if self.data_cbs != None: if pygpgme.pygpgme_clear_generic_cb: - pygpgme.pygpgme_clear_generic_cb(self.last_readcb) + pygpgme.pygpgme_clear_generic_cb(self.data_cbs) if pygpgme.delete_PyObject_p_p: - pygpgme.delete_PyObject_p_p(self.last_readcb) - self.last_readcb = None + pygpgme.delete_PyObject_p_p(self.data_cbs) + self.data_cbs = None def new(self): tmp = pygpgme.new_gpgme_data_t_p() @@ -453,14 +456,18 @@ class Data(GpgmeWrapper): self.wrapped = pygpgme.gpgme_data_t_p_value(tmp) pygpgme.delete_gpgme_data_t_p(tmp) - def new_from_cbs(self, funcs, hook): - """Argument funcs must be a 4 element tuple with callbacks: - (read_cb, write_cb, seek_cb, release_cb)""" + def new_from_cbs(self, read_cb, write_cb, seek_cb, release_cb, hook=None): + assert self.data_cbs == None + self.data_cbs = pygpgme.new_PyObject_p_p() tmp = pygpgme.new_gpgme_data_t_p() - self._free_readcb() - self.last_readcb = pygpgme.new_PyObject_p_p() - hookdata = (funcs, hook) - pygpgme.pygpgme_data_new_from_cbs(tmp, hookdata, self.last_readcb) + if hook != None: + hookdata = (weakref.ref(self), + read_cb, write_cb, seek_cb, release_cb, hook) + else: + hookdata = (weakref.ref(self), + read_cb, write_cb, seek_cb, release_cb) + errorcheck( + pygpgme.pygpgme_data_new_from_cbs(tmp, hookdata, self.data_cbs)) self.wrapped = pygpgme.gpgme_data_t_p_value(tmp) pygpgme.delete_gpgme_data_t_p(tmp) @@ -512,7 +519,10 @@ class Data(GpgmeWrapper): If a string is given, it is implicitly encoded using UTF-8.""" written = pygpgme.gpgme_data_write(self.wrapped, buffer) if written < 0: - raise GPGMEError.fromSyserror() + if self._callback_excinfo: + pygpgme.pygpgme_raise_callback_exception(self) + else: + raise GPGMEError.fromSyserror() return written def read(self, size = -1): @@ -527,11 +537,24 @@ class Data(GpgmeWrapper): return '' if size > 0: - return pygpgme.gpgme_data_read(self.wrapped, size) + try: + result = pygpgme.gpgme_data_read(self.wrapped, size) + except: + if self._callback_excinfo: + pygpgme.pygpgme_raise_callback_exception(self) + else: + raise + return result else: chunks = [] - while 1: - result = pygpgme.gpgme_data_read(self.wrapped, 4096) + while True: + try: + result = pygpgme.gpgme_data_read(self.wrapped, 4096) + except: + if self._callback_excinfo: + pygpgme.pygpgme_raise_callback_exception(self) + else: + raise if len(result) == 0: break chunks.append(result) diff --git a/lang/python/tests/t-callbacks.py b/lang/python/tests/t-callbacks.py index 57975264..3219463c 100755 --- a/lang/python/tests/t-callbacks.py +++ b/lang/python/tests/t-callbacks.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, see . +import os from pyme import core, constants import support @@ -181,3 +182,73 @@ except Exception as e: assert e == myException else: assert False, "Expected an error, got none" + + + +# Test the data callbacks. +def read_cb(amount, hook=None): + assert hook == cookie + return 0 +def release_cb(hook=None): + assert hook == cookie +data = core.Data(cbs=(read_cb, None, None, release_cb, cookie)) +try: + data.read() +except Exception as e: + assert type(e) == TypeError +else: + assert False, "Expected an error, got none" + +def read_cb(amount): + raise myException +data = core.Data(cbs=(read_cb, None, None, lambda: None)) +try: + data.read() +except Exception as e: + assert e == myException +else: + assert False, "Expected an error, got none" + + +def write_cb(what, hook=None): + assert hook == cookie + return "wrong type" +data = core.Data(cbs=(None, write_cb, None, release_cb, cookie)) +try: + data.write(b'stuff') +except Exception as e: + assert type(e) == TypeError +else: + assert False, "Expected an error, got none" + +def write_cb(what): + raise myException +data = core.Data(cbs=(None, write_cb, None, lambda: None)) +try: + data.write(b'stuff') +except Exception as e: + assert e == myException +else: + assert False, "Expected an error, got none" + + +def seek_cb(offset, whence, hook=None): + assert hook == cookie + return "wrong type" +data = core.Data(cbs=(None, None, seek_cb, release_cb, cookie)) +try: + data.seek(0, os.SEEK_SET) +except Exception as e: + assert type(e) == TypeError +else: + assert False, "Expected an error, got none" + +def seek_cb(offset, whence): + raise myException +data = core.Data(cbs=(None, None, seek_cb, lambda: None)) +try: + data.seek(0, os.SEEK_SET) +except Exception as e: + assert e == myException +else: + assert False, "Expected an error, got none" diff --git a/lang/python/tests/t-data.py b/lang/python/tests/t-data.py index 6cf10fab..de60c473 100755 --- a/lang/python/tests/t-data.py +++ b/lang/python/tests/t-data.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this program; if not, see . +import io import os import tempfile from pyme import core @@ -79,3 +80,43 @@ with tempfile.NamedTemporaryFile() as tmp: # Open using name, offset, and length. data = core.Data(file=tmp.name, offset=23, length=42) assert data.read() == binjunk[23:23+42] + +# Test callbacks. +class DataObject(object): + def __init__(self): + self.buffer = io.BytesIO() + self.released = False + + def read(self, amount, hook=None): + assert not self.released + return self.buffer.read(amount) + + def write(self, data, hook=None): + assert not self.released + return self.buffer.write(data) + + def seek(self, offset, whence, hook=None): + assert not self.released + return self.buffer.seek(offset, whence) + + def release(self, hook=None): + assert not self.released + self.released = True + +do = DataObject() +cookie = object() +data = core.Data(cbs=(do.read, do.write, do.seek, do.release, cookie)) +data.write('Hello world!') +data.seek(0, os.SEEK_SET) +assert data.read() == b'Hello world!' +del data +assert do.released + +# Again, without the cookie. +do = DataObject() +data = core.Data(cbs=(do.read, do.write, do.seek, do.release)) +data.write('Hello world!') +data.seek(0, os.SEEK_SET) +assert data.read() == b'Hello world!' +del data +assert do.released