From 616929b6edf00b4a774b727385d39b785a112b90 Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Wed, 8 Jun 2016 17:56:33 +0200 Subject: [PATCH] python: Wrap objects implementing the buffer protocol. * lang/python/Makefile.am: Add the toplevel source directory to CFLAGS when compiling the bindings so that we can use private header files. * lang/python/gpgme.i (gpgme_data_t): Rework the object wrapping. Do not create a Python wrapper object, merely a gpgme_data_t object, and keep references to buffer objects, if any. If necessary, update the buffer after the function call. (pygpgme_wrap_gpgme_data_t): New function. * lang/python/helpers.c (object_to_gpgme_data_t): Rework object wrapping. Also wrap objects implementing the buffer protocol. * lang/python/helpers.h (object_to_gpgme_data_t): Update prototype. (pygpgme_wrap_gpgme_data_t): New prototype. * lang/python/tests/t-idiomatic.py: Demonstrate this. Signed-off-by: Justus Winter --- lang/python/Makefile.am | 2 +- lang/python/gpgme.i | 98 ++++++++++++++++++++++++++--- lang/python/helpers.c | 102 +++++++++++++++++-------------- lang/python/helpers.h | 7 ++- lang/python/tests/t-idiomatic.py | 35 ++++++++--- 5 files changed, 180 insertions(+), 64 deletions(-) diff --git a/lang/python/Makefile.am b/lang/python/Makefile.am index 18005bf9..e156d46e 100644 --- a/lang/python/Makefile.am +++ b/lang/python/Makefile.am @@ -45,7 +45,7 @@ gpgme_wrap.c pyme/pygpgme.py: gpgme.i errors.i gpgme.h copystamp $< all-local: gpgme_wrap.c pyme/pygpgme.py copystamp - CFLAGS="$(CFLAGS)" \ + CFLAGS="$(CFLAGS) -I$(top_srcdir)" \ $(PYTHON) setup.py build --verbose clean-local: diff --git a/lang/python/gpgme.i b/lang/python/gpgme.i index 98f30d55..1e4c9ff6 100644 --- a/lang/python/gpgme.i +++ b/lang/python/gpgme.i @@ -114,17 +114,19 @@ } // Special handling for references to our objects. -%typemap(in) gpgme_data_t DATAIN (PyObject *wrapper) { +%typemap(in) gpgme_data_t DATAIN (gpgme_data_t wrapper = NULL, + PyObject *bytesio = NULL, Py_buffer view) { /* If we create a temporary wrapper object, we will store it in wrapperN, where N is $argnum. Here in this fragment, SWIG will automatically append $argnum. */ - wrapper = NULL; + memset(&view, 0, sizeof view); if ($input == Py_None) $1 = NULL; else { - PyObject *pypointer = NULL; - - if((pypointer=object_to_gpgme_data_t($input, $argnum, &wrapper)) == NULL) + PyObject *pypointer; + pypointer = object_to_gpgme_data_t($input, $argnum, &wrapper, + &bytesio, &view); + if (pypointer == NULL) return NULL; /* input = $input, 1 = $1, 1_descriptor = $1_descriptor */ @@ -141,8 +143,79 @@ } %typemap(freearg) gpgme_data_t DATAIN { + /* See whether we need to update the Python buffer. */ + if (resultobj && wrapper$argnum && view$argnum.buf + && wrapper$argnum->data.mem.buffer != NULL) + { + /* The buffer is dirty. */ + if (view$argnum.readonly) + { + Py_XDECREF(resultobj); + resultobj = NULL; + PyErr_SetString(PyExc_ValueError, "cannot update read-only buffer"); + } + + /* See if we need to truncate the buffer. */ + if (resultobj && view$argnum.len != wrapper$argnum->data.mem.length) + { + if (bytesio$argnum == NULL) + { + Py_XDECREF(resultobj); + resultobj = NULL; + PyErr_SetString(PyExc_ValueError, "cannot resize buffer"); + } + else + { + PyObject *retval; + PyBuffer_Release(&view$argnum); + retval = PyObject_CallMethod(bytesio$argnum, "truncate", "l", + (long) + wrapper$argnum->data.mem.length); + if (retval == NULL) + { + Py_XDECREF(resultobj); + resultobj = NULL; + } + else + { + Py_DECREF(retval); + + retval = PyObject_CallMethod(bytesio$argnum, "getbuffer", NULL); + if (retval == NULL + || PyObject_GetBuffer(retval, &view$argnum, + PyBUF_SIMPLE|PyBUF_WRITABLE) < 0) + { + Py_XDECREF(resultobj); + resultobj = NULL; + } + + Py_XDECREF(retval); + + if (resultobj && view$argnum.len + != wrapper$argnum->data.mem.length) + { + Py_XDECREF(resultobj); + resultobj = NULL; + PyErr_Format(PyExc_ValueError, + "Expected buffer of length %zu, got %zi", + wrapper$argnum->data.mem.length, + view$argnum.len); + } + } + } + } + + if (resultobj) + memcpy(view$argnum.buf, wrapper$argnum->data.mem.buffer, + wrapper$argnum->data.mem.length); + } + /* Free the temporary wrapper, if any. */ - Py_XDECREF(wrapper$argnum); + if (wrapper$argnum) + gpgme_data_release(wrapper$argnum); + Py_XDECREF (bytesio$argnum); + if (wrapper$argnum && view$argnum.buf) + PyBuffer_Release(&view$argnum); } %apply gpgme_data_t DATAIN {gpgme_data_t plain, gpgme_data_t cipher, @@ -240,7 +313,10 @@ version for SWIG. We do, however, want to hide certain fields on some structs, which we provide prior to including the version for SWIG. */ - %{ #include %} +%{ +#include +#include "src/data.h" /* For struct gpgme_data. */ +%} /* This is for notations, where we want to hide the length fields, and the unused bit field block. */ @@ -291,5 +367,13 @@ FILE *fdopen(int fildes, const char *mode); %{ #include "helpers.h" + +/* SWIG support for helpers.c */ +PyObject * +pygpgme_wrap_gpgme_data_t(gpgme_data_t data) +{ + return SWIG_NewPointerObj(data, SWIGTYPE_p_gpgme_data, 0); +} %} + %include "helpers.h" diff --git a/lang/python/helpers.c b/lang/python/helpers.c index 810423d0..ad33d07b 100644 --- a/lang/python/helpers.c +++ b/lang/python/helpers.c @@ -206,64 +206,72 @@ object_to_gpgme_t(PyObject *input, const char *objtype, int argnum) objects with a fileno method, returning it in WRAPPER. This object must be de-referenced when no longer needed. */ PyObject * -object_to_gpgme_data_t(PyObject *input, int argnum, PyObject **wrapper) +object_to_gpgme_data_t(PyObject *input, int argnum, gpgme_data_t *wrapper, + PyObject **bytesio, Py_buffer *view) { - static PyObject *Data = NULL; - PyObject *data = input; + gpgme_error_t err; + PyObject *data; PyObject *fd; - PyObject *result; - *wrapper = NULL; - - if (Data == NULL) { - PyObject *core; - PyObject *from_list = PyList_New(0); - core = PyImport_ImportModuleLevel("core", PyEval_GetGlobals(), - PyEval_GetLocals(), from_list, 1); - Py_XDECREF(from_list); - if (core) { - Data = PyDict_GetItemString(PyModule_GetDict(core), "Data"); - Py_XINCREF(Data); - } - else - return NULL; - } + /* See if it is a file-like object with file number. */ fd = PyObject_CallMethod(input, "fileno", NULL); if (fd) { - /* File-like object with file number. */ - PyObject *args = NULL; - PyObject *kw = NULL; - - /* We don't need the fd, as we have no constructor accepting file - descriptors directly. */ + err = gpgme_data_new_from_fd(wrapper, (int) PyLong_AsLong(fd)); Py_DECREF(fd); + if (err) + return pygpgme_raise_exception (err); - args = PyTuple_New(0); - kw = PyDict_New(); - if (args == NULL || kw == NULL) - { - fail: - Py_XDECREF(args); - Py_XDECREF(kw); - return NULL; - } - - if (PyDict_SetItemString(kw, "file", input) < 0) - goto fail; - - *wrapper = PyObject_Call(Data, args, kw); - if (*wrapper == NULL) - goto fail; - - Py_DECREF(args); - Py_DECREF(kw); - data = *wrapper; + return pygpgme_wrap_gpgme_data_t(*wrapper); } else PyErr_Clear(); - result = object_to_gpgme_t(data, "gpgme_data_t", argnum); - return result; + /* No? Maybe it implements the buffer protocol. */ + data = PyObject_CallMethod(input, "getbuffer", NULL); + if (data) + { + /* Save a reference to input, which seems to be a BytesIO + object. */ + Py_INCREF(input); + *bytesio = input; + } + else + { + PyErr_Clear(); + + /* No, but maybe the user supplied a buffer object? */ + data = input; + } + + /* Do we have a buffer object? */ + if (PyObject_CheckBuffer(data)) + { + if (PyObject_GetBuffer(data, view, PyBUF_SIMPLE) < 0) + return NULL; + + if (data != input) + Py_DECREF(data); + + assert (view->ndim == 1); + assert (view->shape == NULL); + assert (view->strides == NULL); + assert (view->suboffsets == NULL); + + err = gpgme_data_new_from_mem(wrapper, view->buf, (size_t) view->len, 0); + if (err) + return pygpgme_raise_exception (err); + + return pygpgme_wrap_gpgme_data_t(*wrapper); + } + + /* As last resort we assume it is a wrapped data object. */ + if (PyObject_HasAttrString(data, "_getctype")) + return object_to_gpgme_t(data, "gpgme_data_t", argnum); + + return PyErr_Format(PyExc_TypeError, + "arg %d: expected pyme.Data, file, or an object " + "implementing the buffer protocol, got %s", + argnum, data->ob_type->tp_name); } diff --git a/lang/python/helpers.h b/lang/python/helpers.h index 24502631..37362aee 100644 --- a/lang/python/helpers.h +++ b/lang/python/helpers.h @@ -1,4 +1,5 @@ /* +# Copyright (C) 2016 g10 Code GmbH # Copyright (C) 2004 Igor Belyi # Copyright (C) 2002 John Goerzen # @@ -30,7 +31,8 @@ gpgme_error_t pygpgme_exception2code(void); PyObject *object_to_gpgme_t(PyObject *input, const char *objtype, int argnum); PyObject *object_to_gpgme_data_t(PyObject *input, int argnum, - PyObject **wrapper); + gpgme_data_t *wrapper, + PyObject **bytesio, Py_buffer *view); void pygpgme_clear_generic_cb(PyObject **cb); PyObject *pygpgme_raise_callback_exception(PyObject *self); @@ -47,3 +49,6 @@ gpgme_error_t pyEditCb(void *opaque, gpgme_status_code_t status, gpgme_error_t pygpgme_data_new_from_cbs(gpgme_data_t *r_data, PyObject *pycbs, PyObject **freelater); + +/* SWIG support for helpers.c */ +PyObject *pygpgme_wrap_gpgme_data_t(gpgme_data_t data); diff --git a/lang/python/tests/t-idiomatic.py b/lang/python/tests/t-idiomatic.py index 37cfb64a..b2526902 100755 --- a/lang/python/tests/t-idiomatic.py +++ b/lang/python/tests/t-idiomatic.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, constants, errors @@ -33,14 +34,7 @@ with core.Context() as c, core.Data() as d: assert leak_c.wrapped == None assert leak_d.wrapped == None -# Demonstrate automatic wrapping of file-like objects with 'fileno' -# method. -with tempfile.TemporaryFile() as source, \ - tempfile.TemporaryFile() as signed, \ - tempfile.TemporaryFile() as sink: - source.write(b"Hallo Leute\n") - source.seek(0, os.SEEK_SET) - +def sign_and_verify(source, signed, sink): with core.Context() as c: c.op_sign(source, signed, constants.SIG_MODE_NORMAL) signed.seek(0, os.SEEK_SET) @@ -54,3 +48,28 @@ with tempfile.TemporaryFile() as source, \ sink.seek(0, os.SEEK_SET) assert sink.read() == b"Hallo Leute\n" + +# Demonstrate automatic wrapping of file-like objects with 'fileno' +# method. +with tempfile.TemporaryFile() as source, \ + tempfile.TemporaryFile() as signed, \ + tempfile.TemporaryFile() as sink: + source.write(b"Hallo Leute\n") + source.seek(0, os.SEEK_SET) + + sign_and_verify(source, signed, sink) + +# XXX: Python's io.BytesIo.truncate does not work as advertised. +# http://bugs.python.org/issue27261 +bio = io.BytesIO() +bio.truncate(1) +if len(bio.getvalue()) != 1: + # This version of Python is affected, preallocate buffer. + preallocate = 128*b'\x00' +else: + preallocate = b'' + +# Demonstrate automatic wrapping of objects implementing the buffer +# interface, and the use of data objects with the 'with' statement. +with io.BytesIO(preallocate) as signed, core.Data() as sink: + sign_and_verify(b"Hallo Leute\n", signed, sink)