summaryrefslogtreecommitdiffstats
path: root/src/udev/udev-format.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/udev/udev-format.c550
1 files changed, 550 insertions, 0 deletions
diff --git a/src/udev/udev-format.c b/src/udev/udev-format.c
new file mode 100644
index 0000000..05ed9fd
--- /dev/null
+++ b/src/udev/udev-format.c
@@ -0,0 +1,550 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "device-util.h"
+#include "errno-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-event.h"
+#include "udev-format.h"
+#include "udev-util.h"
+
+typedef enum {
+ FORMAT_SUBST_DEVNODE,
+ FORMAT_SUBST_ATTR,
+ FORMAT_SUBST_ENV,
+ FORMAT_SUBST_KERNEL,
+ FORMAT_SUBST_KERNEL_NUMBER,
+ FORMAT_SUBST_DRIVER,
+ FORMAT_SUBST_DEVPATH,
+ FORMAT_SUBST_ID,
+ FORMAT_SUBST_MAJOR,
+ FORMAT_SUBST_MINOR,
+ FORMAT_SUBST_RESULT,
+ FORMAT_SUBST_PARENT,
+ FORMAT_SUBST_NAME,
+ FORMAT_SUBST_LINKS,
+ FORMAT_SUBST_ROOT,
+ FORMAT_SUBST_SYS,
+ _FORMAT_SUBST_TYPE_MAX,
+ _FORMAT_SUBST_TYPE_INVALID = -EINVAL,
+} FormatSubstitutionType;
+
+struct subst_map_entry {
+ const char *name;
+ const char fmt;
+ FormatSubstitutionType type;
+};
+
+static const struct subst_map_entry map[] = {
+ { .name = "devnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE },
+ { .name = "tempnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, /* deprecated */
+ { .name = "attr", .fmt = 's', .type = FORMAT_SUBST_ATTR },
+ { .name = "sysfs", .fmt = 's', .type = FORMAT_SUBST_ATTR }, /* deprecated */
+ { .name = "env", .fmt = 'E', .type = FORMAT_SUBST_ENV },
+ { .name = "kernel", .fmt = 'k', .type = FORMAT_SUBST_KERNEL },
+ { .name = "number", .fmt = 'n', .type = FORMAT_SUBST_KERNEL_NUMBER },
+ { .name = "driver", .fmt = 'd', .type = FORMAT_SUBST_DRIVER },
+ { .name = "devpath", .fmt = 'p', .type = FORMAT_SUBST_DEVPATH },
+ { .name = "id", .fmt = 'b', .type = FORMAT_SUBST_ID },
+ { .name = "major", .fmt = 'M', .type = FORMAT_SUBST_MAJOR },
+ { .name = "minor", .fmt = 'm', .type = FORMAT_SUBST_MINOR },
+ { .name = "result", .fmt = 'c', .type = FORMAT_SUBST_RESULT },
+ { .name = "parent", .fmt = 'P', .type = FORMAT_SUBST_PARENT },
+ { .name = "name", .fmt = 'D', .type = FORMAT_SUBST_NAME },
+ { .name = "links", .fmt = 'L', .type = FORMAT_SUBST_LINKS },
+ { .name = "root", .fmt = 'r', .type = FORMAT_SUBST_ROOT },
+ { .name = "sys", .fmt = 'S', .type = FORMAT_SUBST_SYS },
+};
+
+static const char *format_type_to_string(FormatSubstitutionType t) {
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (map[i].type == t)
+ return map[i].name;
+ return NULL;
+}
+
+static char format_type_to_char(FormatSubstitutionType t) {
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (map[i].type == t)
+ return map[i].fmt;
+ return '\0';
+}
+
+static int get_subst_type(const char **str, bool strict, FormatSubstitutionType *ret_type, char ret_attr[static UDEV_PATH_SIZE]) {
+ const char *p = *str, *q = NULL;
+ size_t i;
+
+ assert(str);
+ assert(*str);
+ assert(ret_type);
+ assert(ret_attr);
+
+ if (*p == '$') {
+ p++;
+ if (*p == '$') {
+ *str = p;
+ return 0;
+ }
+ for (i = 0; i < ELEMENTSOF(map); i++)
+ if ((q = startswith(p, map[i].name)))
+ break;
+ } else if (*p == '%') {
+ p++;
+ if (*p == '%') {
+ *str = p;
+ return 0;
+ }
+
+ for (i = 0; i < ELEMENTSOF(map); i++)
+ if (*p == map[i].fmt) {
+ q = p + 1;
+ break;
+ }
+ } else
+ return 0;
+ if (!q)
+ /* When 'strict' flag is set, then '$' and '%' must be escaped. */
+ return strict ? -EINVAL : 0;
+
+ if (*q == '{') {
+ const char *start, *end;
+ size_t len;
+
+ start = q + 1;
+ end = strchr(start, '}');
+ if (!end)
+ return -EINVAL;
+
+ len = end - start;
+ if (len == 0 || len >= UDEV_PATH_SIZE)
+ return -EINVAL;
+
+ strnscpy(ret_attr, UDEV_PATH_SIZE, start, len);
+ q = end + 1;
+ } else
+ *ret_attr = '\0';
+
+ *str = q;
+ *ret_type = map[i].type;
+ return 1;
+}
+
+static int safe_atou_optional_plus(const char *s, unsigned *ret) {
+ const char *p;
+ int r;
+
+ assert(s);
+ assert(ret);
+
+ /* Returns 1 if plus, 0 if no plus, negative on error */
+
+ p = endswith(s, "+");
+ if (p)
+ s = strndupa_safe(s, p - s);
+
+ r = safe_atou(s, ret);
+ if (r < 0)
+ return r;
+
+ return !!p;
+}
+
+static ssize_t udev_event_subst_format(
+ UdevEvent *event,
+ FormatSubstitutionType type,
+ const char *attr,
+ char *dest,
+ size_t l,
+ Hashmap *global_props,
+ bool *ret_truncated) {
+
+ sd_device *parent, *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ const char *val = NULL;
+ bool truncated = false;
+ char *s = dest;
+ int r;
+
+ switch (type) {
+ case FORMAT_SUBST_DEVPATH:
+ r = sd_device_get_devpath(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_KERNEL:
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_KERNEL_NUMBER:
+ r = sd_device_get_sysnum(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_ID:
+ if (!event->dev_parent)
+ goto null_terminate;
+ r = sd_device_get_sysname(event->dev_parent, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_DRIVER:
+ if (!event->dev_parent)
+ goto null_terminate;
+ r = sd_device_get_driver(event->dev_parent, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_MAJOR:
+ case FORMAT_SUBST_MINOR: {
+ dev_t devnum;
+
+ r = sd_device_get_devnum(dev, &devnum);
+ if (r < 0 && r != -ENOENT)
+ return r;
+ strpcpyf_full(&s, l, &truncated, "%u", r < 0 ? 0 : type == FORMAT_SUBST_MAJOR ? major(devnum) : minor(devnum));
+ break;
+ }
+ case FORMAT_SUBST_RESULT: {
+ unsigned index = 0; /* 0 means whole string */
+ bool has_plus;
+
+ if (!event->program_result)
+ goto null_terminate;
+
+ if (!isempty(attr)) {
+ r = safe_atou_optional_plus(attr, &index);
+ if (r < 0)
+ return r;
+
+ has_plus = r;
+ }
+
+ if (index == 0)
+ strpcpy_full(&s, l, event->program_result, &truncated);
+ else {
+ const char *start, *p;
+ unsigned i;
+
+ p = skip_leading_chars(event->program_result, NULL);
+
+ for (i = 1; i < index; i++) {
+ while (*p && !strchr(WHITESPACE, *p))
+ p++;
+ p = skip_leading_chars(p, NULL);
+ if (*p == '\0')
+ break;
+ }
+ if (i != index) {
+ log_device_debug(dev, "requested part of result string not found");
+ goto null_terminate;
+ }
+
+ start = p;
+ /* %c{2+} copies the whole string from the second part on */
+ if (has_plus)
+ strpcpy_full(&s, l, start, &truncated);
+ else {
+ while (*p && !strchr(WHITESPACE, *p))
+ p++;
+ strnpcpy_full(&s, l, start, p - start, &truncated);
+ }
+ }
+ break;
+ }
+ case FORMAT_SUBST_ATTR: {
+ char vbuf[UDEV_NAME_SIZE];
+ int count;
+ bool t;
+
+ if (isempty(attr))
+ return -EINVAL;
+
+ /* try to read the value specified by "[dmi/id]product_name" */
+ if (udev_resolve_subsys_kernel(attr, vbuf, sizeof(vbuf), true) == 0)
+ val = vbuf;
+
+ /* try to read the attribute the device */
+ if (!val)
+ (void) sd_device_get_sysattr_value(dev, attr, &val);
+
+ /* try to read the attribute of the parent device, other matches have selected */
+ if (!val && event->dev_parent && event->dev_parent != dev)
+ (void) sd_device_get_sysattr_value(event->dev_parent, attr, &val);
+
+ if (!val)
+ goto null_terminate;
+
+ /* strip trailing whitespace, and replace unwanted characters */
+ if (val != vbuf)
+ strscpy_full(vbuf, sizeof(vbuf), val, &truncated);
+ delete_trailing_chars(vbuf, NULL);
+ count = udev_replace_chars(vbuf, UDEV_ALLOWED_CHARS_INPUT);
+ if (count > 0)
+ log_device_debug(dev, "%i character(s) replaced", count);
+ strpcpy_full(&s, l, vbuf, &t);
+ truncated = truncated || t;
+ break;
+ }
+ case FORMAT_SUBST_PARENT:
+ r = sd_device_get_parent(dev, &parent);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ r = sd_device_get_devname(parent, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated);
+ break;
+ case FORMAT_SUBST_DEVNODE:
+ r = sd_device_get_devname(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_NAME:
+ if (event->name)
+ strpcpy_full(&s, l, event->name, &truncated);
+ else if (sd_device_get_devname(dev, &val) >= 0)
+ strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated);
+ else {
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ }
+ break;
+ case FORMAT_SUBST_LINKS:
+ FOREACH_DEVICE_DEVLINK(dev, link) {
+ if (s == dest)
+ strpcpy_full(&s, l, link + STRLEN("/dev/"), &truncated);
+ else
+ strpcpyl_full(&s, l, &truncated, " ", link + STRLEN("/dev/"), NULL);
+ if (truncated)
+ break;
+ }
+ if (s == dest)
+ goto null_terminate;
+ break;
+ case FORMAT_SUBST_ROOT:
+ strpcpy_full(&s, l, "/dev", &truncated);
+ break;
+ case FORMAT_SUBST_SYS:
+ strpcpy_full(&s, l, "/sys", &truncated);
+ break;
+ case FORMAT_SUBST_ENV:
+ if (isempty(attr))
+ return -EINVAL;
+ r = device_get_property_value_with_fallback(dev, attr, global_props, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ return s - dest;
+
+null_terminate:
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ *s = '\0';
+ return 0;
+}
+
+size_t udev_event_apply_format(
+ UdevEvent *event,
+ const char *src,
+ char *dest,
+ size_t size,
+ bool replace_whitespace,
+ Hashmap *global_props,
+ bool *ret_truncated) {
+
+ bool truncated = false;
+ const char *s = ASSERT_PTR(src);
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(dest);
+ assert(size > 0);
+
+ while (*s) {
+ FormatSubstitutionType type;
+ char attr[UDEV_PATH_SIZE];
+ ssize_t subst_len;
+ bool t;
+
+ r = get_subst_type(&s, false, &type, attr);
+ if (r < 0) {
+ log_device_warning_errno(event->dev, r, "Invalid format string, ignoring: %s", src);
+ break;
+ } else if (r == 0) {
+ if (size < 2) {
+ /* need space for this char and the terminating NUL */
+ truncated = true;
+ break;
+ }
+ *dest++ = *s++;
+ size--;
+ continue;
+ }
+
+ subst_len = udev_event_subst_format(event, type, attr, dest, size, global_props, &t);
+ if (subst_len < 0) {
+ log_device_warning_errno(event->dev, subst_len,
+ "Failed to substitute variable '$%s' or apply format '%%%c', ignoring: %m",
+ format_type_to_string(type), format_type_to_char(type));
+ break;
+ }
+
+ truncated = truncated || t;
+
+ /* FORMAT_SUBST_RESULT handles spaces itself */
+ if (replace_whitespace && type != FORMAT_SUBST_RESULT)
+ /* udev_replace_whitespace can replace in-place,
+ * and does nothing if subst_len == 0 */
+ subst_len = udev_replace_whitespace(dest, dest, subst_len);
+
+ dest += subst_len;
+ size -= subst_len;
+ }
+
+ assert(size >= 1);
+
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ *dest = '\0';
+ return size;
+}
+
+int udev_check_format(const char *value, size_t *offset, const char **hint) {
+ FormatSubstitutionType type;
+ const char *s = value;
+ char attr[UDEV_PATH_SIZE];
+ int r;
+
+ while (*s) {
+ r = get_subst_type(&s, true, &type, attr);
+ if (r < 0) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "invalid substitution type";
+ return r;
+ } else if (r == 0) {
+ s++;
+ continue;
+ }
+
+ if (IN_SET(type, FORMAT_SUBST_ATTR, FORMAT_SUBST_ENV) && isempty(attr)) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "attribute value missing";
+ return -EINVAL;
+ }
+
+ if (type == FORMAT_SUBST_RESULT && !isempty(attr)) {
+ unsigned i;
+
+ r = safe_atou_optional_plus(attr, &i);
+ if (r < 0) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "attribute value not a valid number";
+ return r;
+ }
+ }
+ }
+
+ return 0;
+}
+
+int udev_resolve_subsys_kernel(const char *string, char *result, size_t maxsize, bool read_value) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ _cleanup_free_ char *temp = NULL;
+ char *subsys, *sysname, *attr;
+ const char *val;
+ int r;
+
+ assert(string);
+ assert(result);
+
+ /* handle "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */
+
+ if (string[0] != '[')
+ return -EINVAL;
+
+ temp = strdup(string);
+ if (!temp)
+ return -ENOMEM;
+
+ subsys = &temp[1];
+
+ sysname = strchr(subsys, '/');
+ if (!sysname)
+ return -EINVAL;
+ sysname[0] = '\0';
+ sysname = &sysname[1];
+
+ attr = strchr(sysname, ']');
+ if (!attr)
+ return -EINVAL;
+ attr[0] = '\0';
+ attr = &attr[1];
+ if (attr[0] == '/')
+ attr = &attr[1];
+ if (attr[0] == '\0')
+ attr = NULL;
+
+ if (read_value && !attr)
+ return -EINVAL;
+
+ r = sd_device_new_from_subsystem_sysname(&dev, subsys, sysname);
+ if (r < 0)
+ return r;
+
+ if (read_value) {
+ r = sd_device_get_sysattr_value(dev, attr, &val);
+ if (r < 0 && !ERRNO_IS_PRIVILEGE(r) && r != -ENOENT)
+ return r;
+ if (r >= 0)
+ strscpy(result, maxsize, val);
+ else
+ result[0] = '\0';
+ log_debug("value '[%s/%s]%s' is '%s'", subsys, sysname, attr, result);
+ } else {
+ r = sd_device_get_syspath(dev, &val);
+ if (r < 0)
+ return r;
+
+ strscpyl(result, maxsize, val, attr ? "/" : NULL, attr ?: NULL, NULL);
+ log_debug("path '[%s/%s]%s' is '%s'", subsys, sysname, strempty(attr), result);
+ }
+ return 0;
+}