From 8c42fbdfa727fd216f713ed9efa8bc8c925369d0 Mon Sep 17 00:00:00 2001 From: Iustin Pop Date: Mon, 23 Jan 2017 22:56:03 +0100 Subject: [PATCH] New upstream version 0.6.0 --- Makefile | 18 +- NEWS | 21 ++ PKG-INFO | 2 +- README.rst | 2 +- doc/conf.py | 4 +- doc/module.rst | 11 +- doc/news.rst | 183 ++++++++++++++- pyxattr.egg-info/PKG-INFO | 2 +- setup.cfg | 1 - setup.py | 8 +- test/test_xattr.py | 63 +++++- xattr.c | 466 ++++++++++++++++++++------------------ 12 files changed, 541 insertions(+), 240 deletions(-) mode change 120000 => 100644 doc/news.rst diff --git a/Makefile b/Makefile index a41a077..905791d 100644 --- a/Makefile +++ b/Makefile @@ -24,20 +24,28 @@ dist: test: @for ver in 2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3 3.4 3.5; do \ - if type python$$ver >/dev/null; then \ - echo Testing with python$$ver; \ - python$$ver ./setup.py test -q; \ - fi; \ + for flavour in "" "-dbg"; do \ + if type python$$ver$$flavour >/dev/null; then \ + echo Testing with python$$ver$$flavour; \ + python$$ver$$flavour ./setup.py test -q; \ + fi; \ + done; \ done; @if type pypy >/dev/null; then \ echo Testing with pypy; \ pypy ./setup.py test -q; \ fi +coverage: + $(MAKE) clean + $(MAKE) test CFLAGS="-coverage" + lcov --capture --directory . --output-file coverage.info + genhtml coverage.info --output-directory out + clean: rm -rf $(DOCHTML) $(DOCTREES) rm -f $(MODNAME) rm -f *.so rm -rf build -.PHONY: doc test clean dist +.PHONY: doc test clean dist coverage diff --git a/NEWS b/NEWS index 526c085..f9002c7 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,27 @@ News ==== +Version 0.6.0 +------------- + +*released Mon, 23 Jan 2017* + +Bugfix and feature release (hence the version bump). + +The main change is to the implementation of how attributes are listed +and read. This was done due to existing race issues when attributes are +modified while being read (github issue #12), but basically all various +internal paths that dealt with retrieving an attribute value or listing +attributes were unified in a single helper function that does handle +such concurrent modifications. As a side effect, the size of the buffers +used for such reads have changed, which (depending on attribute value) +might change the trade-off between number of syscalls done and memory +usage. + +As feature release, OSX support was contributed by Adam Knight +, thanks a lot! I don't have access to OSX so the testing +for it is done via Travis builds; please report any issues. + Version 0.5.6 ------------- diff --git a/PKG-INFO b/PKG-INFO index da37e9b..0e2792c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyxattr -Version: 0.5.6 +Version: 0.6.0 Summary: Filesystem extended attributes for python Home-page: http://pyxattr.k1024.org/ Author: Iustin Pop diff --git a/README.rst b/README.rst index b3b314c..2844a61 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ to the extended attributes for filesystem objects available in some operating systems. Downloads: go to http://pyxattr.k1024.org/downloads/. Latest -version is 0.5.6. The source repository is either at +version is 0.6.0. The source repository is either at http://git.k1024.org/pyxattr.git or at https://github.com/iustin/pyxattr. diff --git a/doc/conf.py b/doc/conf.py index 77f6b68..e6c994a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ copyright = u'2002, 2003, 2006, 2008, 2012, 2013, 2014, 2015, Iustin Pop' # built documents. # # The short X.Y version. -version = '0.5.6' +version = '0.6.0' # The full version, including alpha/beta/rc tags. -release = '0.5.6' +release = '0.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/module.rst b/doc/module.rst index 0231605..d7146a8 100644 --- a/doc/module.rst +++ b/doc/module.rst @@ -18,21 +18,22 @@ Constants .. data:: NS_SECURITY - The security namespace, used by kernel security modules. + The security name space, used by kernel security modules to store + (for example) capabilities information. .. data:: NS_SYSTEM - The system namespace, used by the kernel to store things such as - ACLs and capabilities. + The system name space, used by the kernel to store (for example) + ACLs. .. data:: NS_TRUSTED - The trusted namespace, visible and accessibly only to trusted + The trusted name space, visible and accessibly only to trusted processes, used to implement mechanisms in user space. .. data:: NS_USER - The user namespace; this is the namespace accessible to + The user name space; this is the name space accessible to non-privileged processes. Functions diff --git a/doc/news.rst b/doc/news.rst deleted file mode 120000 index 0fae0f8..0000000 --- a/doc/news.rst +++ /dev/null @@ -1 +0,0 @@ -../NEWS \ No newline at end of file diff --git a/doc/news.rst b/doc/news.rst new file mode 100644 index 0000000..f9002c7 --- /dev/null +++ b/doc/news.rst @@ -0,0 +1,182 @@ +News +==== + +Version 0.6.0 +------------- + +*released Mon, 23 Jan 2017* + +Bugfix and feature release (hence the version bump). + +The main change is to the implementation of how attributes are listed +and read. This was done due to existing race issues when attributes are +modified while being read (github issue #12), but basically all various +internal paths that dealt with retrieving an attribute value or listing +attributes were unified in a single helper function that does handle +such concurrent modifications. As a side effect, the size of the buffers +used for such reads have changed, which (depending on attribute value) +might change the trade-off between number of syscalls done and memory +usage. + +As feature release, OSX support was contributed by Adam Knight +, thanks a lot! I don't have access to OSX so the testing +for it is done via Travis builds; please report any issues. + +Version 0.5.6 +------------- + +*released Sat, 09 Apr 2016* + +Small bugfix release: + +* Fixes some sign-compare warnings +* Fixes potential name truncation in merge_ns() +* Fixes building on systems which don't have ENODATA + +Tested with Python 2.7.11, Python 3.5.1 and PyPy 5.0.1. + +Version 0.5.5 +------------- + +*released Fri, 01 May 2015* + +Bugfix release: + +* fixes some more memory leaks when handling out-of-memory in get_all() + function +* improve error reporting when an attribute disappears after we asked + for its length but before we managed to read it +* fix int/size_t issues found by RedHat/Fedora, + https://bugzilla.redhat.com/show_bug.cgi?id=1127310; the fix is + different than their fix, but it should accomplish the same thing +* convert all code to only do explicit casts after checking boundaries, + making the code `-Wconversion`-clean (although that warning is not + enabled by default) + +Version 0.5.4 +------------- + +*released Thu, 30 Apr 2015* + +Fix memory leaks on some of the error-handling paths of the `get()` +function. + +Version 0.5.3 +------------- + +*released Fri, 23 May 2014* + +Small optimisations release: + +* ari edelkind contributed a speed-up optimisation for handling of files + without xattrs (which is, in general, the expected case) +* Jonas Borgström contributed a behaviour change to the handling of file + names: under Python 3 and up, unicode paths are encoded/decoded using + the 'surogatee' handler, instead of the 'strict' handler; while this + can hide encoding errors, it mirrors what Python libraries do + (e.g. see os.fsencode/fsdecode) +* Sean Patrick Santos contributed improvements to the test suite so that + it can be used even on files systems which have built-in attributes + (e.g. when using SELinux, or NFSv4); to enable this, define the + attributes in the TEST_IGNORE_XATTRS environment variable + +Version 0.5.2 +------------- + +*released Thu, 03 Jan 2013* + +Bug-fix release. Thanks to Michał Górny, it looked like the library had +problem running under pypy, but actually there was a bug in the +PyArg_ParseTuple use of et# (signed vs. unsigned, and lack of compiler +warnings). This was fixed, and now the test suite passed with many +CPython versions and PyPy (version 1.9). + +Version 0.5.1 +------------- + +*released Wed, 16 May 2012* + +Bug-fix release. Thanks to Dave Malcolm and his cpychecker tool, a +number of significant bugs (refcount leaks and potential NULL-pointer +dereferences) have been fixed. + +Furthermore, compatibility with Python 3 has been improved; this however +required changing the meaning of the ``namespace`` argument to the +functions: if passed, None is no longer a valid value; pass an empty +string if (due to the structure of your program) you have to pass this +argument but want to specify no namespace. + +Also, the project home page has changed from SourceForge to GitHub, and +the documentation has been converted from epydoc-based to sphinx. + + +Version 0.5 +----------- + +*released Sun, 27 Dec 2009* + +Implemented support for Python 3. This required a significant change to +the C module, hence the new version number. + +Version 0.4 +----------- + +*released Mon, 30 Jun 2008* + +API +~~~ + +The old functions ({get,set,list,remove}xattr) are deprecated and replaced with +a new API that is namespace-aware and hopefully will allow other OSes (e.g. +FreeBSD) to be supported more naturally. + +Both the old and the new API are supported in the 0.4 versions, however users +are encouraged to migrate to the new API. + +New features +~~~~~~~~~~~~ + +A new bulk get function called get_all() has been added that should be somewhat +faster in case of querying files which have many attributes. + +License +~~~~~~~ + +Since LGPLv3 is not compatible with GPLv2 (which unfortunately I didn't realize +before), the license was changed to LGPLv2.1 or later. + +Internals +~~~~~~~~~ + +Unittest coverage was improved. + +Version 0.3 +----------- + +*released Sun, 09 Mar 2008* + +* changed licence from GPL to LGPL (3 or later) +* changed listxattr return type from tuple to a list +* developer-related: added unittests + +Version 0.2.2 +------------- + +*released Sun, 01 Jul 2007* + +* fixed listing symlink xattrs + +Version 0.2.1 +------------- + +*released Sat, 11 Feb 2006* + +* fixed a bug when reading symlink EAs (you weren't able to + do it, actually) +* fixed a possible memory leak when the actual read of the EA + failed but the call to get the length of the EA didn't + +.. Local Variables: +.. mode: rst +.. fill-column: 72 +.. End: diff --git a/pyxattr.egg-info/PKG-INFO b/pyxattr.egg-info/PKG-INFO index da37e9b..0e2792c 100644 --- a/pyxattr.egg-info/PKG-INFO +++ b/pyxattr.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyxattr -Version: 0.5.6 +Version: 0.6.0 Summary: Filesystem extended attributes for python Home-page: http://pyxattr.k1024.org/ Author: Iustin Pop diff --git a/setup.cfg b/setup.cfg index a68b051..4e0e866 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,4 @@ requires = libattr [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff --git a/setup.py b/setup.py index f580b19..54259db 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/python import distutils +import platform try: from setuptools import setup, Extension except ImportError: @@ -9,9 +10,12 @@ except ImportError: long_desc = """This is a C extension module for Python which implements extended attributes manipulation. It is a wrapper on top of the attr C library - see attr(5).""" -version = "0.5.6" +version = "0.6.0" author = "Iustin Pop" author_email = "iustin@k1024.org" +libraries = [] +if platform.system() == 'Linux': + libraries.append("attr") macros = [ ("_XATTR_VERSION", '"%s"' % version), ("_XATTR_AUTHOR", '"%s"' % author), @@ -27,7 +31,7 @@ setup(name = "pyxattr", download_url = "http://pyxattr.k1024.org/downloads/", license = "LGPL", ext_modules = [Extension("xattr", ["xattr.c"], - libraries=["attr"], + libraries=libraries, define_macros=macros, extra_compile_args=["-Wall", "-Werror", "-Wsign-compare"], )], diff --git a/test/test_xattr.py b/test/test_xattr.py index 69e939e..084b831 100644 --- a/test/test_xattr.py +++ b/test/test_xattr.py @@ -31,12 +31,16 @@ class xattrTest(unittest.TestCase): USER_NN = "test" USER_ATTR = NS_USER.decode() + "." + USER_NN USER_VAL = "abc" + EMPTY_VAL = "" + LARGE_VAL = "x" * 2048 MANYOPS_COUNT = 131072 if PY3K: USER_NN = USER_NN.encode() USER_VAL = USER_VAL.encode() USER_ATTR = USER_ATTR.encode() + EMPTY_VAL = EMPTY_VAL.encode() + LARGE_VAL = LARGE_VAL.encode() @staticmethod def _ignore_tuples(attrs): @@ -51,11 +55,11 @@ class xattrTest(unittest.TestCase): if attr not in TEST_IGNORE_XATTRS] def checkList(self, attrs, value): - """Helper to check attribute list equivalence, skipping TEST_IGNORE_XATTRS.""" + """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" self.assertEqual(self._ignore(attrs), value) def checkTuples(self, attrs, value): - """Helper to check attribute list equivalence, skipping TEST_IGNORE_XATTRS.""" + """Helper to check list equivalence, skipping TEST_IGNORE_XATTRS.""" self.assertEqual(self._ignore_tuples(attrs), value) def setUp(self): @@ -109,9 +113,11 @@ class xattrTest(unittest.TestCase): xattr.setxattr(item, self.USER_ATTR, self.USER_VAL, 0, symlink) except IOError: err = sys.exc_info()[1] - if err.errno == errno.EPERM and symlink: + if symlink and (err.errno == errno.EPERM or + err.errno == errno.ENOENT): # symlinks may fail, in which case we abort the rest - # of the test for this case + # of the test for this case (Linux returns EPERM; OS X + # returns ENOENT) return raise self.assertRaises(EnvironmentError, xattr.setxattr, item, @@ -146,9 +152,11 @@ class xattrTest(unittest.TestCase): nofollow=symlink) except IOError: err = sys.exc_info()[1] - if err.errno == errno.EPERM and symlink: + if symlink and (err.errno == errno.EPERM or + err.errno == errno.ENOENT): # symlinks may fail, in which case we abort the rest - # of the test for this case + # of the test for this case (Linux returns EPERM; OS X + # returns ENOENT) return raise self.assertRaises(EnvironmentError, xattr.set, item, @@ -188,12 +196,19 @@ class xattrTest(unittest.TestCase): fh, fname = self._getfile() self.checkList(xattr.listxattr(fname), []) self.checkTuples(xattr.get_all(fname), []) + self.assertRaises(EnvironmentError, xattr.getxattr, fname, + self.USER_ATTR) dname = self._getdir() self.checkList(xattr.listxattr(dname), []) self.checkTuples(xattr.get_all(dname), []) + self.assertRaises(EnvironmentError, xattr.getxattr, dname, + self.USER_ATTR) _, sname = self._getsymlink() self.checkList(xattr.listxattr(sname, True), []) self.checkTuples(xattr.get_all(sname, nofollow=True), []) + self.assertRaises(EnvironmentError, xattr.getxattr, fname, + self.USER_ATTR, True) + def testNoXattr(self): """test no attributes""" @@ -202,11 +217,15 @@ class xattrTest(unittest.TestCase): self.assertEqual(xattr.list(fname, namespace=NS_USER), []) self.checkTuples(xattr.get_all(fname), []) self.assertEqual(xattr.get_all(fname, namespace=NS_USER), []) + self.assertRaises(EnvironmentError, xattr.get, fname, + self.USER_NN, namespace=NS_USER) dname = self._getdir() self.checkList(xattr.list(dname), []) self.assertEqual(xattr.list(dname, namespace=NS_USER), []) self.checkTuples(xattr.get_all(dname), []) self.assertEqual(xattr.get_all(dname, namespace=NS_USER), []) + self.assertRaises(EnvironmentError, xattr.get, dname, + self.USER_NN, namespace=NS_USER) _, sname = self._getsymlink() self.checkList(xattr.list(sname, nofollow=True), []) self.assertEqual(xattr.list(sname, nofollow=True, @@ -214,6 +233,8 @@ class xattrTest(unittest.TestCase): self.checkTuples(xattr.get_all(sname, nofollow=True), []) self.assertEqual(xattr.get_all(sname, nofollow=True, namespace=NS_USER), []) + self.assertRaises(EnvironmentError, xattr.get, sname, + self.USER_NN, namespace=NS_USER, nofollow=True) def testFileByNameDeprecated(self): """test set and retrieve one attribute by file name (deprecated)""" @@ -397,6 +418,36 @@ class xattrTest(unittest.TestCase): self.assertRaises(TypeError, xattr.get, fh, self.USER_ATTR, namespace=None) + def testEmptyValue(self): + fh, fname = self._getfile() + xattr.set(fh, self.USER_ATTR, self.EMPTY_VAL) + self.assertEqual(xattr.get(fh, self.USER_ATTR), self.EMPTY_VAL) + + def testWrongCall(self): + for call in [xattr.get, + xattr.list, xattr.listxattr, + xattr.remove, xattr.removexattr, + xattr.set, xattr.setxattr, + xattr.get, xattr.getxattr]: + self.assertRaises(TypeError, call) + + def testWrongType(self): + self.assertRaises(TypeError, xattr.get, object(), self.USER_ATTR) + for call in [xattr.listxattr, xattr.list]: + self.assertRaises(TypeError, call, object()) + for call in [xattr.remove, xattr.removexattr, + xattr.get, xattr.getxattr]: + self.assertRaises(TypeError, call, object(), self.USER_ATTR) + for call in [xattr.set, xattr.setxattr]: + self.assertRaises(TypeError, call, object(), self.USER_ATTR, self.USER_VAL) + + + def testLargeAttribute(self): + fh, fname = self._getfile() + + xattr.set(fh, self.USER_ATTR, self.LARGE_VAL) + self.assertEqual(xattr.get(fh, self.USER_ATTR), self.LARGE_VAL) + if __name__ == "__main__": unittest.main() diff --git a/xattr.c b/xattr.c index c792df3..111cec1 100644 --- a/xattr.c +++ b/xattr.c @@ -23,7 +23,11 @@ #define PY_SSIZE_T_CLEAN #include +#if defined(__APPLE__) +#include +#elif defined(__linux__) #include +#endif #include /* Compatibility with python 2.4 regarding python size type (PEP 353) */ @@ -36,8 +40,10 @@ typedef int Py_ssize_t; #if PY_MAJOR_VERSION >= 3 #define IS_PY3K #define BYTES_CHAR "y" +#define BYTES_TUPLE "yy#" #else #define BYTES_CHAR "s" +#define BYTES_TUPLE "ss#" #define PyBytes_Check PyString_Check #define PyBytes_AS_STRING PyString_AS_STRING #define PyBytes_FromStringAndSize PyString_FromStringAndSize @@ -94,9 +100,14 @@ typedef int Py_ssize_t; " string (byte string under Python 3)." -/* the estimated (startup) attribute buffer size in - multi-operations */ -#define ESTIMATE_ATTR_SIZE 256 +/* The initial I/O buffer size for list and get operations; if the + * actual values will be smaller than this, we save a syscall out of + * two and allocate more memory upfront than needed, otherwise we + * incur three syscalls (get with ENORANGE, get with 0 to compute + * actual size, final get). The test suite is marginally faster (5%) + * with this, so it seems worth doing. +*/ +#define ESTIMATE_ATTR_SIZE 1024 typedef enum {T_FD, T_PATH, T_LINK} target_e; @@ -163,6 +174,8 @@ static int convert_obj(PyObject *myobj, target_t *tgt, int nofollow) { tgt->fd = fd; } else { PyErr_SetString(PyExc_TypeError, "argument must be string or int"); + tgt->type = T_PATH; + tgt->name = NULL; return -1; } return 0; @@ -196,42 +209,203 @@ static int merge_ns(const char *ns, const char *name, return 0; } -static ssize_t _list_obj(target_t *tgt, char *list, size_t size) { +#if defined(__APPLE__) +static inline ssize_t _listxattr(const char *path, char *namebuf, size_t size) { + return listxattr(path, namebuf, size, 0); +} +static inline ssize_t _llistxattr(const char *path, char *namebuf, size_t size) { + return listxattr(path, namebuf, size, XATTR_NOFOLLOW); +} +static inline ssize_t _flistxattr(int fd, char *namebuf, size_t size) { + return flistxattr(fd, namebuf, size, 0); +} + +static inline ssize_t _getxattr (const char *path, const char *name, void *value, size_t size) { + return getxattr(path, name, value, size, 0, 0); +} +static inline ssize_t _lgetxattr (const char *path, const char *name, void *value, size_t size) { + return getxattr(path, name, value, size, 0, XATTR_NOFOLLOW); +} +static inline ssize_t _fgetxattr (int filedes, const char *name, void *value, size_t size) { + return fgetxattr(filedes, name, value, size, 0, 0); +} + +// [fl]setxattr: Both OS X and Linux define XATTR_CREATE and XATTR_REPLACE for the last option. +static inline int _setxattr(const char *path, const char *name, const void *value, size_t size, int flags) { + return setxattr(path, name, value, size, 0, flags); +} +static inline int _lsetxattr(const char *path, const char *name, const void *value, size_t size, int flags) { + return setxattr(path, name, value, size, 0, flags & XATTR_NOFOLLOW); +} +static inline int _fsetxattr(int filedes, const char *name, const void *value, size_t size, int flags) { + return fsetxattr(filedes, name, value, size, 0, flags); +} + +static inline int _removexattr(const char *path, const char *name) { + return removexattr(path, name, 0); +} +static inline int _lremovexattr(const char *path, const char *name) { + return removexattr(path, name, XATTR_NOFOLLOW); +} +static inline int _fremovexattr(int filedes, const char *name) { + return fremovexattr(filedes, name, 0); +} + +#elif defined(__linux__) +#define _listxattr(path, list, size) listxattr(path, list, size) +#define _llistxattr(path, list, size) llistxattr(path, list, size) +#define _flistxattr(fd, list, size) flistxattr(fd, list, size) + +#define _getxattr(path, name, value, size) getxattr(path, name, value, size) +#define _lgetxattr(path, name, value, size) lgetxattr(path, name, value, size) +#define _fgetxattr(fd, name, value, size) fgetxattr(fd, name, value, size) + +#define _setxattr(path, name, value, size, flags) setxattr(path, name, value, size, flags) +#define _lsetxattr(path, name, value, size, flags) lsetxattr(path, name, value, size, flags) +#define _fsetxattr(fd, name, value, size, flags) fsetxattr(fd, name, value, size, flags) + +#define _removexattr(path, name) removexattr(path, name) +#define _lremovexattr(path, name) lremovexattr(path, name) +#define _fremovexattr(fd, name) fremovexattr(fd, name) + +#endif + +typedef ssize_t (*buf_getter)(target_t *tgt, const char *name, + void *output, size_t size); + +static ssize_t _list_obj(target_t *tgt, const char *unused, void *list, + size_t size) { if(tgt->type == T_FD) - return flistxattr(tgt->fd, list, size); + return _flistxattr(tgt->fd, list, size); else if (tgt->type == T_LINK) - return llistxattr(tgt->name, list, size); + return _llistxattr(tgt->name, list, size); else - return listxattr(tgt->name, list, size); + return _listxattr(tgt->name, list, size); } static ssize_t _get_obj(target_t *tgt, const char *name, void *value, size_t size) { if(tgt->type == T_FD) - return fgetxattr(tgt->fd, name, value, size); + return _fgetxattr(tgt->fd, name, value, size); else if (tgt->type == T_LINK) - return lgetxattr(tgt->name, name, value, size); + return _lgetxattr(tgt->name, name, value, size); else - return getxattr(tgt->name, name, value, size); + return _getxattr(tgt->name, name, value, size); } static int _set_obj(target_t *tgt, const char *name, const void *value, size_t size, int flags) { if(tgt->type == T_FD) - return fsetxattr(tgt->fd, name, value, size, flags); + return _fsetxattr(tgt->fd, name, value, size, flags); else if (tgt->type == T_LINK) - return lsetxattr(tgt->name, name, value, size, flags); + return _lsetxattr(tgt->name, name, value, size, flags); else - return setxattr(tgt->name, name, value, size, flags); + return _setxattr(tgt->name, name, value, size, flags); } static int _remove_obj(target_t *tgt, const char *name) { if(tgt->type == T_FD) - return fremovexattr(tgt->fd, name); + return _fremovexattr(tgt->fd, name); else if (tgt->type == T_LINK) - return lremovexattr(tgt->name, name); + return _lremovexattr(tgt->name, name); else - return removexattr(tgt->name, name); + return _removexattr(tgt->name, name); +} + +/* Perform a get/list operation with appropriate buffer size, + * determined dynamically. + * + * Arguments: + * - getter: the function that actually does the I/O. + * - tgt, name: passed to the getter. + * - buffer: pointer to either an already allocated memory area (in + * which case size contains its current size), or NULL to + * allocate. In all cases (success or failure), the caller should + * deallocate the buffer, using PyMem_Free(). Note that if size is + * zero but buffer already points to allocate memory, it will be + * ignored/leaked. + * - size: either size of current buffer (if non-NULL), or size for + * initial allocation (if non-zero), or a zero value which means + * auto-allocate buffer with automatically queried size. Value will + * be updated upon return with the current buffer size. + * - io_errno: if non-NULL, the actual errno will be recorded here; if + * zero, the call was successful and the output/size/nval are valid. + * + * Return value: if positive or zero, buffer will contain the read + * value. Otherwise, io_errno will contain the I/O errno, or zero + * to signify a Python-level error. In all cases, the Python-level + * error is set to the appropriate value. + */ +static ssize_t _generic_get(buf_getter getter, target_t *tgt, + const char *name, + char **buffer, + size_t *size, + int *io_errno) + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; + +static ssize_t _generic_get(buf_getter getter, target_t *tgt, + const char *name, + char **buffer, + size_t *size, + int *io_errno) { + ssize_t res; + /* Clear errno for now, will only set it when it fails in I/O. */ + if (io_errno != NULL) { + *io_errno = 0; + } + +#define EXIT_IOERROR() \ + { \ + if (io_errno != NULL) { \ + *io_errno = errno; \ + } \ + PyErr_SetFromErrno(PyExc_IOError); \ + return -1; \ + } + + /* Initialize the buffer, if needed. */ + if (*size == 0 || *buffer == NULL) { + if (*size == 0) { + ssize_t nalloc; + if ((nalloc = getter(tgt, name, NULL, 0)) == -1) { + EXIT_IOERROR(); + } + if (nalloc == 0) { + /* Empty, so no need to retrieve it. */ + return 0; + } + *size = nalloc; + } + if((*buffer = PyMem_Malloc(*size)) == NULL) { + PyErr_NoMemory(); + return -1; + } + } + // Try to get the value, while increasing the buffer if too small. + while((res = getter(tgt, name, *buffer, *size)) == -1) { + if(errno == ERANGE) { + ssize_t realloc_size_s = getter(tgt, name, NULL, 0); + /* ERANGE + proper size _should_ not fail, but... */ + if(realloc_size_s == -1) { + EXIT_IOERROR(); + } + size_t realloc_size = (size_t) realloc_size_s; + char *tmp_buf; + if((tmp_buf = PyMem_Realloc(*buffer, realloc_size)) == NULL) { + PyErr_NoMemory(); + return -1; + } + *buffer = tmp_buf; + *size = realloc_size; + continue; + } else { + /* else we're dealing with a different error, which we + don't know how to handle nicely, so we return */ + EXIT_IOERROR(); + } + } + return res; +#undef EXIT_IOERROR } /* @@ -277,9 +451,9 @@ pygetxattr(PyObject *self, PyObject *args) target_t tgt; int nofollow = 0; char *attrname = NULL; - char *buf; - ssize_t nalloc_s, nret; - size_t nalloc; + char *buf = NULL; + ssize_t nret; + size_t nalloc = ESTIMATE_ATTR_SIZE; PyObject *res; /* Parse the arguments */ @@ -290,33 +464,17 @@ pygetxattr(PyObject *self, PyObject *args) goto free_arg; } - /* Find out the needed size of the buffer */ - if((nalloc_s = _get_obj(&tgt, attrname, NULL, 0)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_tgt; - } - - nalloc = (size_t) nalloc_s; - - /* Try to allocate the memory, using Python's allocator */ - if((buf = PyMem_Malloc(nalloc)) == NULL) { - res = PyErr_NoMemory(); - goto free_tgt; - } - - /* Now retrieve the attribute value */ - if((nret = _get_obj(&tgt, attrname, buf, nalloc)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf; + nret = _generic_get(_get_obj, &tgt, attrname, &buf, &nalloc, NULL); + if (nret == -1) { + res = NULL; + goto free_buf; } - /* Create the string which will hold the result */ res = PyBytes_FromStringAndSize(buf, nret); free_buf: /* Free the buffer, now it is no longer needed */ PyMem_Free(buf); - free_tgt: free_tgt(&tgt); free_arg: PyMem_Free(attrname); @@ -356,45 +514,29 @@ xattr_get(PyObject *self, PyObject *args, PyObject *keywds) int nofollow = 0; char *attrname = NULL, *namebuf; const char *fullname; - char *buf; + char *buf = NULL; const char *ns = NULL; - ssize_t nalloc_s, nret; - size_t nalloc; - PyObject *res; + ssize_t nret; + size_t nalloc = ESTIMATE_ATTR_SIZE; + PyObject *res = NULL; static char *kwlist[] = {"item", "name", "nofollow", "namespace", NULL}; /* Parse the arguments */ if (!PyArg_ParseTupleAndKeywords(args, keywds, "Oet|i" BYTES_CHAR, kwlist, &myarg, NULL, &attrname, &nofollow, &ns)) return NULL; + res = NULL; if(convert_obj(myarg, &tgt, nofollow) < 0) { - res = NULL; goto free_arg; } if(merge_ns(ns, attrname, &fullname, &namebuf) < 0) { - res = NULL; goto free_tgt; } - /* Find out the needed size of the buffer */ - if((nalloc_s = _get_obj(&tgt, fullname, NULL, 0)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_name_buf; - } - - nalloc = (size_t) nalloc_s; - - /* Try to allocate the memory, using Python's allocator */ - if((buf = PyMem_Malloc(nalloc)) == NULL) { - res = PyErr_NoMemory(); - goto free_name_buf; - } - - /* Now retrieve the attribute value */ - if((nret = _get_obj(&tgt, fullname, buf, nalloc)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf; + nret = _generic_get(_get_obj, &tgt, fullname, &buf, &nalloc, NULL); + if(nret == -1) { + goto free_buf; } /* Create the string which will hold the result */ @@ -403,7 +545,6 @@ xattr_get(PyObject *self, PyObject *args, PyObject *keywds) /* Free the buffers, they are no longer needed */ free_buf: PyMem_Free(buf); - free_name_buf: PyMem_Free(namebuf); free_tgt: free_tgt(&tgt); @@ -458,13 +599,14 @@ get_all(PyObject *self, PyObject *args, PyObject *keywds) PyObject *myarg, *res; int nofollow=0; const char *ns = NULL; - char *buf_list, *buf_val, *buf_val_tmp; + char *buf_list = NULL, *buf_val = NULL; const char *s; - ssize_t nalloc_s, nlist, nval_s; - size_t nalloc, nval; + size_t nalloc = ESTIMATE_ATTR_SIZE; + ssize_t nlist, nval; PyObject *mylist; target_t tgt; static char *kwlist[] = {"item", "nofollow", "namespace", NULL}; + int io_errno; /* Parse the arguments */ if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i" BYTES_CHAR, kwlist, @@ -473,114 +615,48 @@ get_all(PyObject *self, PyObject *args, PyObject *keywds) if(convert_obj(myarg, &tgt, nofollow) < 0) return NULL; + res = NULL; /* Compute first the list of attributes */ - - /* Find out the needed size of the buffer for the attribute list */ - nalloc_s = _list_obj(&tgt, NULL, 0); - - if(nalloc_s == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_tgt; - } - - if(nalloc_s == 0) { - res = PyList_New(0); - goto free_tgt; - } - - nalloc = (size_t) nalloc_s; - - /* Try to allocate the memory, using Python's allocator */ - if((buf_list = PyMem_Malloc(nalloc)) == NULL) { - res = PyErr_NoMemory(); - goto free_tgt; - } - - /* Now retrieve the list of attributes */ - nlist = _list_obj(&tgt, buf_list, nalloc); - - if(nlist == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf_list; + nlist = _generic_get(_list_obj, &tgt, NULL, &buf_list, + &nalloc, &io_errno); + if (nlist == -1) { + /* We can't handle any errors, and the Python error is already + set, just bail out. */ + goto free_tgt; } - /* Create the list which will hold the result */ + /* Create the list which will hold the result. */ mylist = PyList_New(0); if(mylist == NULL) { - res = NULL; goto free_buf_list; } nalloc = ESTIMATE_ATTR_SIZE; - if((buf_val = PyMem_Malloc(nalloc)) == NULL) { - Py_DECREF(mylist); - res = PyErr_NoMemory(); - goto free_buf_list; - } - /* Create and insert the attributes as strings in the list */ for(s = buf_list; s - buf_list < nlist; s += strlen(s) + 1) { PyObject *my_tuple; - int missing; const char *name; - if((name=matches_ns(ns, s))==NULL) + if((name = matches_ns(ns, s)) == NULL) continue; /* Now retrieve the attribute value */ - missing = 0; - while(1) { - nval_s = _get_obj(&tgt, s, buf_val, nalloc); - - if(nval_s == -1) { - if(errno == ERANGE) { - ssize_t realloc_size_s = _get_obj(&tgt, s, NULL, 0); - /* ERANGE + proper size should not fail, but it - still can, so let's check first */ - if(realloc_size_s == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - Py_DECREF(mylist); - goto free_buf_val; - } - size_t realloc_size = (size_t) realloc_size_s; - if((buf_val_tmp = PyMem_Realloc(buf_val, realloc_size)) - == NULL) { - res = PyErr_NoMemory(); - Py_DECREF(mylist); - goto free_buf_val; - } - buf_val = buf_val_tmp; - nalloc = realloc_size; - continue; - } else if( + nval = _generic_get(_get_obj, &tgt, s, &buf_val, &nalloc, &io_errno); + if (nval == -1) { + if ( #ifdef ENODATA - errno == ENODATA || + io_errno == ENODATA || #endif - errno == ENOATTR) { - /* this attribute has gone away since we queried - the attribute list */ - missing = 1; - break; - } - /* else we're dealing with a different error, which we - don't know how to handle nicely, so we abort */ - Py_DECREF(mylist); - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf_val; - } else { - nval = (size_t) nval_s; - } - break; - } - if(missing) + io_errno == ENOATTR) { + PyErr_Clear(); continue; -#ifdef IS_PY3K - my_tuple = Py_BuildValue("yy#", name, buf_val, nval); -#else - my_tuple = Py_BuildValue("ss#", name, buf_val, nval); -#endif + } else { + Py_DECREF(mylist); + goto free_buf_val; + } + } + my_tuple = Py_BuildValue(BYTES_TUPLE, name, buf_val, nval); if (my_tuple == NULL) { Py_DECREF(mylist); - res = NULL; goto free_buf_val; } PyList_Append(mylist, my_tuple); @@ -902,10 +978,10 @@ static char __pylistxattr_doc__[] = static PyObject * pylistxattr(PyObject *self, PyObject *args) { - char *buf; - int nofollow=0; - ssize_t nalloc_s, nret; - size_t nalloc; + char *buf = NULL; + int nofollow = 0; + ssize_t nret; + size_t nalloc = ESTIMATE_ATTR_SIZE; PyObject *myarg; PyObject *mylist; Py_ssize_t nattrs; @@ -918,29 +994,10 @@ pylistxattr(PyObject *self, PyObject *args) if(convert_obj(myarg, &tgt, nofollow) < 0) return NULL; - /* Find out the needed size of the buffer */ - if((nalloc_s = _list_obj(&tgt, NULL, 0)) == -1) { - mylist = PyErr_SetFromErrno(PyExc_IOError); - goto free_tgt; - } - - if(nalloc_s == 0) { - mylist = PyList_New(0); - goto free_tgt; - } - - nalloc = (size_t) nalloc_s; - - /* Try to allocate the memory, using Python's allocator */ - if((buf = PyMem_Malloc(nalloc)) == NULL) { - mylist = PyErr_NoMemory(); - goto free_tgt; - } - - /* Now retrieve the list of attributes */ - if((nret = _list_obj(&tgt, buf, nalloc)) == -1) { - mylist = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf; + nret = _generic_get(_list_obj, &tgt, NULL, &buf, &nalloc, NULL); + if (nret == -1) { + mylist = NULL; + goto free_buf; } /* Compute the number of attributes in the list */ @@ -950,8 +1007,9 @@ pylistxattr(PyObject *self, PyObject *args) /* Create the list which will hold the result */ mylist = PyList_New(nattrs); - if(mylist == NULL) + if(mylist == NULL) { goto free_buf; + } /* Create and insert the attributes as strings in the list */ for(s = buf, nattrs = 0; s - buf < nret; s += strlen(s) + 1) { @@ -968,8 +1026,6 @@ pylistxattr(PyObject *self, PyObject *args) free_buf: /* Free the buffer, now it is no longer needed */ PyMem_Free(buf); - - free_tgt: free_tgt(&tgt); /* Return the result */ @@ -1005,10 +1061,10 @@ static char __list_doc__[] = static PyObject * xattr_list(PyObject *self, PyObject *args, PyObject *keywds) { - char *buf; + char *buf = NULL; int nofollow = 0; - ssize_t nalloc_s, nret; - size_t nalloc; + ssize_t nret; + size_t nalloc = ESTIMATE_ATTR_SIZE; PyObject *myarg; PyObject *res; const char *ns = NULL; @@ -1021,34 +1077,13 @@ xattr_list(PyObject *self, PyObject *args, PyObject *keywds) if (!PyArg_ParseTupleAndKeywords(args, keywds, "O|i" BYTES_CHAR, kwlist, &myarg, &nofollow, &ns)) return NULL; + res = NULL; if(convert_obj(myarg, &tgt, nofollow) < 0) { - res = NULL; goto free_arg; } - - /* Find out the needed size of the buffer */ - if((nalloc_s = _list_obj(&tgt, NULL, 0)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_tgt; - } - - if(nalloc_s == 0) { - res = PyList_New(0); - goto free_tgt; - } - - nalloc = (size_t) nalloc_s; - - /* Try to allocate the memory, using Python's allocator */ - if((buf = PyMem_Malloc(nalloc)) == NULL) { - res = PyErr_NoMemory(); - goto free_tgt; - } - - /* Now retrieve the list of attributes */ - if((nret = _list_obj(&tgt, buf, nalloc)) == -1) { - res = PyErr_SetFromErrno(PyExc_IOError); - goto free_buf; + nret = _generic_get(_list_obj, &tgt, NULL, &buf, &nalloc, NULL); + if (nret == -1) { + goto free_tgt; } /* Compute the number of attributes in the list */ @@ -1056,15 +1091,16 @@ xattr_list(PyObject *self, PyObject *args, PyObject *keywds) if(matches_ns(ns, s) != NULL) nattrs++; } + /* Create the list which will hold the result */ - res = PyList_New(nattrs); - if(res == NULL) + if((res = PyList_New(nattrs)) == NULL) { goto free_buf; + } /* Create and insert the attributes as strings in the list */ for(s = buf, nattrs = 0; s - buf < nret; s += strlen(s) + 1) { const char *name = matches_ns(ns, s); - if(name!=NULL) { + if(name != NULL) { PyObject *item = PyBytes_FromString(name); if(item == NULL) { Py_DECREF(res); -- 2.39.2