From 787d725935e4052fa2b360d727ff3c141afd99af Mon Sep 17 00:00:00 2001 From: Iustin Pop Date: Sun, 29 Nov 2020 03:06:14 +0100 Subject: [PATCH] New upstream version 0.6.0 --- MANIFEST.in | 4 +- Makefile | 47 +- NEWS | 52 +- PKG-INFO | 18 +- README.md | 67 +++ README.rst | 60 -- acl.c | 575 +++++++++--------- doc/conf.py | 13 +- doc/index.rst | 5 +- doc/news.rst | 142 ----- posix1e.pyi | 77 +++ test/__init__.py => py.typed | 0 pylibacl.egg-info/PKG-INFO | 18 +- pylibacl.egg-info/SOURCES.txt | 9 +- pylibacl.egg-info/not-zip-safe | 1 + setup.py | 27 +- test/test_acls.py | 675 --------------------- tests/test_acls.py | 1004 ++++++++++++++++++++++++++++++++ 18 files changed, 1598 insertions(+), 1196 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 doc/news.rst create mode 100644 posix1e.pyi rename test/__init__.py => py.typed (100%) create mode 100644 pylibacl.egg-info/not-zip-safe delete mode 100644 test/test_acls.py create mode 100644 tests/test_acls.py diff --git a/MANIFEST.in b/MANIFEST.in index f173282..9c9a87d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,6 @@ include acl.c include setup.cfg include doc/conf.py include doc/*.rst -include test/*.py +include tests/*.py +include py.typed +include posix1e.pyi diff --git a/Makefile b/Makefile index 3e76944..979f428 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,12 @@ DOCDIR = doc DOCHTML = $(DOCDIR)/html DOCTREES = $(DOCDIR)/doctrees ALLSPHINXOPTS = -d $(DOCTREES) $(SPHINXOPTS) $(DOCDIR) +VERSION = 0.6.0 +FULLVER = pylibacl-$(VERSION) +DISTFILE = $(FULLVER).tar.gz MODNAME = posix1e.so -RSTFILES = doc/index.rst doc/module.rst NEWS README.rst doc/conf.py +RSTFILES = doc/index.rst doc/module.rst doc/news.rst doc/readme.md doc/conf.py all: doc test @@ -20,35 +23,65 @@ $(DOCHTML)/index.html: $(MODNAME) $(RSTFILES) acl.c doc: $(DOCHTML)/index.html +doc/readme.md: README.md + ln -s ../README.md doc/readme.md + +doc/news.rst: NEWS + ln -s ../NEWS doc/news.rst + dist: fakeroot $(PYTHON) ./setup.py sdist +distcheck: dist + set -e; \ + TDIR=$$(mktemp -d) && \ + trap "rm -rf $$TDIR" EXIT; \ + tar xzf dist/$(DISTFILE) -C $$TDIR && \ + (cd $$TDIR/$(FULLVER) && make doc && make test && make dist) && \ + echo "All good, you can upload $(DISTFILE)!" + test: - @for ver in 2.7 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7; do \ + @set -e; \ + for ver in 3.4 3.5 3.6 3.7 3.8 3.9; do \ 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; \ + python$$ver$$flavour ./setup.py build_ext -i; \ + python$$ver$$flavour -m pytest tests ;\ fi; \ done; \ done; \ - for pp in pypy pypy3; do \ + for pp in pypy3; do \ if type $$pp >/dev/null; then \ echo Testing with $$pp; \ - $$pp ./setup.py test -q; \ + $$pp ./setup.py build_ext -i; \ + $$pp -m pytest tests; \ fi; \ done +fast-test: + python3 setup.py build_ext -i + python3 -m pytest tests + +ci: + while inotifywait -e CLOSE_WRITE tests/test_*.py; do \ + python3 -m pytest tests; \ + done + coverage: $(MAKE) clean $(MAKE) test CFLAGS="-coverage" - lcov --capture --directory . --output-file coverage.info + lcov --capture --no-external --directory . --output-file coverage.info genhtml coverage.info --output-directory out clean: rm -rf $(DOCHTML) $(DOCTREES) + rm -f doc/readme.md doc/news.rst rm -f $(MODNAME) rm -f *.so rm -rf build -.PHONY: doc test clean dist coverage +types: + MYPYPATH=. mypy --check-untyped-defs --warn-incomplete-stub tests/test_acls.py + +.PHONY: doc test clean dist coverage ci types diff --git a/NEWS b/NEWS index 1567ec1..2e21058 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,57 @@ News ==== +Version 0.6.0 +------------- + +*Sun, 29 Nov 2020* + +Major release removing Python 2 support. This allow both code cleanup +and new features, such as: + +- Support for pathlib objects in `apply_to` and `has_extended` + functions when running with Python 3.6 and newer. +- Use of built-in C API functions for bytes/unicode/pathlib conversion + when dealing with file names, removing custom code (with the + associated benefits). + +Important API changes/bug fixes: + +- Initialisation protocol has been changed, to disallow uninitialised + objects; this means that `__new__` will always create valid objects, + to prevent the need for checking initialisation status in all code + paths; this also (implicitly) fixes memory leaks on re-initialisation + (calling `__init__(…)` on an existing object) and segfaults (!) on + non-initialised object attribute access. Note ACL re-initialisation is + tricky and (still) leads to undefined behaviour of existing Entry + objects pointing to it. +- Fix another bug in ACL re-initialisation where failures would result + in invalid objects; now failed re-initialisation does not touch the + original object. +- Restore `__setstate__`/`__getstate__` support on Linux; this was + inadvertently removed due a typo(!) when adding support for it in + FreeBSD. Pickle should work again for ACL instances, although not sure + how stable this serialisation format actually is. +- Additionally, slightly change `__setstate__()` input to not allow + Unicode, since the serialisation format is an opaque binary format. +- Fix (and change) entry qualifier (which is a user/group ID) behaviour: + assume/require that uid_t/gid_t are unsigned types (they are with + glibc, MacOS and FreeBSD at least; the standard doesn't document the + signedness), and convert parsing and returning the qualifier to behave + accordingly. The breakage was most apparent on 32-bit architectures, + in which context the problem was originally reported (see issue #13). + +Minor improvements: + +- Added a `data` keyword argument to `ACL()`, which allows restoring an + ACL directly from a serialised form (as given by `__getstate__()`), + which should simplify some uses cases (`a = ACL(); a.__set + state__(…)`). +- When available, add the file path to I/O error messages, which should + lead to easier debugging. +- The test suite has changed to `pytest`, which allows increased + coverage via parameterisation. + Version 0.5.4 ------------- @@ -55,7 +106,6 @@ docstrings. Project reorganisation: the project home page has been moved from SourceForge to GitHub. - Version 0.5 ----------- diff --git a/PKG-INFO b/PKG-INFO index 8781e00..3f83b9c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,12 +1,24 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.2 Name: pylibacl -Version: 0.5.4 +Version: 0.6.0 Summary: POSIX.1e ACLs for python -Home-page: http://pylibacl.k1024.org/ +Home-page: https://pylibacl.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL +Project-URL: Bug Tracker, https://github.com/iustin/pylibacl/issues Description: This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5). Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Operating System :: POSIX :: BSD :: FreeBSD +Classifier: Operating System :: POSIX :: Linux +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Filesystems +Requires-Python: >=3.4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd4c37c --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# pylibacl + +This is a Python 3.4+ extension module allows you to manipulate the +POSIX.1e Access Control Lists present in some OS/file-systems +combinations. + +Downloads: go to . Latest version +is 0.6.0. The source repository is either at + or at +. + +For any issues, please file bugs at +. + +[![Travis](https://img.shields.io/travis/iustin/pylibacl)](https://travis-ci.org/iustin/pylibacl) +[![Codecov](https://img.shields.io/codecov/c/github/iustin/pylibacl)](https://codecov.io/gh/iustin/pylibacl) +[![Read the Docs](https://img.shields.io/readthedocs/pylibacl)](http://pylibacl.readthedocs.io/en/latest/?badge=latest) +[![GitHub issues](https://img.shields.io/github/issues/iustin/pylibacl)](https://github.com/iustin/pylibacl/issues) +![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/iustin/pylibacl) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/iustin/pylibacl)](https://github.com/iustin/pylibacl/releases) +[![PyPI](https://img.shields.io/pypi/v/pylibacl)](https://pypi.org/project/pylibacl/) +![Debian package](https://img.shields.io/debian/v/python-pylibacl) +![Ubuntu package](https://img.shields.io/ubuntu/v/python-pylibacl) +![GitHub Release Date](https://img.shields.io/github/release-date/iustin/pylibacl) +![GitHub commits since latest release](https://img.shields.io/github/commits-since/iustin/pylibacl/latest) +![GitHub last commit](https://img.shields.io/github/last-commit/iustin/pylibacl) + +## Requirements + +pylibacl has been written and tested on Linux, kernel v2.4 or newer, +with XFS filesystems; ext2/ext3 should also work. Since release 0.4.0, +FreeBSD 7 also has quite good support. If any other platform +implements the POSIX.1e draft, pylibacl can be used. I heard that +Solaris does, but I can't test it. + +- Python 3.4 or newer. Python 2.4+ was supported in the 0.5.x branch. +- Operating system: + - Linux, kernel v2.4 or newer, and the libacl library and + development packages (all modern distributions should have this, + under various names); also the file-systems you use must have + ACLs turned on, either as a compile or mount option. + - FreeBSD 7.0 or newer. +- The sphinx python module, for your python version, if building the + documentation. + +## FreeBSD + +Note that on FreeBSD, ACLs are not enabled by default (at least on UFS +file systems). To enable them, run `tunefs -a enabled` on the file +system in question (after mounting it read-only). Then install: + +- `pkg install py36-setuptools py36-sphinx` + +or: + +- `pkg install py37-setuptools` + + +License +------- + +pylibacl is Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop. + +pylibacl is free software; you can redistribute it and/or modify it under the +terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 2.1 of the License, or (at your option) any +later version. See the COPYING file for the full license terms. diff --git a/README.rst b/README.rst deleted file mode 100644 index 53c09b7..0000000 --- a/README.rst +++ /dev/null @@ -1,60 +0,0 @@ -pylibacl -======== - -This is a Python 2.7+ extension module allows you to manipulate the -POSIX.1e Access Control Lists present in some OS/file-systems -combinations. - -Downloads: go to http://pylibacl.k1024.org/downloads. Latest version -is 0.5.4. The source repository is either at -https://git.k1024.org/pylibacl.git or at -https://github.com/iustin/pylibacl. - -For any issues, please file bugs at -https://github.com/iustin/pylibacl/issues. - -Requirements ------------- - -pylibacl has been written and tested on Linux, kernel v2.4 or newer, -with XFS filesystems; ext2/ext3 should also work. Since release 0.4.0, -FreeBSD 7 also has quite good support. If any other platform -implements the POSIX.1e draft, pylibacl can be used. I heard that -Solaris does, but I can't test it. - -- Python 2.7 or newer. -- Operating system: - - Linux, kernel v2.4 or newer, and the libacl library and - development packages (all modern distributions should have this, - under various names); also the file-systems you use must have - ACLs turned on, either as a compile or mount option. - - FreeBSD 7.0 or newer. -- The sphinx python module, for your python version, if building the - documentation. - -Note: to build from source, by default, Python 3 is needed. It can -still be built with Python 2, by calling `make PYTHON=python2`. - -FreeBSD -+++++++ - -Note that on FreeBSD, ACLs are not enabled by default (at least on UFS -file systems). To enable them, run `tunefs -a enabled` on the file -system in question (after mounting it read-only). Then install: - -- pkg install py36-setuptools py36-sphinx - -or: - -- pkg install py37-setuptools - - -License -------- - -pylibacl is Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop. - -pylibacl is free software; you can redistribute it and/or modify it under the -terms of the GNU Lesser General Public License as published by the Free -Software Foundation; either version 2.1 of the License, or (at your option) any -later version. See the COPYING file for the full license terms. diff --git a/acl.c b/acl.c index f13a5c1..f38bada 100644 --- a/acl.c +++ b/acl.c @@ -20,6 +20,7 @@ */ +#define PY_SSIZE_T_CLEAN #include #include @@ -32,33 +33,6 @@ #define get_perm acl_get_perm_np #endif -#if PY_MAJOR_VERSION >= 3 -#define IS_PY3K -#define PyInt_Check(op) PyLong_Check(op) -#define PyInt_FromString PyLong_FromString -#define PyInt_FromUnicode PyLong_FromUnicode -#define PyInt_FromLong PyLong_FromLong -#define PyInt_FromSize_t PyLong_FromSize_t -#define PyInt_FromSsize_t PyLong_FromSsize_t -#define PyInt_AsLong PyLong_AsLong -#define PyInt_AsSsize_t PyLong_AsSsize_t -#define PyInt_AsUnsignedLongMask PyLong_AsUnsignedLongMask -#define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask -#define PyInt_AS_LONG PyLong_AS_LONG -#define MyString_FromFormat PyUnicode_FromFormat -#define MyString_FromString PyUnicode_FromString -#define MyString_FromStringAndSize PyUnicode_FromStringAndSize -#else -#define PyBytes_Check PyString_Check -#define PyBytes_AS_STRING PyString_AS_STRING -#define PyBytes_FromStringAndSize PyString_FromStringAndSize -#define PyBytes_FromString PyString_FromString -#define PyBytes_FromFormat PyString_FromFormat -#define MyString_FromFormat PyBytes_FromFormat -#define MyString_FromString PyBytes_FromString -#define MyString_FromStringAndSize PyBytes_FromStringAndSize -#endif - /* Used for cpychecker: */ /* The checker automatically defines this preprocessor name when creating the custom attribute: */ @@ -131,15 +105,24 @@ typedef struct { static PyObject* ACL_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newacl; + ACL_Object *acl; newacl = type->tp_alloc(type, 0); - if(newacl != NULL) { - ((ACL_Object*)newacl)->acl = NULL; -#ifdef HAVEL_LEVEL2 - ((ACL_Object*)newacl)->entry_id = ACL_FIRST_ENTRY; -#endif + if(newacl == NULL) { + return NULL; } + acl = (ACL_Object*) newacl; + + acl->acl = acl_init(0); + if (acl->acl == NULL) { + PyErr_SetFromErrno(PyExc_IOError); + Py_DECREF(newacl); + return NULL; + } +#ifdef HAVE_LEVEL2 + acl->entry_id = ACL_FIRST_ENTRY; +#endif return newacl; } @@ -147,20 +130,36 @@ static PyObject* ACL_new(PyTypeObject* type, PyObject* args, /* Initialization of a new ACL instance */ static int ACL_init(PyObject* obj, PyObject* args, PyObject *keywds) { ACL_Object* self = (ACL_Object*) obj; -#ifdef HAVE_LINUX static char *kwlist[] = { "file", "fd", "text", "acl", "filedef", - "mode", NULL }; - char *format = "|etisO!si"; +#ifdef HAVE_LINUX + "mode", +#endif +#ifdef HAVE_ACL_COPY_EXT + "data", +#endif + NULL }; + char *format = "|O&OsO!O&" +#ifdef HAVE_LINUX + "i" +#endif +#ifdef HAVE_ACL_COPY_EXT + "y#" +#endif + ; + acl_t new = NULL; +#ifdef HAVE_LINUX int mode = -1; -#else - static char *kwlist[] = { "file", "fd", "text", "acl", "filedef", NULL }; - char *format = "|etisO!s"; #endif - char *file = NULL; - char *filedef = NULL; + PyObject *file = NULL; + PyObject *filedef = NULL; char *text = NULL; - int fd = -1; + PyObject *fd = NULL; ACL_Object* thesrc = NULL; +#ifdef HAVE_ACL_COPY_EXT + const void *buf = NULL; + Py_ssize_t bufsize; +#endif + int set_err = 0; if(!PyTuple_Check(args) || PyTuple_Size(args) != 0 || (keywds != NULL && PyDict_Check(keywds) && PyDict_Size(keywds) > 1)) { @@ -169,41 +168,74 @@ static int ACL_init(PyObject* obj, PyObject* args, PyObject *keywds) { return -1; } if(!PyArg_ParseTupleAndKeywords(args, keywds, format, kwlist, - NULL, &file, &fd, &text, &ACL_Type, - &thesrc, &filedef + PyUnicode_FSConverter, &file, + &fd, &text, &ACL_Type, &thesrc, + PyUnicode_FSConverter, &filedef #ifdef HAVE_LINUX , &mode +#endif +#ifdef HAVE_ACL_COPY_EXT + , &buf, &bufsize #endif )) return -1; - /* Free the old acl_t without checking for error, we don't - * care right now */ - if(self->acl != NULL) - acl_free(self->acl); - - if(file != NULL) - self->acl = acl_get_file(file, ACL_TYPE_ACCESS); - else if(text != NULL) - self->acl = acl_from_text(text); - else if(fd != -1) - self->acl = acl_get_fd(fd); - else if(thesrc != NULL) - self->acl = acl_dup(thesrc->acl); - else if(filedef != NULL) - self->acl = acl_get_file(filedef, ACL_TYPE_DEFAULT); + if(file != NULL) { + char *path = PyBytes_AS_STRING(file); + new = acl_get_file(path, ACL_TYPE_ACCESS); + // Set custom exception on this failure path which includes + // the filename. + if (new == NULL) { + PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); + set_err = 1; + } + Py_DECREF(file); + } else if(text != NULL) + new = acl_from_text(text); + else if(fd != NULL) { + int fdval; + if ((fdval = PyObject_AsFileDescriptor(fd)) != -1) { + new = acl_get_fd(fdval); + } + } else if(thesrc != NULL) + new = acl_dup(thesrc->acl); + else if(filedef != NULL) { + char *path = PyBytes_AS_STRING(filedef); + new = acl_get_file(path, ACL_TYPE_DEFAULT); + // Set custom exception on this failure path which includes + // the filename. + if (new == NULL) { + PyErr_SetFromErrnoWithFilename(PyExc_IOError, path); + set_err = 1; + } + Py_DECREF(filedef); + } #ifdef HAVE_LINUX else if(mode != -1) - self->acl = acl_from_mode(mode); + new = acl_from_mode(mode); +#endif +#ifdef HAVE_ACL_COPY_EXT + else if(buf != NULL) { + new = acl_copy_int(buf); + } #endif else - self->acl = acl_init(0); + new = acl_init(0); - if(self->acl == NULL) { - PyErr_SetFromErrno(PyExc_IOError); + if(new == NULL) { + if (!set_err) { + PyErr_SetFromErrno(PyExc_IOError); + } return -1; } + /* Free the old acl_t without checking for error, we don't + * care right now */ + if(self->acl != NULL) + acl_free(self->acl); + + self->acl = new; + return 0; } @@ -232,7 +264,7 @@ static PyObject* ACL_str(PyObject *obj) { if(text == NULL) { return PyErr_SetFromErrno(PyExc_IOError); } - ret = MyString_FromString(text); + ret = PyUnicode_FromString(text); if(acl_free(text) != 0) { Py_XDECREF(ret); return PyErr_SetFromErrno(PyExc_IOError); @@ -384,16 +416,7 @@ static PyObject* ACL_equiv_mode(PyObject* obj, PyObject* args) { if(acl_equiv_mode(self->acl, &mode) == -1) return PyErr_SetFromErrno(PyExc_IOError); - return PyInt_FromLong(mode); -} -#endif - -#ifndef IS_PY3K -/* Implementation of the compare for ACLs */ -static int ACL_nocmp(PyObject* o1, PyObject* o2) { - - PyErr_SetString(PyExc_TypeError, "cannot compare ACLs using cmp()"); - return -1; + return PyLong_FromLong(mode); } #endif @@ -411,40 +434,38 @@ static char __applyto_doc__[] = /* Applies the ACL to a file */ static PyObject* ACL_applyto(PyObject* obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; - PyObject *myarg; + PyObject *target, *tmp; acl_type_t type = ACL_TYPE_ACCESS; int nret; int fd; - if (!PyArg_ParseTuple(args, "O|I", &myarg, &type)) + if (!PyArg_ParseTuple(args, "O|I", &target, &type)) return NULL; - - if(PyBytes_Check(myarg)) { - char *filename = PyBytes_AS_STRING(myarg); - nret = acl_set_file(filename, type, self->acl); - } else if (PyUnicode_Check(myarg)) { - PyObject *o = - PyUnicode_AsEncodedString(myarg, - Py_FileSystemDefaultEncoding, "strict"); - if (o == NULL) - return NULL; - const char *filename = PyBytes_AS_STRING(o); - nret = acl_set_file(filename, type, self->acl); - Py_DECREF(o); - } else if((fd = PyObject_AsFileDescriptor(myarg)) != -1) { - nret = acl_set_fd(fd, self->acl); + if ((fd = PyObject_AsFileDescriptor(target)) != -1) { + if((nret = acl_set_fd(fd, self->acl)) == -1) { + PyErr_SetFromErrno(PyExc_IOError); + } } else { - PyErr_SetString(PyExc_TypeError, "argument 1 must be string, int," - " or file-like object"); - return 0; + // PyObject_AsFileDescriptor sets an error when failing, so clear + // it such that further code works; some method lookups fail if an + // error already occured when called, which breaks at least + // PyOS_FSPath (called by FSConverter). + PyErr_Clear(); + if(PyUnicode_FSConverter(target, &tmp)) { + char *filename = PyBytes_AS_STRING(tmp); + if ((nret = acl_set_file(filename, type, self->acl)) == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); + } + Py_DECREF(tmp); + } else { + nret = -1; + } } - if(nret == -1) { - return PyErr_SetFromErrno(PyExc_IOError); + if (nret < 0) { + return NULL; + } else { + Py_RETURN_NONE; } - - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; } static char __valid_doc__[] = @@ -516,11 +537,11 @@ static PyObject* ACL_get_state(PyObject *obj, PyObject* args) { static PyObject* ACL_set_state(PyObject *obj, PyObject* args) { ACL_Object *self = (ACL_Object*) obj; const void *buf; - int bufsize; + Py_ssize_t bufsize; acl_t ptr; /* Parse the argument */ - if (!PyArg_ParseTuple(args, "s#", &buf, &bufsize)) + if (!PyArg_ParseTuple(args, "y#", &buf, &bufsize)) return NULL; /* Try to import the external representation */ @@ -535,9 +556,7 @@ static PyObject* ACL_set_state(PyObject *obj, PyObject* args) { self->acl = ptr; - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #endif @@ -601,12 +620,15 @@ static PyObject* ACL_delete_entry(PyObject *obj, PyObject *args) { if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &e)) return NULL; + if (e->parent_acl != obj) { + PyErr_SetString(PyExc_ValueError, + "Can't remove un-owned entry"); + return NULL; + } if(acl_delete_entry(self->acl, e->entry) == -1) return PyErr_SetFromErrno(PyExc_IOError); - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char __ACL_calc_mask_doc__[] = @@ -633,9 +655,7 @@ static PyObject* ACL_calc_mask(PyObject *obj, PyObject *args) { if(acl_calc_mask(&self->acl) == -1) return PyErr_SetFromErrno(PyExc_IOError); - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char __ACL_append_doc__[] = @@ -654,25 +674,22 @@ static char __ACL_append_doc__[] = /* Convenience method to create a new Entry */ static PyObject* ACL_append(PyObject *obj, PyObject *args) { - ACL_Object* self = (ACL_Object*) obj; Entry_Object* newentry; Entry_Object* oldentry = NULL; int nret; - newentry = (Entry_Object*)PyType_GenericNew(&Entry_Type, NULL, NULL); - if(newentry == NULL) { + if (!PyArg_ParseTuple(args, "|O!", &Entry_Type, &oldentry)) { return NULL; } - if (!PyArg_ParseTuple(args, "|O!", &Entry_Type, &oldentry)) { - Py_DECREF(newentry); + PyObject *new_arglist = Py_BuildValue("(O)", obj); + if (new_arglist == NULL) { return NULL; } - - nret = acl_create_entry(&self->acl, &newentry->entry); - if(nret == -1) { - Py_DECREF(newentry); - return PyErr_SetFromErrno(PyExc_IOError); + newentry = (Entry_Object*) PyObject_CallObject((PyObject*)&Entry_Type, new_arglist); + Py_DECREF(new_arglist); + if(newentry == NULL) { + return NULL; } if(oldentry != NULL) { @@ -683,9 +700,6 @@ static PyObject* ACL_append(PyObject *obj, PyObject *args) { } } - newentry->parent_acl = obj; - Py_INCREF(obj); - return (PyObject*)newentry; } @@ -735,18 +749,38 @@ static int get_tag_qualifier(acl_entry_t entry, tag_qual *tq) { return 0; } +#define ENTRY_SET_CHECK(self, attr, value) \ + if (value == NULL) { \ + PyErr_SetString(PyExc_TypeError, \ + attr " deletion is not supported"); \ + return -1; \ + } + /* Creation of a new Entry instance */ static PyObject* Entry_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newentry; + Entry_Object* entry; + ACL_Object* parent = NULL; + + if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) + return NULL; newentry = PyType_GenericNew(type, args, keywds); - if(newentry != NULL) { - ((Entry_Object*)newentry)->entry = NULL; - ((Entry_Object*)newentry)->parent_acl = NULL; + if(newentry == NULL) { + return NULL; } + entry = (Entry_Object*)newentry; + + if(acl_create_entry(&parent->acl, &entry->entry) == -1) { + PyErr_SetFromErrno(PyExc_IOError); + Py_DECREF(newentry); + return NULL; + } + Py_INCREF(parent); + entry->parent_acl = (PyObject*)parent; return newentry; } @@ -758,14 +792,11 @@ static int Entry_init(PyObject* obj, PyObject* args, PyObject *keywds) { if (!PyArg_ParseTuple(args, "O!", &ACL_Type, &parent)) return -1; - if(acl_create_entry(&parent->acl, &self->entry) == -1) { - PyErr_SetFromErrno(PyExc_IOError); + if ((PyObject*)parent != self->parent_acl) { + PyErr_SetString(PyExc_ValueError, + "Can't reinitialize with a different parent"); return -1; } - - self->parent_acl = (PyObject*)parent; - Py_INCREF(parent); - return 0; } @@ -796,70 +827,61 @@ static PyObject* Entry_str(PyObject *obj) { return NULL; } - format = MyString_FromString("ACL entry for "); + format = PyUnicode_FromString("ACL entry for "); if(format == NULL) return NULL; switch(tq.tag) { case ACL_UNDEFINED_TAG: - kind = MyString_FromString("undefined type"); + kind = PyUnicode_FromString("undefined type"); break; case ACL_USER_OBJ: - kind = MyString_FromString("the owner"); + kind = PyUnicode_FromString("the owner"); break; case ACL_GROUP_OBJ: - kind = MyString_FromString("the group"); + kind = PyUnicode_FromString("the group"); break; case ACL_OTHER: - kind = MyString_FromString("the others"); + kind = PyUnicode_FromString("the others"); break; case ACL_USER: /* FIXME: here and in the group case, we're formatting with unsigned, because there's no way to automatically determine the signed-ness of the types; on Linux(glibc) they're unsigned, so we'll go along with that */ - kind = MyString_FromFormat("user with uid %u", tq.uid); + kind = PyUnicode_FromFormat("user with uid %u", tq.uid); break; case ACL_GROUP: - kind = MyString_FromFormat("group with gid %u", tq.gid); + kind = PyUnicode_FromFormat("group with gid %u", tq.gid); break; case ACL_MASK: - kind = MyString_FromString("the mask"); + kind = PyUnicode_FromString("the mask"); break; default: - kind = MyString_FromString("UNKNOWN_TAG_TYPE!"); + kind = PyUnicode_FromString("UNKNOWN_TAG_TYPE!"); break; } if (kind == NULL) { Py_DECREF(format); return NULL; } -#ifdef IS_PY3K PyObject *ret = PyUnicode_Concat(format, kind); Py_DECREF(format); Py_DECREF(kind); return ret; -#else - PyString_ConcatAndDel(&format, kind); - return format; -#endif } /* Sets the tag type of the entry */ static int Entry_set_tag_type(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*) obj; - if(value == NULL) { - PyErr_SetString(PyExc_TypeError, - "tag type deletion is not supported"); - return -1; - } + ENTRY_SET_CHECK(self, "tag type", value); - if(!PyInt_Check(value)) { + if(!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "tag type must be integer"); return -1; } - if(acl_set_tag_type(self->entry, (acl_tag_t)PyInt_AsLong(value)) == -1) { + if(acl_set_tag_type(self->entry, (acl_tag_t)PyLong_AsLong(value)) == -1) { PyErr_SetFromErrno(PyExc_IOError); return -1; } @@ -872,16 +894,12 @@ static PyObject* Entry_get_tag_type(PyObject *obj, void* arg) { Entry_Object *self = (Entry_Object*) obj; acl_tag_t value; - if (self->entry == NULL) { - PyErr_SetString(PyExc_AttributeError, "entry attribute"); - return NULL; - } if(acl_get_tag_type(self->entry, &value) == -1) { PyErr_SetFromErrno(PyExc_IOError); return NULL; } - return PyInt_FromLong(value); + return PyLong_FromLong(value); } /* Sets the qualifier (either uid_t or gid_t) for the entry, @@ -889,24 +907,23 @@ static PyObject* Entry_get_tag_type(PyObject *obj, void* arg) { */ static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*) obj; - long uidgid; + unsigned long uidgid; uid_t uid; gid_t gid; void *p; acl_tag_t tag; - if(value == NULL) { - PyErr_SetString(PyExc_TypeError, - "qualifier deletion is not supported"); - return -1; - } + ENTRY_SET_CHECK(self, "qualifier", value); - if(!PyInt_Check(value)) { + if(!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "qualifier must be integer"); return -1; } - if((uidgid = PyInt_AsLong(value)) == -1) { + /* This is the negative value check, and larger than long + check. If uid_t/gid_t are long-sized, this is enough to check + for both over and underflow. */ + if((uidgid = PyLong_AsUnsignedLong(value)) == (unsigned long) -1) { if(PyErr_Occurred() != NULL) { return -1; } @@ -920,18 +937,20 @@ static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { } uid = uidgid; gid = uidgid; + /* This is an extra overflow check, in case uid_t/gid_t are + int-sized (and int size smaller than long size). */ switch(tag) { case ACL_USER: - if((long)uid != uidgid) { - PyErr_SetString(PyExc_OverflowError, "cannot assign given qualifier"); + if((unsigned long)uid != uidgid) { + PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); return -1; } else { p = &uid; } break; case ACL_GROUP: - if((long)gid != uidgid) { - PyErr_SetString(PyExc_OverflowError, "cannot assign given qualifier"); + if((unsigned long)gid != uidgid) { + PyErr_SetString(PyExc_OverflowError, "Can't assign given qualifier"); return -1; } else { p = &gid; @@ -939,7 +958,7 @@ static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { break; default: PyErr_SetString(PyExc_TypeError, - "can only set qualifiers on ACL_USER or ACL_GROUP entries"); + "Can only set qualifiers on ACL_USER or ACL_GROUP entries"); return -1; } if(acl_set_qualifier(self->entry, p) == -1) { @@ -953,13 +972,9 @@ static int Entry_set_qualifier(PyObject* obj, PyObject* value, void* arg) { /* Returns the qualifier of the entry */ static PyObject* Entry_get_qualifier(PyObject *obj, void* arg) { Entry_Object *self = (Entry_Object*) obj; - long value; + unsigned long value; tag_qual tq; - if (self->entry == NULL) { - PyErr_SetString(PyExc_AttributeError, "entry attribute"); - return NULL; - } if(get_tag_qualifier(self->entry, &tq) < 0) { return NULL; } @@ -969,11 +984,11 @@ static PyObject* Entry_get_qualifier(PyObject *obj, void* arg) { value = tq.gid; } else { PyErr_SetString(PyExc_TypeError, - "given entry doesn't have an user or" + "Given entry doesn't have an user or" " group tag"); return NULL; } - return PyInt_FromLong(value); + return PyLong_FromUnsignedLong(value); } /* Returns the parent ACL of the entry */ @@ -989,23 +1004,15 @@ static PyObject* Entry_get_parent(PyObject *obj, void* arg) { * should be created at init time! */ static PyObject* Entry_get_permset(PyObject *obj, void* arg) { - Entry_Object *self = (Entry_Object*)obj; PyObject *p; - Permset_Object *ps; - p = Permset_new(&Permset_Type, NULL, NULL); - if(p == NULL) - return NULL; - ps = (Permset_Object*)p; - if(acl_get_permset(self->entry, &ps->permset) == -1) { - PyErr_SetFromErrno(PyExc_IOError); - Py_DECREF(p); + PyObject *perm_arglist = Py_BuildValue("(O)", obj); + if (perm_arglist == NULL) { return NULL; } - ps->parent_entry = obj; - Py_INCREF(obj); - - return (PyObject*)p; + p = PyObject_CallObject((PyObject*)&Permset_Type, perm_arglist); + Py_DECREF(perm_arglist); + return p; } /* Sets the permset of the entry to the passed Permset */ @@ -1013,6 +1020,8 @@ static int Entry_set_permset(PyObject* obj, PyObject* value, void* arg) { Entry_Object *self = (Entry_Object*)obj; Permset_Object *p; + ENTRY_SET_CHECK(self, "permset", value); + if(!PyObject_IsInstance(value, (PyObject*)&Permset_Type)) { PyErr_SetString(PyExc_TypeError, "argument 1 must be posix1e.Permset"); return -1; @@ -1046,8 +1055,7 @@ static PyObject* Entry_copy(PyObject *obj, PyObject *args) { if(acl_copy_entry(self->entry, other->entry) == -1) return PyErr_SetFromErrno(PyExc_IOError); - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } /**** Permset type *****/ @@ -1056,14 +1064,30 @@ static PyObject* Entry_copy(PyObject *obj, PyObject *args) { static PyObject* Permset_new(PyTypeObject* type, PyObject* args, PyObject *keywds) { PyObject* newpermset; + Permset_Object* permset; + Entry_Object* parent = NULL; + + if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) { + return NULL; + } newpermset = PyType_GenericNew(type, args, keywds); - if(newpermset != NULL) { - ((Permset_Object*)newpermset)->permset = NULL; - ((Permset_Object*)newpermset)->parent_entry = NULL; + if(newpermset == NULL) { + return NULL; } + permset = (Permset_Object*)newpermset; + + if(acl_get_permset(parent->entry, &permset->permset) == -1) { + PyErr_SetFromErrno(PyExc_IOError); + Py_DECREF(newpermset); + return NULL; + } + + permset->parent_entry = (PyObject*)parent; + Py_INCREF(parent); + return newpermset; } @@ -1075,14 +1099,12 @@ static int Permset_init(PyObject* obj, PyObject* args, PyObject *keywds) { if (!PyArg_ParseTuple(args, "O!", &Entry_Type, &parent)) return -1; - if(acl_get_permset(parent->entry, &self->permset) == -1) { - PyErr_SetFromErrno(PyExc_IOError); + if ((PyObject*)parent != self->parent_entry) { + PyErr_SetString(PyExc_ValueError, + "Can't reinitialize with a different parent"); return -1; } - self->parent_entry = (PyObject*)parent; - Py_INCREF(parent); - return 0; } @@ -1111,7 +1133,7 @@ static PyObject* Permset_str(PyObject *obj) { pstr[0] = get_perm(self->permset, ACL_READ) ? 'r' : '-'; pstr[1] = get_perm(self->permset, ACL_WRITE) ? 'w' : '-'; pstr[2] = get_perm(self->permset, ACL_EXECUTE) ? 'x' : '-'; - return MyString_FromStringAndSize(pstr, 3); + return PyUnicode_FromStringAndSize(pstr, 3); } static char __Permset_clear_doc__[] = @@ -1125,9 +1147,7 @@ static PyObject* Permset_clear(PyObject* obj, PyObject* args) { if(acl_clear_perms(self->permset) == -1) return PyErr_SetFromErrno(PyExc_IOError); - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static PyObject* Permset_get_right(PyObject *obj, void* arg) { @@ -1145,12 +1165,12 @@ static int Permset_set_right(PyObject* obj, PyObject* value, void* arg) { int on; int nerr; - if(!PyInt_Check(value)) { + if(!PyLong_Check(value)) { PyErr_SetString(PyExc_ValueError, "invalid argument, an integer" " is expected"); return -1; } - on = PyInt_AsLong(value); + on = PyLong_AsLong(value); if(on) nerr = acl_add_perm(self->permset, *(acl_perm_t*)arg); else @@ -1186,9 +1206,7 @@ static PyObject* Permset_add(PyObject* obj, PyObject* args) { if(acl_add_perm(self->permset, (acl_perm_t) right) == -1) return PyErr_SetFromErrno(PyExc_IOError); - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char __Permset_delete_doc__[] = @@ -1215,9 +1233,7 @@ static PyObject* Permset_delete(PyObject* obj, PyObject* args) { if(acl_delete_perm(self->permset, (acl_perm_t) right) == -1) return PyErr_SetFromErrno(PyExc_IOError); - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } static char __Permset_test_doc__[] = @@ -1259,11 +1275,11 @@ static char __ACL_Type_doc__[] = "\n" ".. note:: only one keyword parameter should be provided\n" "\n" - ":param string file: creates an ACL representing\n" - " the access ACL of the specified file.\n" - ":param string filedef: creates an ACL representing\n" + ":param string/bytes/path-like file: creates an ACL representing\n" + " the access ACL of the specified file or directory.\n" + ":param string/bytes/path-like filedef: creates an ACL representing\n" " the default ACL of the given directory.\n" - ":param int fd: creates an ACL representing\n" + ":param int/iostream fd: creates an ACL representing\n" " the access ACL of the given file descriptor.\n" ":param string text: creates an ACL from a \n" " textual description; note the ACL must be valid, which\n" @@ -1274,6 +1290,8 @@ static char __ACL_Type_doc__[] = " (e.g. ``mode=0644``); this is valid only when the C library\n" " provides the ``acl_from_mode call``, and\n" " note that no validation is done on the given value.\n" + ":param bytes data: creates an ACL from a serialised form,\n" + " as provided by calling ``__getstate__()`` on an existing ACL\n" "\n" "If no parameters are passed, an empty ACL will be created; this\n" "makes sense only when your OS supports ACL modification\n" @@ -1291,7 +1309,7 @@ static PyMethodDef ACL_methods[] = { {"check", ACL_check, METH_NOARGS, __check_doc__}, {"equiv_mode", ACL_equiv_mode, METH_NOARGS, __equiv_mode_doc__}, #endif -#ifdef HAVE_ACL_COPYEXT +#ifdef HAVE_ACL_COPY_EXT {"__getstate__", ACL_get_state, METH_NOARGS, "Dumps the ACL to an external format."}, {"__setstate__", ACL_set_state, METH_VARARGS, @@ -1308,12 +1326,7 @@ static PyMethodDef ACL_methods[] = { /* The definition of the ACL Type */ static PyTypeObject ACL_Type = { -#ifdef IS_PY3K PyVarObject_HEAD_INIT(NULL, 0) -#else - PyObject_HEAD_INIT(NULL) - 0, -#endif "posix1e.ACL", sizeof(ACL_Object), 0, @@ -1321,12 +1334,8 @@ static PyTypeObject ACL_Type = { 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ -#ifdef IS_PY3K 0, /* formerly tp_compare, in 3.0 deprecated, in 3.5 tp_as_async */ -#else - ACL_nocmp, /* tp_compare */ -#endif 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ @@ -1435,12 +1444,7 @@ static char __Entry_Type_doc__[] = ; /* The definition of the Entry Type */ static PyTypeObject Entry_Type = { -#ifdef IS_PY3K PyVarObject_HEAD_INIT(NULL, 0) -#else - PyObject_HEAD_INIT(NULL) - 0, -#endif "posix1e.Entry", sizeof(Entry_Object), 0, @@ -1547,12 +1551,7 @@ static char __Permset_Type_doc__[] = /* The definition of the Permset Type */ static PyTypeObject Permset_Type = { -#ifdef IS_PY3K PyVarObject_HEAD_INIT(NULL, 0) -#else - PyObject_HEAD_INIT(NULL) - 0, -#endif "posix1e.Permset", sizeof(Permset_Object), 0, @@ -1616,12 +1615,10 @@ static PyObject* aclmodule_delete_default(PyObject* obj, PyObject* args) { return NULL; if(acl_delete_def_file(filename) == -1) { - return PyErr_SetFromErrno(PyExc_IOError); + return PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); } - /* Return the result */ - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } #ifdef HAVE_LINUX @@ -1635,38 +1632,39 @@ static char __has_extended_doc__[] = /* Check for extended ACL a file or fd */ static PyObject* aclmodule_has_extended(PyObject* obj, PyObject* args) { - PyObject *myarg; + PyObject *item, *tmp; int nret; int fd; - if (!PyArg_ParseTuple(args, "O", &myarg)) + if (!PyArg_ParseTuple(args, "O", &item)) return NULL; - if(PyBytes_Check(myarg)) { - const char *filename = PyBytes_AS_STRING(myarg); - nret = acl_extended_file(filename); - } else if (PyUnicode_Check(myarg)) { - PyObject *o = - PyUnicode_AsEncodedString(myarg, - Py_FileSystemDefaultEncoding, "strict"); - if (o == NULL) - return NULL; - const char *filename = PyBytes_AS_STRING(o); - nret = acl_extended_file(filename); - Py_DECREF(o); - } else if((fd = PyObject_AsFileDescriptor(myarg)) != -1) { - nret = acl_extended_fd(fd); + if((fd = PyObject_AsFileDescriptor(item)) != -1) { + if((nret = acl_extended_fd(fd)) == -1) { + PyErr_SetFromErrno(PyExc_IOError); + } } else { - PyErr_SetString(PyExc_TypeError, "argument 1 must be string, int," - " or file-like object"); - return 0; - } - if(nret == -1) { - return PyErr_SetFromErrno(PyExc_IOError); + // PyObject_AsFileDescriptor sets an error when failing, so clear + // it such that further code works; some method lookups fail if an + // error already occured when called, which breaks at least + // PyOS_FSPath (called by FSConverter). + PyErr_Clear(); + if(PyUnicode_FSConverter(item, &tmp)) { + char *filename = PyBytes_AS_STRING(tmp); + if ((nret = acl_extended_file(filename)) == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_IOError, filename); + } + Py_DECREF(tmp); + } else { + nret = -1; + } } - /* Return the result */ - return PyBool_FromLong(nret); + if (nret < 0) { + return NULL; + } else { + return PyBool_FromLong(nret); + } } #endif @@ -1706,6 +1704,8 @@ static char __posix1e_doc__[] = " - :py:data:`HAS_EXTENDED_CHECK` for the module-level\n" " :py:func:`has_extended` function\n" " - :py:data:`HAS_EQUIV_MODE` for the :py:func:`ACL.equiv_mode` method\n" + " - :py:data:`HAS_COPY_EXT` for the :py:func:`ACL.__getstate__` and\n" + " :py:func:`ACL.__setstate__` functions (pickle protocol)\n" "\n" "Example:\n" "\n" @@ -1775,10 +1775,11 @@ static char __posix1e_doc__[] = ".. py:data:: HAS_EQUIV_MODE\n\n" " denotes support for the equiv_mode function\n" "\n" + ".. py:data:: HAS_COPY_EXT\n\n" + " denotes support for __getstate__()/__setstate__() on an ACL\n" + "\n" ; -#ifdef IS_PY3K - static struct PyModuleDef posix1emodule = { PyModuleDef_HEAD_INIT, "posix1e", @@ -1787,49 +1788,34 @@ static struct PyModuleDef posix1emodule = { aclmodule_methods, }; -#define INITERROR return NULL - PyMODINIT_FUNC PyInit_posix1e(void) - -#else -#define INITERROR return - -void initposix1e(void) -#endif { PyObject *m, *d; - Py_TYPE(&ACL_Type) = &PyType_Type; if(PyType_Ready(&ACL_Type) < 0) - INITERROR; + return NULL; #ifdef HAVE_LEVEL2 - Py_TYPE(&Entry_Type) = &PyType_Type; if(PyType_Ready(&Entry_Type) < 0) - INITERROR; + return NULL; - Py_TYPE(&Permset_Type) = &PyType_Type; if(PyType_Ready(&Permset_Type) < 0) - INITERROR; + return NULL; #endif -#ifdef IS_PY3K m = PyModule_Create(&posix1emodule); -#else - m = Py_InitModule3("posix1e", aclmodule_methods, __posix1e_doc__); -#endif if (m==NULL) - INITERROR; + return NULL; d = PyModule_GetDict(m); if (d == NULL) - INITERROR; + return NULL; Py_INCREF(&ACL_Type); if (PyDict_SetItemString(d, "ACL", (PyObject *) &ACL_Type) < 0) - INITERROR; + return NULL; /* 23.3.6 acl_type_t values */ PyModule_AddIntConstant(m, "ACL_TYPE_ACCESS", ACL_TYPE_ACCESS); @@ -1840,12 +1826,12 @@ void initposix1e(void) Py_INCREF(&Entry_Type); if (PyDict_SetItemString(d, "Entry", (PyObject *) &Entry_Type) < 0) - INITERROR; + return NULL; Py_INCREF(&Permset_Type); if (PyDict_SetItemString(d, "Permset", (PyObject *) &Permset_Type) < 0) - INITERROR; + return NULL; /* 23.2.2 acl_perm_t values */ PyModule_AddIntConstant(m, "ACL_READ", ACL_READ); @@ -1891,7 +1877,12 @@ void initposix1e(void) PyModule_AddIntConstant(m, "HAS_EXTENDED_CHECK", LINUX_EXT_VAL); PyModule_AddIntConstant(m, "HAS_EQUIV_MODE", LINUX_EXT_VAL); -#ifdef IS_PY3K - return m; + PyModule_AddIntConstant(m, "HAS_COPY_EXT", +#ifdef HAVE_ACL_COPY_EXT + 1 +#else + 0 #endif + ); + return m; } diff --git a/doc/conf.py b/doc/conf.py index 5083af5..0d2504a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -31,7 +31,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo'] templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ['.rst', '.md'] # The encoding of source files. #source_encoding = 'utf-8-sig' @@ -48,9 +48,9 @@ copyright = u'2002-2009, 2012, 2014, 2015, Iustin Pop' # built documents. # # The short X.Y version. -version = '0.5.4' +version = '0.6.0' # The full version, including alpha/beta/rc tags. -release = '0.5.4' +release = '0.6.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -69,6 +69,8 @@ exclude_patterns = ['_build', 'html'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None +default_domain = 'python' + # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True @@ -88,6 +90,11 @@ pygments_style = 'sphinx' keep_warnings = True +# Note: this is still needed in Sphinx 1.8 with recommonmark 0.4.0 +# (https://github.com/readthedocs/recommonmark/issues/119): +source_parsers = { + '.md': 'recommonmark.parser.CommonMarkParser', +} # -- Options for HTML output --------------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst index 5581815..b47a5b6 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,8 +2,8 @@ Welcome to pylibacl's documentation! ====================================== -.. include:: ../README.rst - :start-line: 2 +See the :doc:`README ` for start, or the detailed :doc:`module +` information. Contents -------- @@ -11,6 +11,7 @@ Contents .. toctree:: :maxdepth: 2 + readme.md module.rst implementation.rst news.rst diff --git a/doc/news.rst b/doc/news.rst deleted file mode 100644 index 1567ec1..0000000 --- a/doc/news.rst +++ /dev/null @@ -1,142 +0,0 @@ -News -==== - -Version 0.5.4 -------------- - -*released Thu, 14 Nov 2019* - -Maintenance release: - -- Switch build system to Python 3 by default (can be overridden if - needed). -- Internal improvements for better cpychecker support. -- Fix compatibility with PyPy. -- Test improvements (both local and on Travis), testing more variations - (debug, PyPy). -- Improve test coverage, and allow gathering test coverage results. -- Drop support (well, drop testing) for Python lower than 2.7. -- Minor documentation improvements (closes #9, #12). - -Version 0.5.3 -------------- - -*released Thu, 30 Apr 2015* - -FreeBSD fixes: - -- Enable all FreeBSD versions after 7.x at level 2 (thanks to Garrett - Cooper). -- Make test suite pass under FreeBSD, which has a stricter behaviour - with regards to invalid ACLs (which we do exercise in the test suite), - thanks again to Garret for the bug reports. - -Version 0.5.2 -------------- - -*released Sat, 24 May 2014* - -No visible changes release: just fix tests when running under pypy. - -Version 0.5.1 -------------- - -*released Sun, 13 May 2012* - -A bug-fix only release. Critical bugs (memory leaks and possible -segmentation faults) have been fixed thanks to Dave Malcolm and his -``cpychecker`` tool. Additionally, some compatibility issues with Python -3.x have been fixed (str() methods returning bytes). - -The documentation has been improved and changed from epydoc to sphinx; -note however that the documentation is still auto-generated from the -docstrings. - -Project reorganisation: the project home page has been moved from -SourceForge to GitHub. - - -Version 0.5 ------------ - -*released Sun, 27 Dec 2009* - -Added support for Python 3.x and improved support for Unicode filenames. - -Version 0.4 ------------ - -*released Sat, 28 Jun 2008* - -License -~~~~~~~ - -Starting with this version, pylibacl is licensed under LGPL 2.1, -Febryary 1999 or any later versions (see README.rst and COPYING). - -Linux support -~~~~~~~~~~~~~ - -A few more Linux-specific functions: - -- add the ACL.equiv_mode() method, which will return the equivalent - octal mode if this is a basic ACL and raise an IOError exception - otherwise - -- add the acl_extended(...) function, which will check if an fd or path - has an extended ACL - -FreeBSD support -~~~~~~~~~~~~~~~ - -FreeBSD 7.x will have almost all the acl manipulation functions that -Linux has, with the exception of __getstate__/__setstate__. As a -workaround, use the str() and ACL(text=...) methods to pass around -textual representations. - -Interface -~~~~~~~~~ - -At module level there are now a few constants exported for easy-checking -at runtime what features have been compiled in: - -- HAS_ACL_FROM_MODE, denoting whether the ACL constructor supports the - mode=0xxx parameter - -- HAS_ACL_CHECK, denoting whether ACL instances support the check() - method - -- HAS_ACL_ENTRY, denoting whether ACL manipulation is possible and the - Entry and Permset classes are available - -- HAS_EXTENEDED_CHECK, denoting whether the acl_extended function is - supported - -- HAS_EQUIV_MODE, denoting whether ACL instances support the - equiv_mode() method - -Internals -~~~~~~~~~ - -Many functions have now unittests, which is a good thing. - - -Version 0.3 ------------ - -*released Sun, 21 Oct 2007* - -Linux support -~~~~~~~~~~~~~ - -Under Linux, implement more functions from libacl: - -- add ACL(mode=...), implementing acl_from_mode -- add ACL().to_any_text, implementing acl_to_any_text -- add ACL comparison, using acl_cmp -- add ACL().check, which is a more descriptive function than validate - -.. Local Variables: -.. mode: rst -.. fill-column: 72 -.. End: diff --git a/posix1e.pyi b/posix1e.pyi new file mode 100644 index 0000000..716d4a8 --- /dev/null +++ b/posix1e.pyi @@ -0,0 +1,77 @@ +# Stubs for posix1e (Python 3) +# +# NOTE: This dynamically typed stub was automatically generated by stubgen. + +from typing import Optional, Union, Tuple, TypeVar +from builtins import _PathLike as PathLike + +from typing import overload +ACL_DUPLICATE_ERROR: int +ACL_ENTRY_ERROR: int +ACL_EXECUTE: int +ACL_GROUP: int +ACL_GROUP_OBJ: int +ACL_MASK: int +ACL_MISS_ERROR: int +ACL_MULTI_ERROR: int +ACL_OTHER: int +ACL_READ: int +ACL_TYPE_ACCESS: int +ACL_TYPE_DEFAULT: int +ACL_UNDEFINED_TAG: int +ACL_USER: int +ACL_USER_OBJ: int +ACL_WRITE: int +HAS_ACL_CHECK: int +HAS_ACL_ENTRY: int +HAS_ACL_FROM_MODE: int +HAS_COPY_EXT: int +HAS_EQUIV_MODE: int +HAS_EXTENDED_CHECK: int +TEXT_ABBREVIATE: int +TEXT_ALL_EFFECTIVE: int +TEXT_NUMERIC_IDS: int +TEXT_SMART_INDENT: int +TEXT_SOME_EFFECTIVE: int + +S = TypeVar('S', str, bytes, int, PathLike) + +def delete_default(path: str) -> None: ... +def has_extended(item: S) -> bool: ... + +class ACL: + def __init__(*args, **kwargs) -> None: ... + @overload + def append(self) -> 'Entry': ... + @overload + def append(self, __entry: 'Entry') -> 'Entry': ... + def applyto(self, __item, flag: Optional[int]=0) -> None: ... + def calc_mask(self) -> None: ... + def check(self) -> Union[bool, Tuple[int, int]]: ... + def delete_entry(self, __entry: 'Entry') -> None: ... + def equiv_mode(self) -> int: ... + def to_any_text(self, prefix: str=..., separator: str=..., options: int=...) -> bytes: ... + def valid(self) -> bool: ... + def __iter__(self) -> 'ACL': ... + def __next__(self) -> 'Entry': ... + def __getstate__(self) -> bytes: ... + def __setstate__(self, state: bytes) -> None: ... + +class Entry: + parent: ACL = ... + permset: 'Permset' = ... + qualifier: int = ... + tag_type: int = ... + def __init__(self, __acl: ACL) -> None: ... + def copy(self, __src: 'Entry') -> None: ... + +class Permset: + execute: bool = ... + read: bool = ... + write: bool = ... + def __init__(self, __entry: Entry) -> None: ... + @classmethod + def add(self, perm: int) -> None: ... + def clear(self) -> None: ... + def delete(self, perm: int) -> None: ... + def test(self, perm: int) -> bool: ... diff --git a/test/__init__.py b/py.typed similarity index 100% rename from test/__init__.py rename to py.typed diff --git a/pylibacl.egg-info/PKG-INFO b/pylibacl.egg-info/PKG-INFO index 8781e00..3f83b9c 100644 --- a/pylibacl.egg-info/PKG-INFO +++ b/pylibacl.egg-info/PKG-INFO @@ -1,12 +1,24 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.2 Name: pylibacl -Version: 0.5.4 +Version: 0.6.0 Summary: POSIX.1e ACLs for python -Home-page: http://pylibacl.k1024.org/ +Home-page: https://pylibacl.k1024.org/ Author: Iustin Pop Author-email: iustin@k1024.org License: LGPL +Project-URL: Bug Tracker, https://github.com/iustin/pylibacl/issues Description: This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5). Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Operating System :: POSIX :: BSD :: FreeBSD +Classifier: Operating System :: POSIX :: Linux +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Filesystems +Requires-Python: >=3.4 diff --git a/pylibacl.egg-info/SOURCES.txt b/pylibacl.egg-info/SOURCES.txt index 9f01a8e..ca9fffd 100644 --- a/pylibacl.egg-info/SOURCES.txt +++ b/pylibacl.egg-info/SOURCES.txt @@ -2,18 +2,19 @@ COPYING MANIFEST.in Makefile NEWS -README.rst +README.md acl.c +posix1e.pyi +py.typed setup.cfg setup.py doc/conf.py doc/implementation.rst doc/index.rst doc/module.rst -doc/news.rst pylibacl.egg-info/PKG-INFO pylibacl.egg-info/SOURCES.txt pylibacl.egg-info/dependency_links.txt +pylibacl.egg-info/not-zip-safe pylibacl.egg-info/top_level.txt -test/__init__.py -test/test_acls.py \ No newline at end of file +tests/test_acls.py \ No newline at end of file diff --git a/pylibacl.egg-info/not-zip-safe b/pylibacl.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pylibacl.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/setup.py b/setup.py index bdc5c2d..bb69a61 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ libs = [] if u_sysname == "Linux": macros.append(("HAVE_LINUX", None)) macros.append(("HAVE_LEVEL2", None)) + macros.append(("HAVE_ACL_COPY_EXT", None)) libs.append("acl") elif u_sysname == "GNU/kFreeBSD": macros.append(("HAVE_LINUX", None)) @@ -30,7 +31,7 @@ long_desc = """This is a C extension module for Python which implements POSIX ACLs manipulation. It is a wrapper on top of the systems's acl C library - see acl(5).""" -version = "0.5.4" +version = "0.6.0" setup(name="pylibacl", version=version, @@ -38,11 +39,31 @@ setup(name="pylibacl", long_description=long_desc, author="Iustin Pop", author_email="iustin@k1024.org", - url="http://pylibacl.k1024.org/", + url="https://pylibacl.k1024.org/", license="LGPL", ext_modules=[Extension("posix1e", ["acl.c"], libraries=libs, define_macros=macros, )], - test_suite="test", + python_requires = ">=3.4", + # Note: doesn't work since it's not a package. Sigh. + package_data = { + '': ['py.typed', 'posix1e.pyi'], + }, + zip_safe=False, + project_urls={ + "Bug Tracker": "https://github.com/iustin/pylibacl/issues", + }, + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: POSIX :: BSD :: FreeBSD", + "Operating System :: POSIX :: Linux", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Filesystems", + ] ) diff --git a/test/test_acls.py b/test/test_acls.py deleted file mode 100644 index c86477f..0000000 --- a/test/test_acls.py +++ /dev/null @@ -1,675 +0,0 @@ -# -# - -"""Unittests for the posix1e module""" - -# Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 USA - - -import unittest -import os -import tempfile -import sys -import platform -import re -import errno -import operator - -import posix1e -from posix1e import * - -try: - import __pypy__ -except ImportError: - __pypy__ = None - -TEST_DIR = os.environ.get("TEST_DIR", ".") - -BASIC_ACL_TEXT = "u::rw,g::r,o::-" - -# This is to workaround python 2/3 differences at syntactic level -# (which can't be worked around via if's) -M0500 = 320 # octal 0500 -M0644 = 420 # octal 0644 -M0755 = 493 # octal 755 - -# Permset permission information -PERMSETS = { - posix1e.ACL_READ: ("read", posix1e.Permset.read), - posix1e.ACL_WRITE: ("write", posix1e.Permset.write), - posix1e.ACL_EXECUTE: ("execute", posix1e.Permset.execute), - } - - -# Check if running under Python 3 -IS_PY_3K = sys.hexversion >= 0x03000000 - -def ignore_ioerror(errnum, fn, *args, **kwargs): - """Call a function while ignoring some IOErrors. - - This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL) - when doing certain operations on an invalid ACL. - - """ - try: - fn(*args, **kwargs) - except IOError: - err = sys.exc_info()[1] - if err.errno == errnum: - return - raise - -def encode(s): - """Encode a string if needed (under Python 3)""" - if IS_PY_3K: - return s.encode() - else: - return s - - -class aclTest: - """Support functions ACLs""" - - def setUp(self): - """set up function""" - self.rmfiles = [] - self.rmdirs = [] - - def tearDown(self): - """tear down function""" - for fname in self.rmfiles: - os.unlink(fname) - for dname in self.rmdirs: - os.rmdir(dname) - - def _getfile(self): - """create a temp file""" - fh, fname = tempfile.mkstemp(".test", "xattr-", TEST_DIR) - self.rmfiles.append(fname) - return fh, fname - - def _getdir(self): - """create a temp dir""" - dname = tempfile.mkdtemp(".test", "xattr-", TEST_DIR) - self.rmdirs.append(dname) - return dname - - def _getsymlink(self): - """create a symlink""" - fh, fname = self._getfile() - os.close(fh) - os.unlink(fname) - os.symlink(fname + ".non-existent", fname) - return fname - - -class LoadTests(aclTest, unittest.TestCase): - """Load/create tests""" - def testFromFile(self): - """Test loading ACLs from a file""" - _, fname = self._getfile() - acl1 = posix1e.ACL(file=fname) - self.assertTrue(acl1.valid(), "ACL read from file should be valid") - - def testFromDir(self): - """Test loading ACLs from a directory""" - dname = self._getdir() - acl1 = posix1e.ACL(file=dname) - acl2 = posix1e.ACL(filedef=dname) - self.assertTrue(acl1.valid(), - "ACL read from directory should be valid") - # default ACLs might or might not be valid; missing ones are - # not valid, so we don't test acl2 for validity - - def testFromFd(self): - """Test loading ACLs from a file descriptor""" - fd, _ = self._getfile() - acl1 = posix1e.ACL(fd=fd) - self.assertTrue(acl1.valid(), "ACL read from fd should be valid") - - def testFromEmpty(self): - """Test creating an empty ACL""" - acl1 = posix1e.ACL() - self.assertFalse(acl1.valid(), "Empty ACL should not be valid") - - def testFromText(self): - """Test creating an ACL from text""" - acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl1.valid(), - "ACL based on standard description should be valid") - - def testFromACL(self): - """Test creating an ACL from an existing ACL""" - acl1 = posix1e.ACL() - acl2 = posix1e.ACL(acl=acl1) - - def testInvalidCreationParams(self): - """Test that creating an ACL from multiple objects fails""" - fd, _ = self._getfile() - self.assertRaises(ValueError, posix1e.ACL, text=BASIC_ACL_TEXT, fd=fd) - - def testInvalidValueCreation(self): - """Test that creating an ACL from wrong specification fails""" - self.assertRaises(EnvironmentError, posix1e.ACL, text="foobar") - self.assertRaises(TypeError, posix1e.ACL, foo="bar") - - def testDoubleInit(self): - acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl1.valid()) - acl1.__init__(text=BASIC_ACL_TEXT) - self.assertTrue(acl1.valid()) - -class AclExtensions(aclTest, unittest.TestCase): - """ACL extensions checks""" - - @unittest.skipUnless(HAS_ACL_FROM_MODE, "Missing HAS_ACL_FROM_MODE") - def testFromMode(self): - """Test loading ACLs from an octal mode""" - acl1 = posix1e.ACL(mode=M0644) - self.assertTrue(acl1.valid(), - "ACL created via octal mode shoule be valid") - - @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported") - def testAclCheck(self): - """Test the acl_check method""" - acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertFalse(acl1.check(), "ACL is not valid") - acl2 = posix1e.ACL() - self.assertTrue(acl2.check(), "Empty ACL should not be valid") - - @unittest.skipUnless(HAS_EXTENDED_CHECK, "Extended ACL check not supported") - def testExtended(self): - """Test the acl_extended function""" - fd, fname = self._getfile() - basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) - basic_acl.applyto(fd) - for item in fd, fname: - self.assertFalse(has_extended(item), - "A simple ACL should not be reported as extended") - enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") - self.assertTrue(enhanced_acl.valid(), - "Failure to build an extended ACL") - enhanced_acl.applyto(fd) - for item in fd, fname: - self.assertTrue(has_extended(item), - "An extended ACL should be reported as such") - - @unittest.skipUnless(HAS_EXTENDED_CHECK, "Extended ACL check not supported") - def testExtendedArgHandling(self): - self.assertRaises(TypeError, has_extended) - self.assertRaises(TypeError, has_extended, object()) - - @unittest.skipUnless(HAS_EQUIV_MODE, "equiv_mode not supported") - def testEquivMode(self): - """Test the equiv_mode function""" - if HAS_ACL_FROM_MODE: - for mode in M0644, M0755: - acl = posix1e.ACL(mode=mode) - self.assertEqual(acl.equiv_mode(), mode) - acl = posix1e.ACL(text="u::rw,g::r,o::r") - self.assertEqual(acl.equiv_mode(), M0644) - acl = posix1e.ACL(text="u::rx,g::-,o::-") - self.assertEqual(acl.equiv_mode(), M0500) - - @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported") - def testToAnyText(self): - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertIn(encode("u::"), - acl.to_any_text(options=posix1e.TEXT_ABBREVIATE)) - self.assertIn(encode("user::"), acl.to_any_text()) - - @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported") - def testToAnyTextWrongArgs(self): - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertRaises(TypeError, acl.to_any_text, foo="bar") - - - @unittest.skipUnless(HAS_ACL_CHECK, "ACL check not supported") - def testRichCompare(self): - acl1 = posix1e.ACL(text="u::rw,g::r,o::r") - acl2 = posix1e.ACL(acl=acl1) - acl3 = posix1e.ACL(text="u::rw,g::rw,o::r") - self.assertEqual(acl1, acl2) - self.assertNotEqual(acl1, acl3) - self.assertRaises(TypeError, operator.lt, acl1, acl2) - self.assertRaises(TypeError, operator.ge, acl1, acl3) - self.assertTrue(acl1 != True) - self.assertFalse(acl1 == 1) - self.assertRaises(TypeError, operator.gt, acl1, True) - - @unittest.skipUnless(hasattr(posix1e.ACL, "__cmp__"), "__cmp__ is missing") - @unittest.skipUnless(__pypy__ is None, "Disabled under pypy") - def testCmp(self): - acl1 = posix1e.ACL() - self.assertRaises(TypeError, acl1.__cmp__, acl1) - - def testApplyToWithWrongObject(self): - acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl1.valid()) - self.assertRaises(TypeError, acl1.applyto, object()) - self.assertRaises(TypeError, acl1.applyto, object(), object()) - - @unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported") - def testAclIterator(self): - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - #self.assertEqual(len(acl), 3) - for entry in acl: - self.assertIs(entry.parent, acl) - - -class WriteTests(aclTest, unittest.TestCase): - """Write tests""" - - def testDeleteDefault(self): - """Test removing the default ACL""" - dname = self._getdir() - posix1e.delete_default(dname) - - @unittest.skipUnless(__pypy__ is None, "Disabled under pypy") - def testDeleteDefaultWrongArg(self): - self.assertRaises(TypeError, posix1e.delete_default, object()) - - def testReapply(self): - """Test re-applying an ACL""" - fd, fname = self._getfile() - acl1 = posix1e.ACL(fd=fd) - acl1.applyto(fd) - acl1.applyto(fname) - dname = self._getdir() - acl2 = posix1e.ACL(file=fname) - acl2.applyto(dname) - - -@unittest.skipUnless(HAS_ACL_ENTRY, "ACL entries not supported") -class ModificationTests(aclTest, unittest.TestCase): - """ACL modification tests""" - - def checkRef(self, obj): - """Checks if a given obj has a 'sane' refcount""" - if platform.python_implementation() == "PyPy": - return - ref_cnt = sys.getrefcount(obj) - # FIXME: hardcoded value for the max ref count... but I've - # seen it overflow on bad reference counting, so it's better - # to be safe - if ref_cnt < 2 or ref_cnt > 1024: - self.fail("Wrong reference count, expected 2-1024 and got %d" % - ref_cnt) - - def testStr(self): - """Test str() of an ACL.""" - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - str_acl = str(acl) - self.checkRef(str_acl) - - def testAppend(self): - """Test append a new Entry to the ACL""" - acl = posix1e.ACL() - e = acl.append() - e.tag_type = posix1e.ACL_OTHER - ignore_ioerror(errno.EINVAL, acl.calc_mask) - str_format = str(e) - self.checkRef(str_format) - e2 = acl.append(e) - ignore_ioerror(errno.EINVAL, acl.calc_mask) - self.assertFalse(acl.valid()) - - def testWrongAppend(self): - """Test append a new Entry to the ACL based on wrong object type""" - acl = posix1e.ACL() - self.assertRaises(TypeError, acl.append, object()) - - def testEntryCreation(self): - acl = posix1e.ACL() - e = posix1e.Entry(acl) - ignore_ioerror(errno.EINVAL, acl.calc_mask) - str_format = str(e) - self.checkRef(str_format) - - def testEntryFailedCreation(self): - # Checks for partial initialisation and deletion on error - # path. - self.assertRaises(TypeError, posix1e.Entry, object()) - - def testDelete(self): - """Test delete Entry from the ACL""" - acl = posix1e.ACL() - e = acl.append() - e.tag_type = posix1e.ACL_OTHER - ignore_ioerror(errno.EINVAL, acl.calc_mask) - acl.delete_entry(e) - ignore_ioerror(errno.EINVAL, acl.calc_mask) - - def testDoubleDelete(self): - """Test delete Entry from the ACL""" - # This is not entirely valid/correct, since the entry object - # itself is invalid after the first deletion, so we're - # actually testing deleting an invalid object, not a - # non-existing entry... - acl = posix1e.ACL() - e = acl.append() - e.tag_type = posix1e.ACL_OTHER - ignore_ioerror(errno.EINVAL, acl.calc_mask) - acl.delete_entry(e) - ignore_ioerror(errno.EINVAL, acl.calc_mask) - self.assertRaises(EnvironmentError, acl.delete_entry, e) - - # This currently fails as this deletion seems to be accepted :/ - @unittest.skip("Entry deletion is unreliable") - def testDeleteInvalidEntry(self): - """Test delete foreign Entry from the ACL""" - acl1 = posix1e.ACL() - acl2 = posix1e.ACL() - e = acl1.append() - e.tag_type = posix1e.ACL_OTHER - ignore_ioerror(errno.EINVAL, acl1.calc_mask) - self.assertRaises(EnvironmentError, acl2.delete_entry, e) - - def testDeleteInvalidObject(self): - """Test delete a non-Entry from the ACL""" - acl = posix1e.ACL() - self.assertRaises(TypeError, acl.delete_entry, object()) - - def testDoubleEntries(self): - """Test double entries""" - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl.valid(), "ACL is not valid") - for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ, - posix1e.ACL_OTHER): - e = acl.append() - e.tag_type = tag_type - e.permset.clear() - self.assertFalse(acl.valid(), - "ACL containing duplicate entries" - " should not be valid") - acl.delete_entry(e) - - def testMultipleGoodEntries(self): - """Test multiple valid entries""" - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl.valid(), "ACL is not valid") - for tag_type in (posix1e.ACL_USER, - posix1e.ACL_GROUP): - for obj_id in range(5): - e = acl.append() - e.tag_type = tag_type - e.qualifier = obj_id - e.permset.clear() - acl.calc_mask() - self.assertTrue(acl.valid(), - "ACL should be able to hold multiple" - " user/group entries") - - def testMultipleBadEntries(self): - """Test multiple invalid entries""" - for tag_type in (posix1e.ACL_USER, - posix1e.ACL_GROUP): - acl = posix1e.ACL(text=BASIC_ACL_TEXT) - self.assertTrue(acl.valid(), "ACL built from standard description" - " should be valid") - e1 = acl.append() - e1.tag_type = tag_type - e1.qualifier = 0 - e1.permset.clear() - acl.calc_mask() - self.assertTrue(acl.valid(), "ACL should be able to add a" - " user/group entry") - e2 = acl.append() - e2.tag_type = tag_type - e2.qualifier = 0 - e2.permset.clear() - ignore_ioerror(errno.EINVAL, acl.calc_mask) - self.assertFalse(acl.valid(), "ACL should not validate when" - " containing two duplicate entries") - acl.delete_entry(e1) - # FreeBSD trips over itself here and can't delete the - # entry, even though it still exists. - ignore_ioerror(errno.EINVAL, acl.delete_entry, e2) - - def testCopy(self): - acl = ACL() - e1 = acl.append() - e1.tag_type = ACL_USER - p1 = e1.permset - p1.clear() - p1.read = True - p1.write = True - e2 = acl.append() - e2.tag_type = ACL_GROUP - p2 = e2.permset - p2.clear() - p2.read = True - self.assertFalse(p2.write) - e2.copy(e1) - self.assertTrue(p2.write) - self.assertEqual(e1.tag_type, e2.tag_type) - - def testCopyWrongArg(self): - acl = ACL() - e = acl.append() - self.assertRaises(TypeError, e.copy, object()) - - def testSetPermset(self): - acl = ACL() - e1 = acl.append() - e1.tag_type = ACL_USER - p1 = e1.permset - p1.clear() - p1.read = True - p1.write = True - e2 = acl.append() - e2.tag_type = ACL_GROUP - p2 = e2.permset - p2.clear() - p2.read = True - self.assertFalse(p2.write) - e2.permset = p1 - self.assertTrue(e2.permset.write) - self.assertEqual(e2.tag_type, ACL_GROUP) - - def testSetPermsetWrongArg(self): - acl = ACL() - e = acl.append() - def setter(v): - e.permset = v - self.assertRaises(TypeError, setter, object()) - - def testPermsetCreation(self): - acl = ACL() - e = acl.append() - p1 = e.permset - p2 = Permset(e) - #self.assertEqual(p1, p2) - - def testPermsetCreationWrongArg(self): - self.assertRaises(TypeError, Permset, object()) - - def testPermset(self): - """Test permissions""" - acl = posix1e.ACL() - e = acl.append() - ps = e.permset - ps.clear() - str_ps = str(ps) - self.checkRef(str_ps) - for perm in PERMSETS: - str_ps = str(ps) - txt = PERMSETS[perm][0] - self.checkRef(str_ps) - self.assertFalse(ps.test(perm), "Empty permission set should not" - " have permission '%s'" % txt) - ps.add(perm) - self.assertTrue(ps.test(perm), "Permission '%s' should exist" - " after addition" % txt) - str_ps = str(ps) - self.checkRef(str_ps) - ps.delete(perm) - self.assertFalse(ps.test(perm), "Permission '%s' should not exist" - " after deletion" % txt) - - def testPermsetViaAccessors(self): - """Test permissions""" - acl = posix1e.ACL() - e = acl.append() - ps = e.permset - ps.clear() - str_ps = str(ps) - self.checkRef(str_ps) - def getter(perm): - return PERMSETS[perm][1].__get__(ps) - def setter(parm, value): - return PERMSETS[perm][1].__set__(ps, value) - for perm in PERMSETS: - str_ps = str(ps) - self.checkRef(str_ps) - txt = PERMSETS[perm][0] - self.assertFalse(getter(perm), "Empty permission set should not" - " have permission '%s'" % txt) - setter(perm, True) - self.assertTrue(ps.test(perm), "Permission '%s' should exist" - " after addition" % txt) - self.assertTrue(getter(perm), "Permission '%s' should exist" - " after addition" % txt) - str_ps = str(ps) - self.checkRef(str_ps) - setter(perm, False) - self.assertFalse(ps.test(perm), "Permission '%s' should not exist" - " after deletion" % txt) - self.assertFalse(getter(perm), "Permission '%s' should not exist" - " after deletion" % txt) - - def testPermsetInvalidType(self): - acl = posix1e.ACL() - e = acl.append() - ps = e.permset - ps.clear() - def setter(): - ps.write = object() - self.assertRaises(TypeError, ps.add, "foobar") - self.assertRaises(TypeError, ps.delete, "foobar") - self.assertRaises(TypeError, ps.test, "foobar") - self.assertRaises(ValueError, setter) - - @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3") - def testQualifierValues(self): - """Tests qualifier correct store/retrieval""" - acl = posix1e.ACL() - e = acl.append() - # work around deprecation warnings - if hasattr(self, 'assertRegex'): - fn = self.assertRegex - else: - fn = self.assertRegexpMatches - for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: - qualifier = 1 - e.tag_type = tag - while True: - if tag == posix1e.ACL_USER: - regex = re.compile("user with uid %d" % qualifier) - else: - regex = re.compile("group with gid %d" % qualifier) - try: - e.qualifier = qualifier - except OverflowError: - # reached overflow condition, break - break - self.assertEqual(e.qualifier, qualifier) - fn(str(e), regex) - qualifier *= 2 - - @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3") - def testQualifierOverflow(self): - """Tests qualifier overflow handling""" - acl = posix1e.ACL() - e = acl.append() - qualifier = sys.maxsize * 2 - for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: - e.tag_type = tag - with self.assertRaises(OverflowError): - e.qualifier = qualifier - - @unittest.skipUnless(IS_PY_3K, "Only supported under Python 3") - def testNegativeQualifier(self): - """Tests negative qualifier handling""" - # Note: this presumes that uid_t/gid_t in C are unsigned... - acl = posix1e.ACL() - e = acl.append() - for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: - e.tag_type = tag - for qualifier in [-10, -5, -1]: - with self.assertRaises(OverflowError): - e.qualifier = qualifier - - def testInvalidQualifier(self): - """Tests invalid qualifier handling""" - acl = posix1e.ACL() - e = acl.append() - def set_qual(x): - e.qualifier = x - def del_qual(): - del e.qualifier - self.assertRaises(TypeError, set_qual, object()) - self.assertRaises((TypeError, AttributeError), del_qual) - - def testQualifierOnWrongTag(self): - """Tests qualifier setting on wrong tag""" - acl = posix1e.ACL() - e = acl.append() - e.tag_type = posix1e.ACL_OTHER - def set_qual(x): - e.qualifier = x - def get_qual(): - return e.qualifier - self.assertRaises(TypeError, set_qual, 1) - self.assertRaises(TypeError, get_qual) - - - def testTagTypes(self): - """Tests tag type correct set/get""" - acl = posix1e.ACL() - e = acl.append() - for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ, - posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK, - posix1e.ACL_OTHER]: - e.tag_type = tag - self.assertEqual(e.tag_type, tag) - # check we can show all tag types without breaking - self.assertTrue(str(e)) - - def testInvalidTags(self): - """Tests tag type incorrect set/get""" - acl = posix1e.ACL() - e = acl.append() - def set_tag(x): - e.tag_type = x - self.assertRaises(TypeError, set_tag, object()) - def delete_tag(): - del e.tag_type - # For some reason, PyPy raises AttributeError. Strange... - self.assertRaises((TypeError, AttributeError), delete_tag) - - e.tag_type = posix1e.ACL_USER_OBJ - tag = max([posix1e.ACL_USER, posix1e.ACL_GROUP, posix1e.ACL_USER_OBJ, - posix1e.ACL_GROUP_OBJ, posix1e.ACL_MASK, - posix1e.ACL_OTHER]) + 1 - self.assertRaises(EnvironmentError, set_tag, tag) - # Check tag is still valid. - self.assertEqual(e.tag_type, posix1e.ACL_USER_OBJ) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_acls.py b/tests/test_acls.py new file mode 100644 index 0000000..2272d05 --- /dev/null +++ b/tests/test_acls.py @@ -0,0 +1,1004 @@ +# +# + +"""Unittests for the posix1e module""" + +# Copyright (C) 2002-2009, 2012, 2014, 2015 Iustin Pop +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + + +import unittest +import os +import tempfile +import sys +import platform +import re +import errno +import operator +import pytest # type: ignore +import contextlib +import pathlib +import io + +import posix1e +from posix1e import * + +TEST_DIR = os.environ.get("TEST_DIR", ".") + +BASIC_ACL_TEXT = "u::rw,g::r,o::-" +TEXT_0755 = "u::rwx,g::rx,o::rx" + +# Permset permission information +PERMSETS = [ + (ACL_READ, "read", Permset.read), + (ACL_WRITE, "write", Permset.write), + (ACL_EXECUTE, "execute", Permset.execute), +] + +PERMSETS_IDS = [p[1] for p in PERMSETS] + +ALL_TAGS = [ + (posix1e.ACL_USER, "user"), + (posix1e.ACL_GROUP, "group"), + (posix1e.ACL_USER_OBJ, "user object"), + (posix1e.ACL_GROUP_OBJ, "group object"), + (posix1e.ACL_MASK, "mask"), + (posix1e.ACL_OTHER, "other"), +] + +ALL_TAG_VALUES = [i[0] for i in ALL_TAGS] +ALL_TAG_DESCS = [i[1] for i in ALL_TAGS] + +# Fixtures and helpers + +def ignore_ioerror(errnum, fn, *args, **kwargs): + """Call a function while ignoring some IOErrors. + + This is needed as some OSes (e.g. FreeBSD) return failure (EINVAL) + when doing certain operations on an invalid ACL. + + """ + try: + fn(*args, **kwargs) + except IOError as err: + if err.errno == errnum: + return + raise + +def assert_acl_eq(a, b): + if HAS_ACL_CHECK: + assert a == b + assert str(a) == str(b) + +@pytest.fixture +def testdir(): + """per-test temp dir based in TEST_DIR""" + with tempfile.TemporaryDirectory(dir=TEST_DIR) as dname: + yield dname + +def get_file(path): + fh, fname = tempfile.mkstemp(".test", "xattr-", path) + return fh, fname + +@contextlib.contextmanager +def get_file_name(path): + fh, fname = get_file(path) + os.close(fh) + yield fname + +@contextlib.contextmanager +def get_file_fd(path): + fd = get_file(path)[0] + yield fd + os.close(fd) + +@contextlib.contextmanager +def get_file_object(path): + fd = get_file(path)[0] + with os.fdopen(fd) as f: + yield f + +@contextlib.contextmanager +def get_dir(path): + yield tempfile.mkdtemp(".test", "xattr-", path) + +def get_symlink(path, dangling=True): + """create a symlink""" + fh, fname = get_file(path) + os.close(fh) + if dangling: + os.unlink(fname) + sname = fname + ".symlink" + os.symlink(fname, sname) + return fname, sname + +@contextlib.contextmanager +def get_valid_symlink(path): + yield get_symlink(path, dangling=False)[1] + +@contextlib.contextmanager +def get_dangling_symlink(path): + yield get_symlink(path, dangling=True)[1] + +@contextlib.contextmanager +def get_file_and_symlink(path): + yield get_symlink(path, dangling=False) + +@contextlib.contextmanager +def get_file_and_fobject(path): + fh, fname = get_file(path) + with os.fdopen(fh) as fo: + yield fname, fo + +# Wrappers that build upon existing values + +def as_wrapper(call, fn, closer=None): + @contextlib.contextmanager + def f(path): + with call(path) as r: + val = fn(r) + yield val + if closer is not None: + closer(val) + return f + +def as_bytes(call): + return as_wrapper(call, lambda r: r.encode()) + +def as_fspath(call): + return as_wrapper(call, pathlib.PurePath) + +def as_iostream(call): + opener = lambda f: io.open(f, "r") + closer = lambda r: r.close() + return as_wrapper(call, opener, closer) + +NOT_BEFORE_36 = pytest.mark.xfail(condition="sys.version_info < (3,6)", + strict=True) +NOT_PYPY = pytest.mark.xfail(condition="platform.python_implementation() == 'PyPy'", + strict=False) + +require_acl_from_mode = pytest.mark.skipif("not HAS_ACL_FROM_MODE") +require_acl_check = pytest.mark.skipif("not HAS_ACL_CHECK") +require_acl_entry = pytest.mark.skipif("not HAS_ACL_ENTRY") +require_extended_check = pytest.mark.skipif("not HAS_EXTENDED_CHECK") +require_equiv_mode = pytest.mark.skipif("not HAS_EQUIV_MODE") +require_copy_ext = pytest.mark.skipif("not HAS_COPY_EXT") + +# Note: ACLs are valid only for files/directories, not symbolic links +# themselves, so we only create valid symlinks. +FILE_P = [ + get_file_name, + as_bytes(get_file_name), + pytest.param(as_fspath(get_file_name), + marks=[NOT_BEFORE_36, NOT_PYPY]), + get_dir, + as_bytes(get_dir), + pytest.param(as_fspath(get_dir), + marks=[NOT_BEFORE_36, NOT_PYPY]), + get_valid_symlink, + as_bytes(get_valid_symlink), + pytest.param(as_fspath(get_valid_symlink), + marks=[NOT_BEFORE_36, NOT_PYPY]), +] + +FILE_D = [ + "file name", + "file name (bytes)", + "file name (path)", + "directory", + "directory (bytes)", + "directory (path)", + "file via symlink", + "file via symlink (bytes)", + "file via symlink (path)", +] + +FD_P = [ + get_file_fd, + get_file_object, + as_iostream(get_file_name), +] + +FD_D = [ + "file FD", + "file object", + "file io stream", +] + +ALL_P = FILE_P + FD_P +ALL_D = FILE_D + FD_D + +@pytest.fixture(params=FILE_P, ids=FILE_D) +def file_subject(testdir, request): + with request.param(testdir) as value: + yield value + +@pytest.fixture(params=FD_P, ids=FD_D) +def fd_subject(testdir, request): + with request.param(testdir) as value: + yield value + +@pytest.fixture(params=ALL_P, ids=ALL_D) +def subject(testdir, request): + with request.param(testdir) as value: + yield value + + +class TestLoad: + """Load/create tests""" + def test_from_file(self, file_subject): + """Test loading ACLs from a file/directory""" + acl = posix1e.ACL(file=file_subject) + assert acl.valid() + + def test_from_dir(self, testdir): + """Test loading ACLs from a directory""" + with get_dir(testdir) as dname: + acl2 = posix1e.ACL(filedef=dname) + # default ACLs might or might not be valid; missing ones are + # not valid, so we don't test acl2 for validity + + def test_from_fd(self, fd_subject): + """Test loading ACLs from a file descriptor""" + acl = posix1e.ACL(fd=fd_subject) + assert acl.valid() + + def test_from_nonexisting(self, testdir): + _, fname = get_file(testdir) + with pytest.raises(IOError): + posix1e.ACL(file="fname"+".no-such-file") + with pytest.raises(IOError): + posix1e.ACL(filedef="fname"+".no-such-file") + + def test_from_invalid_fd(self, testdir): + fd, _ = get_file(testdir) + os.close(fd) + with pytest.raises(IOError): + posix1e.ACL(fd=fd) + + def test_from_empty_invalid(self): + """Test creating an empty ACL""" + acl1 = posix1e.ACL() + assert not acl1.valid() + + def test_from_text(self): + """Test creating an ACL from text""" + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl1.valid() + + # This is acl_check, but should actually be have_linux... + @require_acl_check + def test_from_acl(self): + """Test creating an ACL from an existing ACL""" + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + acl2 = posix1e.ACL(acl=acl1) + assert acl1 == acl2 + + def test_from_acl_via_str(self): + # This is needed for not HAVE_LINUX cases. + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + acl2 = posix1e.ACL(acl=acl1) + assert str(acl1) == str(acl2) + + def test_invalid_creation_params(self, testdir): + """Test that creating an ACL from multiple objects fails""" + fd, _ = get_file(testdir) + with pytest.raises(ValueError): + posix1e.ACL(text=BASIC_ACL_TEXT, fd=fd) + + def test_invalid_value_creation(self): + """Test that creating an ACL from wrong specification fails""" + with pytest.raises(EnvironmentError): + posix1e.ACL(text="foobar") + with pytest.raises(TypeError): + posix1e.ACL(foo="bar") + + def test_uninit(self): + """Checks that uninit is actually empty init""" + acl = posix1e.ACL.__new__(posix1e.ACL) + assert not acl.valid() + e = acl.append() + e.permset + acl.delete_entry(e) + + def test_double_init(self): + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl1.valid() + acl1.__init__(text=BASIC_ACL_TEXT) # type: ignore + assert acl1.valid() + acl2 = ACL(text=TEXT_0755) + assert acl1 != acl2 + acl1.__init__(acl=acl2) # type: ignore + assert_acl_eq(acl1, acl2) + + def test_reinit_failure_noop(self): + a = posix1e.ACL(text=TEXT_0755) + b = posix1e.ACL(acl=a) + assert_acl_eq(a, b) + with pytest.raises(IOError): + a.__init__(text='foobar') + assert_acl_eq(a, b) + + @pytest.mark.xfail(reason="Unreliable test, re-init doesn't always invalidate children") + def test_double_init_breaks_children(self): + acl = posix1e.ACL() + e = acl.append() + e.permset.write = True + acl.__init__() # type: ignore + with pytest.raises(EnvironmentError): + e.permset.write = False + + +class TestAclExtensions: + """ACL extensions checks""" + + @require_acl_from_mode + def test_from_mode(self): + """Test loading ACLs from an octal mode""" + acl1 = posix1e.ACL(mode=0o644) + assert acl1.valid() + + @require_acl_check + def test_acl_check(self): + """Test the acl_check method""" + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + assert not acl1.check() + acl2 = posix1e.ACL() + c = acl2.check() + assert c == (ACL_MISS_ERROR, 0) + assert isinstance(c, tuple) + assert c[0] == ACL_MISS_ERROR + e = acl2.append() + c = acl2.check() + assert c == (ACL_ENTRY_ERROR, 0) + + def test_applyto(self, subject): + """Test the apply_to function""" + # TODO: add read/compare with before, once ACL can be init'ed + # from any source. + basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) + basic_acl.applyto(subject) + enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") + assert enhanced_acl.valid() + enhanced_acl.applyto(subject) + + def test_apply_to_with_wrong_object(self): + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl1.valid() + with pytest.raises(TypeError): + acl1.applyto(object()) + with pytest.raises(TypeError): + acl1.applyto(object(), object()) # type: ignore + + def test_apply_to_fail(self, testdir): + acl1 = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl1.valid() + fd, fname = get_file(testdir) + os.close(fd) + with pytest.raises(IOError): + acl1.applyto(fd) + with pytest.raises(IOError, match="no-such-file"): + acl1.applyto(fname+".no-such-file") + + @require_extended_check + def test_applyto_extended(self, subject): + """Test the acl_extended function""" + basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) + basic_acl.applyto(subject) + assert not has_extended(subject) + enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") + assert enhanced_acl.valid() + enhanced_acl.applyto(subject) + assert has_extended(subject) + + @require_extended_check + @pytest.mark.parametrize( + "gen", [ get_file_and_symlink, get_file_and_fobject ]) + def test_applyto_extended_mixed(self, testdir, gen): + """Test the acl_extended function""" + with gen(testdir) as (a, b): + basic_acl = posix1e.ACL(text=BASIC_ACL_TEXT) + basic_acl.applyto(a) + for item in a, b: + assert not has_extended(item) + enhanced_acl = posix1e.ACL(text="u::rw,g::-,o::-,u:root:rw,mask::r") + assert enhanced_acl.valid() + enhanced_acl.applyto(b) + for item in a, b: + assert has_extended(item) + + @require_extended_check + def test_extended_fail(self, testdir): + fd, fname = get_file(testdir) + os.close(fd) + with pytest.raises(IOError): + has_extended(fd) + with pytest.raises(IOError, match="no-such-file"): + has_extended(fname+".no-such-file") + + @require_extended_check + def test_extended_arg_handling(self): + with pytest.raises(TypeError): + has_extended() # type: ignore + with pytest.raises(TypeError): + has_extended(object()) # type: ignore + + @require_equiv_mode + def test_equiv_mode(self): + """Test the equiv_mode function""" + if HAS_ACL_FROM_MODE: + for mode in 0o644, 0o755: + acl = posix1e.ACL(mode=mode) + assert acl.equiv_mode() == mode + acl = posix1e.ACL(text="u::rw,g::r,o::r") + assert acl.equiv_mode() == 0o644 + acl = posix1e.ACL(text="u::rx,g::-,o::-") + assert acl.equiv_mode() == 0o500 + + @require_equiv_mode + @pytest.mark.xfail(reason="It seems equiv mode always passes, even for empty ACLs") + def test_equiv_mode_invalid(self): + """Test equiv_mode on invalid ACLs""" + a = posix1e.ACL() + with pytest.raises(EnvironmentError): + a.equiv_mode() + + @require_acl_check + def test_to_any_text(self): + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + assert b"u::" in \ + acl.to_any_text(options=posix1e.TEXT_ABBREVIATE) + assert b"user::" in acl.to_any_text() + + @require_acl_check + def test_to_any_text_wrong_args(self): + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + with pytest.raises(TypeError): + acl.to_any_text(foo="bar") # type: ignore + + + @require_acl_check + def test_rich_compare(self): + acl1 = posix1e.ACL(text="u::rw,g::r,o::r") + acl2 = posix1e.ACL(acl=acl1) + acl3 = posix1e.ACL(text="u::rw,g::rw,o::r") + assert acl1 == acl2 + assert acl1 != acl3 + with pytest.raises(TypeError): + acl1 < acl2 # type: ignore + with pytest.raises(TypeError): + acl1 >= acl3 # type: ignore + assert acl1 != True # type: ignore + assert not (acl1 == 1) # type: ignore + with pytest.raises(TypeError): + acl1 > True # type: ignore + + @require_acl_entry + def test_acl_iterator(self): + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + for entry in acl: + assert entry.parent is acl + + @require_copy_ext + def test_acl_copy_ext(self): + a = posix1e.ACL(text=BASIC_ACL_TEXT) + b = posix1e.ACL() + c = posix1e.ACL(acl=b) + assert a != b + assert b == c + state = a.__getstate__() + b.__setstate__(state) + assert a == b + assert b != c + + @require_copy_ext + def test_acl_copy_ext_args(self): + a = posix1e.ACL() + with pytest.raises(TypeError): + a.__setstate__(None) + + @require_copy_ext + def test_acl_init_copy_ext(self): + a = posix1e.ACL(text=BASIC_ACL_TEXT) + b = posix1e.ACL() + c = posix1e.ACL(data=a.__getstate__()) + assert c != b + assert c == a + + @require_copy_ext + def test_acl_init_copy_ext_invalid(self): + with pytest.raises(IOError): + posix1e.ACL(data=b"foobar") + + +class TestWrite: + """Write tests""" + + def test_delete_default(self, testdir): + """Test removing the default ACL""" + with get_dir(testdir) as dname: + posix1e.delete_default(dname) + + def test_delete_default_fail(self, testdir): + """Test removing the default ACL""" + with get_file_name(testdir) as fname: + with pytest.raises(IOError, match="no-such-file"): + posix1e.delete_default(fname+".no-such-file") + + @NOT_PYPY + def test_delete_default_wrong_arg(self): + with pytest.raises(TypeError): + posix1e.delete_default(object()) # type: ignore + + def test_reapply(self, testdir): + """Test re-applying an ACL""" + fd, fname = get_file(testdir) + acl1 = posix1e.ACL(fd=fd) + acl1.applyto(fd) + acl1.applyto(fname) + with get_dir(testdir) as dname: + acl2 = posix1e.ACL(file=fname) + acl2.applyto(dname) + + + +@require_acl_entry +class TestModification: + """ACL modification tests""" + + def checkRef(self, obj): + """Checks if a given obj has a 'sane' refcount""" + if platform.python_implementation() == "PyPy": + return + ref_cnt = sys.getrefcount(obj) + # FIXME: hardcoded value for the max ref count... but I've + # seen it overflow on bad reference counting, so it's better + # to be safe + if ref_cnt < 2 or ref_cnt > 1024: + pytest.fail("Wrong reference count, expected 2-1024 and got %d" % + ref_cnt) + + def test_str(self): + """Test str() of an ACL.""" + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + str_acl = str(acl) + self.checkRef(str_acl) + + def test_append(self): + """Test append a new Entry to the ACL""" + acl = posix1e.ACL() + e = acl.append() + e.tag_type = posix1e.ACL_OTHER + ignore_ioerror(errno.EINVAL, acl.calc_mask) + str_format = str(e) + self.checkRef(str_format) + e2 = acl.append(e) + ignore_ioerror(errno.EINVAL, acl.calc_mask) + assert not acl.valid() + + def test_wrong_append(self): + """Test append a new Entry to the ACL based on wrong object type""" + acl = posix1e.ACL() + with pytest.raises(TypeError): + acl.append(object()) # type: ignore + + @pytest.mark.xfail(reason="Behaviour not conform to specification") + def test_append_invalid_source(self): + a = posix1e.ACL() + b = posix1e.ACL() + f = b.append() + b.delete_entry(f) + with pytest.raises(EnvironmentError): + f.permset.write = True + with pytest.raises(EnvironmentError): + e = a.append(f) + + def test_entry_creation(self): + acl = posix1e.ACL() + e = posix1e.Entry(acl) + ignore_ioerror(errno.EINVAL, acl.calc_mask) + str_format = str(e) + self.checkRef(str_format) + + def test_entry_failed_creation(self): + # Checks for partial initialisation and deletion on error + # path. + with pytest.raises(TypeError): + posix1e.Entry(object()) # type: ignore + + def test_entry_reinitialisations(self): + a = posix1e.ACL() + b = posix1e.ACL() + e = posix1e.Entry(a) + e.__init__(a) # type: ignore + with pytest.raises(ValueError, match="different parent"): + e.__init__(b) # type: ignore + + @NOT_PYPY + def test_entry_reinit_leaks_refcount(self): + acl = posix1e.ACL() + e = acl.append() + ref = sys.getrefcount(acl) + e.__init__(acl) # type: ignore + assert ref == sys.getrefcount(acl), "Uh-oh, ref leaks..." + + def test_delete(self): + """Test delete Entry from the ACL""" + acl = posix1e.ACL() + e = acl.append() + e.tag_type = posix1e.ACL_OTHER + ignore_ioerror(errno.EINVAL, acl.calc_mask) + acl.delete_entry(e) + ignore_ioerror(errno.EINVAL, acl.calc_mask) + + def test_double_delete(self): + """Test delete Entry from the ACL""" + # This is not entirely valid/correct, since the entry object + # itself is invalid after the first deletion, so we're + # actually testing deleting an invalid object, not a + # non-existing entry... + acl = posix1e.ACL() + e = acl.append() + e.tag_type = posix1e.ACL_OTHER + ignore_ioerror(errno.EINVAL, acl.calc_mask) + acl.delete_entry(e) + ignore_ioerror(errno.EINVAL, acl.calc_mask) + with pytest.raises(EnvironmentError): + acl.delete_entry(e) + + def test_delete_unowned(self): + """Test delete Entry from the ACL""" + a = posix1e.ACL() + b = posix1e.ACL() + e = a.append() + e.tag_type = posix1e.ACL_OTHER + with pytest.raises(ValueError, match="un-owned entry"): + b.delete_entry(e) + + # This currently fails as this deletion seems to be accepted :/ + @pytest.mark.xfail(reason="Entry deletion is unreliable") + def testDeleteInvalidEntry(self): + """Test delete foreign Entry from the ACL""" + acl1 = posix1e.ACL() + acl2 = posix1e.ACL() + e = acl1.append() + e.tag_type = posix1e.ACL_OTHER + ignore_ioerror(errno.EINVAL, acl1.calc_mask) + with pytest.raises(EnvironmentError): + acl2.delete_entry(e) + + def test_delete_invalid_object(self): + """Test delete a non-Entry from the ACL""" + acl = posix1e.ACL() + with pytest.raises(TypeError): + acl.delete_entry(object()) # type: ignore + + def test_double_entries(self): + """Test double entries""" + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl.valid() + for tag_type in (posix1e.ACL_USER_OBJ, posix1e.ACL_GROUP_OBJ, + posix1e.ACL_OTHER): + e = acl.append() + e.tag_type = tag_type + e.permset.clear() + assert not acl.valid(), ("ACL containing duplicate entries" + " should not be valid") + acl.delete_entry(e) + + def test_multiple_good_entries(self): + """Test multiple valid entries""" + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl.valid() + for tag_type in (posix1e.ACL_USER, + posix1e.ACL_GROUP): + for obj_id in range(5): + e = acl.append() + e.tag_type = tag_type + e.qualifier = obj_id + e.permset.clear() + acl.calc_mask() + assert acl.valid(), ("ACL should be able to hold multiple" + " user/group entries") + + def test_multiple_bad_entries(self): + """Test multiple invalid entries""" + for tag_type in (posix1e.ACL_USER, + posix1e.ACL_GROUP): + acl = posix1e.ACL(text=BASIC_ACL_TEXT) + assert acl.valid() + e1 = acl.append() + e1.tag_type = tag_type + e1.qualifier = 0 + e1.permset.clear() + acl.calc_mask() + assert acl.valid(), ("ACL should be able to add a" + " user/group entry") + e2 = acl.append() + e2.tag_type = tag_type + e2.qualifier = 0 + e2.permset.clear() + ignore_ioerror(errno.EINVAL, acl.calc_mask) + assert not acl.valid(), ("ACL should not validate when" + " containing two duplicate entries") + acl.delete_entry(e1) + # FreeBSD trips over itself here and can't delete the + # entry, even though it still exists. + ignore_ioerror(errno.EINVAL, acl.delete_entry, e2) + + def test_copy(self): + acl = ACL() + e1 = acl.append() + e1.tag_type = ACL_USER + p1 = e1.permset + p1.clear() + p1.read = True + p1.write = True + e2 = acl.append() + e2.tag_type = ACL_GROUP + p2 = e2.permset + p2.clear() + p2.read = True + assert not p2.write + e2.copy(e1) + assert p2.write + assert e1.tag_type == e2.tag_type + + def test_copy_wrong_arg(self): + acl = ACL() + e = acl.append() + with pytest.raises(TypeError): + e.copy(object()) # type: ignore + + def test_set_permset(self): + acl = ACL() + e1 = acl.append() + e1.tag_type = ACL_USER + p1 = e1.permset + p1.clear() + p1.read = True + p1.write = True + e2 = acl.append() + e2.tag_type = ACL_GROUP + p2 = e2.permset + p2.clear() + p2.read = True + assert not p2.write + e2.permset = p1 + assert e2.permset.write + assert e2.tag_type == ACL_GROUP + + def test_set_permset_wrong_arg(self): + acl = ACL() + e = acl.append() + with pytest.raises(TypeError): + e.permset = object() # type: ignore + + def test_permset_creation(self): + acl = ACL() + e = acl.append() + p1 = e.permset + p2 = Permset(e) + #assert p1 == p2 + + def test_permset_creation_wrong_arg(self): + with pytest.raises(TypeError): + Permset(object()) # type: ignore + + def test_permset_reinitialisations(self): + a = posix1e.ACL() + e = posix1e.Entry(a) + f = posix1e.Entry(a) + p = e.permset + p.__init__(e) # type: ignore + with pytest.raises(ValueError, match="different parent"): + p.__init__(f) # type: ignore + + @NOT_PYPY + def test_permset_reinit_leaks_refcount(self): + acl = posix1e.ACL() + e = acl.append() + p = e.permset + ref = sys.getrefcount(e) + p.__init__(e) # type: ignore + assert ref == sys.getrefcount(e), "Uh-oh, ref leaks..." + + @pytest.mark.parametrize("perm, txt, accessor", + PERMSETS, ids=PERMSETS_IDS) + def test_permset(self, perm, txt, accessor): + """Test permissions""" + del accessor + acl = posix1e.ACL() + e = acl.append() + ps = e.permset + ps.clear() + str_ps = str(ps) + self.checkRef(str_ps) + assert not ps.test(perm), ("Empty permission set should not" + " have permission '%s'" % txt) + ps.add(perm) + assert ps.test(perm), ("Permission '%s' should exist" + " after addition" % txt) + str_ps = str(ps) + self.checkRef(str_ps) + ps.delete(perm) + assert not ps.test(perm), ("Permission '%s' should not exist" + " after deletion" % txt) + ps.add(perm) + assert ps.test(perm), ("Permission '%s' should exist" + " after addition" % txt) + ps.clear() + assert not ps.test(perm), ("Permission '%s' should not exist" + " after clearing" % txt) + + + + @pytest.mark.parametrize("perm, txt, accessor", + PERMSETS, ids=PERMSETS_IDS) + def test_permset_via_accessors(self, perm, txt, accessor): + """Test permissions""" + acl = posix1e.ACL() + e = acl.append() + ps = e.permset + ps.clear() + def getter(): + return accessor.__get__(ps) # type: ignore + def setter(value): + return accessor.__set__(ps, value) # type: ignore + str_ps = str(ps) + self.checkRef(str_ps) + assert not getter(), ("Empty permission set should not" + " have permission '%s'" % txt) + setter(True) + assert ps.test(perm), ("Permission '%s' should exist" + " after addition" % txt) + assert getter(), ("Permission '%s' should exist" + " after addition" % txt) + str_ps = str(ps) + self.checkRef(str_ps) + setter(False) + assert not ps.test(perm), ("Permission '%s' should not exist" + " after deletion" % txt) + assert not getter(), ("Permission '%s' should not exist" + " after deletion" % txt) + setter(True) + assert getter() + ps.clear() + assert not getter() + + def test_permset_invalid_type(self): + acl = posix1e.ACL() + e = acl.append() + ps = e.permset + ps.clear() + with pytest.raises(TypeError): + ps.add("foobar") # type: ignore + with pytest.raises(TypeError): + ps.delete("foobar") # type: ignore + with pytest.raises(TypeError): + ps.test("foobar") # type: ignore + with pytest.raises(ValueError): + ps.write = object() # type: ignore + + @pytest.mark.parametrize("tag", [ACL_USER, ACL_GROUP], + ids=["ACL_USER", "ACL_GROUP"]) + def test_qualifier_values(self, tag): + """Tests qualifier correct store/retrieval""" + acl = posix1e.ACL() + e = acl.append() + qualifier = 1 + e.tag_type = tag + while True: + regex = re.compile("(user|group) with (u|g)id %d" % qualifier) + try: + e.qualifier = qualifier + except OverflowError: + # reached overflow condition, break + break + assert e.qualifier == qualifier + assert regex.search(str(e)) is not None + qualifier *= 2 + + def test_qualifier_overflow(self): + """Tests qualifier overflow handling""" + acl = posix1e.ACL() + e = acl.append() + # the uid_t/gid_t are unsigned, so they can hold slightly more + # than sys.maxsize*2 (on Linux). + qualifier = (sys.maxsize + 1) * 2 + for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: + e.tag_type = tag + with pytest.raises(OverflowError): + e.qualifier = qualifier + + def test_qualifier_underflow(self): + """Tests negative qualifier handling""" + # Note: this presumes that uid_t/gid_t in C are unsigned... + acl = posix1e.ACL() + e = acl.append() + for tag in [posix1e.ACL_USER, posix1e.ACL_GROUP]: + e.tag_type = tag + for qualifier in [-10, -5, -1]: + with pytest.raises(OverflowError): + e.qualifier = qualifier + + def test_invalid_qualifier(self): + """Tests invalid qualifier handling""" + acl = posix1e.ACL() + e = acl.append() + with pytest.raises(TypeError): + e.qualifier = object() # type: ignore + with pytest.raises((TypeError, AttributeError)): + del e.qualifier + + def test_qualifier_on_wrong_tag(self): + """Tests qualifier setting on wrong tag""" + acl = posix1e.ACL() + e = acl.append() + e.tag_type = posix1e.ACL_OTHER + with pytest.raises(TypeError): + e.qualifier = 1 + with pytest.raises(TypeError): + e.qualifier + + @pytest.mark.parametrize("tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) + def test_tag_types(self, tag): + """Tests tag type correct set/get""" + acl = posix1e.ACL() + e = acl.append() + e.tag_type = tag + assert e.tag_type == tag + # check we can show all tag types without breaking + assert str(e) + + @pytest.mark.parametrize("src_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) + @pytest.mark.parametrize("dst_tag", ALL_TAG_VALUES, ids=ALL_TAG_DESCS) + def test_tag_overwrite(self, src_tag, dst_tag): + """Tests tag type correct set/get""" + acl = posix1e.ACL() + e = acl.append() + e.tag_type = src_tag + assert e.tag_type == src_tag + assert str(e) + e.tag_type = dst_tag + assert e.tag_type == dst_tag + assert str(e) + + def test_invalid_tags(self): + """Tests tag type incorrect set/get""" + acl = posix1e.ACL() + e = acl.append() + with pytest.raises(TypeError): + e.tag_type = object() # type: ignore + e.tag_type = posix1e.ACL_USER_OBJ + # For some reason, PyPy raises AttributeError. Strange... + with pytest.raises((TypeError, AttributeError)): + del e.tag_type + + def test_tag_wrong_overwrite(self): + acl = posix1e.ACL() + e = acl.append() + e.tag_type = posix1e.ACL_USER_OBJ + tag = max(ALL_TAG_VALUES) + 1 + with pytest.raises(EnvironmentError): + e.tag_type = tag + # Check tag is still valid. + assert e.tag_type == posix1e.ACL_USER_OBJ + +if __name__ == "__main__": + unittest.main() -- 2.39.2