From 86ca3dfecd50a3046c0ba491f3e93ce7d371b4cd Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Tue, 23 Dec 2025 03:11:06 +0100 Subject: [PATCH 1/6] lib: add chrooting execvp like function --- include/xbps_api_impl.h | 4 ++- lib/external/fexec.c | 66 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/include/xbps_api_impl.h b/include/xbps_api_impl.h index 11aba1585..b538b08fd 100644 --- a/include/xbps_api_impl.h +++ b/include/xbps_api_impl.h @@ -103,7 +103,9 @@ char HIDDEN *xbps_get_remote_repo_string(const char *); int HIDDEN xbps_repo_sync(struct xbps_handle *, const char *); int HIDDEN xbps_file_hash_check_dictionary(struct xbps_handle *, xbps_dictionary_t, const char *, const char *); -int HIDDEN xbps_file_exec(struct xbps_handle *, const char *, ...); +int HIDDEN xbps_file_exec(const struct xbps_handle *, const char *, ...); +int HIDDEN xbps_file_exec_argv(const struct xbps_handle *xhp, const char **argv); +int HIDDEN xbps_file_execp_argv(const struct xbps_handle *xhp, const char **argv); void HIDDEN xbps_set_cb_fetch(struct xbps_handle *, off_t, off_t, off_t, const char *, bool, bool, bool); int HIDDEN xbps_set_cb_state(struct xbps_handle *, xbps_state_t, int, diff --git a/lib/external/fexec.c b/lib/external/fexec.c index 787d28270..35a999fa2 100644 --- a/lib/external/fexec.c +++ b/lib/external/fexec.c @@ -40,7 +40,7 @@ #include "xbps_api_impl.h" static int -pfcexec(struct xbps_handle *xhp, const char *file, const char **argv) +pfcexec(const struct xbps_handle *xhp, const char *file, const char **argv) { pid_t child; int status; @@ -86,7 +86,54 @@ pfcexec(struct xbps_handle *xhp, const char *file, const char **argv) } static int -vfcexec(struct xbps_handle *xhp, const char *arg, va_list ap) +pfcexecp(const struct xbps_handle *xhp, const char *file, const char **argv) +{ + pid_t child; + int status; + + child = fork(); + switch (child) { + case 0: + /* + * If rootdir != / and uid==0 and bin/sh exists, + * change root directory and exec command. + */ + if (strcmp(xhp->rootdir, "/")) { + if ((geteuid() == 0) && (access("bin/sh", X_OK) == 0)) { + if (chroot(xhp->rootdir) == -1) { + xbps_dbg_printf("%s: chroot() " + "failed: %s\n", *argv, strerror(errno)); + _exit(errno); + } + if (chdir("/") == -1) { + xbps_dbg_printf("%s: chdir() " + "failed: %s\n", *argv, strerror(errno)); + _exit(errno); + } + } + } + umask(022); + (void)execvp(file, __UNCONST(argv)); + _exit(errno); + /* NOTREACHED */ + case -1: + return -1; + } + + while (waitpid(child, &status, 0) < 0) { + if (errno != EINTR) + return -1; + } + + if (!WIFEXITED(status)) + return -1; + + return WEXITSTATUS(status); +} + + +static int +vfcexec(const struct xbps_handle *xhp, const char *arg, va_list ap) { const char **argv; size_t argv_size, argc; @@ -122,8 +169,9 @@ vfcexec(struct xbps_handle *xhp, const char *arg, va_list ap) return retval; } + int HIDDEN -xbps_file_exec(struct xbps_handle *xhp, const char *arg, ...) +xbps_file_exec(const struct xbps_handle *xhp, const char *arg, ...) { va_list ap; int result; @@ -134,3 +182,15 @@ xbps_file_exec(struct xbps_handle *xhp, const char *arg, ...) return result; } + +int HIDDEN +xbps_file_exec_argv(const struct xbps_handle *xhp, const char **argv) +{ + return pfcexecp(xhp, argv[0], argv); +} + +int HIDDEN +xbps_file_execp_argv(const struct xbps_handle *xhp, const char **argv) +{ + return pfcexecp(xhp, argv[0], argv); +} From aabb95c03aa92fff904c1d663f11aa17996b7742 Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Mon, 22 Dec 2025 22:27:18 +0100 Subject: [PATCH 2/6] lib/external: add inih --- lib/Makefile | 16 +- lib/external/inih/ini.c | 328 ++++++++++++++++++++++++++++++++++++++++ lib/external/inih/ini.h | 189 +++++++++++++++++++++++ 3 files changed, 531 insertions(+), 2 deletions(-) create mode 100644 lib/external/inih/ini.c create mode 100644 lib/external/inih/ini.h diff --git a/lib/Makefile b/lib/Makefile index 2b4efe450..e19700238 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -31,6 +31,13 @@ endif LIBFETCH_INCS = fetch/common.h LIBFETCH_GEN = fetch/ftperr.h fetch/httperr.h +INIH_OBJS = external/inih/ini.o +INIH_CPPFLAGS = -DINI_HANDLER_LINENO=1 -DINI_CALL_HANDLER_ON_NEW_SECTION=1 +INIH_CFLAGS= -Wno-error=cast-qual +ifdef HAVE_VISIBILITY +INIH_CFLAGS+= -fvisibility=hidden +endif + # External code used by libxbps EXTOBJS = external/dewey.o external/fexec.o external/mkpath.o @@ -76,11 +83,16 @@ $(LIBPROP_OBJS): %.o: %.c ${SILENT}$(CC) $(CPPFLAGS) $(LIBPROP_CPPFLAGS) \ $(CFLAGS) $(LIBPROP_CFLAGS) -c $< -o $@ +$(INIH_OBJS): %.o: %.c + @printf " [CC]\t\t$@\n" + ${SILENT}$(CC) $(CPPFLAGS) $(INIH_CPPFLAGS) \ + $(CFLAGS) $(INIH_CFLAGS) -c $< -o $@ + $(OBJS): %.o: %.c @printf " [CC]\t\t$@\n" ${SILENT}$(CC) $(CPPFLAGS) $(CFLAGS) $(SHAREDLIB_CFLAGS) -c $< -o $@ -libxbps.so: $(LIBFETCH_OBJS) $(LIBPROP_OBJS) $(OBJS) +libxbps.so: $(LIBFETCH_OBJS) $(LIBPROP_OBJS) $(INIH_OBJS) $(OBJS) @printf " [CCLD]\t\t$@\n" ${SILENT}$(CC) $^ $(CFLAGS) $(LDFLAGS) -o $(LIBXBPS_SHLIB) @-ln -sf $(LIBXBPS_SHLIB) libxbps.so.$(LIBXBPS_MAJOR) @@ -106,5 +118,5 @@ uninstall: .PHONY: clean clean: - -rm -f libxbps* $(OBJS) $(LIBFETCH_OBJS) $(LIBPROP_OBJS) + -rm -f libxbps* $(OBJS) $(LIBFETCH_OBJS) $(LIBPROP_OBJS) $(INIH_OBJS) -rm -f $(LIBFETCH_GEN) diff --git a/lib/external/inih/ini.c b/lib/external/inih/ini.c new file mode 100644 index 000000000..08333cf0e --- /dev/null +++ b/lib/external/inih/ini.c @@ -0,0 +1,328 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2025, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#include "ini.h" + +#if !INI_USE_STACK +#if INI_CUSTOM_ALLOCATOR +#include +void* ini_malloc(size_t size); +void ini_free(void* ptr); +void* ini_realloc(void* ptr, size_t size); +#else +#include +#define ini_malloc malloc +#define ini_free free +#define ini_realloc realloc +#endif +#endif + +#define MAX_SECTION 50 +#define MAX_NAME 50 + +/* Used by ini_parse_string() to keep track of string parsing state. */ +typedef struct { + const char* ptr; + size_t num_left; +} ini_parse_string_ctx; + +/* Strip whitespace chars off end of given string, in place. end must be a + pointer to the NUL terminator at the end of the string. Return s. */ +static char* ini_rstrip(char* s, char* end) +{ + while (end > s && isspace((unsigned char)(*--end))) + *end = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +static char* ini_lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to NUL at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +static char* ini_find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Similar to strncpy, but ensures dest (size bytes) is + NUL-terminated, and doesn't pad with NULs. */ +static char* ini_strncpy0(char* dest, const char* src, size_t size) +{ + /* Could use strncpy internally, but it causes gcc warnings (see issue #91) */ + size_t i; + for (i = 0; i < size - 1 && src[i]; i++) + dest[i] = src[i]; + dest[i] = '\0'; + return dest; +} + +/* See documentation in header file. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; + size_t max_line = INI_MAX_LINE; +#else + char* line; + size_t max_line = INI_INITIAL_ALLOC; +#endif +#if INI_ALLOW_REALLOC && !INI_USE_STACK + char* new_line; +#endif + char section[MAX_SECTION] = ""; +#if INI_ALLOW_MULTILINE + char prev_name[MAX_NAME] = ""; +#endif + + size_t offset; + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + char abyss[16]; /* Used to consume input when a line is too long. */ + size_t abyss_len; + +#if !INI_USE_STACK + line = (char*)ini_malloc(INI_INITIAL_ALLOC); + if (!line) { + return -2; + } +#endif + +#if INI_HANDLER_LINENO +#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno) +#else +#define HANDLER(u, s, n, v) handler(u, s, n, v) +#endif + + /* Scan through stream line by line */ + while (reader(line, (int)max_line, stream) != NULL) { + offset = strlen(line); + +#if INI_ALLOW_REALLOC && !INI_USE_STACK + while (max_line < INI_MAX_LINE && + offset == max_line - 1 && line[offset - 1] != '\n') { + max_line *= 2; + if (max_line > INI_MAX_LINE) + max_line = INI_MAX_LINE; + new_line = ini_realloc(line, max_line); + if (!new_line) { + ini_free(line); + return -2; + } + line = new_line; + if (reader(line + offset, (int)(max_line - offset), stream) == NULL) + break; + offset += strlen(line + offset); + } +#endif + + lineno++; + + /* If line exceeded INI_MAX_LINE bytes, discard till end of line. */ + if (offset == max_line - 1 && line[offset - 1] != '\n') { + while (reader(abyss, sizeof(abyss), stream) != NULL) { + if (!error) + error = lineno; + abyss_len = strlen(abyss); + if (abyss_len > 0 && abyss[abyss_len - 1] == '\n') + break; + } + } + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = ini_rstrip(ini_lskip(start), line + offset); + + if (strchr(INI_START_COMMENT_PREFIXES, *start)) { + /* Start-of-line comment */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { +#if INI_ALLOW_INLINE_COMMENTS + end = ini_find_chars_or_comment(start, NULL); + *end = '\0'; + ini_rstrip(start, end); +#endif + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!HANDLER(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = ini_find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + ini_strncpy0(section, start + 1, sizeof(section)); +#if INI_ALLOW_MULTILINE + *prev_name = '\0'; +#endif +#if INI_CALL_HANDLER_ON_NEW_SECTION + if (!HANDLER(user, section, NULL, NULL) && !error) + error = lineno; +#endif + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = ini_find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = ini_rstrip(start, end); + value = end + 1; +#if INI_ALLOW_INLINE_COMMENTS + end = ini_find_chars_or_comment(value, NULL); + *end = '\0'; +#endif + value = ini_lskip(value); + ini_rstrip(value, end); + +#if INI_ALLOW_MULTILINE + ini_strncpy0(prev_name, name, sizeof(prev_name)); +#endif + /* Valid name[=:]value pair found, call handler */ + if (!HANDLER(user, section, name, value) && !error) + error = lineno; + } + else { + /* No '=' or ':' found on name[=:]value line */ +#if INI_ALLOW_NO_VALUE + *end = '\0'; + name = ini_rstrip(start, end); + if (!HANDLER(user, section, name, NULL) && !error) + error = lineno; +#else + if (!error) + error = lineno; +#endif + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + ini_free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + file = fopen(filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +/* An ini_reader function to read the next line from a string buffer. This + is the fgets() equivalent used by ini_parse_string(). */ +static char* ini_reader_string(char* str, int num, void* stream) { + ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream; + const char* ctx_ptr = ctx->ptr; + size_t ctx_num_left = ctx->num_left; + char* strp = str; + char c; + + if (ctx_num_left == 0 || num < 2) + return NULL; + + while (num > 1 && ctx_num_left != 0) { + c = *ctx_ptr++; + ctx_num_left--; + *strp++ = c; + if (c == '\n') + break; + num--; + } + + *strp = '\0'; + ctx->ptr = ctx_ptr; + ctx->num_left = ctx_num_left; + return str; +} + +/* See documentation in header file. */ +int ini_parse_string(const char* string, ini_handler handler, void* user) { + return ini_parse_string_length(string, strlen(string), handler, user); +} + +/* See documentation in header file. */ +int ini_parse_string_length(const char* string, size_t length, + ini_handler handler, void* user) { + ini_parse_string_ctx ctx; + + ctx.ptr = string; + ctx.num_left = length; + return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler, + user); +} diff --git a/lib/external/inih/ini.h b/lib/external/inih/ini.h new file mode 100644 index 000000000..07aa7f48f --- /dev/null +++ b/lib/external/inih/ini.h @@ -0,0 +1,189 @@ +/* inih -- simple .INI file parser + +SPDX-License-Identifier: BSD-3-Clause + +Copyright (C) 2009-2025, Ben Hoyt + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef INI_H +#define INI_H + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* Nonzero if ini_handler callback should accept lineno parameter. */ +#ifndef INI_HANDLER_LINENO +#define INI_HANDLER_LINENO 0 +#endif + +/* Visibility symbols, required for Windows DLLs */ +#ifndef INI_API +#if defined _WIN32 || defined __CYGWIN__ +# ifdef INI_SHARED_LIB +# ifdef INI_SHARED_LIB_BUILDING +# define INI_API __declspec(dllexport) +# else +# define INI_API __declspec(dllimport) +# endif +# else +# define INI_API +# endif +#else +# if defined(__GNUC__) && __GNUC__ >= 4 +# define INI_API __attribute__ ((visibility ("default"))) +# else +# define INI_API +# endif +#endif +#endif + +/* Typedef for prototype of handler function. + + Note that even though the value parameter has type "const char*", the user + may cast to "char*" and modify its content, as the value is not used again + after the call to ini_handler. This is not true of section and name -- + those must not be modified. +*/ +#if INI_HANDLER_LINENO +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value, + int lineno); +#else +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); +#endif + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +INI_API int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O (see also + ini_parse_string). */ +INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Same as ini_parse(), but takes a zero-terminated string with the INI data + instead of a file. Useful for parsing INI data from a network socket or + which is already in memory. */ +INI_API int ini_parse_string(const char* string, ini_handler handler, void* user); + +/* Same as ini_parse_string(), but takes a string and its length, avoiding + strlen(). Useful for parsing INI data from a network socket or which is + already in memory, or interfacing with C++ std::string_view. */ +INI_API int ini_parse_string_length(const char* string, size_t length, ini_handler handler, void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See https://github.com/benhoyt/inih/issues/21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Chars that begin a start-of-line comment. Per Python configparser, allow + both ; and # comments at the start of a line by default. */ +#ifndef INI_START_COMMENT_PREFIXES +#define INI_START_COMMENT_PREFIXES ";#" +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Maximum line length for any line in INI file (stack or heap). Note that + this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +/* Nonzero to allow heap line buffer to grow via realloc(), zero for a + fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is + zero. */ +#ifndef INI_ALLOW_REALLOC +#define INI_ALLOW_REALLOC 0 +#endif + +/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK + is zero. */ +#ifndef INI_INITIAL_ALLOC +#define INI_INITIAL_ALLOC 200 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Nonzero to call the handler at the start of each new section (with + name and value NULL). Default is to only call the handler on + each name=value pair. */ +#ifndef INI_CALL_HANDLER_ON_NEW_SECTION +#define INI_CALL_HANDLER_ON_NEW_SECTION 0 +#endif + +/* Nonzero to allow a name without a value (no '=' or ':' on the line) and + call the handler with value NULL in this case. Default is to treat + no-value lines as an error. */ +#ifndef INI_ALLOW_NO_VALUE +#define INI_ALLOW_NO_VALUE 0 +#endif + +/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory + allocation functions (INI_USE_STACK must also be 0). These functions must + have the same signatures as malloc/free/realloc and behave in a similar + way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */ +#ifndef INI_CUSTOM_ALLOCATOR +#define INI_CUSTOM_ALLOCATOR 0 +#endif + + +#ifdef __cplusplus +} +#endif + +#endif /* INI_H */ From 7765dd78065f89114f54b424b08bc132ab8078ea Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Tue, 23 Dec 2025 03:14:37 +0100 Subject: [PATCH 3/6] lib: initial hooks implementation --- include/xbps_api_impl.h | 6 + lib/Makefile | 2 +- lib/hooks.c | 854 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 861 insertions(+), 1 deletion(-) create mode 100644 lib/hooks.c diff --git a/include/xbps_api_impl.h b/include/xbps_api_impl.h index b538b08fd..ef2b2bd75 100644 --- a/include/xbps_api_impl.h +++ b/include/xbps_api_impl.h @@ -130,4 +130,10 @@ struct xbps_repo HIDDEN *xbps_regget_repo(struct xbps_handle *, const char *); int HIDDEN xbps_conf_init(struct xbps_handle *); +struct xbps_hooks; +struct xbps_hooks *xbps_hooks_init(struct xbps_handle *xhp); +int xbps_hooks_pre_transaction(struct xbps_handle *xhp, struct xbps_hooks *hooks); +int xbps_hooks_post_transaction(struct xbps_handle *xhp, struct xbps_hooks *hooks); +void xbps_hooks_free(struct xbps_hooks *hooks); + #endif /* !_XBPS_API_IMPL_H_ */ diff --git a/lib/Makefile b/lib/Makefile index e19700238..9734025dd 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -58,7 +58,7 @@ OBJS += plist_remove.o plist_fetch.o util.o util_path.o util_hash.o OBJS += repo.o repo_sync.o OBJS += rpool.o cb_util.o proplib_wrapper.o OBJS += package_alternatives.o -OBJS += conf.o log.o +OBJS += conf.o log.o hooks.o OBJS += $(EXTOBJS) $(COMPAT_OBJS) # unnecessary unless pkgdb format changes # OBJS += pkgdb_conversion.o diff --git a/lib/hooks.c b/lib/hooks.c new file mode 100644 index 000000000..3dca082a8 --- /dev/null +++ b/lib/hooks.c @@ -0,0 +1,854 @@ +#include +#include +#include +#include +#include +#include + +#define INI_HANDLER_LINENO 1 +#define INI_CALL_HANDLER_ON_NEW_SECTION 1 +#include "external/inih/ini.h" + +#include "xbps.h" +#include "xbps_api_impl.h" + +enum match_pkg_action { + MATCH_PKG_ALL = 0, + MATCH_PKG_INSTALL, + MATCH_PKG_UPDATE, + MATCH_PKG_REMOVE, + MATCH_PKG_REINSTALL, + MATCH_PKG_CONFIGURE, +}; + +struct match_pkg { + enum match_pkg_action action; + enum { + MATCH_PKG_NAME, + MATCH_PKG_CONSTRAINT, + MATCH_PKG_PATTERN, + } kind; + char *name; + char *pattern; +}; + +enum match_path_action { + MATCH_PATH_CHANGED = 0, + MATCH_PATH_CREATED, + MATCH_PATH_MODIFIED, + MATCH_PATH_DELETED, +}; + +struct match_path { + enum match_path_action action; + enum { + PATH_STR, + PATH_PATTERN, + } match; + char *pattern; +}; + +struct match { + size_t npackages; + struct match_pkg *packages; + size_t npaths; + struct match_path *paths; +}; + +static void +match_free(struct match *m) +{ + if (!m) + return; + for (size_t i = 0; i < m->npackages; i++) { + free(m->packages[i].name); + free(m->packages[i].pattern); + } + for (size_t i = 0; i < m->npaths; i++) { + free(m->paths[i].pattern); + } + free(m->paths); + free(m); +} + +enum when { + HOOK_PRE_TRANSACTION = 1 << 0, + HOOK_POST_TRANSACTION = 1 << 1, +}; + +struct hook { + char *filename; + char *name; + + enum when when; + + int argc; + char **argv; + + size_t nmatches; + struct match **matches; +}; + +static void +hook_free(struct hook *hook) +{ + if (!hook) + return; + + free(hook->filename); + free(hook->name); + + if (hook->argv) { + for (char **pp = hook->argv; *pp; pp++) + free(*pp); + } + free(hook->argv); + + for (size_t i = 0; i < hook->nmatches; i++) + match_free(hook->matches[i]); + free(hook->matches); + + free(hook); +} + +struct parse_ctx { + const char *path; + struct hook *hook; + enum { + SECTION_HOOK = 1, + SECTION_MATCH, + } section; + struct match *match; +}; + +static void __attribute__((format(printf, 3, 4))) +syntax_error(struct parse_ctx *ctx, int lineno, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "ERROR: syntax error: %s:%d: ", ctx->path, lineno); + vfprintf(stderr, fmt, ap); + fputc('\n', stderr); + va_end(ap); +} + +static size_t +word_count(const char *s) +{ + size_t n = 0; + + for (const char *p = s; *p;) { + for (; *p == ' ' || *p == '\t'; p++) + ; + for (; *p; p++) { + switch (*p) { + case '\t': + case ' ': + break; + case '\\': + if (p[1] == ' ' || p[1] == '\t') { + p += 1; + continue; + } + // fallthrough + default: + continue; + } + break; + } + n += 1; + } + + return n; +} + +static int +word_iter(char *dst, size_t dstsz, const char **pp) +{ + const char *p = *pp; + size_t n = 0; + + for (; *p == ' ' || *p == '\t'; p++) + ; + if (*p == '\0') + return 0; + + for (; *p; p++) { + switch (*p) { + case '\t': + case ' ': + break; + case '\\': + if (p[1] == ' ' || p[1] == '\t') { + p += 1; + if (n + 1 >= dstsz) + return -ENOBUFS; + dst[n++] = *p; + continue; + } + // fallthrough + default: + if (n + 1 >= dstsz) + return -ENOBUFS; + dst[n++] = *p; + continue; + } + break; + } + + dst[n] = '\0'; + + *pp = p; + return 1; +} + +static bool UNUSED +word_empty(const char *s) +{ + return s[strspn(s, " \t")] == '\0'; +} + +static int +parse_exec(struct parse_ctx *ctx, int lineno, const char *value) +{ + char buf[4096]; + char **argv = 0; + int r; + int n = 0; + int argc = 0; + + if (ctx->hook->argv) { + syntax_error(ctx, lineno, "Hook: Exec: defined multiple times"); + return -EINVAL; + } + + n = word_count(value); + if (n == 0) { + syntax_error(ctx, lineno, "Hook: Exec: missing command"); + return -EINVAL; + } + + argv = calloc(n + 1, sizeof(*argv)); + if (!argv) { + r = xbps_error_oom(); + goto err; + } + + for (const char *iter = value;;) { + r = word_iter(buf, sizeof(buf), &iter); + if (r < 0) { + syntax_error(ctx, lineno, "Hook: Exec: %s", strerror(-r)); + r = -EINVAL; + goto err; + } + if (r == 0) + break; + + argv[argc] = strdup(buf); + if (!argv[argc]) { + r = xbps_error_oom(); + goto err; + } + xbps_dbg_printf("argv[%d]='%s'\n", argc, argv[argc]); + argc++; + } + xbps_dbg_printf("argc=%d\n", argc); + + ctx->hook->argc = argc; + ctx->hook->argv = argv; + + return 0; +err: + for (int i = 0; i < argc; i++) + free(argv[i]); + free(argv); + return r; +} + +static int +parse_when(struct parse_ctx *ctx, int lineno, const char *value) +{ + for (const char *p = value; *p;) { + const char *e = strchrnul(p, ' '); + + if (strncmp("PreTransaction", p, e - p) == 0) { + ctx->hook->when |= HOOK_PRE_TRANSACTION; + } else if (strncmp("PostTransaction", p, e - p) == 0) { + ctx->hook->when |= HOOK_POST_TRANSACTION; + } else { + syntax_error(ctx, lineno, "Hook: When: unknown value: %.*s", + (int)(e - p), p); + return -EINVAL; + } + + for (; *e == ' '; e++) + ; + p = e; + } + return 0; +} + +static struct match_pkg * +match_alloc_pkg(struct match *m) +{ + size_t nmemb = m->npackages + 1; + struct match_pkg *tmp = reallocarray(m->packages, nmemb, sizeof(*tmp)); + if (!tmp) + return NULL; + m->packages = tmp; + return &m->packages[m->npackages++]; +} + +static int +match_parse_package(struct parse_ctx *ctx, int lineno UNUSED, const char *value, + enum match_pkg_action action) +{ + struct match_pkg *m; + const char *d; + + xbps_dbg_printf("[hooks] %s: match package: %s\n", ctx->path, value); + + m = match_alloc_pkg(ctx->match); + if (!m) + return xbps_error_oom(); + + m->action = action; + + d = strpbrk(value, "><*?[]"); + if (!d) { + m->kind = MATCH_PKG_NAME; + m->name = strdup(value); + if (!m->name) + return xbps_error_oom(); + return 0; + } + switch (*d) { + case '>': + case '<': + m->kind = MATCH_PKG_CONSTRAINT; + m->name = strndup(value, d - value); + if (!m->name) + return xbps_error_oom(); + m->pattern = strdup(value); + if (!m->pattern) + return xbps_error_oom(); + return 0; + default: + m->kind = MATCH_PKG_PATTERN; + m->pattern = strdup(value); + if (!m->pattern) + return xbps_error_oom(); + return 0; + } +} + +static struct match_path * +match_alloc_path(struct match *m) +{ + size_t nmemb = m->npaths + 1; + struct match_path *tmp = reallocarray(m->paths, nmemb, sizeof(*tmp)); + if (!tmp) + return NULL; + m->paths = tmp; + return &m->paths[m->npaths++]; +} + +static int +match_parse_path(struct parse_ctx *ctx, int lineno UNUSED, const char *value, + enum match_path_action action) +{ + struct match_path *m; + + xbps_dbg_printf("[hooks] %s: match path: %s\n", ctx->path, value); + + m = match_alloc_path(ctx->match); + if (!m) + return xbps_error_oom(); + m->action = action; + m->pattern = strdup(value); + if (!m->pattern) + return xbps_error_oom(); + + return 0; +} + +#define STRLEN(a) (sizeof((a)) - sizeof((a)[0])) +#define HASPREFIX(a, b) (strncmp(, STRLEN((a))) == 0) + +static int +match_section_handler(struct parse_ctx *ctx, int lineno, const char *name, const char *value) +{ + if (strcmp(name, "PackageInstall") == 0) { + int r = match_parse_package(ctx, lineno, value, MATCH_PKG_INSTALL); + return r == 0; + } else if (strcmp(name, "PackageUpdate") == 0) { + int r = match_parse_package(ctx, lineno, value, MATCH_PKG_UPDATE); + return r == 0; + } else if (strcmp(name, "PackageRemove") == 0) { + int r = match_parse_package(ctx, lineno, value, MATCH_PKG_REMOVE); + return r == 0; + } else if (strcmp(name, "PackageReinstall") == 0) { + int r = match_parse_package(ctx, lineno, value, MATCH_PKG_REINSTALL); + return r == 0; + } else if (strcmp(name, "PackageConfigure") == 0) { + int r = match_parse_package(ctx, lineno, value, MATCH_PKG_CONFIGURE); + return r == 0; + } else if (strcmp(name, "PathCreated") == 0) { + int r = match_parse_path(ctx, lineno, value, MATCH_PATH_CREATED); + return r == 0; + } else if (strcmp(name, "PathChanged") == 0) { + int r = match_parse_path(ctx, lineno, value, MATCH_PATH_CHANGED); + return r == 0; + } else if (strcmp(name, "PathModified") == 0) { + int r = match_parse_path(ctx, lineno, value, MATCH_PATH_MODIFIED); + return r == 0; + } else if (strcmp(name, "PathDeleted") == 0) { + int r = match_parse_path(ctx, lineno, value, MATCH_PATH_DELETED); + return r == 0; + } + syntax_error(ctx, lineno, + "section: Match: unknown key: %s", name); + return 0; +} + +static int +hook_handler( + struct parse_ctx *ctx, int lineno, const char *name, const char *value) +{ + if (strcmp(name, "Name") == 0) { + ctx->hook->name = strdup(value); + if (!ctx->hook->name) + return 0; + return 1; + } else if (strcmp(name, "Exec") == 0) { + int r = parse_exec(ctx, lineno, value); + if (r < 0) + return 0; + return 1; + } else if (strcmp(name, "When") == 0) { + int r = parse_when(ctx, lineno, value); + if (r < 0) + return 0; + return 1; + } + syntax_error(ctx, lineno, + "section: Hook: unknown key: %s", name); + return 0; +} + +static int +add_match(struct hook *hook, struct match *item) +{ + size_t nmemb = hook->nmatches + 1; + struct match **tmp = reallocarray(hook->matches, nmemb, sizeof(*tmp)); + if (!tmp) + return xbps_error_oom(); + hook->matches = tmp; + hook->matches[hook->nmatches++] = item; + return 0; +} + +static int +hook_ini_handler(void *user, const char *section, const char *name, + const char *value, int lineno) +{ + struct parse_ctx *ctx = user; + int r; + + // xbps_dbg_printf("[hooks] %s %s %s %s\n",ctx->path, section, name, value); + + // new section + if (!name) { + if (strcmp(section, "Hook") == 0) { + ctx->section = SECTION_HOOK; + return 1; + } else if (strcmp(section, "Match") == 0) { + ctx->section = SECTION_MATCH; + ctx->match = calloc(1, sizeof(*ctx->match)); + if (!ctx->match) { + xbps_error_oom(); + return 0; + } + r = add_match(ctx->hook, ctx->match); + if (r < 0) + return 0; + return 1; + } + syntax_error(ctx, lineno, "unknown section: %s", section); + return 0; + } + + if (section[0] == '\0') { + syntax_error(ctx, lineno, + "variable defined outside of section: %s", name); + return 0; + } + + switch (ctx->section) { + case SECTION_HOOK: + return hook_handler(ctx, lineno, name, value); + break; + case SECTION_MATCH: + return match_section_handler(ctx, lineno, name, value); + break; + } + return 1; +} + +static struct hook * +hook_parse(const char *dir, const char *filename) +{ + char path[PATH_MAX]; + struct parse_ctx ctx; + struct hook *hook; + FILE *fp; + int r; + + if (xbps_path_join(path, sizeof(path), dir, filename, (char *)NULL) == -1) { + xbps_error_printf("failed to open hook: %s/%s: %s\n", dir, + filename, strerror(ENAMETOOLONG)); + return NULL; + } + + hook = calloc(1, sizeof(*hook)); + if (!hook) { + xbps_error_oom(); + return NULL; + } + + hook->filename = strdup(filename); + if (!hook->filename) { + xbps_error_oom(); + free(hook); + return NULL; + } + + fp = fopen(path, "rb"); + if (!fp) { + r = -errno; + xbps_error_printf( + "failed to open hook file: %s: %s\n", path, strerror(-r)); + hook_free(hook); + return NULL; + } + + ctx.path = path; + ctx.hook = hook; + r = ini_parse_file(fp, hook_ini_handler, &ctx); + if (r < 0) { + if (r == -2) + r = -ENOMEM; + else + r= -EIO; + xbps_error_printf( + "failed to parse hook: %s: %s\n", path, strerror(-r)); + goto err; + } + if (r > 0) { + xbps_error_printf( + "failed to parse hook: %s:%d: syntax error\n", path, r); + goto err; + } + + fclose(fp); + return hook; +err: + fclose(fp); + hook_free(hook); + return NULL; +} + +struct xbps_hooks { + size_t nhooks; + struct hook **hooks; +}; + +static int +hooks_add_hook(struct xbps_hooks *hooks, struct hook *item) +{ + size_t nmemb = hooks->nhooks + 1; + struct hook **tmp = reallocarray(hooks->hooks, nmemb, sizeof(*tmp)); + if (!tmp) + return xbps_error_oom(); + hooks->hooks = tmp; + hooks->hooks[hooks->nhooks++] = item; + return 0; +} + +static bool +seen_hook_filename(struct xbps_hooks *hooks, const char *filename) +{ + for (size_t i = 0; i < hooks->nhooks; i++) { + if (strcmp(hooks->hooks[i]->filename, filename) == 0) + return true; + } + return false; +} + +static int +hooks_scan_dir(struct xbps_hooks *hooks, const char *dir) +{ + struct dirent **namelist; + int n; + int r; + + xbps_dbg_printf("[hooks] scanning directory: %s\n", dir); + + n = scandir(dir, &namelist, NULL, alphasort); + if (n == -1) { + if (errno != ENOENT) + return -errno; + return 0; + } + + for (int i = 0; i < n; i++) { + struct hook *hook; + if (namelist[i]->d_name[0] == '.') + continue; + if (seen_hook_filename(hooks, namelist[i]->d_name)) { + xbps_dbg_printf( + "[hooks] skipping hook: %s/%s: filename masked\n", + dir, namelist[i]->d_name); + continue; + } + xbps_dbg_printf( + "[hooks] parsing hook: %s/%s\n", + dir, namelist[i]->d_name); + hook = hook_parse(dir, namelist[i]->d_name); + if (!hook) { + r = -errno; + goto err; + } + r = hooks_add_hook(hooks, hook); + if (r < 0) + goto err; + } + + r = 0; +err: + for (int i = 0; i < n; i++) + free(namelist[i]); + free(namelist); + return r; +} + +struct xbps_hooks * +xbps_hooks_init(struct xbps_handle *xhp) +{ + char dir[PATH_MAX]; + struct xbps_hooks *hooks = NULL; + int r; + + hooks = calloc(1, sizeof(*hooks)); + if (!hooks) { + xbps_error_oom(); + return NULL; + } + + if (xbps_path_join(dir, sizeof(dir), xhp->confdir, "hooks", (char *)NULL) == -1) { + xbps_error_printf("%s: %s\n", xhp->confdir, strerror(ENAMETOOLONG)); + r = -ENAMETOOLONG; + goto err; + } + r = hooks_scan_dir(hooks, dir); + if (r < 0) + goto err; + + if (xbps_path_join(dir, sizeof(dir), xhp->sysconfdir, "hooks", (char *)NULL) == -1) { + xbps_error_printf("%s: %s\n", xhp->confdir, strerror(ENAMETOOLONG)); + r = -ENAMETOOLONG; + goto err; + } + r = hooks_scan_dir(hooks, dir); + if (r < 0) + goto err; + + return hooks; +err: + xbps_hooks_free(hooks); + errno = -r; + return NULL; +} + +void +xbps_hooks_free(struct xbps_hooks *hooks) +{ + if (!hooks) + return; + free(hooks); +} + +static bool +match_package(const struct hook *hook, const char *pkgver UNUSED, + const char *pkgname, enum match_pkg_action action) +{ + if (hook->matches == 0) + return false; + for (size_t i = 0; i < hook->nmatches; i++) { + struct match *m = hook->matches[i]; + for (size_t j = 0; j < m->npackages; j++) { + struct match_pkg *p = &m->packages[j]; + if (p->action != action) + continue; + switch (p->kind) { + case MATCH_PKG_NAME: + return strcmp(m->packages[i].name, pkgname) == 0; + case MATCH_PKG_CONSTRAINT: + xbps_error_printf("match constraint not implemented\n"); + return false; + case MATCH_PKG_PATTERN: + xbps_error_printf("match pattern not implemented\n"); + return false; + } + } + } + return false; +} + +static int +match_package_hooks(struct xbps_handle *xhp, struct xbps_hooks *hooks, + bool *matches, enum when when) +{ + xbps_object_t pkgd; + xbps_object_iterator_t iter; + int r; + + iter = xbps_array_iter_from_dict(xhp->transd, "packages"); + if (!iter) + return xbps_error_oom(); + + while ((pkgd = xbps_object_iterator_next(iter))) { + char pkgname[XBPS_NAME_SIZE] = {0}; + const char *pkgver = NULL; + xbps_trans_type_t ttype; + enum match_pkg_action action; + + if(!xbps_dictionary_get_cstring_nocopy(pkgd, "pkgver", &pkgver)) + abort(); + + if (!xbps_pkg_name(pkgname, sizeof(pkgname), pkgver)) + abort(); + + ttype = xbps_transaction_pkg_type(pkgd); + switch (ttype) { + case XBPS_TRANS_INSTALL: + action = MATCH_PKG_INSTALL; + break; + case XBPS_TRANS_REINSTALL: + action = MATCH_PKG_REINSTALL; + break; + case XBPS_TRANS_UPDATE: + action = MATCH_PKG_UPDATE; + break; + case XBPS_TRANS_CONFIGURE: + action = MATCH_PKG_CONFIGURE; + break; + case XBPS_TRANS_REMOVE: + action = MATCH_PKG_REMOVE; + break; + case XBPS_TRANS_UNKNOWN: + case XBPS_TRANS_HOLD: + case XBPS_TRANS_DOWNLOAD: + continue; + } + + for (size_t i = 0; i < hooks->nhooks; i++) { + const struct hook *h = hooks->hooks[i]; + + // hook already matched + if (matches[i]) + continue; + + if ((h->when & when) == 0) + continue; + + if (!match_package(h, pkgver, pkgname, action)) + continue; + + matches[i] = true; + } + } + + r = 0; +// err: + xbps_object_iterator_release(iter); + return r; +} + +static int +hook_run(const struct xbps_handle *xhp, const struct hook *hook, enum when when UNUSED) +{ + int r; + + xbps_dbg_printf("[hooks] running hook: %s\n", hook->filename); + + r = xbps_file_exec_argv(xhp, __UNCONST(hook->argv)); + if (r == -1) + return -errno; + xbps_dbg_printf("%d\n", r); + + return 0; +} + +static int +run_hooks(struct xbps_handle *xhp, struct xbps_hooks *hooks, enum when when) +{ + int r; + bool *matches; + + if (hooks->nhooks == 0) + return 0; + + // XXX: get a bitset? + matches = calloc(hooks->nhooks, sizeof(*matches)); + if (!matches) + return xbps_error_oom(); + + r = match_package_hooks(xhp, hooks, matches, when); + if (r < 0) + goto err; + + for (size_t i = 0; i < hooks->nhooks; i++) { + const struct hook *h = hooks->hooks[i]; + if (!matches[i]) + continue; + + r = hook_run(xhp, h, when); + if (r < 0) + goto err; + } + + free(matches); + return 0; +err: + free(matches); + return r; +} + +int +xbps_hooks_pre_transaction(struct xbps_handle *xhp, struct xbps_hooks *hooks) +{ + int r; + + xbps_dbg_printf("[hooks] running pre-transaction hooks\n"); + + r = run_hooks(xhp, hooks, HOOK_PRE_TRANSACTION); + if (r < 0) + return r; + + return 0; +} + +int +xbps_hooks_post_transaction(struct xbps_handle *xhp, struct xbps_hooks *hooks) +{ + int r; + + xbps_dbg_printf("[hooks] running post-transaction hooks\n"); + + r = run_hooks(xhp, hooks, HOOK_POST_TRANSACTION); + if (r < 0) + return r; + + return 0; +} From f1b2093088fcf81e5b85b80ee66322c0cc28f686 Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Tue, 23 Dec 2025 03:14:58 +0100 Subject: [PATCH 4/6] tests: add very basic hooks test --- tests/xbps/libxbps/shell/Kyuafile | 1 + tests/xbps/libxbps/shell/Makefile | 1 + tests/xbps/libxbps/shell/hooks_test.sh | 109 +++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 tests/xbps/libxbps/shell/hooks_test.sh diff --git a/tests/xbps/libxbps/shell/Kyuafile b/tests/xbps/libxbps/shell/Kyuafile index e27f783e3..b8c4f3842 100644 --- a/tests/xbps/libxbps/shell/Kyuafile +++ b/tests/xbps/libxbps/shell/Kyuafile @@ -30,3 +30,4 @@ atf_test_program{name="orphans_test"} atf_test_program{name="noextract_files_test"} atf_test_program{name="transaction_check_revdeps_test"} atf_test_program{name="repo_test"} +atf_test_program{name="hooks_test"} diff --git a/tests/xbps/libxbps/shell/Makefile b/tests/xbps/libxbps/shell/Makefile index 22c526472..6af405a08 100644 --- a/tests/xbps/libxbps/shell/Makefile +++ b/tests/xbps/libxbps/shell/Makefile @@ -10,6 +10,7 @@ TESTSHELL+= update_shlibs_test update_hold_test update_repolock_test TESTSHELL+= cyclic_deps_test conflicts_test update_itself_test TESTSHELL+= hold_test ignore_test preserve_test repo_test TESTSHELL+= noextract_files_test orphans_test transaction_check_revdeps_test +TESTSHELL+= hooks_test EXTRA_FILES = Kyuafile include $(TOPDIR)/mk/test.mk diff --git a/tests/xbps/libxbps/shell/hooks_test.sh b/tests/xbps/libxbps/shell/hooks_test.sh new file mode 100644 index 000000000..5a307f1c8 --- /dev/null +++ b/tests/xbps/libxbps/shell/hooks_test.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env atf-sh + +atf_test_case hooks_basic + +hooks_basic_head() { + atf_set "descr" "Tests for basic hooks" +} + +hooks_basic_body() { + mkdir -p repo pkg_A root/etc/xbps.d/hooks + + cat <<-EOF > root/etc/xbps.d/hooks/00-pre-install-hook.hook + [Hook] + When = PreTransaction + Exec = sh -c echo\ pre-install-hook\ >&2 + + [Match] + PackageInstall = A + EOF + + cat <<-EOF > root/etc/xbps.d/hooks/00-post-install-hook.hook + [Hook] + When = PostTransaction + Exec = sh -c echo\ post-install-hook\ >&2 + + [Match] + PackageInstall = A + EOF + + cat <<-EOF > root/etc/xbps.d/hooks/00-pre-update-hook.hook + [Hook] + When = PreTransaction + Exec = sh -c echo\ pre-update-hook\ >&2 + + [Match] + PackageUpdate = A + EOF + + cat <<-EOF > root/etc/xbps.d/hooks/00-post-update-hook.hook + [Hook] + When = PostTransaction + Exec = sh -c echo\ post-update-hook\ >&2 + + [Match] + PackageUpdate = A + EOF + + cat <<-EOF > root/etc/xbps.d/hooks/00-pre-remove-hook.hook + [Hook] + When = PreTransaction + Exec = sh -c echo\ pre-remove-hook\ >&2 + + [Match] + PackageRemove = A + EOF + + cat <<-EOF > root/etc/xbps.d/hooks/00-post-remove-hook.hook + [Hook] + When = PostTransaction + Exec = sh -c echo\ post-remove-hook\ >&2 + + [Match] + PackageRemove = A + EOF + + cd repo + atf_check -o ignore -- xbps-create -A noarch -n A-1.0_1 -s "A pkg" ../pkg_A + atf_check -o ignore -- xbps-rindex -a $PWD/*.xbps + cd .. + + atf_check \ + -o ignore \ + -e match:pre-install-hook \ + -e match:post-install-hook \ + -e not-match:pre-update-hook \ + -e not-match:post-update-hook \ + -e not-match:pre-remove-hook \ + -e not-match:post-remove-hook \ + -- xbps-install -r root --repository=repo -y A + + cd repo + atf_check -o ignore -- xbps-create -A noarch -n A-1.1_1 -s "A pkg" ../pkg_A + atf_check -o ignore -e ignore -- xbps-rindex -a $PWD/*.xbps + cd .. + + atf_check \ + -o ignore \ + -e not-match:pre-install-hook \ + -e not-match:post-install-hook \ + -e match:pre-update-hook \ + -e match:post-update-hook \ + -e not-match:pre-remove-hook \ + -e not-match:post-remove-hook \ + -- xbps-install -r root --repository=repo -yu + + atf_check \ + -o ignore \ + -e not-match:pre-install-hook \ + -e not-match:post-install-hook \ + -e not-match:pre-update-hook \ + -e not-match:post-update-hook \ + -e match:pre-remove-hook \ + -e match:post-remove-hook \ + -- xbps-remove -r root -y A +} + +atf_init_test_cases() { + atf_add_test_case hooks_basic +} From 13b454d4373c4f158ac253111476d6710a297c3c Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Mon, 22 Dec 2025 23:30:21 +0100 Subject: [PATCH 5/6] data: add xbps.hook manpage --- data/Makefile | 1 + data/xbps.hook.5 | 187 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 data/xbps.hook.5 diff --git a/data/Makefile b/data/Makefile index 25a439e27..e0dba78d1 100644 --- a/data/Makefile +++ b/data/Makefile @@ -13,6 +13,7 @@ all: install: install -d $(DESTDIR)$(MANDIR)/man5 install -m644 xbps.d.5 $(DESTDIR)$(MANDIR)/man5 + install -m644 xbps.hook.5 $(DESTDIR)$(MANDIR)/man5 install -d $(DESTDIR)$(MANDIR)/man7 install -m644 xbps.7 $(DESTDIR)$(MANDIR)/man7 install -d $(DESTDIR)$(PKGCONFIGDIR) diff --git a/data/xbps.hook.5 b/data/xbps.hook.5 new file mode 100644 index 000000000..f7dffe396 --- /dev/null +++ b/data/xbps.hook.5 @@ -0,0 +1,187 @@ +.Dd December 22, 2025 +.Dt XBPS.HOOK 5 +.Os +.Sh NAME +.Nm xbps.hook +.Nd XBPS hook configuration +.Sh SYNOPSIS +.Bl -item -compact +.It +/etc/xbps.d/hooks/*.hook +.It +/usr/share/xbps.d/hooks/*.hook +.El +.Sh DESCRIPTION +The +.Nm +files are +.So .ini Sc Ns -style +configuration files to define hooks that execute commands at certain points of +the xbps transactions +.Po +package updates, +installs, +and removals. +.Pc +.Pp +Comments can be put anywhere in the file +and using hashmarks +.Pq Sq # , +or +semi-colons +.Pq Sq \&; , +and are continued to the end of the line. +.Sh [HOOK] SECTION OPTIONS +The +.Ic Hook +section is required and defines when the hook is executed and the command that +is being executed as part of the hook. +.Bl -tag -width -x +.It Sy Name = Ar name +Set the hook name used for logging. +.It Ic When = Ar option ... +Defines when the hook is executed. +The following values are supported +and multiple values can be set by +separating them with a space or defining +the +.Sy When +option multiple times. +Valid options are: +.Bl -tag -width -x +.It Ic PreTransaction +Before all packages are installed, +updated, +or removed. +.It Ic PostTransaction +After all packages are installed, +updated, +or removed. +.El +.It Sy Exec = Ar command Op Ar args ... +Command that is executed. +The +.Ar command +and +.Ar args +are split into words according to +.Sx WORD SPLITTING . +.El +.Sh [MATCH] SECTION OPTIONS +A hook is executed if it's +.Ic Match +section matches the current transaction. +.Bl -tag -width -x +.It Ic Package = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are installed, +updated, +removed, +reinstalled, +or configured in the transaction. +.It Ic PackageInstall = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are installed in the transaction. +.It Ic PackageUpdate = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are updated in the transaction. +.It Ic PackageRemove = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are removed in the transaction. +.It Ic PackageReinstall = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are reinstalled in the transaction. +.It Ic PackageConfigure = Ar pkgname | Ar pkgpattern +Matches if +.Ar pkgname +or packages matching +.Ar pkgpattern +are configured in the transaction. +.It Ic Path = Ar path | Ar pattern +Matches if +.Ar path +or paths matching +.Ar pattern +are created, +modified, +or deleted. +.It Ic PathCreated = Ar path | Ar pattern +Matches if +.Ar path +or paths matching +.Ar pattern +are created in the transaction. +.It Ic PathModified = Ar path | Ar pattern +Matches if +.Ar path +or paths matching +.Ar pattern +are modified in the transaction. +.It Ic PathDeleted = Ar path | Ar pattern +Matches if +.Ar path +or paths matching +.Ar pattern +are deleted in the transaction. +.El +.Sh WORD SPLITTING +Options which support multiple free text values will be split into separate +words. +The word boundaries are spaces +.Pq Sq \~ +and tabs +.Pq Sq \et . +.Pp +If a word contains a literal space +.Pq Sq \~ +or tab +.Pq Sq \et , +use a backslash +.Pq Sq \e +to escape the character. +.Sh EXAMPLES +The following example restarts the +.Ic sshd +service after the +.Ic ssh +package was updated. +.Bd -literal -offset indent +[Hook] +Name = Restart sshd. +When = PostTransaction +Exec = sv restart /var/service/sshd + +[Match] +PackageUpdate = ssh +.Ed +.Pp +This example hook will sign the +.Pa systemd-bootx64.efi +file whenever it is updated or installed. +.Bd -literal -offset indent +[Hook] +Name = Sign the bootloader file. +When = PostTransaction +Exec = sbsign --key /etc/kernel/secure-boot.key.pem \e + --cert /etc/kernel/secure-boot.cert.pem \e + --output /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \e + /usr/lib/systemd/boot/efi/systemd-bootx64.efi + +[Match] +PathModified = /usr/lib/systemd/boot/efi/systemd-bootx64.efi +.Ed From 6dc52ade3704bcf8c615c27177fbcfa0e6cf3e31 Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Tue, 23 Dec 2025 03:13:10 +0100 Subject: [PATCH 6/6] lib: add hooks into the transaction --- lib/transaction_commit.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/transaction_commit.c b/lib/transaction_commit.c index 8ef8a8de6..d1cb694ca 100644 --- a/lib/transaction_commit.c +++ b/lib/transaction_commit.c @@ -107,6 +107,7 @@ xbps_transaction_commit(struct xbps_handle *xhp) xbps_object_t obj; xbps_object_iterator_t iter; xbps_trans_type_t ttype; + struct xbps_hooks *hooks = NULL; const char *pkgver = NULL, *pkgname = NULL; int rv = 0; bool update; @@ -146,6 +147,12 @@ xbps_transaction_commit(struct xbps_handle *xhp) if (iter == NULL) return EINVAL; + hooks = xbps_hooks_init(xhp); + if (!hooks) { + rv = errno; + goto out; + } + /* * Download and verify binary packages. */ @@ -299,6 +306,14 @@ xbps_transaction_commit(struct xbps_handle *xhp) } xbps_object_iterator_reset(iter); + /* + * Run pre-transaction hooks + */ + rv = xbps_hooks_pre_transaction(xhp, hooks); + if (rv < 0) { + rv = -rv; + goto out; + } while ((obj = xbps_object_iterator_next(iter)) != NULL) { xbps_dictionary_get_cstring_nocopy(obj, "pkgver", &pkgver); @@ -429,7 +444,25 @@ xbps_transaction_commit(struct xbps_handle *xhp) } } + /* + * Re-read hooks after packages have been unpackaed so we can run new + * post-transaction hooks. + */ + xbps_hooks_free(hooks); + hooks = xbps_hooks_init(xhp); + if (!hooks) { + rv = errno; + goto out; + } + + rv = xbps_hooks_post_transaction(xhp, hooks); + if (rv < 0) { + rv = -rv; + goto out; + } + out: + xbps_hooks_free(hooks); xbps_object_release(remove_scripts); xbps_object_iterator_release(iter); if (rv == 0) {