From 7303e4767dc874157ae28b27b79b52dc360da30b Mon Sep 17 00:00:00 2001 From: Mohamed Date: Tue, 25 Aug 2026 23:04:42 +0300 Subject: [PATCH 1/2] MDEV-40786 Make mariadb-plugin a package manager Add search, install, and uninstall subcommands to mariadb-plugin with input validation and case normalization, while keeping legacy ENABLE and DISABLE syntax functional for backward compatibility. Bake the installation layout into the binary at compile time (RPM, DEB, or tarball), as it cannot be derived from the path alone since RPM and DEB use the same directory, and verify the executable path at runtime via argv[0] and mysys path helpers to ensure package managers and tarball directories target the correct installation. Delegate install and uninstall to the system package manager on RPM and DEB installations, so that plugin files stay owned by it. The plugin name maps to the uniform package name mariadb-plugin-. Commands run through fork and execvp with an argument vector, never a shell, and inherit the standard streams, so the package manager prompts the user itself and its exit code is passed through. RPM packages are named differently, so they get the uniform name as a Provides, derived from the plugin component in cmake/plugin.cmake. DEB packages already carry it. On RPM, uninstall resolves the real package name with rpm --whatprovides, because dnf 5 does not accept Provides names for removal. Search lists the plugins the installation can install: the plugin name, installed or available, and the description, filtered by an optional search term. The distribution's package index is queried for everything providing mariadb-plugin-* (repoquery for dnf, apt-cache, zypper search plus info --provides) and each output is parsed into the same uniform format, so the user always sees plugin names, not the distribution's package names. Add a --dry-run option that prints the commands install and uninstall would run instead of running them, with the package name resolved, and without requiring root. It is long only, as -n is taken by the legacy --no-defaults option. Test the command line handling in main.mariadb-plugin: unknown commands, argument counts, plugin name validation and that the deprecated ENABLE|DISABLE syntax still reaches the old code path. Installing and removing packages is not testable there, as the tool acts only when it runs from the location it was installed to. --- client/CMakeLists.txt | 5 + client/mysql_plugin.c | 1089 ++++++++++++++++++++++++- cmake/plugin.cmake | 7 + mysql-test/main/mariadb-plugin.result | 38 + mysql-test/main/mariadb-plugin.test | 77 ++ 5 files changed, 1211 insertions(+), 5 deletions(-) create mode 100644 mysql-test/main/mariadb-plugin.result create mode 100644 mysql-test/main/mariadb-plugin.test diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 1a56e55d5ac3c..db6fa588f5473 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -76,6 +76,11 @@ TARGET_LINK_LIBRARIES(mariadb-show ${CLIENT_LIB}) MYSQL_ADD_EXECUTABLE(mariadb-plugin mysql_plugin.c) TARGET_LINK_LIBRARIES(mariadb-plugin ${CLIENT_LIB}) +# The tool needs to know how MariaDB was installed to pick the way plugins +# are installed. The install layout is only known when building, so it is +# passed to the tool, together with the directories it has to verify. +SET_TARGET_PROPERTIES(mariadb-plugin PROPERTIES COMPILE_DEFINITIONS + "INSTALL_LAYOUT_${INSTALL_LAYOUT};INSTALL_PLUGINDIR=${INSTALL_PLUGINDIR};INSTALL_BINDIRABS=${INSTALL_BINDIRABS}") MYSQL_ADD_EXECUTABLE(mariadb-binlog mysqlbinlog.cc mysqlbinlog-engine.cc) TARGET_LINK_LIBRARIES(mariadb-binlog ${CLIENT_LIB} mysys_ssl) diff --git a/client/mysql_plugin.c b/client/mysql_plugin.c index 00870fa72e026..1ec342c5641d9 100644 --- a/client/mysql_plugin.c +++ b/client/mysql_plugin.c @@ -24,9 +24,36 @@ #include #include +#define STR(s) _STR(s) +#define _STR(s) #s + +/* + The build system defines INSTALL_LAYOUT_RPM or INSTALL_LAYOUT_DEB for the + packaged builds, and neither of them for a binary tarball. +*/ +#if defined(INSTALL_LAYOUT_RPM) +#define INSTALL_METHOD_NAME "rpm" +#elif defined(INSTALL_LAYOUT_DEB) +#define INSTALL_METHOD_NAME "deb" +#else +#define INSTALL_METHOD_NAME "tarball" +#endif + +/* + On rpm and deb installations install/uninstall delegate to the system + package manager. Tarball installations manage plugin files themselves, + so none of the delegation code applies (and neither do its unix-only + process primitives). +*/ +#if defined(INSTALL_LAYOUT_RPM) || defined(INSTALL_LAYOUT_DEB) +#define PKG_DELEGATION 1 +#include +#endif + /* Global variables. */ static uint my_end_arg= 0; static uint opt_verbose=0; +static my_bool opt_dry_run= 0; static uint opt_no_defaults= 0; static uint opt_print_defaults= 0; static char *opt_datadir=0, *opt_basedir=0, @@ -58,6 +85,9 @@ static struct my_option my_long_options[] = {"plugin-ini", 'i', "Read plugin information from configuration file " "specified instead of from /.ini.", 0, 0, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"dry-run", 0, "Print the commands that install and uninstall would run, " + "without running them.", + &opt_dry_run, &opt_dry_run, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, {"no-defaults", 'n', "Do not read values from configuration file.", 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}, {"print-defaults", 'P', "Show default values from configuration file.", @@ -87,6 +117,14 @@ static int find_plugin(char *tp_path); static int build_bootstrap_file(char *operation, char *bootstrap); static int dump_bootstrap_file(char *bootstrap_file); static int bootstrap_server(char *server_path, char *bootstrap_file); +static void usage(void); +static int run_new_command(int argc, char **argv); +static int validate_plugin_name(const char *name); +static int is_legacy_syntax(int argc, char **argv); +static int detect_install_method(char *basedir, size_t basedir_size); +static int do_search(const char *name, const char *basedir); +static int do_install(const char *name, const char *basedir); +static int do_uninstall(const char *name, const char *basedir); int main(int argc,char *argv[]) @@ -100,6 +138,20 @@ int main(int argc,char *argv[]) sf_leaking_memory=1; /* don't report memory leaks on early exits */ plugin_data.name= 0; /* initialize name */ + /* + The new package-manager style commands (search|install|uninstall) are + handled in run_new_command(). The legacy " ENABLE|DISABLE" + syntax is recognized by scanning the raw arguments, before any option + parsing, and continues through the original code path below unchanged, + for backward compatibility. + */ + if (!is_legacy_syntax(argc, argv)) + { + error= run_new_command(argc, argv); + my_end(my_end_arg); + exit(error); + } + /* The following operations comprise the method for enabling or disabling a plugin. We begin by processing the command options then check the @@ -416,11 +468,14 @@ static int get_default_values() static void usage(void) { print_version(); - puts("Copyright (c) 2011, 2015, Oracle and/or its affiliates. " - "All rights reserved.\n"); - puts("Enable or disable plugins."); - printf("\nUsage: %s [options] ENABLE|DISABLE\n\nOptions:\n", - my_progname); + puts(ORACLE_WELCOME_COPYRIGHT_NOTICE("2011")); + puts("Manage MariaDB plugins across package managers and binary distributions."); + printf("\nUsage:\n"); + printf(" %s search []\n", my_progname); + printf(" %s install \n", my_progname); + printf(" %s uninstall \n\n", my_progname); + printf("Legacy syntax (deprecated, kept for backward compatibility):\n"); + printf(" %s [options] ENABLE|DISABLE\n\nOptions:\n", my_progname); my_print_help(my_long_options); puts("\n"); } @@ -1239,3 +1294,1027 @@ static int bootstrap_server(char *server_path, char *bootstrap_file) return error; } + + +/** + Detect the legacy " ENABLE|DISABLE" command line syntax. + + The check is done on the raw arguments, before any option parsing, so + that legacy invocations take the original code path unchanged. + + @param[in] argc The number of arguments. + @param[in] argv The arguments. + + @retval int legacy syntax = 1, new syntax = 0 +*/ + +static int is_legacy_syntax(int argc, char **argv) +{ + int i; + + for (i= 1; i < argc; i++) + { + /* + Whichever keyword comes first decides, so that "search enable" is a + search for the word enable, while "myplugin ENABLE" stays the + deprecated syntax. + */ + if (strcmp(argv[i], "search") == 0 || + strcmp(argv[i], "install") == 0 || + strcmp(argv[i], "uninstall") == 0) + return 0; + if (strcasecmp(argv[i], "ENABLE") == 0 || + strcasecmp(argv[i], "DISABLE") == 0) + return 1; + } + return 0; +} + + +/** + Check that a plugin name contains only safe characters. + + The name is later used to construct package names and file paths, so + only lower case alphanumerics, '_' and '-' are accepted. The name is + expected to be normalized to lower case before this check. + + @param[in] name The normalized plugin name. + + @retval int error = 1, success = 0 +*/ + +static int validate_plugin_name(const char *name) +{ + const char *p; + + if (*name == '\0') + { + fprintf(stderr, "ERROR: plugin name cannot be empty.\n"); + return 1; + } + for (p= name; *p; p++) + { + if (!isalnum((unsigned char) *p) && *p != '_' && *p != '-') + { + fprintf(stderr, "ERROR: invalid character '%c' in plugin name. " + "Use only [a-z0-9_-].\n", *p); + return 1; + } + } + return 0; +} + +/** + Verify that the tool is part of the installation it was built for. + + The installation method is known at build time, so only the location has + to be checked. It is taken from argv[0] and not from the server, as one + machine can have several server installations. + + @param[out] basedir The base directory, empty for rpm and deb, + where the package manager owns the files. + @param[in] basedir_size The size of the basedir buffer. + + @retval int error = 1, success = 0 +*/ + +static int detect_install_method(char *basedir, size_t basedir_size) +{ + char self_path[FN_REFLEN], real_path[FN_REFLEN], real_dir[FN_REFLEN]; + size_t length; +#if !defined(INSTALL_LAYOUT_RPM) && !defined(INSTALL_LAYOUT_DEB) + char plugin_dir[FN_REFLEN]; + char *slash; +#endif + + /* + my_path() searches PATH when argv[0] is a bare program name. The path + is resolved afterwards, so that a symbolic link, like the one for the + old mysql_plugin name, does not hide where the tool is installed. + */ + my_path(self_path, my_progname, ""); + safe_strcat(self_path, sizeof(self_path), base_name(my_progname)); + if (my_realpath(real_path, self_path, MYF(0))) + safe_strcpy(real_path, sizeof(real_path), self_path); + + dirname_part(real_dir, real_path, &length); + + length= strlen(real_dir); + while (length > 1 && (real_dir[length - 1] == FN_LIBCHAR || + real_dir[length - 1] == FN_LIBCHAR2)) + real_dir[--length]= '\0'; + +#if defined(INSTALL_LAYOUT_RPM) || defined(INSTALL_LAYOUT_DEB) + if (strcmp(real_dir, STR(INSTALL_BINDIRABS)) != 0) + { + fprintf(stderr, "ERROR: this is a %s build, but it runs from '%s' " + "instead of '%s', so it is not part of a %s installation.\n", + INSTALL_METHOD_NAME, real_dir, STR(INSTALL_BINDIRABS), + INSTALL_METHOD_NAME); + return 1; + } + basedir[0]= '\0'; +#else + /* The base directory is one level above the directory of the tool. */ + safe_strcpy(basedir, basedir_size, real_dir); + slash= strrchr(basedir, FN_LIBCHAR); + if (!slash) + slash= strrchr(basedir, FN_LIBCHAR2); + if (!slash) + { + fprintf(stderr, "ERROR: cannot determine the MariaDB base directory " + "from '%s'.\n", real_dir); + return 1; + } + *slash= '\0'; + + safe_strcpy(plugin_dir, sizeof(plugin_dir), basedir); + safe_strcat(plugin_dir, sizeof(plugin_dir), "/" STR(INSTALL_PLUGINDIR)); + if (!file_exists(plugin_dir)) + { + fprintf(stderr, "ERROR: '%s' does not look like a MariaDB installation, " + "'%s' not found.\n", basedir, plugin_dir); + return 1; + } +#endif + return 0; +} + + +#ifdef PKG_DELEGATION + +/** + Pick the package manager to delegate to. + + On deb installations it is always apt-get (the script-stable interface, + unlike apt). On rpm installations dnf and zypper manage the same rpm + database, so whichever is present is usable; dnf is tried first. + + @retval const char* the program name, or NULL with an error printed +*/ + +static const char *get_package_manager(void) +{ +#if defined(INSTALL_LAYOUT_DEB) + return "apt-get"; +#else + char dir[FN_REFLEN]; + + if (find_file_in_path(dir, "dnf")) + return "dnf"; + if (find_file_in_path(dir, "zypper")) + return "zypper"; + fprintf(stderr, "ERROR: no package manager found: neither dnf nor zypper " + "is in PATH.\n"); + return NULL; +#endif +} + + +/** + Refuse to continue without root privileges. + + The package manager would fail anyway, but only after a repository + refresh, with an error that does not mention this tool. + + @param[in] verb The command name, for the error message. + + @retval int error = 1, success = 0 +*/ + +static int check_root(const char *verb) +{ + if (!opt_dry_run && geteuid() != 0) + { + fprintf(stderr, "ERROR: '%s' requires root privileges. " + "Run as root or with sudo.\n", verb); + return 1; + } + return 0; +} + + +/** + Run a command and wait for it to finish. + + The command is executed directly, not through a shell, so the arguments + cannot be reinterpreted. The child inherits the standard streams: the + package manager talks to the user directly, including its own + confirmation prompts and progress output. + + @param[in] cmd_argv NULL-terminated argument vector. + + @retval int the command exit code, 127 if it could not be run +*/ + +static int run_argv(char **cmd_argv) +{ + pid_t pid; + int status; + + /* + --dry-run only stops the commands that change the system. The queries + that read the package database still run, so that what is printed is + what would really be executed, package names resolved and all. + */ + if (opt_dry_run) + { + int i; + for (i= 0; cmd_argv[i]; i++) + printf("%s%s", i ? " " : "", cmd_argv[i]); + printf("\n"); + return 0; + } + + fflush(stdout); + fflush(stderr); + if ((pid= fork()) < 0) + { + fprintf(stderr, "ERROR: cannot fork: %s.\n", strerror(errno)); + return 127; + } + if (pid == 0) + { + execvp(cmd_argv[0], cmd_argv); + fprintf(stderr, "ERROR: cannot run '%s': %s.\n", cmd_argv[0], + strerror(errno)); + _exit(127); + } + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + { + fprintf(stderr, "ERROR: cannot wait for '%s': %s.\n", cmd_argv[0], + strerror(errno)); + return 127; + } + } + if (WIFSIGNALED(status)) + { + fprintf(stderr, "ERROR: '%s' was terminated by signal %d.\n", + cmd_argv[0], WTERMSIG(status)); + return 127; + } + return WEXITSTATUS(status); +} + + +/** + Run a command and capture its standard output. + + Standard error stays on the terminal, unless quiet_stderr is set, for + commands whose failure is an expected answer and not an error. The pipe + is read to the end, so that the child never blocks writing. + + @param[in] cmd_argv NULL-terminated argument vector. + @param[out] out Initialized string, replaced by the output. + @param[in] quiet_stderr Discard the command's standard error. + + @retval int the command exit code, 127 if it could not be run +*/ + +static int run_argv_capture(char **cmd_argv, DYNAMIC_STRING *out, + int quiet_stderr) +{ + char buf[4096]; + int fds[2]; + pid_t pid; + int status; + ssize_t n; + my_bool oom= FALSE; + + dynstr_set(out, ""); + if (pipe(fds)) + { + fprintf(stderr, "ERROR: cannot create a pipe: %s.\n", strerror(errno)); + return 127; + } + fflush(stdout); + fflush(stderr); + if ((pid= fork()) < 0) + { + fprintf(stderr, "ERROR: cannot fork: %s.\n", strerror(errno)); + close(fds[0]); + close(fds[1]); + return 127; + } + if (pid == 0) + { + dup2(fds[1], STDOUT_FILENO); + if (quiet_stderr) + { + int devnull= open("/dev/null", O_WRONLY); + if (devnull >= 0) + dup2(devnull, fileno(stderr)); + } + close(fds[0]); + close(fds[1]); + execvp(cmd_argv[0], cmd_argv); + fprintf(stderr, "ERROR: cannot run '%s': %s.\n", cmd_argv[0], + strerror(errno)); + _exit(127); + } + close(fds[1]); + while ((n= read(fds[0], buf, sizeof(buf)))) + { + if (n < 0) + { + if (errno == EINTR) + continue; + break; + } + /* keep reading after a failed append, so the child can still finish */ + if (!oom) + oom= dynstr_append_mem(out, buf, (size_t) n); + } + close(fds[0]); + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + { + fprintf(stderr, "ERROR: cannot wait for '%s': %s.\n", cmd_argv[0], + strerror(errno)); + return 127; + } + } + if (oom) + { + fprintf(stderr, "ERROR: out of memory reading the output of '%s'.\n", + cmd_argv[0]); + return 127; + } + if (WIFSIGNALED(status)) + return 127; + return WEXITSTATUS(status); +} + + +#define PLUGIN_PREFIX "mariadb-plugin-" +#define PLUGIN_PREFIX_LEN (sizeof(PLUGIN_PREFIX) - 1) +#define PACKAGE_NAME_SIZE (PLUGIN_PREFIX_LEN + NAME_CHAR_LEN + 1) + + +/** + Build the distribution-independent package name (D2): mariadb-plugin- + followed by the plugin name, which is already validated and lowercased. + + @param[out] to Buffer for the package name. + @param[in] size Size of the buffer. + @param[in] name The normalized plugin name. +*/ + +static void build_package_name(char *to, size_t size, const char *name) +{ + safe_strcpy(to, size, PLUGIN_PREFIX); + safe_strcat(to, size, name); +} + + +/* + Search: every backend answers the same two questions - which packages + provide mariadb-plugin-* and which of them are installed - through a + different query. The results are normalized into plugin_list and + printed in one format, so the user sees plugin names as install expects + them, never the distribution's own package names. +*/ + +struct plugin_entry +{ + char name[NAME_CHAR_LEN + 1]; /* uniform name, prefix stripped */ + char package[NAME_CHAR_LEN + 1]; /* real package name, rpm only */ + char description[160]; + int installed; +}; + +static DYNAMIC_ARRAY plugin_list; +static DYNAMIC_STRING search_output; + +#define PLUGIN_AT(i) (dynamic_element(&plugin_list, (i), struct plugin_entry *)) + + +static struct plugin_entry *find_plugin_entry(const char *name) +{ + size_t i; + + for (i= 0; i < plugin_list.elements; i++) + if (strcmp(PLUGIN_AT(i)->name, name) == 0) + return PLUGIN_AT(i); + return NULL; +} + + +/** + Add a plugin to the result list, or return the existing entry with the + same uniform name. The prefix is stripped from the stored name. + + The list reallocates, so the entry is only valid until the next one. + + @param[in] package_name The uniform package name, mariadb-plugin-x. + + @retval struct plugin_entry* the entry, NULL when out of memory +*/ + +static struct plugin_entry *add_plugin_entry(const char *package_name) +{ + struct plugin_entry e, *found; + const char *name= package_name + PLUGIN_PREFIX_LEN; + + if ((found= find_plugin_entry(name))) + return found; + bzero(&e, sizeof(e)); + safe_strcpy(e.name, sizeof(e.name), name); + if (insert_dynamic(&plugin_list, &e)) + return NULL; + return PLUGIN_AT(plugin_list.elements - 1); +} + + +static int cmp_plugin_entries(const void *a, const void *b) +{ + return strcmp(((const struct plugin_entry *) a)->name, + ((const struct plugin_entry *) b)->name); +} + + +/** + Print the collected plugins that match the search term. + + @param[in] term Substring to match against plugin names, "" for all. + + @retval int no matches = 1, matches printed = 0 +*/ + +static int print_search_results(const char *term) +{ + size_t i, width= 0, matches= 0; + + sort_dynamic(&plugin_list, cmp_plugin_entries); + for (i= 0; i < plugin_list.elements; i++) + { + if (*term && !strstr(PLUGIN_AT(i)->name, term)) + continue; + matches++; + if (strlen(PLUGIN_AT(i)->name) > width) + width= strlen(PLUGIN_AT(i)->name); + } + if (!matches) + { + if (*term) + printf("No plugins matching '%s' found.\n", term); + else + printf("No plugins found.\n"); + return 1; + } + for (i= 0; i < plugin_list.elements; i++) + { + struct plugin_entry *e= PLUGIN_AT(i); + + if (*term && !strstr(e->name, term)) + continue; + printf("%-*s %-9s %s\n", (int) width, e->name, + e->installed ? "installed" : "available", e->description); + } + return 0; +} + + +#ifndef INSTALL_LAYOUT_DEB + +static struct plugin_entry *find_plugin_by_package(const char *package) +{ + size_t i; + + for (i= 0; i < plugin_list.elements; i++) + if (strcmp(PLUGIN_AT(i)->package, package) == 0) + return PLUGIN_AT(i); + return NULL; +} + + +/** + Parse dnf repoquery output in the format + @@@| + + into the result list. The uniform name is one of the capabilities, so + no name mapping is needed in the tool. + + @param[in] output The captured repoquery output, modified in place. + @param[in] installed Mark the found plugins as installed. +*/ + +static void parse_dnf_records(char *output, int installed) +{ + struct plugin_entry *e= NULL; + char *line, *next, *sep; + char package[NAME_CHAR_LEN + 1], summary[160]; + + package[0]= summary[0]= '\0'; + for (line= output; line && *line; line= next) + { + if ((next= strchr(line, '\n'))) + *next++= '\0'; + if (strncmp(line, "@@@", 3) == 0) + { + line+= 3; + if ((sep= strchr(line, '|'))) + *sep++= '\0'; + safe_strcpy(package, sizeof(package), line); + safe_strcpy(summary, sizeof(summary), sep ? sep : ""); + continue; + } + /* a capability line; the version part after the name is irrelevant */ + if ((sep= strchr(line, ' '))) + *sep= '\0'; + if (strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) != 0 || + !package[0]) + continue; + if (!(e= add_plugin_entry(line))) + return; + safe_strcpy(e->package, sizeof(e->package), package); + if (!e->description[0]) + safe_strcpy(e->description, sizeof(e->description), summary); + if (installed) + e->installed= 1; + } +} + + +static int search_dnf(void) +{ + char *repo_argv[]= { + (char *) "dnf", (char *) "-q", (char *) "repoquery", + (char *) "--whatprovides", (char *) PLUGIN_PREFIX "*", + (char *) "--qf", (char *) "@@@%{name}|%{summary}\\n%{provides}\\n", 0 }; + char *inst_argv[]= { + (char *) "dnf", (char *) "-q", (char *) "repoquery", + (char *) "--installed", (char *) "--whatprovides", + (char *) PLUGIN_PREFIX "*", + (char *) "--qf", (char *) "@@@%{name}|%{summary}\\n%{provides}\\n", 0 }; + int error; + + if ((error= run_argv_capture(repo_argv, &search_output, 0))) + return error; + parse_dnf_records(search_output.str, 0); + + /* same query against the installed packages only, for the status */ + if (run_argv_capture(inst_argv, &search_output, 0) == 0) + parse_dnf_records(search_output.str, 1); + return 0; +} + + +/** + Parse "zypper --xmlout search --provides" solvable lines, e.g. + . + zypper never reports which capability matched, so the uniform names are + filled in afterwards by search_zypper_names(). + + @param[in] output The captured zypper output, modified in place. +*/ + +static void parse_zypper_solvables(char *output) +{ + struct plugin_entry e; + char *line, *next, *val, *end; + + for (line= output; line && *line; line= next) + { + if ((next= strchr(line, '\n'))) + *next++= '\0'; + if (!strstr(line, "package; + cmd_argv[n]= 0; + error= run_argv_capture(cmd_argv, &search_output, 0); + my_free(cmd_argv); + if (error) + return 1; + + /* nothing is added below, so the entry a header selects stays valid */ + for (line= search_output.str; line && *line; line= next) + { + if ((next= strchr(line, '\n'))) + *next++= '\0'; + if (strncmp(line, "Name", 4) == 0 && (sep= strchr(line, ':'))) + { + for (sep++; *sep == ' '; sep++) ; + e= find_plugin_by_package(sep); + continue; + } + for (cap= line; *cap == ' '; cap++) ; + if (cap == line || !e || + strncmp(cap, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) != 0) + continue; + if ((sep= strchr(cap, ' '))) + *sep= '\0'; + safe_strcpy(e->name, sizeof(e->name), cap + PLUGIN_PREFIX_LEN); + } + + /* drop packages whose uniform name never showed up */ + for (i= 0; i < plugin_list.elements; ) + { + if (PLUGIN_AT(i)->name[0]) + i++; + else + delete_dynamic_element(&plugin_list, i); + } + return 0; +} + + +static int search_zypper(void) +{ + char *cmd_argv[7]; + int error; + + cmd_argv[0]= (char *) "zypper"; + cmd_argv[1]= (char *) "-n"; + cmd_argv[2]= (char *) "--xmlout"; + cmd_argv[3]= (char *) "search"; + cmd_argv[4]= (char *) "--provides"; + cmd_argv[5]= (char *) PLUGIN_PREFIX "*"; + cmd_argv[6]= 0; + /* zypper exits with 104 when nothing matches: an answer, not an error */ + error= run_argv_capture(cmd_argv, &search_output, 0); + if (error && error != 104) + return error; + parse_zypper_solvables(search_output.str); + if (plugin_list.elements && search_zypper_names()) + return 1; + return 0; +} + +#else /* INSTALL_LAYOUT_DEB */ + +/** + Parse "apt-cache search" output, " - " per line, + into the result list. deb package names are already the uniform names. + + @param[in] output The captured apt-cache output, modified in place. +*/ + +static void parse_apt_records(char *output) +{ + struct plugin_entry *e; + char *line, *next, *sep; + + for (line= output; line && *line; line= next) + { + if ((next= strchr(line, '\n'))) + *next++= '\0'; + if ((sep= strstr(line, " - "))) + *sep= '\0'; + if (strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) != 0) + continue; + if (!(e= add_plugin_entry(line))) + return; + if (sep && !e->description[0]) + safe_strcpy(e->description, sizeof(e->description), sep + 3); + } +} + + +static int search_apt(void) +{ + char *cmd_argv[6]; + char *line, *next, *sep; + struct plugin_entry *e; + int error; + + cmd_argv[0]= (char *) "apt-cache"; + cmd_argv[1]= (char *) "search"; + cmd_argv[2]= (char *) "--names-only"; + cmd_argv[3]= (char *) "^" PLUGIN_PREFIX; + cmd_argv[4]= 0; + if ((error= run_argv_capture(cmd_argv, &search_output, 0))) + return error; + parse_apt_records(search_output.str); + + /* + dpkg-query prints "no packages found" on stderr and exits nonzero + when nothing is installed, which is an answer here, not an error. + */ + cmd_argv[0]= (char *) "dpkg-query"; + cmd_argv[1]= (char *) "-W"; + cmd_argv[2]= (char *) "-f=${Package} ${db:Status-Status}\n"; + cmd_argv[3]= (char *) PLUGIN_PREFIX "*"; + cmd_argv[4]= 0; + if (run_argv_capture(cmd_argv, &search_output, 1)) + return 0; + for (line= search_output.str; line && *line; line= next) + { + if ((next= strchr(line, '\n'))) + *next++= '\0'; + if (!(sep= strchr(line, ' '))) + continue; + *sep++= '\0'; + if (strcmp(sep, "installed") == 0 && + strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) == 0 && + (e= add_plugin_entry(line))) + e->installed= 1; + } + return 0; +} + +#endif /* INSTALL_LAYOUT_DEB */ + +#endif /* PKG_DELEGATION */ + + +/** + Search for plugins. + + On rpm and deb installations the distribution's package index is the + plugin metadata: it is queried for everything providing mariadb-plugin-* + and the result is shown uniformly as plugin names, never as the + distribution's own package names. Needs no root. + + @param[in] term Substring to match, empty to list all plugins. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error or no matches = 1, matches printed = 0 +*/ + +static int do_search(const char *term, const char *basedir) +{ +#ifdef PKG_DELEGATION + int error; + + if (my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &plugin_list, + sizeof(struct plugin_entry), 32, 32, MYF(MY_WME))) + return 1; + if (init_dynamic_string(&search_output, "", 16 * 1024, 16 * 1024)) + { + delete_dynamic(&plugin_list); + return 1; + } +#ifdef INSTALL_LAYOUT_DEB + error= search_apt(); +#else + { + const char *pm= get_package_manager(); + error= pm ? (strcmp(pm, "dnf") == 0 ? search_dnf() : search_zypper()) : 1; + } +#endif + if (!error) + error= print_search_results(term); + dynstr_free(&search_output); + delete_dynamic(&plugin_list); + return error ? 1 : 0; +#else + printf("search: not available for %s installations yet, the plugin " + "index does not exist\n", INSTALL_METHOD_NAME); + return 1; +#endif +} + + +/** + Install a plugin. + + On rpm and deb installations the work is delegated to the system package + manager, which resolves the uniform package name through its own real + package names (via Provides on rpm). Its exit code is passed through. + + @param[in] name The normalized plugin name. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error = nonzero, success = 0 +*/ + +static int do_install(const char *name, const char *basedir) +{ +#ifdef PKG_DELEGATION + char package[PACKAGE_NAME_SIZE]; + const char *pm; + char *cmd_argv[4]; + + if (check_root("install")) + return 1; + if (!(pm= get_package_manager())) + return 1; + + build_package_name(package, sizeof(package), name); + cmd_argv[0]= (char *) pm; + cmd_argv[1]= (char *) "install"; + cmd_argv[2]= package; + cmd_argv[3]= 0; + return run_argv(cmd_argv); +#else + printf("install: '%s' (%s installation%s%s) not implemented yet\n", name, + INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + return 0; +#endif +} + + +/** + Uninstall a plugin. + + On deb installations the packages carry the uniform name, so it is passed + to apt-get directly. On rpm installations the uniform name is only a + Provides alias of the real package name, and dnf 5 does not resolve + "remove" arguments through Provides (dnf 4 and zypper do), so the alias + is first translated by querying the rpm database. This also gives a + clear error when the plugin is not installed. + + @param[in] name The normalized plugin name. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error = nonzero, success = 0 +*/ + +static int do_uninstall(const char *name, const char *basedir) +{ +#ifdef PKG_DELEGATION + char package[PACKAGE_NAME_SIZE]; + const char *pm; + const char *target; + char *cmd_argv[7]; + int error; +#ifdef INSTALL_LAYOUT_RPM + DYNAMIC_STRING providers; + char *nl; +#endif + + if (check_root("uninstall")) + return 1; + if (!(pm= get_package_manager())) + return 1; + + build_package_name(package, sizeof(package), name); + target= package; + +#ifdef INSTALL_LAYOUT_RPM + if (init_dynamic_string(&providers, "", 256, 256)) + return 1; + cmd_argv[0]= (char *) "rpm"; + cmd_argv[1]= (char *) "-q"; + cmd_argv[2]= (char *) "--whatprovides"; + cmd_argv[3]= package; + cmd_argv[4]= (char *) "--qf"; + cmd_argv[5]= (char *) "%{NAME}\n"; + cmd_argv[6]= 0; + if (run_argv_capture(cmd_argv, &providers, 0) || !providers.length) + { + fprintf(stderr, "ERROR: plugin '%s' is not installed.\n", name); + dynstr_free(&providers); + return 1; + } + if (!(nl= strchr(providers.str, '\n'))) + nl= strend(providers.str); + if (nl[0] && nl[1]) + { + fprintf(stderr, "ERROR: several packages provide '%s':\n%s" + "Remove the right one with the package manager directly.\n", + package, providers.str); + dynstr_free(&providers); + return 1; + } + *nl= '\0'; + target= providers.str; +#endif + + cmd_argv[0]= (char *) pm; + cmd_argv[1]= (char *) "remove"; + cmd_argv[2]= (char *) target; + cmd_argv[3]= 0; + error= run_argv(cmd_argv); +#ifdef INSTALL_LAYOUT_RPM + dynstr_free(&providers); +#endif + return error; +#else + printf("uninstall: '%s' (%s installation%s%s) not implemented yet\n", name, + INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + return 0; +#endif +} + + +/** + Run the new package-manager style commands. + + Parses the options (--help, --version, etc. are handled by + handle_options), then validates the verb and the plugin name and + dispatches to the appropriate command handler. The plugin name is + normalized to lower case before validation. + + @param[in] argc The number of arguments. + @param[in] argv The arguments. + + @retval int error = 1, success = 0 +*/ + +static int run_new_command(int argc, char **argv) +{ + char name[NAME_CHAR_LEN + 1]; + char basedir[FN_REFLEN]; + const char *verb; + size_t i, len; + int error, is_search; + + if ((error= handle_options(&argc, &argv, my_long_options, get_one_option))) + return 1; + + if (argc < 1) + { + usage(); + return 1; + } + + verb= argv[0]; + if (strcmp(verb, "search") != 0 && strcmp(verb, "install") != 0 && + strcmp(verb, "uninstall") != 0) + { + fprintf(stderr, "ERROR: unknown command '%s'.\n", verb); + usage(); + return 1; + } + + /* the search term is optional: without it every plugin is listed */ + is_search= strcmp(verb, "search") == 0; + if (is_search ? argc > 2 : argc != 2) + { + fprintf(stderr, is_search ? + "ERROR: '%s' takes at most one search term.\n" : + "ERROR: '%s' requires exactly one plugin name.\n", verb); + usage(); + return 1; + } + + name[0]= '\0'; + if (argc == 2) + { + len= strlen(argv[1]); + if (len > NAME_CHAR_LEN) + { + fprintf(stderr, "ERROR: plugin name is too long (max %d characters).\n", + NAME_CHAR_LEN); + return 1; + } + for (i= 0; i <= len; i++) + name[i]= (char) tolower((unsigned char) argv[1][i]); + + if (validate_plugin_name(name)) + return 1; + } + + if (detect_install_method(basedir, sizeof(basedir))) + return 1; + + if (is_search) + return do_search(name, basedir); + if (strcmp(verb, "install") == 0) + return do_install(name, basedir); + return do_uninstall(name, basedir); +} diff --git a/cmake/plugin.cmake b/cmake/plugin.cmake index 07c9220525558..a59fadcf4dfef 100644 --- a/cmake/plugin.cmake +++ b/cmake/plugin.cmake @@ -278,6 +278,13 @@ MACRO(MYSQL_ADD_PLUGIN) IF (NOT ARG_CLIENT) SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_REQUIRES "MariaDB-server${ver}" PARENT_SCOPE) ENDIF() + + # rpm packages have their own names, but plugins are installed + # everywhere by the deb-style name, mariadb-plugin- + STRING(REGEX REPLACE "-engine(-|$)" "\\1" plugin_package "${ARG_COMPONENT}") + SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_PROVIDES + "mariadb-plugin-${plugin_package}" PARENT_SCOPE) + SET(CPACK_RPM_${ARG_COMPONENT}_USER_FILELIST ${ignored} PARENT_SCOPE) IF (ARG_VERSION) SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_VERSION ${SERVER_VERSION}_${ARG_VERSION} PARENT_SCOPE) diff --git a/mysql-test/main/mariadb-plugin.result b/mysql-test/main/mariadb-plugin.result new file mode 100644 index 0000000000000..1f4db80f195ae --- /dev/null +++ b/mysql-test/main/mariadb-plugin.result @@ -0,0 +1,38 @@ +# +# Unknown command +# +ERROR: unknown command 'nosuchcommand'. +# +# A plugin name is required, and only one +# +ERROR: 'install' requires exactly one plugin name. +ERROR: 'uninstall' requires exactly one plugin name. +ERROR: 'install' requires exactly one plugin name. +# +# search takes an optional term, but not more than one +# +ERROR: 'search' takes at most one search term. +# +# Plugin names are validated, as they become package names +# +ERROR: invalid character ';' in plugin name. Use only [a-z0-9_-]. +ERROR: invalid character ' ' in plugin name. Use only [a-z0-9_-]. +ERROR: invalid character '.' in plugin name. Use only [a-z0-9_-]. +ERROR: plugin name cannot be empty. +ERROR: plugin name is too long (max 64 characters). +# +# A command word wins over the deprecated keywords, so that a plugin +# can be searched for by a name containing them +# +ERROR: 'search' takes at most one search term. +ERROR: 'install' requires exactly one plugin name. +# +# --dry-run is accepted as an option +# +# +# The deprecated syntax still reaches the old code path, which +# asks for its own options +# +ERROR: Missing --basedir option. +ERROR: Missing --basedir option. +ERROR: Missing --basedir option. diff --git a/mysql-test/main/mariadb-plugin.test b/mysql-test/main/mariadb-plugin.test new file mode 100644 index 0000000000000..2e785ca5162f5 --- /dev/null +++ b/mysql-test/main/mariadb-plugin.test @@ -0,0 +1,77 @@ +# +# MDEV-40786 mariadb-plugin as a plugin package manager. +# +# Only the command line handling is tested here, as it is the same for +# every installation method. Installing, removing and searching for +# plugins is not testable in mtr: the tool acts only when it runs from +# the location it was installed to, never from a build tree, so those +# commands are covered by the install and upgrade builders instead. +# +# Errors are printed on stderr, the usage text on stdout, so only stderr +# is kept below to leave paths and version strings out of the result. +# + +--echo # +--echo # Unknown command +--echo # +--error 1 +--exec $MYSQL_PLUGIN nosuchcommand foo 2>&1 > /dev/null + +--echo # +--echo # A plugin name is required, and only one +--echo # +--error 1 +--exec $MYSQL_PLUGIN install 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN uninstall 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install foo bar 2>&1 > /dev/null + +--echo # +--echo # search takes an optional term, but not more than one +--echo # +--error 1 +--exec $MYSQL_PLUGIN search foo bar 2>&1 > /dev/null + +--echo # +--echo # Plugin names are validated, as they become package names +--echo # +--error 1 +--exec $MYSQL_PLUGIN install "a;b" 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install "a b" 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install "../etc" 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install "" 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2>&1 > /dev/null + +--echo # +--echo # A command word wins over the deprecated keywords, so that a plugin +--echo # can be searched for by a name containing them +--echo # +--error 1 +--exec $MYSQL_PLUGIN search enable extra 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN install disable extra 2>&1 > /dev/null + +--echo # +--echo # --dry-run is accepted as an option +--echo # +--exec $MYSQL_PLUGIN --dry-run --help > /dev/null 2>&1 + +# +# --no-defaults keeps my_print_defaults out of it: it is looked up in +# hardcoded system paths, so whether it is found depends on the machine. +# +--echo # +--echo # The deprecated syntax still reaches the old code path, which +--echo # asks for its own options +--echo # +--error 1 +--exec $MYSQL_PLUGIN --no-defaults foo ENABLE 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN --no-defaults foo DISABLE 2>&1 > /dev/null +--error 1 +--exec $MYSQL_PLUGIN --no-defaults foo enable 2>&1 > /dev/null From a20f13e89c2c19d7f7d6f29ff8c3b7fefd7010fa Mon Sep 17 00:00:00 2001 From: Mohamed Date: Thu, 3 Sep 2026 11:43:21 +0300 Subject: [PATCH 2/2] MDEV-40786 Tarball phase Uninstall removes a plugin from a tarball installation using its manifest, /.mariadb-plugin/.list: remove the listed files and empty recorded directories, then the manifest itself. Paths are validated before deletion, and failed file removals keep the manifest so uninstall can be retried. Install reads the plugin tarball itself and checks entries before extraction: links, devices, absolute paths and ".." are refused, setuid bits dropped, existing files never overwritten, and the CPack top directory stripped when it is named like the archive. Files are recorded in the manifest for rollback and uninstall. --file=PATH supplies a local tarball and --sha256=HEX verifies it. Installation prints activation instructions without editing server configuration. Add index-based downloads with system libcurl and --base-url. Verify downloaded archives against the index's SHA-256. Keep local and downloaded archives in private temporary files for verification and extraction. Resolve file operations through checked parent directories, fix command routing, reject legacy --dry-run, and propagate native search errors. Improve libcurl build integration. Add tarball search using the same repository index and compatibility checks as install. List matching plugin names with descriptions and installed status from local manifests, without downloading archives or changing the installation. Retain author and license metadata. Reject invalid or ambiguous metadata before printing search results. Validate manifest identity before uninstall and reject non-regular manifests without blocking, while preserving partial-install rollback. Tests cover tarball search filtering, compatible-build selection, metadata validation, installed status, manifest safety, rollback, metadata-only network access and CLI regressions. --- client/CMakeLists.txt | 6 +- client/mysql_plugin.c | 2224 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 2024 insertions(+), 206 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index db6fa588f5473..93916d5f870b3 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -75,7 +75,11 @@ MYSQL_ADD_EXECUTABLE(mariadb-show mysqlshow.c) TARGET_LINK_LIBRARIES(mariadb-show ${CLIENT_LIB}) MYSQL_ADD_EXECUTABLE(mariadb-plugin mysql_plugin.c) -TARGET_LINK_LIBRARIES(mariadb-plugin ${CLIENT_LIB}) +TARGET_LINK_LIBRARIES(mariadb-plugin ${CLIENT_LIB} mysys_ssl) +IF(NOT INSTALL_LAYOUT MATCHES "^(RPM|DEB)$") + FIND_PACKAGE(CURL REQUIRED) + TARGET_LINK_LIBRARIES(mariadb-plugin CURL::libcurl) +ENDIF() # The tool needs to know how MariaDB was installed to pick the way plugins # are installed. The install layout is only known when building, so it is # passed to the tool, together with the directories it has to verify. diff --git a/client/mysql_plugin.c b/client/mysql_plugin.c index 1ec342c5641d9..ee5d2a34819b5 100644 --- a/client/mysql_plugin.c +++ b/client/mysql_plugin.c @@ -48,12 +48,25 @@ #if defined(INSTALL_LAYOUT_RPM) || defined(INSTALL_LAYOUT_DEB) #define PKG_DELEGATION 1 #include +#elif defined(_WIN32) +#include #endif +#ifndef PKG_DELEGATION +#include +#include +#include +#endif + +#define KV_LINE_SIZE 1024 + /* Global variables. */ static uint my_end_arg= 0; static uint opt_verbose=0; static my_bool opt_dry_run= 0; +static char *opt_file= 0; +static char *opt_sha256= 0; +static char *opt_base_url= 0; static uint opt_no_defaults= 0; static uint opt_print_defaults= 0; static char *opt_datadir=0, *opt_basedir=0, @@ -88,6 +101,15 @@ static struct my_option my_long_options[] = {"dry-run", 0, "Print the commands that install and uninstall would run, " "without running them.", &opt_dry_run, &opt_dry_run, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"file", 0, "Install the plugin from this local tarball instead of " + "downloading it.", + &opt_file, &opt_file, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"base-url", 0, "Plugin repository URL. Overrides the default download " + "location for tarball installations.", + &opt_base_url, &opt_base_url, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + {"sha256", 0, "Expected SHA-256 checksum of the tarball, as published " + "beside it. Refuse to install if it does not match.", + &opt_sha256, &opt_sha256, 0, GET_STR, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, {"no-defaults", 'n', "Do not read values from configuration file.", 0, 0, 0, GET_NO_ARG, NO_ARG, 0, 0, 0, 0, 0, 0}, {"print-defaults", 'P', "Show default values from configuration file.", @@ -122,9 +144,14 @@ static int run_new_command(int argc, char **argv); static int validate_plugin_name(const char *name); static int is_legacy_syntax(int argc, char **argv); static int detect_install_method(char *basedir, size_t basedir_size); -static int do_search(const char *name, const char *basedir); +static int do_search(const char *term, const char *basedir); +#ifndef PKG_DELEGATION +static int search_tarball(const char *basedir); +#endif static int do_install(const char *name, const char *basedir); static int do_uninstall(const char *name, const char *basedir); +static my_bool get_one_option(const struct my_option *, const char *, + const char *); int main(int argc,char *argv[]) @@ -138,13 +165,12 @@ int main(int argc,char *argv[]) sf_leaking_memory=1; /* don't report memory leaks on early exits */ plugin_data.name= 0; /* initialize name */ - /* - The new package-manager style commands (search|install|uninstall) are - handled in run_new_command(). The legacy " ENABLE|DISABLE" - syntax is recognized by scanning the raw arguments, before any option - parsing, and continues through the original code path below unchanged, - for backward compatibility. - */ + /* Route only positional arguments, never values belonging to options. */ + if (handle_options(&argc, &argv, my_long_options, get_one_option)) + { + my_end(my_end_arg); + return 1; + } if (!is_legacy_syntax(argc, argv)) { error= run_new_command(argc, argv); @@ -152,22 +178,17 @@ int main(int argc,char *argv[]) exit(error); } - /* - The following operations comprise the method for enabling or disabling - a plugin. We begin by processing the command options then check the - directories specified for --datadir, --basedir, --plugin-dir, and - --plugin-ini (if specified). If the directories are Ok, we then look - for the mysqld executable and the plugin soname. Finally, we build a - bootstrap command file for use in bootstraping the server. - - If any step fails, the method issues an error message and the tool exits. - - 1) Parse, execute, and verify command options. - 2) Check access to directories. - 3) Look for mysqld executable. - 4) Look for the plugin. - 5) Build a bootstrap file with commands to enable or disable plugin. + if (opt_dry_run) + { + fprintf(stderr, "ERROR: --dry-run is not supported with ENABLE/DISABLE.\n"); + my_end(my_end_arg); + return 1; + } + /* + Parse and validate legacy options, check that configured paths exist, + locate mysqld and the plugin library, then write the bootstrap SQL. + Stop if any step fails. */ if ((error= process_options(argc, argv, operation)) || (error= check_access()) || @@ -595,7 +616,8 @@ get_one_option(const struct my_option *opt, @param[in] filename File to locate. - @retval int file not found = 1, file found = 0 + @retval 1 The path could be stat'ed. + @retval 0 The stat call failed. */ static int file_exists(char * filename) @@ -898,27 +920,20 @@ static int check_options(int argc, char **argv, char *operation) /** - Parse, execute, and verify command options. - - This method handles all of the option processing including the optional - features for displaying data (--print-defaults, --help ,etc.) that do not - result in an attempt to ENABLE or DISABLE of a plugin. + Read defaults unless disabled and validate the already-parsed legacy inputs. + Handle informational options without enabling or disabling a plugin. - @param[in] arc Count of arguments + @param[in] argc Count of arguments @param[in] argv Array of arguments @param[out] operation Operation (ENABLE or DISABLE) - @retval int error = 1, success = 0, exit program = -1 + @return 0 on success, an option error code, or -1 to stop processing. */ static int process_options(int argc, char *argv[], char *operation) { int error= 0; - /* Parse and execute command-line options */ - if ((error= handle_options(&argc, &argv, my_long_options, get_one_option))) - return error; - /* If the print defaults option used, exit. */ if (opt_print_defaults) return -1; @@ -1299,8 +1314,7 @@ static int bootstrap_server(char *server_path, char *bootstrap_file) /** Detect the legacy " ENABLE|DISABLE" command line syntax. - The check is done on the raw arguments, before any option parsing, so - that legacy invocations take the original code path unchanged. + Options have already been parsed; only positional arguments remain. @param[in] argc The number of arguments. @param[in] argv The arguments. @@ -1312,7 +1326,7 @@ static int is_legacy_syntax(int argc, char **argv) { int i; - for (i= 1; i < argc; i++) + for (i= 0; i < argc; i++) { /* Whichever keyword comes first decides, so that "search enable" is a @@ -1335,7 +1349,7 @@ static int is_legacy_syntax(int argc, char **argv) Check that a plugin name contains only safe characters. The name is later used to construct package names and file paths, so - only lower case alphanumerics, '_' and '-' are accepted. The name is + only lower case ASCII letters, digits, '_' and '-' are accepted. The name is expected to be normalized to lower case before this check. @param[in] name The normalized plugin name. @@ -1354,7 +1368,8 @@ static int validate_plugin_name(const char *name) } for (p= name; *p; p++) { - if (!isalnum((unsigned char) *p) && *p != '_' && *p != '-') + if (!(*p >= 'a' && *p <= 'z') && !(*p >= '0' && *p <= '9') && + *p != '_' && *p != '-') { fprintf(stderr, "ERROR: invalid character '%c' in plugin name. " "Use only [a-z0-9_-].\n", *p); @@ -1365,7 +1380,7 @@ static int validate_plugin_name(const char *name) } /** - Verify that the tool is part of the installation it was built for. + Check the argv[0]-derived location against the compiled install layout. The installation method is known at build time, so only the location has to be checked. It is taken from argv[0] and not from the server, as one @@ -1393,9 +1408,14 @@ static int detect_install_method(char *basedir, size_t basedir_size) old mysql_plugin name, does not hide where the tool is installed. */ my_path(self_path, my_progname, ""); - safe_strcat(self_path, sizeof(self_path), base_name(my_progname)); - if (my_realpath(real_path, self_path, MYF(0))) - safe_strcpy(real_path, sizeof(real_path), self_path); + if (!self_path[0] || + safe_strcat(self_path, sizeof(self_path), base_name(my_progname)) || + my_realpath(real_path, self_path, MYF(0))) + { + fprintf(stderr, "ERROR: cannot resolve the location of '%s'.\n", + my_progname); + return 1; + } dirname_part(real_dir, real_path, &length); @@ -1441,6 +1461,109 @@ static int detect_install_method(char *basedir, size_t basedir_size) } +/* + Search results use bare plugin names for native and tarball repositories. + Native package names are retained separately for installed-state queries. +*/ + +struct plugin_entry +{ + char name[NAME_CHAR_LEN + 1]; /* uniform name, prefix stripped */ + char package[NAME_CHAR_LEN + 1]; /* real package name, rpm only */ + char description[KV_LINE_SIZE]; + int installed; +}; + +static DYNAMIC_ARRAY plugin_list; + +#define PLUGIN_AT(i) (dynamic_element(&plugin_list, (i), struct plugin_entry *)) + + +static struct plugin_entry *find_plugin_entry(const char *name) +{ + size_t i; + + for (i= 0; i < plugin_list.elements; i++) + if (strcmp(PLUGIN_AT(i)->name, name) == 0) + return PLUGIN_AT(i); + return NULL; +} + + +/** + Add a plugin to the result list, or return the existing entry with the + same normalized name. + + The list reallocates, so the entry is only valid until the next one. + + @param[in] name The normalized plugin name, without the package prefix. + + @retval struct plugin_entry* the entry, NULL when out of memory +*/ + +static struct plugin_entry *add_plugin_entry(const char *name) +{ + struct plugin_entry e, *found; + + if ((found= find_plugin_entry(name))) + return found; + bzero(&e, sizeof(e)); + safe_strcpy(e.name, sizeof(e.name), name); + if (insert_dynamic(&plugin_list, &e)) + return NULL; + return PLUGIN_AT(plugin_list.elements - 1); +} + + +static int cmp_plugin_entries(const void *a, const void *b) +{ + return strcmp(((const struct plugin_entry *) a)->name, + ((const struct plugin_entry *) b)->name); +} + + +/** + Print the collected plugins that match the search term. + + @param[in] term Substring to match against plugin names, "" for all. + + @retval int no matches = 1, matches printed = 0 +*/ + +static int print_search_results(const char *term) +{ + size_t i, width= 0, matches= 0; + + sort_dynamic(&plugin_list, cmp_plugin_entries); + for (i= 0; i < plugin_list.elements; i++) + { + if (*term && !strstr(PLUGIN_AT(i)->name, term)) + continue; + matches++; + if (strlen(PLUGIN_AT(i)->name) > width) + width= strlen(PLUGIN_AT(i)->name); + } + if (!matches) + { + if (*term) + printf("No plugins matching '%s' found.\n", term); + else + printf("No plugins found.\n"); + return 1; + } + for (i= 0; i < plugin_list.elements; i++) + { + struct plugin_entry *e= PLUGIN_AT(i); + + if (*term && !strstr(e->name, term)) + continue; + printf("%-*s %-9s %s\n", (int) width, e->name, + e->installed ? "installed" : "available", e->description); + } + return 0; +} + + #ifdef PKG_DELEGATION /** @@ -1459,7 +1582,6 @@ static const char *get_package_manager(void) return "apt-get"; #else char dir[FN_REFLEN]; - if (find_file_in_path(dir, "dnf")) return "dnf"; if (find_file_in_path(dir, "zypper")) @@ -1472,10 +1594,7 @@ static const char *get_package_manager(void) /** - Refuse to continue without root privileges. - - The package manager would fail anyway, but only after a repository - refresh, with an error that does not mention this tool. + Require root for native package changes, but allow non-root dry-runs. @param[in] verb The command name, for the error message. @@ -1497,8 +1616,8 @@ static int check_root(const char *verb) /** Run a command and wait for it to finish. - The command is executed directly, not through a shell, so the arguments - cannot be reinterpreted. The child inherits the standard streams: the + Execute directly without shell expansion. The package manager still + interprets its own arguments. The child inherits the standard streams: the package manager talks to the user directly, including its own confirmation prompts and progress output. @@ -1601,6 +1720,9 @@ static int run_argv_capture(char **cmd_argv, DYNAMIC_STRING *out, if (pid == 0) { dup2(fds[1], STDOUT_FILENO); + /* Metadata parsers consume stable field labels, not translated output. */ + if (setenv("LC_ALL", "C", 1)) + _exit(127); if (quiet_stderr) { int devnull= open("/dev/null", O_WRONLY); @@ -1643,6 +1765,11 @@ static int run_argv_capture(char **cmd_argv, DYNAMIC_STRING *out, cmd_argv[0]); return 127; } + if (n < 0) + { + fprintf(stderr, "ERROR: cannot read the output of '%s'.\n", cmd_argv[0]); + return 127; + } if (WIFSIGNALED(status)) return 127; return WEXITSTATUS(status); @@ -1655,7 +1782,7 @@ static int run_argv_capture(char **cmd_argv, DYNAMIC_STRING *out, /** - Build the distribution-independent package name (D2): mariadb-plugin- + Build the distribution-independent package name: mariadb-plugin- followed by the plugin name, which is already validated and lowercased. @param[out] to Buffer for the package name. @@ -1670,113 +1797,8 @@ static void build_package_name(char *to, size_t size, const char *name) } -/* - Search: every backend answers the same two questions - which packages - provide mariadb-plugin-* and which of them are installed - through a - different query. The results are normalized into plugin_list and - printed in one format, so the user sees plugin names as install expects - them, never the distribution's own package names. -*/ - -struct plugin_entry -{ - char name[NAME_CHAR_LEN + 1]; /* uniform name, prefix stripped */ - char package[NAME_CHAR_LEN + 1]; /* real package name, rpm only */ - char description[160]; - int installed; -}; - -static DYNAMIC_ARRAY plugin_list; static DYNAMIC_STRING search_output; -#define PLUGIN_AT(i) (dynamic_element(&plugin_list, (i), struct plugin_entry *)) - - -static struct plugin_entry *find_plugin_entry(const char *name) -{ - size_t i; - - for (i= 0; i < plugin_list.elements; i++) - if (strcmp(PLUGIN_AT(i)->name, name) == 0) - return PLUGIN_AT(i); - return NULL; -} - - -/** - Add a plugin to the result list, or return the existing entry with the - same uniform name. The prefix is stripped from the stored name. - - The list reallocates, so the entry is only valid until the next one. - - @param[in] package_name The uniform package name, mariadb-plugin-x. - - @retval struct plugin_entry* the entry, NULL when out of memory -*/ - -static struct plugin_entry *add_plugin_entry(const char *package_name) -{ - struct plugin_entry e, *found; - const char *name= package_name + PLUGIN_PREFIX_LEN; - - if ((found= find_plugin_entry(name))) - return found; - bzero(&e, sizeof(e)); - safe_strcpy(e.name, sizeof(e.name), name); - if (insert_dynamic(&plugin_list, &e)) - return NULL; - return PLUGIN_AT(plugin_list.elements - 1); -} - - -static int cmp_plugin_entries(const void *a, const void *b) -{ - return strcmp(((const struct plugin_entry *) a)->name, - ((const struct plugin_entry *) b)->name); -} - - -/** - Print the collected plugins that match the search term. - - @param[in] term Substring to match against plugin names, "" for all. - - @retval int no matches = 1, matches printed = 0 -*/ - -static int print_search_results(const char *term) -{ - size_t i, width= 0, matches= 0; - - sort_dynamic(&plugin_list, cmp_plugin_entries); - for (i= 0; i < plugin_list.elements; i++) - { - if (*term && !strstr(PLUGIN_AT(i)->name, term)) - continue; - matches++; - if (strlen(PLUGIN_AT(i)->name) > width) - width= strlen(PLUGIN_AT(i)->name); - } - if (!matches) - { - if (*term) - printf("No plugins matching '%s' found.\n", term); - else - printf("No plugins found.\n"); - return 1; - } - for (i= 0; i < plugin_list.elements; i++) - { - struct plugin_entry *e= PLUGIN_AT(i); - - if (*term && !strstr(e->name, term)) - continue; - printf("%-*s %-9s %s\n", (int) width, e->name, - e->installed ? "installed" : "available", e->description); - } - return 0; -} - #ifndef INSTALL_LAYOUT_DEB @@ -1802,11 +1824,11 @@ static struct plugin_entry *find_plugin_by_package(const char *package) @param[in] installed Mark the found plugins as installed. */ -static void parse_dnf_records(char *output, int installed) +static int parse_dnf_records(char *output, int installed) { struct plugin_entry *e= NULL; char *line, *next, *sep; - char package[NAME_CHAR_LEN + 1], summary[160]; + char package[NAME_CHAR_LEN + 1], summary[KV_LINE_SIZE]; package[0]= summary[0]= '\0'; for (line= output; line && *line; line= next) @@ -1828,14 +1850,15 @@ static void parse_dnf_records(char *output, int installed) if (strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) != 0 || !package[0]) continue; - if (!(e= add_plugin_entry(line))) - return; + if (!(e= add_plugin_entry(line + PLUGIN_PREFIX_LEN))) + return 1; safe_strcpy(e->package, sizeof(e->package), package); if (!e->description[0]) safe_strcpy(e->description, sizeof(e->description), summary); if (installed) e->installed= 1; } + return 0; } @@ -1854,12 +1877,13 @@ static int search_dnf(void) if ((error= run_argv_capture(repo_argv, &search_output, 0))) return error; - parse_dnf_records(search_output.str, 0); + if (parse_dnf_records(search_output.str, 0)) + return 1; /* same query against the installed packages only, for the status */ - if (run_argv_capture(inst_argv, &search_output, 0) == 0) - parse_dnf_records(search_output.str, 1); - return 0; + if ((error= run_argv_capture(inst_argv, &search_output, 0))) + return error; + return parse_dnf_records(search_output.str, 1); } @@ -1872,7 +1896,7 @@ static int search_dnf(void) @param[in] output The captured zypper output, modified in place. */ -static void parse_zypper_solvables(char *output) +static int parse_zypper_solvables(char *output) { struct plugin_entry e; char *line, *next, *val, *end; @@ -1902,8 +1926,9 @@ static void parse_zypper_solvables(char *output) safe_strcpy(e.description, sizeof(e.description), val); } if (insert_dynamic(&plugin_list, &e)) - return; + return 1; } + return 0; } @@ -1987,7 +2012,8 @@ static int search_zypper(void) error= run_argv_capture(cmd_argv, &search_output, 0); if (error && error != 104) return error; - parse_zypper_solvables(search_output.str); + if (parse_zypper_solvables(search_output.str)) + return 1; if (plugin_list.elements && search_zypper_names()) return 1; return 0; @@ -2002,7 +2028,7 @@ static int search_zypper(void) @param[in] output The captured apt-cache output, modified in place. */ -static void parse_apt_records(char *output) +static int parse_apt_records(char *output) { struct plugin_entry *e; char *line, *next, *sep; @@ -2015,11 +2041,12 @@ static void parse_apt_records(char *output) *sep= '\0'; if (strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) != 0) continue; - if (!(e= add_plugin_entry(line))) - return; + if (!(e= add_plugin_entry(line + PLUGIN_PREFIX_LEN))) + return 1; if (sep && !e->description[0]) safe_strcpy(e->description, sizeof(e->description), sep + 3); } + return 0; } @@ -2037,19 +2064,21 @@ static int search_apt(void) cmd_argv[4]= 0; if ((error= run_argv_capture(cmd_argv, &search_output, 0))) return error; - parse_apt_records(search_output.str); + if (parse_apt_records(search_output.str)) + return 1; /* - dpkg-query prints "no packages found" on stderr and exits nonzero - when nothing is installed, which is an answer here, not an error. + dpkg-query exits 1 with no output when the pattern matches no packages. + Execution/capture failures and dpkg operational errors must propagate. */ cmd_argv[0]= (char *) "dpkg-query"; cmd_argv[1]= (char *) "-W"; cmd_argv[2]= (char *) "-f=${Package} ${db:Status-Status}\n"; cmd_argv[3]= (char *) PLUGIN_PREFIX "*"; cmd_argv[4]= 0; - if (run_argv_capture(cmd_argv, &search_output, 1)) - return 0; + error= run_argv_capture(cmd_argv, &search_output, 1); + if (error) + return error == 1 && !search_output.length ? 0 : error; for (line= search_output.str; line && *line; line= next) { if ((next= strchr(line, '\n'))) @@ -2058,9 +2087,12 @@ static int search_apt(void) continue; *sep++= '\0'; if (strcmp(sep, "installed") == 0 && - strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) == 0 && - (e= add_plugin_entry(line))) + strncmp(line, PLUGIN_PREFIX, PLUGIN_PREFIX_LEN) == 0) + { + if (!(e= add_plugin_entry(line + PLUGIN_PREFIX_LEN))) + return 1; e->installed= 1; + } } return 0; } @@ -2073,25 +2105,23 @@ static int search_apt(void) /** Search for plugins. - On rpm and deb installations the distribution's package index is the - plugin metadata: it is queried for everything providing mariadb-plugin-* - and the result is shown uniformly as plugin names, never as the - distribution's own package names. Needs no root. + RPM searches provided capabilities; DEB searches package names with the + mariadb-plugin- prefix. Tarball builds read the repository index and + local manifests. Needs no root. @param[in] term Substring to match, empty to list all plugins. @param[in] basedir The base directory, empty for packaged installations. - @retval int error or no matches = 1, matches printed = 0 */ static int do_search(const char *term, const char *basedir) { -#ifdef PKG_DELEGATION int error; if (my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &plugin_list, sizeof(struct plugin_entry), 32, 32, MYF(MY_WME))) return 1; +#ifdef PKG_DELEGATION if (init_dynamic_string(&search_output, "", 16 * 1024, 16 * 1024)) { delete_dynamic(&plugin_list); @@ -2104,26 +2134,1593 @@ static int do_search(const char *term, const char *basedir) const char *pm= get_package_manager(); error= pm ? (strcmp(pm, "dnf") == 0 ? search_dnf() : search_zypper()) : 1; } +#endif + dynstr_free(&search_output); +#else + error= search_tarball(basedir); #endif if (!error) error= print_search_results(term); - dynstr_free(&search_output); + else + fprintf(stderr, "ERROR: could not determine plugin availability or " + "installed state.\n"); delete_dynamic(&plugin_list); return error ? 1 : 0; -#else - printf("search: not available for %s installations yet, the plugin " - "index does not exist\n", INSTALL_METHOD_NAME); - return 1; -#endif } -/** - Install a plugin. +#ifndef PKG_DELEGATION - On rpm and deb installations the work is delegated to the system package - manager, which resolves the uniform package name through its own real - package names (via Provides on rpm). Its exit code is passed through. +/* + On tarball installations nothing tracks what a plugin put on disk, so + install writes a manifest and uninstall acts strictly on it: the header + lines describe the plugin, each "file:" or "dir:" line is one path, + relative to the basedir, that install created and uninstall removes. +*/ + +#define MANIFEST_SUBDIR ".mariadb-plugin" +#define PLUGIN_BASE_URL "" +#define PLUGIN_INDEX "plugins.index" + +struct manifest_entry +{ + char path[FN_REFLEN]; + my_bool is_dir; +}; + +enum plugin_file_operation {PLUGIN_OPEN, PLUGIN_DELETE, PLUGIN_MKDIR, + PLUGIN_RMDIR, PLUGIN_CHECK_DIR}; + +static int valid_relative_path(const char *path); + + +static int build_full_path(char *to, size_t size, const char *basedir, + const char *rel) +{ + if (safe_strcpy_truncated(to, size, basedir) || + safe_strcat(to, size, "/") || safe_strcat(to, size, rel)) + { + fprintf(stderr, "ERROR: path is too long: '%s/%s'.\n", basedir, rel); + return 1; + } + return 0; +} + + +/* Resolve parents without following links. The basedir itself is trusted. */ +static int plugin_file_op(const char *basedir, const char *rel, + enum plugin_file_operation op, int flags) +{ + char full[FN_REFLEN]; + int result= -1, saved_errno; +#ifdef _WIN32 + HANDLE parents[FN_REFLEN / 2 + 1]; + size_t count= 0; + char *p; +#else + int parent= -1, next; + const char *part= rel, *slash; + char component[FN_REFLEN]; + size_t len; +#endif + + if (!valid_relative_path(rel) || + build_full_path(full, sizeof(full), basedir, rel)) + { + errno= my_errno= EINVAL; + return -1; + } +#ifdef _WIN32 + /* Deny writes and renames to parent directories during the operation. */ + for (p= full + strlen(basedir); ; p++) + { + char end= *p; + BY_HANDLE_FILE_INFORMATION info; + HANDLE handle; + if (end != '/' && end != '\0') + continue; + if (!end && op != PLUGIN_CHECK_DIR && + (op != PLUGIN_OPEN || (flags & O_CREAT))) + break; + *p= '\0'; + handle= CreateFile(full, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, + NULL, OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + *p= end; + if (handle == INVALID_HANDLE_VALUE) + { + my_osmaperr(GetLastError()); + goto end; + } + if (count == array_elements(parents)) + { + CloseHandle(handle); + errno= ENAMETOOLONG; + goto end; + } + parents[count++]= handle; + if (!GetFileInformationByHandle(handle, &info) || + (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || + ((end || op == PLUGIN_CHECK_DIR) && + !(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))) + { + errno= EACCES; + goto end; + } + if (!end) + break; + } + switch (op) { + case PLUGIN_OPEN: result= my_open(full, flags, MYF(0)); break; + case PLUGIN_DELETE: result= my_delete(full, MYF(0)); break; + case PLUGIN_MKDIR: result= my_mkdir(full, 0755, MYF(0)); break; + case PLUGIN_RMDIR: result= rmdir(full); break; + case PLUGIN_CHECK_DIR: result= 0; break; + } +#else + parent= open(basedir, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (parent < 0) + goto end; + while ((slash= strchr(part, '/'))) + { + len= (size_t) (slash - part); + if (len) + { + memcpy(component, part, len); + component[len]= '\0'; + next= openat(parent, component, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (next < 0) + goto end; + close(parent); + parent= next; + } + part= slash + 1; + } + switch (op) { + case PLUGIN_OPEN: + result= openat(parent, part, flags | O_NOFOLLOW | O_CLOEXEC, my_umask); + if (result >= 0) + result= my_register_filename(result, full, FILE_BY_OPEN, 0, MYF(0)); + break; + case PLUGIN_DELETE: result= unlinkat(parent, part, 0); break; + case PLUGIN_MKDIR: result= mkdirat(parent, part, 0755); break; + case PLUGIN_RMDIR: result= unlinkat(parent, part, AT_REMOVEDIR); break; + case PLUGIN_CHECK_DIR: + next= openat(parent, part, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (next >= 0) + { + close(next); + result= 0; + } + break; + } +#endif +end: + saved_errno= errno; +#ifdef _WIN32 + while (count) + CloseHandle(parents[--count]); +#else + if (parent >= 0) + close(parent); +#endif + if (result < 0) + errno= my_errno= saved_errno; + return result; +} + + +static int build_manifest_path(char *to, size_t size, const char *basedir, + const char *name) +{ + if (build_full_path(to, size, basedir, MANIFEST_SUBDIR "/") || + safe_strcat(to, size, name) || safe_strcat(to, size, ".list")) + return 1; + return 0; +} + + +/** + Check that a relative path stays inside the basedir. + + Used for manifest lines and archive entries alike, neither of which is + trusted input: an absolute path or a ".." component would let install + write, and uninstall delete, files the plugin never owned. + + @param[in] path The path, relative to the basedir. + + @retval int acceptable = 1, not = 0 +*/ + +static int valid_relative_path(const char *path) +{ + const uchar *p; + /* tar paths use '/', so a backslash only ever comes from hostile input */ + if (!*path || *path == '/' || strchr(path, '\\') || strchr(path, ':') || + strstr(path, "..")) + return 0; + /* a control character, above all a newline, would let a crafted entry + name inject extra lines into the manifest and make uninstall act on + files this plugin never installed */ + for (p= (const uchar *) path; *p; p++) + if (*p < 0x20 || *p == 0x7f) + return 0; + return 1; +} + + +/* 1 = line, 0 = EOF, -1 = error. Long manifest headers remain readable. */ +static int read_kv_line(FILE *file, const char *source, char *line, + my_bool manifest) +{ + size_t len= 0; + int c, oversized= 0, invalid= 0; + + while ((c= fgetc(file)) != EOF && c != '\n') + { + if (c == '\0') + invalid= 1; + if (len < KV_LINE_SIZE - 1) + line[len++]= (char) c; + else + oversized= 1; + } + line[len]= '\0'; + if (ferror(file) || invalid) + goto corrupt; + if (oversized) + { + if (!manifest || !strncmp(line, "dir: ", 5) || + !strncmp(line, "file: ", 6)) + goto corrupt; + /* Preserve the prefix so identity checks can reject an oversized name. */ + return 1; + } + if (c == EOF && !len) + return 0; + if (len && line[len - 1] == '\r') + line[--len]= '\0'; + return 1; + +corrupt: + fprintf(stderr, "ERROR: '%s' contains an invalid or oversized line, " + "or could not be read.\n", source); + return -1; +} + + +struct download_target +{ + FILE *file; + size_t remaining; +}; + + +static size_t download_write(char *data, size_t size, size_t count, void *arg) +{ + struct download_target *target= (struct download_target *) arg; + size_t bytes; + if (size && count > target->remaining / size) + return 0; + bytes= size * count; + target->remaining-= bytes; + return fwrite(data, 1, bytes, target->file); +} + + +static FILE *plugin_tmpfile(void) +{ + char name[FN_REFLEN]; + File fd= create_temp_file(name, NULL, "plugin", O_BINARY, + MYF(MY_WME | MY_TEMPORARY)); + FILE *file; + if (fd < 0) + return NULL; + if (!(file= my_fdopen(fd, name, O_RDWR | O_BINARY, MYF(MY_WME)))) + my_close(fd, MYF(0)); + return file; +} + + +/* Verify and extract a private snapshot, not a replaceable local pathname. */ +static FILE *copy_local_archive(const char *name) +{ + File fd; + MY_STAT info; + FILE *input, *output; + uchar buf[8192]; + size_t n; + int flags= O_RDONLY | O_BINARY, error= 0; +#ifndef _WIN32 + flags|= O_NONBLOCK; +#endif + if ((fd= my_open(name, flags, MYF(MY_WME))) < 0) + return NULL; + if (my_fstat(fd, &info, MYF(MY_WME)) || !MY_S_ISREG(info.st_mode)) + { + fprintf(stderr, "ERROR: '%s' is not a readable regular archive file.\n", + name); + my_close(fd, MYF(0)); + return NULL; + } + if (!(input= my_fdopen(fd, name, O_RDONLY | O_BINARY, MYF(MY_WME)))) + { + my_close(fd, MYF(0)); + return NULL; + } + output= plugin_tmpfile(); + if (output) + { + while ((n= fread(buf, 1, sizeof(buf), input)) > 0) + if (fwrite(buf, 1, n, output) != n) + { + error= 1; + break; + } + if (error || ferror(input) || fflush(output) || fseek(output, 0, SEEK_SET)) + { + fprintf(stderr, "ERROR: cannot copy archive '%s': %s.\n", name, + strerror(errno)); + my_fclose(output, MYF(0)); + output= NULL; + } + } + my_fclose(input, MYF(0)); + return output; +} + + +/* Keep downloads open and anonymous, including between verification passes. */ +static FILE *download_file(const char *url, size_t limit) +{ + char detail[CURL_ERROR_SIZE]= ""; + struct download_target target; + CURL *curl; + CURLcode rc; + long status= 0; + + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) + { + fprintf(stderr, "ERROR: cannot initialize libcurl.\n"); + return NULL; + } + if (!(curl= curl_easy_init())) + { + curl_global_cleanup(); + return NULL; + } + target.file= plugin_tmpfile(); + target.remaining= limit; + if (!target.file) + goto end; + +#define DOWNLOAD_OPTION(option, value) \ + if ((rc= curl_easy_setopt(curl, option, value)) != CURLE_OK) goto failed + + DOWNLOAD_OPTION(CURLOPT_URL, url); + DOWNLOAD_OPTION(CURLOPT_ERRORBUFFER, detail); + DOWNLOAD_OPTION(CURLOPT_WRITEFUNCTION, download_write); + DOWNLOAD_OPTION(CURLOPT_WRITEDATA, &target); + DOWNLOAD_OPTION(CURLOPT_FAILONERROR, 1L); + DOWNLOAD_OPTION(CURLOPT_FOLLOWLOCATION, 1L); + DOWNLOAD_OPTION(CURLOPT_MAXREDIRS, 5L); + DOWNLOAD_OPTION(CURLOPT_CONNECTTIMEOUT, 10L); + DOWNLOAD_OPTION(CURLOPT_TIMEOUT, 300L); + DOWNLOAD_OPTION(CURLOPT_LOW_SPEED_LIMIT, 1L); + DOWNLOAD_OPTION(CURLOPT_LOW_SPEED_TIME, 30L); + DOWNLOAD_OPTION(CURLOPT_NOSIGNAL, 1L); +#if LIBCURL_VERSION_NUM >= 0x075500 + DOWNLOAD_OPTION(CURLOPT_PROTOCOLS_STR, "http,https"); + DOWNLOAD_OPTION(CURLOPT_REDIR_PROTOCOLS_STR, + strncmp(url, "https://", 8) ? "http,https" : "https"); +#else + DOWNLOAD_OPTION(CURLOPT_PROTOCOLS, (long) (CURLPROTO_HTTP | CURLPROTO_HTTPS)); + DOWNLOAD_OPTION(CURLOPT_REDIR_PROTOCOLS, (long) (strncmp(url, "https://", 8) ? + CURLPROTO_HTTP | CURLPROTO_HTTPS : CURLPROTO_HTTPS)); +#endif +#undef DOWNLOAD_OPTION + + if ((rc= curl_easy_perform(curl)) != CURLE_OK || + (rc= curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status)) != CURLE_OK) + goto failed; + if (status != 200 || fflush(target.file) || + fseek(target.file, 0, SEEK_SET)) + { + fprintf(stderr, "ERROR: download of '%s' failed (HTTP %ld or file I/O).\n", + url, status); + goto discard; + } + goto end; + +failed: + fprintf(stderr, "ERROR: cannot download '%s': %s.\n", url, + detail[0] ? detail : curl_easy_strerror(rc)); +discard: + my_fclose(target.file, MYF(0)); + target.file= NULL; +end: + curl_easy_cleanup(curl); + curl_global_cleanup(); + return target.file; +} + + +static int build_download_url(char *url, size_t size, const char *file) +{ + const char *base= opt_base_url ? opt_base_url : PLUGIN_BASE_URL; + const char *host, *p; + size_t len= strlen(base); + + if (!len) + { + fprintf(stderr, "ERROR: no plugin repository configured; use " + "--base-url=.\n"); + return 1; + } + if (!strncmp(base, "https://", 8)) + host= base + 8; + else if (!strncmp(base, "http://", 7)) + host= base + 7; + else + goto invalid; + if (!*host || *host == '/' || strpbrk(host, "\\?#@")) + goto invalid; + for (p= base; *p; p++) + if ((uchar) *p <= 0x20 || *p == 0x7f) + goto invalid; + if (safe_strcpy_truncated(url, size, base) || + (base[len - 1] != '/' && safe_strcat(url, size, "/")) || + safe_strcat(url, size, file)) + { + fprintf(stderr, "ERROR: plugin repository URL is too long.\n"); + return 1; + } + return 0; + +invalid: + fprintf(stderr, "ERROR: --base-url must be an HTTP or HTTPS directory " + "URL without credentials, a query or a fragment.\n"); + return 1; +} + + +struct index_entry +{ + char name[NAME_CHAR_LEN + 1]; + char version[64]; + char server[32]; + char platform[64]; + char arch[64]; + char file[FN_REFLEN]; + char sha256[65]; + char author[KV_LINE_SIZE]; + char description[KV_LINE_SIZE]; + char license[KV_LINE_SIZE]; +}; + + +/* Blank-separated prototype records. Returns 1 = record, 0 = EOF, -1 = error. */ +static int read_index_entry(FILE *file, const char *source, + struct index_entry *e) +{ + char line[KV_LINE_SIZE]; + const char *keys[]= {"name", "version", "server", "platform", "arch", + "file", "sha256", "author", "description", "license"}; + char *values[]= {e->name, e->version, e->server, e->platform, e->arch, + e->file, e->sha256, e->author, e->description, e->license}; + size_t sizes[]= {sizeof(e->name), sizeof(e->version), sizeof(e->server), + sizeof(e->platform), sizeof(e->arch), sizeof(e->file), + sizeof(e->sha256), sizeof(e->author), + sizeof(e->description), sizeof(e->license)}; + uint seen= 0; + my_bool have_fields= FALSE; + int rc; + size_t i, len; + + bzero(e, sizeof(*e)); + for (;;) + { + rc= read_kv_line(file, source, line, FALSE); + if (rc < 0) + return -1; + if (rc && line[0]) + { + char *value= strchr(line, ':'); + if (!value || value == line || value[1] != ' ') + goto invalid; + *value++= '\0'; + while (*value == ' ') + value++; + len= strlen(value); + while (len && value[len - 1] == ' ') + value[--len]= '\0'; + for (i= 0; value[i]; i++) + if ((uchar) value[i] < 0x20 || value[i] == 0x7f) + goto invalid; + for (i= 0; i < array_elements(keys); i++) + if (!strcmp(line, keys[i])) + break; + if (i < array_elements(keys)) + { + if ((i < 7 && !len) || (seen & (1U << i)) || + safe_strcpy_truncated(values[i], sizes[i], value)) + goto invalid; + seen|= 1U << i; + } + have_fields= TRUE; + continue; + } + if (have_fields) + { + if ((seen & 127) != 127 || validate_plugin_name(e->name)) + goto invalid; + len= strlen(e->file); + if (len < 7 || strcmp(e->file + len - 7, ".tar.gz") || + !valid_relative_path(e->file)) + goto invalid; + for (i= 0; i < len; i++) + if (!isalnum((uchar) e->file[i]) && e->file[i] != '.' && + e->file[i] != '_' && e->file[i] != '-') + goto invalid; + if (strlen(e->sha256) != 64) + goto invalid; + for (i= 0; i < 64; i++) + if (!isxdigit((uchar) e->sha256[i])) + goto invalid; + return 1; + } + if (!rc) + return 0; + } + +invalid: + fprintf(stderr, "ERROR: invalid plugin record in '%s'.\n", source); + return -1; +} + + +static int index_entry_compatible(const struct index_entry *e) +{ + char server[32]; + my_snprintf(server, sizeof(server), "%u.%u", MYSQL_VERSION_ID / 10000, + MYSQL_VERSION_ID / 100 % 100); + return !strcmp(e->server, server) && !strcmp(e->platform, SYSTEM_TYPE) && + !strcmp(e->arch, MACHINE_TYPE); +} + + +static int read_index(FILE *file, const char *source, const char *name, + struct index_entry *result) +{ + struct index_entry e; + int rc, found= 0; + + while ((rc= read_index_entry(file, source, &e)) > 0) + { + if (strcmp(e.name, name) || !index_entry_compatible(&e)) + continue; + if (found) + { + fprintf(stderr, "ERROR: multiple compatible entries for '%s' " + "in '%s'.\n", name, source); + return 1; + } + *result= e; + found= 1; + } + if (rc < 0) + return 1; + if (!found) + fprintf(stderr, "ERROR: no compatible download for '%s' " + "(server %u.%u, %s, %s).\n", name, MYSQL_VERSION_ID / 10000, + MYSQL_VERSION_ID / 100 % 100, SYSTEM_TYPE, MACHINE_TYPE); + return !found; +} + + +/** + Read and validate all manifest entries before any deletion. + + @param[in] manifest Path of the manifest file. + @param[in] name Plugin the manifest must belong to, or NULL to skip + the ownership check (rollback of a manifest the tool + has just written itself). + @param[out] entries Initialized array, filled with manifest_entry. + + @retval int error = 1, success = 0 +*/ +static int read_manifest(const char *basedir, const char *manifest, + const char *name, DYNAMIC_ARRAY *entries) +{ + FILE *file; + File fd; + MY_STAT info; + char line[KV_LINE_SIZE]; + struct manifest_entry e; + const char *path; + int rc= 0, error= 0, flags= O_RDONLY | O_BINARY; + my_bool have_name= FALSE; + +#ifndef _WIN32 + /* A FIFO or device in place of the manifest must fail, not block. */ + flags|= O_NONBLOCK; +#endif + fd= plugin_file_op(basedir, manifest + strlen(basedir) + 1, + PLUGIN_OPEN, flags); + if (fd < 0) + { + fprintf(stderr, "ERROR: cannot read '%s': %s.\n", manifest, + strerror(errno)); + return 1; + } + if (my_fstat(fd, &info, MYF(MY_WME)) || !MY_S_ISREG(info.st_mode)) + { + fprintf(stderr, "ERROR: '%s' is not a readable regular manifest.\n", + manifest); + my_close(fd, MYF(0)); + return 1; + } + if (!(file= my_fdopen(fd, manifest, O_RDONLY | O_BINARY, MYF(MY_WME)))) + { + my_close(fd, MYF(0)); + return 1; + } + while (!error && (rc= read_kv_line(file, manifest, line, TRUE)) > 0) + { + if (!strncmp(line, "name: ", 6)) + { + /* uninstall may only remove what a manifest of this plugin owns */ + if (name && strcmp(line + 6, name)) + { + fprintf(stderr, "ERROR: manifest '%s' belongs to plugin '%s', not " + "'%s'; nothing was removed.\n", manifest, line + 6, name); + error= 1; + break; + } + if (have_name) + { + fprintf(stderr, "ERROR: invalid plugin manifest '%s'.\n", manifest); + error= 1; + break; + } + have_name= TRUE; + continue; + } + e.is_dir= strncmp(line, "dir: ", 5) == 0; + if (e.is_dir) + path= line + 5; + else if (strncmp(line, "file: ", 6) == 0) + path= line + 6; + else + continue; /* header lines; uninstall only consumes the paths */ + + if (!valid_relative_path(path) || strlen(path) >= sizeof(e.path)) + { + fprintf(stderr, "ERROR: unsafe path '%s' in '%s', nothing was " + "removed.\n", path, manifest); + error= 1; + break; + } + safe_strcpy(e.path, sizeof(e.path), path); + error= insert_dynamic(entries, &e); + } + my_fclose(file, MYF(0)); + if (!error && name && !have_name) + { + fprintf(stderr, "ERROR: invalid plugin manifest '%s'.\n", manifest); + error= 1; + } + return error || rc < 0; +} + + +/* Validate ownership metadata, without checking file health or server state. */ +static int tarball_plugin_installed(const char *basedir, const char *name, + int *installed) +{ + char manifest[FN_REFLEN], line[KV_LINE_SIZE]; + MY_STAT info; + FILE *file; + File fd; + int rc, error= 1, flags= O_RDONLY | O_BINARY; + my_bool have_name= FALSE; + + *installed= 0; + if (build_manifest_path(manifest, sizeof(manifest), basedir, name)) + return 1; +#ifndef _WIN32 + flags|= O_NONBLOCK; +#endif + fd= plugin_file_op(basedir, manifest + strlen(basedir) + 1, + PLUGIN_OPEN, flags); + if (fd < 0) + { + if (errno == ENOENT) + return 0; + fprintf(stderr, "ERROR: cannot read '%s': %s.\n", manifest, + strerror(errno)); + return 1; + } + if (my_fstat(fd, &info, MYF(MY_WME)) || !MY_S_ISREG(info.st_mode)) + { + fprintf(stderr, "ERROR: '%s' is not a readable regular manifest.\n", + manifest); + my_close(fd, MYF(0)); + return 1; + } + if (!(file= my_fdopen(fd, manifest, O_RDONLY | O_BINARY, MYF(MY_WME)))) + { + my_close(fd, MYF(0)); + return 1; + } + while ((rc= read_kv_line(file, manifest, line, TRUE)) > 0) + { + char *value; + + if (!line[0]) + continue; + value= strchr(line, ':'); + if (!value || value == line || value[1] != ' ') + goto invalid; + *value= '\0'; + value+= 2; + if (!strcmp(line, "name")) + { + if (have_name || strcmp(value, name)) + goto invalid; + have_name= TRUE; + } + else if (!strcmp(line, "file") || !strcmp(line, "dir")) + { + if (!valid_relative_path(value) || strlen(value) >= FN_REFLEN) + goto invalid; + } + } + if (rc < 0) + goto end; + if (!have_name) + goto invalid; + *installed= 1; + error= 0; + goto end; + +invalid: + fprintf(stderr, "ERROR: invalid plugin manifest '%s'.\n", manifest); +end: + if (my_fclose(file, MYF(MY_WME))) + error= 1; + return error; +} + + +static int search_tarball(const char *basedir) +{ + char url[KV_LINE_SIZE * 2]; + struct index_entry entry; + FILE *index; + int rc, error= 1; + + if (build_download_url(url, sizeof(url), PLUGIN_INDEX)) + return 1; + if (!(index= download_file(url, 8 * 1024 * 1024))) + return 1; + while ((rc= read_index_entry(index, url, &entry)) > 0) + { + struct plugin_entry *plugin; + + if (!index_entry_compatible(&entry)) + continue; + if (find_plugin_entry(entry.name)) + { + fprintf(stderr, "ERROR: multiple compatible entries for '%s' " + "in '%s'.\n", entry.name, url); + goto end; + } + if (!(plugin= add_plugin_entry(entry.name))) + goto end; + safe_strcpy(plugin->description, sizeof(plugin->description), + entry.description); + if (tarball_plugin_installed(basedir, entry.name, &plugin->installed)) + goto end; + } + error= rc < 0; +end: + if (my_fclose(index, MYF(MY_WME))) + error= 1; + return error; +} + + +/** + Delete everything a manifest lists, then the manifest. + + Remove files before directories. Keep the manifest if file removal fails + so the user can retry; nonempty directories are reported but retained. + + @param[in] basedir The base directory. + @param[in] manifest Path of the manifest file. + @param[in] name Plugin the manifest must belong to, or NULL when + rolling back a manifest the tool just wrote. + + @retval int error = 1, success = 0 +*/ + +static int manifest_remove(const char *basedir, const char *manifest, + const char *name) +{ + char full[FN_REFLEN]; + DYNAMIC_ARRAY entries; + struct manifest_entry *e; + size_t i; + int failed= 0; + + if (my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &entries, + sizeof(struct manifest_entry), 16, 16, + MYF(MY_WME))) + return 1; + if (read_manifest(basedir, manifest, name, &entries)) + { + delete_dynamic(&entries); + return 1; + } + + for (i= 0; i < entries.elements; i++) + { + e= dynamic_element(&entries, i, struct manifest_entry *); + if (e->is_dir) + continue; + if (build_full_path(full, sizeof(full), basedir, e->path)) + { + /* an undeletable file must keep the manifest, or it is orphaned */ + failed= 1; + continue; + } + if (opt_dry_run) + printf("would delete %s\n", full); + else if (plugin_file_op(basedir, e->path, PLUGIN_DELETE, 0)) + { + /* a file someone already removed by hand must not block uninstall */ + if (my_errno == ENOENT) + fprintf(stderr, "WARNING: '%s' was already gone.\n", full); + else + { + fprintf(stderr, "ERROR: cannot delete '%s': %s.\n", full, + strerror(my_errno)); + failed= 1; + } + } + } + + /* directories in reverse manifest order, so children come before parents */ + for (i= entries.elements; i-- > 0; ) + { + e= dynamic_element(&entries, i, struct manifest_entry *); + if (!e->is_dir || build_full_path(full, sizeof(full), basedir, e->path)) + continue; + if (opt_dry_run) + printf("would remove directory %s\n", full); + else if (plugin_file_op(basedir, e->path, PLUGIN_RMDIR, 0) && + errno != ENOENT) + fprintf(stderr, "WARNING: directory '%s' was not removed: %s.\n", full, + strerror(errno)); + } + delete_dynamic(&entries); + + if (failed) + { + fprintf(stderr, "ERROR: not all files could be deleted; the manifest " + "was kept, so uninstall can be run again.\n"); + return 1; + } + if (opt_dry_run) + { + printf("would delete %s\n", manifest); + return 0; + } + if (plugin_file_op(basedir, manifest + strlen(basedir) + 1, + PLUGIN_DELETE, 0)) + return 1; + /* the manifest directory goes with the last plugin; busy is fine */ + plugin_file_op(basedir, MANIFEST_SUBDIR, PLUGIN_RMDIR, 0); + return 0; +} + + +/* + The tool reads plugin tarballs itself instead of running tar: every entry + is judged before anything is written, no tar binary is needed on the + machine, and no command line is ever built from a user-chosen path. + + Supported entries are ustar headers with regular files and + directories, plus the two ways a long path is spelled, GNU long-name + entries and pax "path" records. Links and devices are refused. +*/ + +#define TAR_BLOCK 512 + +struct tar_reader +{ + gzFile gz; + const char *file; + ulonglong data_left; /* unread bytes of the current entry, plus padding */ +}; + +struct tar_entry +{ + char path[FN_REFLEN]; + ulonglong size; + uint mode; + my_bool is_dir; +}; + + +static int tar_open(struct tar_reader *r, const char *file, FILE *contents) +{ + int fd; + r->file= file; + r->data_left= 0; + r->gz= NULL; + if (fseek(contents, 0, SEEK_SET)) + goto error; + /* gzdopen takes a CRT descriptor, not a Windows mysys descriptor. */ +#ifdef _WIN32 + fd= _dup(_fileno(contents)); +#else + fd= dup(fileno(contents)); +#endif + if (fd < 0) + goto error; + if (!(r->gz= gzdopen(fd, "rb"))) + { +#ifdef _WIN32 + _close(fd); +#else + close(fd); +#endif + goto error; + } + return 0; + +error: + fprintf(stderr, "ERROR: cannot open '%s': %s.\n", file, strerror(errno)); + return 1; +} + + +static void tar_close(struct tar_reader *r) +{ + gzclose(r->gz); +} + + +static int tar_read_bytes(struct tar_reader *r, void *buf, size_t len) +{ + int n= gzread(r->gz, buf, (unsigned) len); + if (n != (int) len) + { + int err; + const char *msg= gzerror(r->gz, &err); + fprintf(stderr, "ERROR: '%s' is truncated or not a gzip file%s%s.\n", + r->file, err == Z_ERRNO || err == Z_OK ? "" : ": ", + err == Z_ERRNO || err == Z_OK ? "" : msg); + return 1; + } + return 0; +} + + +static int tar_skip_data(struct tar_reader *r) +{ + if (r->data_left && gzseek(r->gz, (z_off_t) r->data_left, SEEK_CUR) < 0) + { + fprintf(stderr, "ERROR: '%s' is truncated.\n", r->file); + return 1; + } + r->data_left= 0; + return 0; +} + + +/** + Parse a tar numeric field: octal digits, terminated by NUL or space. + + @retval int error = 1, success = 0 +*/ + +static int tar_number(const uchar *field, size_t len, ulonglong *out) +{ + ulonglong v= 0; + size_t i; + + /* Only octal numeric fields are supported. */ + if (field[0] & 0x80) + return 1; + for (i= 0; i < len && field[i] == ' '; i++) ; + for (; i < len && field[i] != '\0' && field[i] != ' '; i++) + { + if (field[i] < '0' || field[i] > '7') + return 1; + v= v * 8 + (field[i] - '0'); + } + for (; i < len; i++) + if (field[i] != '\0' && field[i] != ' ') + return 1; + *out= v; + return 0; +} + + +static int tar_checksum_ok(const uchar *block) +{ + ulonglong stored; + unsigned sum= 0; + size_t i; + + if (tar_number(block + 148, 8, &stored)) + return 0; + for (i= 0; i < TAR_BLOCK; i++) + sum+= (i >= 148 && i < 156) ? ' ' : block[i]; + return sum == stored; +} + + +/** + Read the next file or directory entry. + + Long-name and pax entries are consumed here and applied to the entry that + follows them, so callers only ever see real files and directories. The + entry's data is left unread; call tar_skip_data() before the next entry. + + @param[in] r The open reader. + @param[out] e The entry. + + @retval int 1 = entry returned, 0 = end of archive, -1 = error +*/ + +static int tar_next(struct tar_reader *r, struct tar_entry *e) +{ + uchar block[TAR_BLOCK]; + char longname[FN_REFLEN]; + ulonglong size, mode; + size_t len; + char type; + + longname[0]= '\0'; + for (;;) + { + if (tar_skip_data(r) || tar_read_bytes(r, block, TAR_BLOCK)) + return -1; + if (block[0] == '\0') + { + static const uchar zero[TAR_BLOCK]= {0}; + int n, err; + if (longname[0] || memcmp(block, zero, TAR_BLOCK) || + tar_read_bytes(r, block, TAR_BLOCK) || memcmp(block, zero, TAR_BLOCK)) + goto bad_end; + /* Consume padding and the gzip trailer, including its checksum. */ + while ((n= gzread(r->gz, block, TAR_BLOCK)) > 0) + if (memcmp(block, zero, (size_t) n)) + goto bad_end; + gzerror(r->gz, &err); + if (n == 0 && err == Z_OK) + return 0; +bad_end: + fprintf(stderr, "ERROR: '%s' has an invalid archive ending.\n", r->file); + return -1; + } + if (memcmp(block + 257, "ustar", 5) != 0 || !tar_checksum_ok(block)) + { + fprintf(stderr, "ERROR: '%s' is not a valid tar archive.\n", r->file); + return -1; + } + if (tar_number(block + 124, 12, &size) || tar_number(block + 100, 8, &mode)) + { + fprintf(stderr, "ERROR: '%s' has a corrupt entry header.\n", r->file); + return -1; + } + r->data_left= (size + TAR_BLOCK - 1) / TAR_BLOCK * TAR_BLOCK; + type= block[156]; + + if (type == 'L' || type == 'x') + { + /* the data of these entries names the entry after them */ + char *buf, *p, *end; + int invalid= 0; + if (size >= sizeof(longname) * 4) + { + fprintf(stderr, "ERROR: '%s' has an entry name that is too long.\n", + r->file); + return -1; + } + if (!(buf= (char *) my_malloc(PSI_NOT_INSTRUMENTED, (size_t) size + 1, + MYF(MY_WME)))) + return -1; + if (tar_read_bytes(r, buf, (size_t) size)) + { + my_free(buf); + return -1; + } + buf[size]= '\0'; + r->data_left-= size; + if (type == 'L') + { + len= (size_t) size; + if (len && buf[len - 1] == '\0') + len--; + if (!len || len >= sizeof(longname) || memchr(buf, '\0', len)) + invalid= 1; + else + { + memcpy(longname, buf, len); + longname[len]= '\0'; + } + } + else + { + /* PAX lengths include the digits, separator and terminating newline. */ + for (p= buf; p < buf + size; p= end) + { + char *key= p; + len= 0; + while (*key >= '0' && *key <= '9' && len <= size) + len= len * 10 + (uint) (*key++ - '0'); + if (key == p || *key != ' ' || len > (size_t) (buf + size - p) || + len <= (size_t) (key - p) + 1) + { + invalid= 1; + break; + } + end= p + len; + key++; + if (end[-1] != '\n' || memchr(key, '\0', (size_t) (end - key)) || + !memchr(key, '=', (size_t) (end - key - 1))) + { + invalid= 1; + break; + } + if (end - key > 5 && !memcmp(key, "path=", 5)) + { + len= (size_t) (end - key - 6); + if (!len || len >= sizeof(longname)) + { + invalid= 1; + break; + } + memcpy(longname, key + 5, len); + longname[len]= '\0'; + } + } + } + my_free(buf); + if (invalid) + { + fprintf(stderr, "ERROR: '%s' has an invalid or oversized extended " + "header.\n", r->file); + return -1; + } + continue; + } + if (type == 'g') /* pax global header, carries nothing we use */ + continue; + break; + } + + switch (type) { + case '0': case '\0': case '7': + e->is_dir= FALSE; + break; + case '5': + e->is_dir= TRUE; + break; + case '1': case '2': + fprintf(stderr, "ERROR: '%s' contains a link, which plugin archives " + "must not have.\n", r->file); + return -1; + default: + fprintf(stderr, "ERROR: '%s' contains an entry of unsupported type " + "'%c'.\n", r->file, type); + return -1; + } + + if (longname[0]) + safe_strcpy(e->path, sizeof(e->path), longname); + else + { + /* ustar splits long paths into prefix (155) and name (100) */ + e->path[0]= '\0'; + if (block[345]) + { + safe_strcpy_truncated(e->path, MY_MIN(sizeof(e->path), 156), + (char *) block + 345); + safe_strcat(e->path, sizeof(e->path), "/"); + } + len= strlen(e->path); + safe_strcpy_truncated(e->path + len, MY_MIN(sizeof(e->path) - len, 101), + (char *) block + 0); + } + /* "./x" and "x/" spell the same thing; normalize before judging */ + while (strncmp(e->path, "./", 2) == 0) + memmove(e->path, e->path + 2, strlen(e->path) - 1); + len= strlen(e->path); + while (len > 1 && e->path[len - 1] == '/') + e->path[--len]= '\0'; + + e->size= size; + /* setuid and setgid bits from an archive are never honored */ + e->mode= (uint) mode & 0777; + return 1; +} + + + +/** + Compute the SHA-256 of a file as 64 lower case hex digits. + + @retval int error = 1, success = 0 +*/ + +static int file_sha256(const char *file, FILE *input, char *hex) +{ + uchar buf[8192], digest[32]; + void *ctx; + size_t n, i; + int error= 0; + + if (!(ctx= my_malloc(PSI_NOT_INSTRUMENTED, my_sha256_context_size(), + MYF(MY_WME)))) + return 1; + my_sha256_init(ctx); + while ((n= fread(buf, 1, sizeof(buf), input)) > 0) + my_sha256_input(ctx, buf, n); + if (ferror(input)) + { + fprintf(stderr, "ERROR: cannot read '%s': %s.\n", file, strerror(errno)); + error= 1; + } + my_sha256_result(ctx, digest); + my_free(ctx); + for (i= 0; i < sizeof(digest); i++) + sprintf(hex + 2 * i, "%02x", digest[i]); + return error; +} + + +/** + Compare the archive against the checksum the user was given for it. + + Case does not matter; whitespace and the "sha256:" prefix some sites + print are tolerated. A mismatch means the file is not the one that was + published, whatever the reason, so nothing is installed from it. + + @retval int error = 1, success = 0 +*/ + +static int verify_sha256(const char *file, FILE *contents, + const char *expected, char *hex) +{ + const char *p= expected; + size_t i; + + if (file_sha256(file, contents, hex)) + return 1; + while (*p == ' ' || *p == '\t') p++; + if (strncasecmp(p, "sha256:", 7) == 0) + p+= 7; + for (i= 0; i < 64 && p[i]; i++) + if (tolower((uchar) p[i]) != hex[i]) + break; + if (i != 64 || (p[64] && !isspace((uchar) p[64]))) + { + fprintf(stderr, "ERROR: '%s' does not match the expected checksum.\n" + " expected: %s\n actual: %s\n", file, expected, hex); + return 1; + } + return 0; +} + +/** + Read a whole archive, judging every entry, without writing anything. + + CPack wraps an archive's contents in one directory named after the + archive file. When every entry lives under such a directory it is + stripped from the paths and dropped from the list, so the remaining paths + are relative to the basedir. Any other layout is taken as is: "lib/x" and + "top/lib/x" cannot be told apart by shape, only by that name. + + @param[in] file Original archive name, used to identify its wrapper. + @param[in] contents Private archive snapshot. + @param[out] entries Initialized array, filled with tar_entry. + @param[out] topdir The stripped directory, "" when nothing was stripped. + + @retval int error = 1, success = 0 +*/ + +static int tar_scan(const char *file, FILE *contents, + DYNAMIC_ARRAY *entries, char *topdir) +{ + struct tar_reader r; + struct tar_entry e, *p; + const char *base; + my_bool have_topdir= TRUE; + size_t i, len; + int rc; + + if (tar_open(&r, file, contents)) + return 1; + topdir[0]= '\0'; + base= file + dirname_length(file); + while ((rc= tar_next(&r, &e)) > 0) + { + const char *slash; + if (!valid_relative_path(e.path)) + { + fprintf(stderr, "ERROR: '%s' contains the unsafe path '%s'.\n", file, + e.path); + rc= -1; + break; + } + /* a top directory exists only if no entry sits beside it at the root */ + slash= strchr(e.path, '/'); + len= slash ? (size_t) (slash - e.path) : strlen(e.path); + if (!slash && !e.is_dir) + have_topdir= FALSE; + if (!topdir[0]) + safe_strcpy_truncated(topdir, MY_MIN(FN_REFLEN, len + 1), e.path); + else if (strlen(topdir) != len || strncmp(topdir, e.path, len) != 0) + have_topdir= FALSE; + if (insert_dynamic(entries, &e)) + { + rc= -1; + break; + } + } + tar_close(&r); + if (rc < 0) + return 1; + if (!entries->elements) + { + fprintf(stderr, "ERROR: '%s' is empty.\n", file); + return 1; + } + len= strlen(topdir); + if (!have_topdir || strncmp(base, topdir, len) != 0 || + (base[len] != '\0' && base[len] != '.')) + { + topdir[0]= '\0'; + return 0; + } + + for (i= 0; i < entries->elements; ) + { + p= dynamic_element(entries, i, struct tar_entry *); + if (strlen(p->path) == len) /* the top directory itself */ + delete_dynamic_element(entries, i); + else + { + memmove(p->path, p->path + len + 1, strlen(p->path) - len); + i++; + } + } + if (!entries->elements) + { + fprintf(stderr, "ERROR: '%s' contains only an empty directory.\n", file); + return 1; + } + return 0; +} + + +/** + Append and flush one manifest record for rollback and later uninstall. + + @retval int error = 1, success = 0 +*/ + +static int manifest_append(FILE *m, const char *key, const char *value) +{ + /* a newline in a value, such as the --file path written as "source", + would be read back as a second manifest line: refuse it */ + if (strpbrk(value, "\r\n")) + { + fprintf(stderr, "ERROR: refusing to write a '%s' value that contains a " + "newline into the manifest.\n", key); + return 1; + } + if (fprintf(m, "%s: %s\n", key, value) < 0 || fflush(m)) + { + fprintf(stderr, "ERROR: cannot write the manifest: %s.\n", + strerror(errno)); + return 1; + } + return 0; +} + + +/** + Copy one entry's data from the archive into a new file. + + Create exclusively relative to a checked parent directory. + + @retval int error = 1, success = 0 +*/ + +static int tar_extract_file(struct tar_reader *r, struct tar_entry *e, + const char *basedir, const char *full, FILE *m, + const char *relpath) +{ + char buf[8192]; + ulonglong left= e->size; + File fd; + int error= 0; + + fd= plugin_file_op(basedir, relpath, PLUGIN_OPEN, + O_WRONLY | O_CREAT | O_EXCL | O_BINARY); + if (fd < 0) + { + fprintf(stderr, "ERROR: cannot create '%s': %s.\n", full, + strerror(my_errno)); + return 1; + } + /* Record ownership before writing contents so a failed write is tracked. */ + if (manifest_append(m, "file", relpath)) + { + my_close(fd, MYF(0)); + plugin_file_op(basedir, relpath, PLUGIN_DELETE, 0); + return 1; + } + while (left && !error) + { + size_t n= (size_t) MY_MIN(left, sizeof(buf)); + if (tar_read_bytes(r, buf, n) || + my_write(fd, (uchar *) buf, n, MYF(MY_WME | MY_NABP))) + error= 1; + left-= n; + r->data_left-= n; + } +#ifndef _WIN32 + /* Change the opened file, not a pathname that could have been replaced. */ + if (!error && e->mode && fchmod(fd, e->mode)) + { + fprintf(stderr, "ERROR: cannot set permissions on '%s': %s.\n", + full, strerror(errno)); + error= 1; + } +#endif + if (my_close(fd, MYF(MY_WME))) + error= 1; + return error; +} + + +/** + Create the scanned entries and record ownership. Existing directories + are checked and reused without recording ownership of them. + + The archive must list newly created parent directories before children. + + @retval int error = 1, success = 0 +*/ + +static int tar_extract(const char *file, FILE *contents, const char *basedir, + const char *topdir, DYNAMIC_ARRAY *entries, FILE *m) +{ + struct tar_reader r; + struct tar_entry e; + char full[FN_REFLEN]; + size_t skip= topdir[0] ? strlen(topdir) + 1 : 0; + size_t i; + int rc; + + if (tar_open(&r, file, contents)) + return 1; + /* the archive is walked again, in step with the approved entry list */ + for (i= 0; i < entries->elements; i++) + { + struct tar_entry *ok= dynamic_element(entries, i, struct tar_entry *); + do + { + if ((rc= tar_next(&r, &e)) <= 0) + { + if (rc == 0) + fprintf(stderr, "ERROR: '%s' changed while it was being read.\n", + file); + tar_close(&r); + return 1; + } + } while (strlen(e.path) < skip || strcmp(e.path + skip, ok->path) != 0); + + if (build_full_path(full, sizeof(full), basedir, ok->path)) + goto err; + if (e.is_dir) + { + if (file_exists(full)) + { + if (plugin_file_op(basedir, ok->path, PLUGIN_CHECK_DIR, 0)) + { + fprintf(stderr, "ERROR: '%s' is not an accessible directory " + "without symlinks.\n", full); + goto err; + } + continue; + } + if (plugin_file_op(basedir, ok->path, PLUGIN_MKDIR, 0)) + { + fprintf(stderr, "ERROR: cannot create directory '%s': %s.\n", full, + strerror(my_errno)); + goto err; + } + if (manifest_append(m, "dir", ok->path)) + goto err; + } + else + { + if (tar_extract_file(&r, &e, basedir, full, m, ok->path)) + goto err; + } + } + tar_close(&r); + return 0; + +err: + tar_close(&r); + return 1; +} + + +/** + Tell the user how to enable what was just installed. Install never + edits the server configuration: a tarball installation has no conf.d + and no convention for one, and the user may keep my.cnf anywhere. +*/ + +static void print_enable_instructions(const char *basedir, + DYNAMIC_ARRAY *entries) +{ + size_t i, prefix= sizeof(STR(INSTALL_PLUGINDIR)) - 1; + int pass, shown= 0; + + /* two passes: the INSTALL SONAME lines, then the plugin-load-add lines */ + for (pass= 0; pass < 2; pass++) + { + for (i= 0; i < entries->elements; i++) + { + struct tar_entry *e= dynamic_element(entries, i, struct tar_entry *); + const char *name= e->path + prefix + 1, *ext; + if (e->is_dir || + strncmp(e->path, STR(INSTALL_PLUGINDIR), prefix) != 0 || + e->path[prefix] != '/' || strchr(name, '/') || + !(ext= strstr(name, SO_EXT))) + continue; + if (pass == 0) + { + if (!shown++) + printf("To enable it, either run in the server:\n"); + printf(" INSTALL SONAME '%.*s';\n", (int) (ext - name), name); + } + else + printf(" plugin-load-add=%s\n", name); + } + if (pass == 0 && shown) + printf("or add to your server configuration and restart:\n" + " [mariadb]\n"); + } + if (!shown) + printf("No plugin library was found under %s/%s; nothing to enable.\n", + basedir, STR(INSTALL_PLUGINDIR)); +} + +#endif /* !PKG_DELEGATION */ + + +/** + Install a plugin. + + On rpm and deb installations the work is delegated to the system package + manager, which resolves the uniform package name through its own real + package names (via Provides on rpm). Its exit code is passed through. @param[in] name The normalized plugin name. @param[in] basedir The base directory, empty for packaged installations. @@ -2138,6 +3735,14 @@ static int do_install(const char *name, const char *basedir) const char *pm; char *cmd_argv[4]; + /* Do not silently replace a requested archive with a repository package. */ + if (opt_file || opt_sha256 || opt_base_url) + { + fprintf(stderr, "ERROR: --file, --sha256 and --base-url are only for tarball " + "installations; on this system plugins are installed by the " + "package manager.\n"); + return 1; + } if (check_root("install")) return 1; if (!(pm= get_package_manager())) @@ -2150,9 +3755,181 @@ static int do_install(const char *name, const char *basedir) cmd_argv[3]= 0; return run_argv(cmd_argv); #else - printf("install: '%s' (%s installation%s%s) not implemented yet\n", name, - INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); - return 0; + char manifest[FN_REFLEN], full[FN_REFLEN], topdir[FN_REFLEN]; + char sha256[65]; + DYNAMIC_ARRAY entries; + struct tar_entry *e; + struct index_entry remote; + FILE *m= 0, *contents= NULL; + char url[KV_LINE_SIZE * 2]; + const char *archive= opt_file, *source= opt_file, *expected= opt_sha256; + size_t i; + int error= 1; + + if ((opt_sha256 && !opt_file) || (opt_base_url && opt_file)) + { + fprintf(stderr, "ERROR: --sha256 requires --file; --base-url cannot " + "be combined with --file.\n"); + return 1; + } + if (build_manifest_path(manifest, sizeof(manifest), basedir, name)) + return 1; + if (file_exists(manifest)) + { + fprintf(stderr, "ERROR: plugin '%s' is already installed.\n", name); + return 1; + } + if (!archive) + { + FILE *index; + int invalid; + if (build_download_url(url, sizeof(url), PLUGIN_INDEX)) + return 1; + if (!(index= download_file(url, 8 * 1024 * 1024))) + return 1; + invalid= read_index(index, url, name, &remote); + my_fclose(index, MYF(0)); + if (invalid || build_download_url(url, sizeof(url), remote.file)) + return 1; + archive= remote.file; + expected= remote.sha256; + source= url; + if (opt_dry_run) + { + printf("would download %s\nwould verify SHA-256 %s\n" + "would install plugin '%s' into %s\n", + source, expected, name, basedir); + return 0; + } + printf("Downloading %s\n", source); + if (!(contents= download_file(source, 1024 * 1024 * 1024))) + return 1; + } + if (!contents && !(contents= copy_local_archive(archive))) + return 1; + if (expected) + { + if (verify_sha256(archive, contents, expected, sha256)) + goto close_archive; + } + else + { + safe_strcpy(sha256, sizeof(sha256), "unverified"); + fprintf(stderr, "WARNING: no --sha256 given, the tarball is not " + "verified.\n"); + } + if (my_init_dynamic_array(PSI_NOT_INSTRUMENTED, &entries, + sizeof(struct tar_entry), 64, 64, MYF(MY_WME))) + goto close_archive; + if (tar_scan(archive, contents, &entries, topdir)) + goto end; + + /* + Everything is judged before anything is written: a file that already + exists is refused, as it belongs to the server or to another plugin. + */ + for (i= 0; i < entries.elements; i++) + { + e= dynamic_element(&entries, i, struct tar_entry *); + if (!strncmp(e->path, MANIFEST_SUBDIR, sizeof(MANIFEST_SUBDIR) - 1) && + (e->path[sizeof(MANIFEST_SUBDIR) - 1] == '/' || + e->path[sizeof(MANIFEST_SUBDIR) - 1] == '\0')) + { + fprintf(stderr, "ERROR: archive entry '%s' uses the reserved manifest " + "directory.\n", e->path); + goto end; + } + if (build_full_path(full, sizeof(full), basedir, e->path)) + goto end; + if (!e->is_dir && file_exists(full)) + { + fprintf(stderr, "ERROR: '%s' already exists, refusing to overwrite " + "it.\n", full); + goto end; + } + } + + if (opt_dry_run) + { + for (i= 0; i < entries.elements; i++) + { + e= dynamic_element(&entries, i, struct tar_entry *); + build_full_path(full, sizeof(full), basedir, e->path); + if (e->is_dir && file_exists(full)) + continue; + printf("would %s %s\n", e->is_dir ? "create directory" : "install", + full); + } + printf("would write %s\n", manifest); + error= 0; + goto end; + } + + /* + Write ownership records during extraction so ordinary failures can use + the same removal logic as uninstall. This is not a crash-atomic journal. + */ + if (build_full_path(full, sizeof(full), basedir, MANIFEST_SUBDIR)) + goto end; + if (!file_exists(full) && + plugin_file_op(basedir, MANIFEST_SUBDIR, PLUGIN_MKDIR, 0)) + goto end; + /* my_fopen() would map these flags to fopen("w"), dropping O_EXCL and + following a dangling symlink; my_open() honours O_EXCL, so the manifest + is created only if the name does not already exist */ + { + File mfd= plugin_file_op(basedir, manifest + strlen(basedir) + 1, + PLUGIN_OPEN, + O_WRONLY | O_CREAT | O_EXCL | O_BINARY); + if (mfd < 0) + { + fprintf(stderr, "ERROR: cannot create '%s': %s.\n", manifest, + strerror(my_errno)); + goto end; + } + if (!(m= my_fdopen(mfd, manifest, O_WRONLY, MYF(MY_WME)))) + { + my_close(mfd, MYF(0)); + plugin_file_op(basedir, manifest + strlen(basedir) + 1, PLUGIN_DELETE, 0); + goto end; + } + } + { + char today[16]; + struct tm *t; + time_t now= time(0); + t= localtime(&now); + strftime(today, sizeof(today), "%Y-%m-%d", t); + if (manifest_append(m, "name", name) || + manifest_append(m, "source", source) || + manifest_append(m, "sha256", sha256) || + manifest_append(m, "date", today) || + (topdir[0] && manifest_append(m, "topdir", topdir))) + goto rollback; + } + if (tar_extract(archive, contents, basedir, topdir, &entries, m)) + goto rollback; + my_fclose(m, MYF(0)); + m= 0; + + printf("Plugin '%s' installed into %s.\n", name, basedir); + print_enable_instructions(basedir, &entries); + error= 0; + goto end; + +rollback: + if (m) + my_fclose(m, MYF(0)); + fprintf(stderr, "ERROR: installation of '%s' failed, removing what was " + "written.\n", name); + /* the manifest was just written by this tool and may be incomplete */ + manifest_remove(basedir, manifest, 0); +end: + delete_dynamic(&entries); +close_archive: + if (contents) + my_fclose(contents, MYF(0)); + return error; #endif } @@ -2234,8 +4011,19 @@ static int do_uninstall(const char *name, const char *basedir) #endif return error; #else - printf("uninstall: '%s' (%s installation%s%s) not implemented yet\n", name, - INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + char manifest[FN_REFLEN]; + + if (build_manifest_path(manifest, sizeof(manifest), basedir, name)) + return 1; + if (!file_exists(manifest)) + { + fprintf(stderr, "ERROR: plugin '%s' is not installed.\n", name); + return 1; + } + if (manifest_remove(basedir, manifest, name)) + return 1; + if (!opt_dry_run) + printf("Plugin '%s' uninstalled from %s.\n", name, basedir); return 0; #endif } @@ -2244,8 +4032,7 @@ static int do_uninstall(const char *name, const char *basedir) /** Run the new package-manager style commands. - Parses the options (--help, --version, etc. are handled by - handle_options), then validates the verb and the plugin name and + Options have already been parsed by main. Validate the verb and name and dispatches to the appropriate command handler. The plugin name is normalized to lower case before validation. @@ -2261,10 +4048,12 @@ static int run_new_command(int argc, char **argv) char basedir[FN_REFLEN]; const char *verb; size_t i, len; - int error, is_search; + int is_search; - if ((error= handle_options(&argc, &argv, my_long_options, get_one_option))) - return 1; + /* --print-defaults only displays information; it must not fall through + into an install or uninstall that changes the system */ + if (opt_print_defaults) + return 0; if (argc < 1) { @@ -2309,6 +4098,31 @@ static int run_new_command(int argc, char **argv) return 1; } +#ifdef INSTALL_LAYOUT_DEB + /* APT interprets a trailing '-' on an install/remove operand as removal. */ + if (!is_search && name[strlen(name) - 1] == '-') + { + fprintf(stderr, "ERROR: plugin names ending in '-' cannot be passed " + "to apt-get.\n"); + return 1; + } +#endif + + if (is_search && (opt_file || opt_sha256)) + { + fprintf(stderr, "ERROR: --file and --sha256 are only supported with " + "install.\n"); + return 1; + } +#ifdef PKG_DELEGATION + if (is_search && opt_base_url) + { + fprintf(stderr, "ERROR: --base-url is only for tarball installations; " + "search uses the native package repositories on this system.\n"); + return 1; + } +#endif + if (detect_install_method(basedir, sizeof(basedir))) return 1;