dhcpc: code shrink in good_hostname
[oweals/busybox.git] / procps / sysctl.c
index 3607a2364918a26b6b25f178ec6e4250a6825012..6d77185caec8ef5b2c6887b2a1df85f749acab13 100644 (file)
  *
  * Copyright 1999 George Staikos
  *
- * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
+ * Licensed under GPLv2 or later, see file LICENSE in this source tree.
  *
  * Changelog:
- *     v1.01:
- *             - added -p <preload> to preload values from a file
- *     v1.01.1
- *             - busybox applet aware by <solar@gentoo.org>
- *
+ * v1.01   - added -p <preload> to preload values from a file
+ * v1.01.1 - busybox applet aware by <solar@gentoo.org>
  */
+//config:config BB_SYSCTL
+//config:      bool "sysctl (7.4 kb)"
+//config:      default y
+//config:      help
+//config:      Configure kernel parameters at runtime.
+
+//applet:IF_BB_SYSCTL(APPLET_NOEXEC(sysctl, sysctl, BB_DIR_SBIN, BB_SUID_DROP, sysctl))
+
+//kbuild:lib-$(CONFIG_BB_SYSCTL) += sysctl.o
+
+//usage:#define sysctl_trivial_usage
+//usage:       "-p [-enq] [FILE...] / [-enqaw] [KEY[=VALUE]]..."
+//usage:#define sysctl_full_usage "\n\n"
+//usage:       "Show/set kernel parameters\n"
+//usage:     "\n       -p      Set values from FILEs (default /etc/sysctl.conf)"
+//usage:     "\n       -e      Don't warn about unknown keys"
+//usage:     "\n       -n      Don't show key names"
+//usage:     "\n       -q      Quiet"
+//usage:     "\n       -a      Show all values"
+/* Same as -a, no need to show it */
+/* //usage:     "\n    -A      Show all values in table form" */
+//usage:     "\n       -w      Set values"
+//usage:
+//usage:#define sysctl_example_usage
+//usage:       "sysctl [-n] [-e] variable...\n"
+//usage:       "sysctl [-n] [-e] [-q] -w variable=value...\n"
+//usage:       "sysctl [-n] [-e] -a\n"
+//usage:       "sysctl [-n] [-e] [-q] -p file  (default /etc/sysctl.conf)\n"
+//usage:       "sysctl [-n] [-e] -A\n"
 
 #include "libbb.h"
 
-static int sysctl_read_setting(const char *setting);
-static int sysctl_write_setting(const char *setting);
-static int sysctl_display_all(const char *path);
-static int sysctl_preload_file_and_exit(const char *filename);
-
-static const char ETC_SYSCTL_CONF[] ALIGN1 = "/etc/sysctl.conf";
-static const char PROC_SYS[] ALIGN1 = "/proc/sys/";
-enum { strlen_PROC_SYS = sizeof(PROC_SYS) - 1 };
-
-/* error messages */
-static const char ERR_MALFORMED_SETTING[] ALIGN1 =
-       "error: malformed setting '%s'";
-static const char ERR_NO_EQUALS[] ALIGN1 =
-       "error: '%s' must be of the form name=value";
-static const char ERR_INVALID_KEY[] ALIGN1 =
-       "error: '%s' is an unknown key";
-static const char ERR_UNKNOWN_WRITING[] ALIGN1 =
-       "error setting key '%s'";
-static const char ERR_UNKNOWN_READING[] ALIGN1 =
-       "error reading key '%s'";
-static const char ERR_PERMISSION_DENIED[] ALIGN1 =
-       "error: permission denied on key '%s'";
-static const char WARN_BAD_LINE[] ALIGN1 =
-       "warning: %s(%d): invalid syntax, continuing";
-
-
-static void dwrite_str(int fd, const char *buf)
-{
-       write(fd, buf, strlen(buf));
-}
-
 enum {
        FLAG_SHOW_KEYS       = 1 << 0,
        FLAG_SHOW_KEY_ERRORS = 1 << 1,
        FLAG_TABLE_FORMAT    = 1 << 2, /* not implemented */
        FLAG_SHOW_ALL        = 1 << 3,
        FLAG_PRELOAD_FILE    = 1 << 4,
+       /* NB: procps 3.2.8 does not require -w for KEY=VAL to work, it only rejects non-KEY=VAL form */
        FLAG_WRITE           = 1 << 5,
+       FLAG_QUIET           = 1 << 6,
 };
+#define OPTION_STR "neAapwq"
 
-int sysctl_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
-int sysctl_main(int argc UNUSED_PARAM, char **argv)
-{
-       int retval;
-       int opt;
-
-       opt = getopt32(argv, "+neAapw"); /* '+' - stop on first non-option */
-       argv += optind;
-       opt ^= (FLAG_SHOW_KEYS | FLAG_SHOW_KEY_ERRORS);
-       option_mask32 ^= (FLAG_SHOW_KEYS | FLAG_SHOW_KEY_ERRORS);
-
-       if (opt & (FLAG_TABLE_FORMAT | FLAG_SHOW_ALL))
-               return sysctl_display_all(PROC_SYS);
-       if (opt & FLAG_PRELOAD_FILE)
-               return sysctl_preload_file_and_exit(*argv ? *argv : ETC_SYSCTL_CONF);
-
-       retval = 0;
-       while (*argv) {
-               if (opt & FLAG_WRITE)
-                       retval |= sysctl_write_setting(*argv);
-               else
-                       retval |= sysctl_read_setting(*argv);
-               argv++;
-       }
-
-       return retval;
-} /* end sysctl_main() */
-
-/*
- * preload the sysctl's from a conf file
- * - we parse the file and then reform it (strip out whitespace)
- */
-#define PRELOAD_BUF 256
-
-static int sysctl_preload_file_and_exit(const char *filename)
+static void sysctl_dots_to_slashes(char *name)
 {
-       int lineno;
-       char oneline[PRELOAD_BUF];
-       char buffer[PRELOAD_BUF];
-       char *name, *value;
-       FILE *fp;
-
-       fp = xfopen(filename, "r");
-
-       lineno = 0;
-       while (fgets(oneline, sizeof(oneline) - 1, fp)) {
-               lineno++;
-               trim(oneline);
-               if (oneline[0] == '#' || oneline[0] == ';')
-                       continue;
-               if (!oneline[0] || !oneline[1])
-                       continue;
-
-               name = strtok(oneline, "=");
-               if (!name) {
-                       bb_error_msg(WARN_BAD_LINE, filename, lineno);
-                       continue;
-               }
-               trim(name);
-               if (!*name) {
-                       bb_error_msg(WARN_BAD_LINE, filename, lineno);
-                       continue;
+       char *cptr, *last_good, *end, *slash;
+       char end_ch;
+
+       end = strchrnul(name, '=');
+
+       slash = strchrnul(name, '/');
+       if (slash < end
+        && strchrnul(name, '.') < slash
+       ) {
+               /* There are both dots and slashes, and 1st dot is
+                * before 1st slash.
+                * (IOW: not raw, unmangled a/b/c.d format)
+                *
+                * procps supports this syntax for names with dots:
+                *  net.ipv4.conf.eth0/100.mc_forwarding
+                * (dots and slashes are simply swapped)
+                */
+               while (end != name) {
+                       end--;
+                       if (*end == '.') *end = '/';
+                       else if (*end == '/') *end = '.';
                }
-
-               value = strtok(NULL, "\n\r");
-               if (!value) {
-                       bb_error_msg(WARN_BAD_LINE, filename, lineno);
-                       continue;
-               }
-               while (*value == ' ' || *value == '\t')
-                       value++;
-               if (!*value) {
-                       bb_error_msg(WARN_BAD_LINE, filename, lineno);
-                       continue;
+               return;
+       }
+       /* else: use our old behavior: */
+
+       /* Convert minimum number of '.' to '/' so that
+        * we end up with existing file's name.
+        *
+        * Example from bug 3894:
+        * net.ipv4.conf.eth0.100.mc_forwarding ->
+        * net/ipv4/conf/eth0.100/mc_forwarding
+        * NB: net/ipv4/conf/eth0/mc_forwarding *also exists*,
+        * therefore we must start from the end, and if
+        * we replaced even one . -> /, start over again,
+        * but never replace dots before the position
+        * where last replacement occurred.
+        *
+        * Another bug we later had is that
+        * net.ipv4.conf.eth0.100
+        * (without .mc_forwarding) was mishandled.
+        *
+        * To set up testing: modprobe 8021q; vconfig add eth0 100
+        */
+       end_ch = *end;
+       *end = '.'; /* trick the loop into trying full name too */
+
+       last_good = name - 1;
+ again:
+       cptr = end;
+       while (cptr > last_good) {
+               if (*cptr == '.') {
+                       *cptr = '\0';
+                       //bb_error_msg("trying:'%s'", name);
+                       if (access(name, F_OK) == 0) {
+                               *cptr = '/';
+                               //bb_error_msg("replaced:'%s'", name);
+                               last_good = cptr;
+                               goto again;
+                       }
+                       *cptr = '.';
                }
-
-               /* safe because sizeof(oneline) == sizeof(buffer) */
-               sprintf(buffer, "%s=%s", name, value);
-               sysctl_write_setting(buffer);
+               cptr--;
        }
-       if (ENABLE_FEATURE_CLEAN_UP)
-               fclose(fp);
-       return 0;
-} /* end sysctl_preload_file_and_exit() */
+       *end = end_ch;
+}
 
-/*
- *     Write a single sysctl setting
- */
-static int sysctl_write_setting(const char *setting)
+static int sysctl_act_on_setting(char *setting)
 {
-       int retval;
-       const char *name;
-       const char *value;
-       const char *equals;
-       char *tmpname, *outname, *cptr;
-       int fd;
-
-       name = setting;
-       equals = strchr(setting, '=');
-       if (!equals) {
-               bb_error_msg(ERR_NO_EQUALS, setting);
-               return EXIT_FAILURE;
+       int fd, retval = EXIT_SUCCESS;
+       char *cptr, *outname;
+       char *value = value; /* for compiler */
+       bool writing = (option_mask32 & FLAG_WRITE);
+
+       outname = xstrdup(setting);
+
+       cptr = outname;
+       while (*cptr) {
+               if (*cptr == '/')
+                       *cptr = '.';
+               else if (*cptr == '.')
+                       *cptr = '/';
+               cptr++;
        }
 
-       value = equals + 1;     /* point to the value in name=value */
-       if (name == equals || !*value) {
-               bb_error_msg(ERR_MALFORMED_SETTING, setting);
-               return EXIT_FAILURE;
+       cptr = strchr(setting, '=');
+       if (cptr)
+               writing = 1;
+       if (writing) {
+               if (!cptr) {
+                       bb_error_msg("error: '%s' must be of the form name=value",
+                               outname);
+                       retval = EXIT_FAILURE;
+                       goto end;
+               }
+               value = cptr + 1;  /* point to the value in name=value */
+               if (setting == cptr /* "name" can't be empty */
+                /* || !*value - WRONG: "sysctl net.ipv4.ip_local_reserved_ports=" is a valid syntax (clears the value) */
+               ) {
+                       bb_error_msg("error: malformed setting '%s'", outname);
+                       retval = EXIT_FAILURE;
+                       goto end;
+               }
+               *cptr = '\0';
+               outname[cptr - setting] = '\0';
+               /* procps 3.2.7 actually uses these flags */
+               fd = open(setting, O_WRONLY|O_CREAT|O_TRUNC, 0666);
+       } else {
+               fd = open(setting, O_RDONLY);
        }
 
-       tmpname = xasprintf("%s%.*s", PROC_SYS, (int)(equals - name), name);
-       outname = xstrdup(tmpname + strlen_PROC_SYS);
-
-       while ((cptr = strchr(tmpname, '.')) != NULL)
-               *cptr = '/';
-
-       while ((cptr = strchr(outname, '/')) != NULL)
-               *cptr = '.';
-
-       fd = open(tmpname, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        if (fd < 0) {
                switch (errno) {
+               case EACCES:
+                       /* Happens for write-only settings, e.g. net.ipv6.route.flush */
+                       goto end;
                case ENOENT:
                        if (option_mask32 & FLAG_SHOW_KEY_ERRORS)
-                               bb_error_msg(ERR_INVALID_KEY, outname);
-                       break;
-               case EACCES:
-                       bb_perror_msg(ERR_PERMISSION_DENIED, outname);
+                               bb_error_msg("error: '%s' is an unknown key", outname);
                        break;
                default:
-                       bb_perror_msg(ERR_UNKNOWN_WRITING, outname);
+                       bb_perror_msg("error %sing key '%s'",
+                                       writing ?
+                                               "sett" : "read",
+                                       outname);
                        break;
                }
                retval = EXIT_FAILURE;
+               goto end;
+       }
+
+       if (writing) {
+//TODO: procps 3.2.7 writes "value\n", note trailing "\n"
+               xwrite_str(fd, value);
+               close(fd);
+               if (!(option_mask32 & FLAG_QUIET)) {
+                       if (option_mask32 & FLAG_SHOW_KEYS)
+                               printf("%s = ", outname);
+                       puts(value);
+               }
        } else {
-               dwrite_str(fd, value);
+               char c;
+
+               value = cptr = xmalloc_read(fd, NULL);
                close(fd);
-               if (option_mask32 & FLAG_SHOW_KEYS) {
-                       printf("%s = ", outname);
+               if (value == NULL) {
+                       bb_perror_msg("error reading key '%s'", outname);
+                       retval = EXIT_FAILURE;
+                       goto end;
                }
-               puts(value);
-               retval = EXIT_SUCCESS;
-       }
 
-       free(tmpname);
+               /* dev.cdrom.info and sunrpc.transports, for example,
+                * are multi-line. Try "sysctl sunrpc.transports"
+                */
+               while ((c = *cptr) != '\0') {
+                       if (option_mask32 & FLAG_SHOW_KEYS)
+                               printf("%s = ", outname);
+                       while (1) {
+                               fputc(c, stdout);
+                               cptr++;
+                               if (c == '\n')
+                                       break;
+                               c = *cptr;
+                               if (c == '\0')
+                                       break;
+                       }
+               }
+               free(value);
+       }
+ end:
        free(outname);
        return retval;
-} /* end sysctl_write_setting() */
+}
 
-/*
- *     Read a sysctl setting
- */
-static int sysctl_read_setting(const char *name)
+static int sysctl_act_recursive(const char *path)
 {
-       int retval;
-       char *tmpname, *outname, *cptr;
-       char inbuf[1025];
-       FILE *fp;
-
-       if (!*name) {
-               if (option_mask32 & FLAG_SHOW_KEY_ERRORS)
-                       bb_error_msg(ERR_INVALID_KEY, name);
-               return -1;
-       }
-
-       tmpname = concat_path_file(PROC_SYS, name);
-       outname = xstrdup(tmpname + strlen_PROC_SYS);
-
-       while ((cptr = strchr(tmpname, '.')) != NULL)
-               *cptr = '/';
-       while ((cptr = strchr(outname, '/')) != NULL)
-               *cptr = '.';
+       struct stat buf;
+       int retval = 0;
 
-       fp = fopen(tmpname, "r");
-       if (fp == NULL) {
-               switch (errno) {
-               case ENOENT:
-                       if (option_mask32 & FLAG_SHOW_KEY_ERRORS)
-                               bb_error_msg(ERR_INVALID_KEY, outname);
-                       break;
-               case EACCES:
-                       bb_error_msg(ERR_PERMISSION_DENIED, outname);
-                       break;
-               default:
-                       bb_perror_msg(ERR_UNKNOWN_READING, outname);
-                       break;
+       if (!(option_mask32 & FLAG_WRITE)
+        && stat(path, &buf) == 0
+        && S_ISDIR(buf.st_mode)
+       ) {
+               struct dirent *entry;
+               DIR *dirp;
+
+               dirp = opendir(path);
+               if (dirp == NULL)
+                       return -1;
+               while ((entry = readdir(dirp)) != NULL) {
+                       char *next = concat_subpath_file(path, entry->d_name);
+                       if (next == NULL)
+                               continue; /* d_name is "." or ".." */
+                       /* if path was ".", drop "./" prefix: */
+                       retval |= sysctl_act_recursive((next[0] == '.' && next[1] == '/') ?
+                                       next + 2 : next);
+                       free(next);
                }
-               retval = EXIT_FAILURE;
+               closedir(dirp);
        } else {
-               while (fgets(inbuf, sizeof(inbuf) - 1, fp)) {
-                       if (option_mask32 & FLAG_SHOW_KEYS) {
-                               printf("%s = ", outname);
-                       }
-                       fputs(inbuf, stdout);
-               }
-               fclose(fp);
-               retval = EXIT_SUCCESS;
+               char *name = xstrdup(path);
+               retval |= sysctl_act_on_setting(name);
+               free(name);
        }
 
-       free(tmpname);
-       free(outname);
        return retval;
-} /* end sysctl_read_setting() */
+}
 
-/*
- *     Display all the sysctl settings
+/* Set sysctl's from a conf file. Format example:
+ * # Controls IP packet forwarding
+ * net.ipv4.ip_forward = 0
  */
-static int sysctl_display_all(const char *path)
+static int sysctl_handle_preload_file(const char *filename)
 {
-       int retval = 0;
-       DIR *dp;
-       struct dirent *de;
-       char *tmpdir;
-       struct stat ts;
-
-       dp = opendir(path);
-       if (!dp) {
-               return EXIT_FAILURE;
+       char *token[2];
+       parser_t *parser;
+       int parse_flags;
+
+       parser = config_open(filename);
+       /* Must do it _after_ config_open(): */
+       xchdir("/proc/sys");
+
+       parse_flags = 0;
+       parse_flags &= ~PARSE_COLLAPSE;   // NO (var==val is not var=val) - treat consecutive delimiters as one
+       parse_flags &= ~PARSE_TRIM;       // NO - trim leading and trailing delimiters
+       parse_flags |= PARSE_GREEDY;      // YES - last token takes entire remainder of the line
+       parse_flags &= ~PARSE_MIN_DIE;    // NO - die if < min tokens found
+       parse_flags &= ~PARSE_EOL_COMMENTS; // NO (only first char) - comments are recognized even if not first char
+       parse_flags |= PARSE_ALT_COMMENTS;// YES - two comment chars: ';' and '#'
+       /* <space><tab><space>#comment is also comment, not strictly 1st char only */
+       parse_flags |= PARSE_WS_COMMENTS; // YES - comments are recognized even if there is whitespace before
+       while (config_read(parser, token, 2, 2, ";#=", parse_flags)) {
+               char *tp;
+
+               trim(token[1]);
+               tp = trim(token[0]);
+               sysctl_dots_to_slashes(token[0]);
+               /* ^^^converted in-place. tp still points to NUL */
+               /* now, add "=TOKEN1" */
+               *tp++ = '=';
+               overlapping_strcpy(tp, token[1]);
+
+               sysctl_act_on_setting(token[0]);
+       }
+       if (ENABLE_FEATURE_CLEAN_UP)
+               config_close(parser);
+       return 0;
+}
+
+int sysctl_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int sysctl_main(int argc UNUSED_PARAM, char **argv)
+{
+       int retval;
+       int opt;
+
+       opt = getopt32(argv, "+" OPTION_STR); /* '+' - stop on first non-option */
+       argv += optind;
+       opt ^= (FLAG_SHOW_KEYS | FLAG_SHOW_KEY_ERRORS);
+       option_mask32 = opt;
+
+       if (opt & FLAG_PRELOAD_FILE) {
+               int cur_dir_fd;
+               option_mask32 |= FLAG_WRITE;
+               if (!*argv)
+                       *--argv = (char*)"/etc/sysctl.conf";
+               cur_dir_fd = xopen(".", O_RDONLY | O_DIRECTORY);
+               do {
+                       /* xchdir("/proc/sys") is inside */
+                       sysctl_handle_preload_file(*argv);
+                       xfchdir(cur_dir_fd); /* files can be relative, must restore cwd */
+               } while (*++argv);
+               return 0; /* procps-ng 3.3.10 does not flag parse errors */
+       }
+       xchdir("/proc/sys");
+       if (opt & (FLAG_TABLE_FORMAT | FLAG_SHOW_ALL)) {
+               return sysctl_act_recursive(".");
+       }
+
+//TODO: if(!argv[0]) bb_show_usage() ?
+
+       retval = 0;
+       while (*argv) {
+               sysctl_dots_to_slashes(*argv);
+               retval |= sysctl_act_recursive(*argv);
+               argv++;
        }
-       while ((de = readdir(dp)) != NULL) {
-               tmpdir = concat_subpath_file(path, de->d_name);
-               if (tmpdir == NULL)
-                       continue; /* . or .. */
-               if (stat(tmpdir, &ts) != 0) {
-                       bb_perror_msg(tmpdir);
-               } else if (S_ISDIR(ts.st_mode)) {
-                       retval |= sysctl_display_all(tmpdir);
-               } else {
-                       retval |= sysctl_read_setting(tmpdir + strlen_PROC_SYS);
-               }
-               free(tmpdir);
-       } /* end while */
-       closedir(dp);
 
        return retval;
-} /* end sysctl_display_all() */
+}