diff --git a/bin/xbps-pkgdb/check_pkg_files.c b/bin/xbps-pkgdb/check_pkg_files.c index ca7a9cf6..742f0fd8 100644 --- a/bin/xbps-pkgdb/check_pkg_files.c +++ b/bin/xbps-pkgdb/check_pkg_files.c @@ -95,6 +95,9 @@ check_pkg_files(struct xbps_handle *xhp, const char *pkgname, void *arg) while ((obj = xbps_object_iterator_next(iter))) { xbps_dictionary_get_cstring_nocopy(obj, "file", &file); + /* skip noextract files */ + if (xhp->noextract && xbps_patterns_match(xhp->noextract, file)) + continue; path = xbps_xasprintf("%s/%s", xhp->rootdir, file); xbps_dictionary_get_cstring_nocopy(obj, "sha256", &sha256); @@ -150,6 +153,9 @@ check_pkg_files(struct xbps_handle *xhp, const char *pkgname, void *arg) while ((obj = xbps_object_iterator_next(iter))) { xbps_dictionary_get_cstring_nocopy(obj, "file", &file); + /* skip noextract files */ + if (xhp->noextract && xbps_patterns_match(xhp->noextract, file)) + continue; path = xbps_xasprintf("%s/%s", xhp->rootdir, file); if (access(path, R_OK) == -1) { if (errno == ENOENT) { diff --git a/bin/xbps-pkgdb/check_pkg_symlinks.c b/bin/xbps-pkgdb/check_pkg_symlinks.c index 41854d22..851e6777 100644 --- a/bin/xbps-pkgdb/check_pkg_symlinks.c +++ b/bin/xbps-pkgdb/check_pkg_symlinks.c @@ -66,6 +66,10 @@ check_pkg_symlinks(struct xbps_handle *xhp, const char *pkgname, void *arg) if (!xbps_dictionary_get_cstring_nocopy(obj, "file", &file)) continue; + /* skip noextract files */ + if (xhp->noextract && xbps_patterns_match(xhp->noextract, file)) + continue; + if (!xbps_dictionary_get_cstring_nocopy(obj, "target", &tgt)) { xbps_warn_printf("%s: `%s' symlink with " "empty target object!\n", pkgname, file); diff --git a/data/xbps.d.5 b/data/xbps.d.5 index e34a99b6..f1ab414c 100644 --- a/data/xbps.d.5 +++ b/data/xbps.d.5 @@ -1,4 +1,4 @@ -.Dd June 12, 2019 +.Dd January 18, 2020 .Dt XBPS-D 5 .Sh NAME .Nm xbps.d @@ -60,6 +60,18 @@ If path starts with '/' it's an absolute path, otherwise it will be relative to Declares a ignored package. If a package depends on an ignored package the dependency is always satisfied, without installing the ignored package. +.It Sy noextract=pattern +Skip extraction of matching files. +Patterns starting with a exclamation mark negate the previous match, +a single backslash can be used to escape the exclamation mark. +.Pp +In the following example all files matching the first pattern will not be extracted, +but files that also match the second pattern will still be extracted. +.Pp +.Bl -tag -compact -width noextract=!/usr/bin/foo +.It Sy noextract=/usr/bin/f* +.It Sy noextract=!/usr/bin/foo +.El .It Sy include=path/file.conf Imports settings from the specified configuration file. .Em NOTE diff --git a/include/xbps.h.in b/include/xbps.h.in index 75c01c00..3a9beb77 100644 --- a/include/xbps.h.in +++ b/include/xbps.h.in @@ -50,7 +50,7 @@ * * This header documents the full API for the XBPS Library. */ -#define XBPS_API_VERSION "20191229" +#define XBPS_API_VERSION "20200118" #ifndef XBPS_VERSION #define XBPS_VERSION "UNSET" @@ -540,6 +540,7 @@ struct xbps_handle { */ xbps_array_t preserved_files; xbps_array_t ignored_pkgs; + xbps_array_t noextract; /** * @var repositories * @@ -2168,6 +2169,18 @@ char *xbps_sanitize_path(const char *src); char *xbps_symlink_target(struct xbps_handle *xhp, const char *path, const char *target); +/** + * Returns true if any of the fnmatch patterns in \a patterns matches + * and is not negated by a later match. + * + * @param[in] patterns The patterns to match against. + * @param[in] path The path that is matched against the patterns. + * + * @return true if any pattern matches, false otherwise. + * The returned buffer must be free(3)d when it's no longer necessary. + */ +bool xbps_patterns_match(xbps_array_t patterns, const char *path); + /** * Internalizes a plist file declared in \f and returns a proplib array. * diff --git a/lib/conf.c b/lib/conf.c index 1a824984..8825afa2 100644 --- a/lib/conf.c +++ b/lib/conf.c @@ -145,6 +145,19 @@ store_ignored_pkg(struct xbps_handle *xhp, const char *pkgname) xbps_dbg_printf(xhp, "Added ignored package: %s\n", pkgname); } +static void +store_noextract(struct xbps_handle *xhp, const char *value) +{ + if (*value == '\0') + return; + if (xhp->noextract == NULL) { + xhp->noextract = xbps_array_create(); + assert(xhp->noextract); + } + xbps_array_add_cstring(xhp->noextract, value); + xbps_dbg_printf(xhp, "Added noextract pattern: %s\n", value); +} + enum { KEY_ERROR = 0, KEY_ARCHITECTURE, @@ -152,6 +165,7 @@ enum { KEY_CACHEDIR, KEY_IGNOREPKG, KEY_INCLUDE, + KEY_NOEXTRACT, KEY_PRESERVE, KEY_REPOSITORY, KEY_ROOTDIR, @@ -169,6 +183,7 @@ static const struct key { { "cachedir", 8, KEY_CACHEDIR }, { "ignorepkg", 9, KEY_IGNOREPKG }, { "include", 7, KEY_INCLUDE }, + { "noextract", 9, KEY_NOEXTRACT }, { "preserve", 8, KEY_PRESERVE }, { "repository", 10, KEY_REPOSITORY }, { "rootdir", 7, KEY_ROOTDIR }, @@ -353,6 +368,9 @@ parse_file(struct xbps_handle *xhp, const char *path, bool nested) case KEY_IGNOREPKG: store_ignored_pkg(xhp, val); break; + case KEY_NOEXTRACT: + store_noextract(xhp, val); + break; case KEY_INCLUDE: /* Avoid double-nested parsing, only allow it once */ if (nested) { diff --git a/lib/package_unpack.c b/lib/package_unpack.c index e995d40c..cc09aa66 100644 --- a/lib/package_unpack.c +++ b/lib/package_unpack.c @@ -305,6 +305,15 @@ unpack_archive(struct xbps_handle *xhp, xucd_stats = true; } } + /* + * Skip files that match noextract patterns from configuration file. + */ + if (xhp->noextract && xbps_patterns_match(xhp->noextract, entry_pname+1)) { + xbps_set_cb_state(xhp, XBPS_STATE_UNPACK_FILE_PRESERVED, 0, + pkgver, "%s: file `%s' won't be extracted, " + "it matches a noextract pattern.", pkgver, entry_pname); + continue; + } /* * Always check that extracted file exists and hash * doesn't match, in that case overwrite the file. @@ -323,7 +332,7 @@ unpack_archive(struct xbps_handle *xhp, "and must be preserved, skipping.\n", entry_pname); xbps_set_cb_state(xhp, XBPS_STATE_UNPACK_FILE_PRESERVED, 0, pkgver, "%s: file `%s' won't be extracted, " - "it's preserved.\n", pkgver, entry_pname); + "it's preserved.", pkgver, entry_pname); continue; } diff --git a/lib/util.c b/lib/util.c index e150f27f..eea05e57 100644 --- a/lib/util.c +++ b/lib/util.c @@ -648,3 +648,27 @@ xbps_symlink_target(struct xbps_handle *xhp, const char *path, const char *tgt) return res; } + +bool +xbps_patterns_match(xbps_array_t patterns, const char *path) +{ + bool match = false; + + if (patterns == NULL) + return false; + + for (unsigned int i = 0; i < xbps_array_count(patterns); i++) { + const char *pattern = NULL; + bool negate = false; + if (!xbps_array_get_cstring_nocopy(patterns, i, &pattern)) + continue; + if (pattern == NULL) + continue; + if ((negate = *pattern == '!') || *pattern == '\\') + pattern++; + if (fnmatch(pattern, path, 0) == 0) + match = !negate; + } + + return match; +} diff --git a/tests/xbps/libxbps/shell/Kyuafile b/tests/xbps/libxbps/shell/Kyuafile index 4558b72b..3a2ad4ac 100644 --- a/tests/xbps/libxbps/shell/Kyuafile +++ b/tests/xbps/libxbps/shell/Kyuafile @@ -26,3 +26,4 @@ atf_test_program{name="conflicts_test"} atf_test_program{name="downgrade_hold_test"} atf_test_program{name="ignore_test"} atf_test_program{name="preserve_test"} +atf_test_program{name="noextract_files_test"} diff --git a/tests/xbps/libxbps/shell/Makefile b/tests/xbps/libxbps/shell/Makefile index 8c7d5df7..8eca8fd9 100644 --- a/tests/xbps/libxbps/shell/Makefile +++ b/tests/xbps/libxbps/shell/Makefile @@ -9,6 +9,7 @@ TESTSHELL+= vpkg_test install_test preserve_files_test configure_test TESTSHELL+= update_shlibs_test update_hold_test update_repolock_test TESTSHELL+= cyclic_deps_test conflicts_test update_itself_test TESTSHELL+= downgrade_hold_test ignore_test preserve_test +TESTSHELL+= noextract_files_test EXTRA_FILES = Kyuafile include $(TOPDIR)/mk/test.mk diff --git a/tests/xbps/libxbps/shell/noextract_files_test.sh b/tests/xbps/libxbps/shell/noextract_files_test.sh new file mode 100644 index 00000000..a63a4481 --- /dev/null +++ b/tests/xbps/libxbps/shell/noextract_files_test.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env atf-sh + +atf_test_case tc1 + +tc1_head() { + atf_set "descr" "Tests for pkg install with noextract: match whole directory" +} + +tc1_body() { + mkdir some_repo + mkdir -p pkg_A/usr/bin pkg_A/usr/lib + touch pkg_A/usr/bin/blah pkg_A/usr/bin/foo pkg_A/usr/lib/foo + cd some_repo + xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check_equal $? 0 + xbps-rindex -d -a $PWD/*.xbps + atf_check_equal $? 0 + cd .. + + mkdir -p root/xbps.d + echo "noextract=/usr/bin/*" > root/xbps.d/foo.conf + + xbps-install -C xbps.d -r root --repository=$PWD/some_repo -yd A + atf_check_equal $? 0 + + rv=0 + [ -e root/usr/lib/foo ] || rv=1 + [ -e root/usr/bin/blah ] && rv=1 + [ -e root/usr/bin/foo ] && rv=1 + atf_check_equal $rv 0 + + xbps-pkgdb -C xbps.d -r root A + atf_check_equal $? 0 +} + +atf_test_case tc2 + +tc2_head() { + atf_set "descr" "Tests for pkg install with noextract: match certain file" +} + +tc2_body() { + mkdir some_repo + mkdir -p pkg_A/usr/bin pkg_A/usr/lib + touch pkg_A/usr/bin/blah pkg_A/usr/bin/foo pkg_A/usr/lib/foo + cd some_repo + xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check_equal $? 0 + xbps-rindex -d -a $PWD/*.xbps + atf_check_equal $? 0 + cd .. + + mkdir -p root/xbps.d + echo "noextract=/usr/bin/f*" > root/xbps.d/foo.conf + + xbps-install -C xbps.d -r root --repository=$PWD/some_repo -yd A + atf_check_equal $? 0 + + tree root + rv=0 + [ -e root/usr/lib/foo ] || rv=1 + [ -e root/usr/bin/blah ] || rv=1 + [ -e root/usr/bin/foo ] && rv=1 + atf_check_equal $rv 0 + + xbps-pkgdb -C xbps.d -r root A + atf_check_equal $? 0 +} + +atf_test_case tc3 + +tc3_head() { + atf_set "descr" "Tests for pkg install with noextract: negate pattern" +} + +tc3_body() { + mkdir some_repo + mkdir -p pkg_A/usr/bin pkg_A/usr/lib + touch pkg_A/usr/bin/blah pkg_A/usr/bin/foo pkg_A/usr/lib/foo + cd some_repo + xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check_equal $? 0 + xbps-rindex -d -a $PWD/*.xbps + atf_check_equal $? 0 + cd .. + + mkdir -p root/xbps.d + echo "noextract=/usr/bin/*" > root/xbps.d/foo.conf + echo "noextract=!/usr/bin/blah" >> root/xbps.d/foo.conf + + xbps-install -C xbps.d -r root --repository=$PWD/some_repo -yd A + atf_check_equal $? 0 + + tree root + rv=0 + [ -e root/usr/lib/foo ] || rv=1 + [ -e root/usr/bin/blah ] || rv=1 + [ -e root/usr/bin/foo ] && rv=1 + atf_check_equal $rv 0 + + xbps-pkgdb -C xbps.d -r root A + atf_check_equal $? 0 +} + +tc4_head() { + atf_set "descr" "Tests for pkg install with noextract: negate and match again" +} + +tc4_body() { + mkdir some_repo + mkdir -p pkg_A/usr/bin pkg_A/usr/lib + touch pkg_A/usr/bin/blah pkg_A/usr/bin/foo pkg_A/usr/lib/foo + cd some_repo + xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check_equal $? 0 + xbps-rindex -d -a $PWD/*.xbps + atf_check_equal $? 0 + cd .. + + mkdir -p root/xbps.d + echo "noextract=/usr/bin/*" > root/xbps.d/foo.conf + echo "noextract=!/usr/bin/blah" >> root/xbps.d/foo.conf + echo "noextract=/usr/bin/bla*" >> root/xbps.d/foo.conf + + xbps-install -C xbps.d -r root --repository=$PWD/some_repo -yd A + atf_check_equal $? 0 + + tree root + rv=0 + [ -e root/usr/lib/foo ] || rv=1 + [ -e root/usr/bin/blah ] && rv=1 + [ -e root/usr/bin/foo ] && rv=1 + atf_check_equal $rv 0 + + xbps-pkgdb -C xbps.d -r root A + atf_check_equal $? 0 +} + +atf_test_case tc5 + +tc5_head() { + atf_set "descr" "Tests for pkg install with noextract: match full path" +} + +tc5_body() { + mkdir some_repo + mkdir -p pkg_A/usr/bin pkg_A/usr/lib + touch pkg_A/usr/bin/blah pkg_A/usr/bin/foo pkg_A/usr/lib/foo + cd some_repo + xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check_equal $? 0 + xbps-rindex -d -a $PWD/*.xbps + atf_check_equal $? 0 + cd .. + + mkdir -p root/xbps.d + echo "noextract=*foo" > root/xbps.d/foo.conf + + xbps-install -C xbps.d -r root --repository=$PWD/some_repo -yd A + atf_check_equal $? 0 + + rv=0 + [ -e root/usr/lib/foo ] && rv=1 + [ -e root/usr/bin/foo ] && rv=1 + [ -e root/usr/bin/blah ] || rv=2 + atf_check_equal $rv 0 + + xbps-pkgdb -C xbps.d -r root A + atf_check_equal $? 0 +} + +atf_init_test_cases() { + atf_add_test_case tc1 + atf_add_test_case tc2 + atf_add_test_case tc3 + atf_add_test_case tc4 + atf_add_test_case tc5 +}