diff --git a/man/udevadm.xml b/man/udevadm.xml index 33af155021..95b8da2b0a 100644 --- a/man/udevadm.xml +++ b/man/udevadm.xml @@ -48,6 +48,11 @@ udevadm test-builtin options command devpath + + udevadm verify + options + file + udevadm wait options device|syspath @@ -737,6 +742,37 @@ + + udevadm verify + <arg choice="opt"><replaceable>options</replaceable></arg> + <arg choice="plain" rep="repeat"><replaceable>file</replaceable></arg> + … + + + Verify syntactic and semantic correctness of udev rules files. + + Positional arguments should be used to specify one or more files to check. + + The exit status is 0 if all specified udev rules files + are syntactically and semantically correct, and a non-zero error code otherwise. + + + + + + + Specify when udevadm should resolve names of users + and groups. When set to early (the + default), names will be resolved when the rules are + parsed. When set to never, names will + never be resolved. + + + + + + + udevadm wait <arg choice="opt"><replaceable>options</replaceable></arg> diff --git a/meson.build b/meson.build index ff8706d94c..6885884a00 100644 --- a/meson.build +++ b/meson.build @@ -3847,6 +3847,13 @@ udevadm = executable( install_dir : rootbindir) public_programs += udevadm +if want_tests != 'false' + test('udev-rules-check', + udevadm, + suite : 'dist-check', + args : ['verify', '--resolve-names=never', all_rules]) +endif + if conf.get('ENABLE_REPART') == 1 exe = executable( 'systemd-repart', diff --git a/rules.d/60-persistent-storage.rules b/rules.d/60-persistent-storage.rules.in similarity index 99% rename from rules.d/60-persistent-storage.rules rename to rules.d/60-persistent-storage.rules.in index 3265cd23c5..498b24c22d 100644 --- a/rules.d/60-persistent-storage.rules +++ b/rules.d/60-persistent-storage.rules.in @@ -106,6 +106,7 @@ ENV{DEVTYPE}=="partition", ENV{ID_PATH_ATA_COMPAT}=="?*", SYMLINK+="disk/by-path KERNEL=="vd*[!0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}" KERNEL=="vd*[0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}-part%n" +{% if HAVE_BLKID %} # allow admin to disable probing the filesystem for slow devices like floppy disk drives ENV{UDEV_DISABLE_PERSISTENT_STORAGE_BLKID_FLAG}=="1", GOTO="persistent_storage_blkid_probe_end" @@ -120,6 +121,7 @@ KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ENV{ID_CDROM_MEDIA_TRACK_COUNT_DAT KERNEL!="sr*|mmcblk[0-9]boot[0-9]", IMPORT{builtin}="blkid" LABEL="persistent_storage_blkid_probe_end" +{% endif %} # by-label/by-uuid links (filesystem metadata) ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_UUID_ENC}=="?*", SYMLINK+="disk/by-uuid/$env{ID_FS_UUID_ENC}" diff --git a/rules.d/meson.build b/rules.d/meson.build index cba9dd4cc4..09edd58da2 100644 --- a/rules.d/meson.build +++ b/rules.d/meson.build @@ -15,7 +15,6 @@ rules = [ '60-input-id.rules', '60-persistent-alsa.rules', '60-persistent-input.rules', - '60-persistent-storage.rules', '60-persistent-storage-tape.rules', '60-persistent-v4l.rules', '60-sensor.rules', @@ -53,6 +52,7 @@ endforeach rules_in = [ ['50-udev-default.rules'], + ['60-persistent-storage.rules'], ['64-btrfs.rules'], ['99-systemd.rules'], diff --git a/shell-completion/bash/udevadm b/shell-completion/bash/udevadm index 99d4aa55c9..b6e14e1d36 100644 --- a/shell-completion/bash/udevadm +++ b/shell-completion/bash/udevadm @@ -64,11 +64,12 @@ _udevadm() { [MONITOR_ARG]='-s --subsystem-match -t --tag-match' [TEST]='-a --action -N --resolve-names' [TEST_BUILTIN]='-a --action' + [VERIFY]='-N --resolve-names' [WAIT]='-t --timeout --initialized=no --removed --settle' [LOCK]='-t --timeout -d --device -b --backing -p --print' ) - local verbs=(info trigger settle control monitor test-builtin test wait lock) + local verbs=(info trigger settle control monitor test-builtin test verify wait lock) local builtins=(blkid btrfs hwdb input_id keyboard kmod net_id net_setup_link path_id usb_id uaccess) for ((i=0; i < COMP_CWORD; i++)); do @@ -247,6 +248,27 @@ _udevadm() { fi ;; + 'verify') + if __contains_word "$prev" ${OPTS[VERIFY]}; then + case $prev in + -N|--resolve-names) + comps='early never' + ;; + *) + comps='' + ;; + esac + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 + fi + + if [[ $cur = -* ]]; then + comps="${OPTS[COMMON]} ${OPTS[VERIFY]}" + else + comps=$( compgen -A file -- "$cur" ) + fi + ;; + 'wait') if __contains_word "$prev" ${OPTS[WAIT]}; then case $prev in diff --git a/shell-completion/zsh/_udevadm b/shell-completion/zsh/_udevadm index f7c3384eae..074d367a9d 100644 --- a/shell-completion/zsh/_udevadm +++ b/shell-completion/zsh/_udevadm @@ -104,6 +104,14 @@ _udevadm_test-builtin(){ fi } +(( $+functions[_udevadm_verify] )) || +_udevadm_verify(){ + _arguments \ + {-N+,--resolve-names=}'[When to resolve names.]:resolve:(early never)' \ + {-h,--help}'[Print help text.]' \ + '*::files:_files' +} + (( $+functions[_udevadm_wait] )) || _udevadm_wait(){ _arguments \ @@ -154,6 +162,7 @@ _udevadm_commands(){ 'monitor:listen to kernel and udev events' 'test:test an event run' 'test-builtin:test a built-in command' + 'verify:verify udev rules files' 'wait:wait for devices or device symlinks being created' 'lock:lock a block device and run a comand' ) diff --git a/src/udev/meson.build b/src/udev/meson.build index 3f4ab00ac9..1cac581e7f 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -12,6 +12,7 @@ udevadm_sources = files( 'udevadm-test-builtin.c', 'udevadm-trigger.c', 'udevadm-util.c', + 'udevadm-verify.c', 'udevadm-wait.c', 'udevd.c', ) diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c index 68c4bc903c..9d831a6004 100644 --- a/src/udev/udev-rules.c +++ b/src/udev/udev-rules.c @@ -137,6 +137,7 @@ typedef enum { LINE_HAS_GOTO = 1 << 3, /* has GOTO= */ LINE_HAS_LABEL = 1 << 4, /* has LABEL= */ LINE_UPDATE_SOMETHING = 1 << 5, /* has other TK_A_* or TK_M_IMPORT tokens */ + LINE_IS_REFERENCED = 1 << 6, /* is referenced by GOTO */ } UdevRuleLineType; typedef struct UdevRuleFile UdevRuleFile; @@ -172,6 +173,7 @@ struct UdevRuleLine { struct UdevRuleFile { char *filename; UdevRuleLine *current_line; + unsigned issues; LIST_HEAD(UdevRuleLine, rule_lines); LIST_FIELDS(UdevRuleFile, rule_files); }; @@ -190,12 +192,14 @@ struct UdevRules { #define log_rule_full_errno_zerook(device, rules, level, error, fmt, ...) \ ({ \ UdevRules *_r = (rules); \ + int _lv = (level); \ UdevRuleFile *_f = _r ? _r->current_file : NULL; \ UdevRuleLine *_l = _f ? _f->current_line : NULL; \ const char *_n = _f ? _f->filename : NULL; \ \ + rule_file_mark_issue(_f, _lv); \ log_device_full_errno_zerook( \ - device, level, error, "%s:%u " fmt, \ + device, _lv, error, "%s:%u " fmt, \ strna(_n), _l ? _l->line_number : 0, \ ##__VA_ARGS__); \ }) @@ -254,6 +258,48 @@ struct UdevRules { "Invalid value \"%s\" for %s (char %zu: %s), ignoring.", \ value, key, offset, hint) +#define log_line_full_errno_zerook(rule_file, line_nr, level, error, fmt, ...) \ + ({ \ + UdevRuleFile *_f = (rule_file); \ + int _lv = (level); \ + const char *_n = _f ? _f->filename : NULL; \ + \ + rule_file_mark_issue(_f, _lv); \ + log_device_full_errno_zerook( \ + NULL, _lv, error, "%s:%u " fmt, \ + strna(_n), line_nr, \ + ##__VA_ARGS__); \ + }) + +#define log_line_full_errno(rule_file, line_nr, level, error, fmt, ...) \ + ({ \ + int _error = (error); \ + ASSERT_NON_ZERO(_error); \ + log_line_full_errno_zerook( \ + rule_file, line_nr, level, _error, \ + fmt, ##__VA_ARGS__); \ + }) + +#define log_line_full(rule_file, line_nr, level, ...) \ + (void) log_line_full_errno_zerook(rule_file, line_nr, level, 0, __VA_ARGS__) + +#define log_line_debug(rule_file, line_nr, ...) log_line_full(rule_file, line_nr, LOG_DEBUG, __VA_ARGS__) +#define log_line_info(rule_file, line_nr, ...) log_line_full(rule_file, line_nr, LOG_INFO, __VA_ARGS__) +#define log_line_notice(rule_file, line_nr, ...) log_line_full(rule_file, line_nr, LOG_NOTICE, __VA_ARGS__) +#define log_line_warning(rule_file, line_nr, ...) log_line_full(rule_file, line_nr, LOG_WARNING, __VA_ARGS__) +#define log_line_error(rule_file, line_nr, ...) log_line_full(rule_file, line_nr, LOG_ERR, __VA_ARGS__) + +#define log_line_debug_errno(rule_file, line_nr, error, ...) log_line_full_errno(rule_file, line_nr, LOG_DEBUG, error, __VA_ARGS__) +#define log_line_info_errno(rule_file, line_nr, error, ...) log_line_full_errno(rule_file, line_nr, LOG_INFO, error, __VA_ARGS__) +#define log_line_notice_errno(rule_file, line_nr, error, ...) log_line_full_errno(rule_file, line_nr, LOG_NOTICE, error, __VA_ARGS__) +#define log_line_warning_errno(rule_file, line_nr, error, ...) log_line_full_errno(rule_file, line_nr, LOG_WARNING, error, __VA_ARGS__) +#define log_line_error_errno(rule_file, line_nr, error, ...) log_line_full_errno(rule_file, line_nr, LOG_ERR, error, __VA_ARGS__) + +static void rule_file_mark_issue(UdevRuleFile *rule_file, uint8_t log_level) { + if (rule_file) + rule_file->issues |= (1U << log_level); +} + static void log_unknown_owner(sd_device *dev, UdevRules *rules, int error, const char *entity, const char *name) { if (IN_SET(abs(error), ENOENT, ESRCH)) log_rule_error(dev, rules, "Unknown %s '%s', ignoring", entity, name); @@ -1146,20 +1192,23 @@ static void rule_resolve_goto(UdevRuleFile *rule_file) { LIST_FOREACH(rule_lines, i, line->rule_lines_next) if (streq_ptr(i->label, line->goto_label)) { line->goto_line = i; + SET_FLAG(i->type, LINE_IS_REFERENCED, true); break; } if (!line->goto_line) { - log_error("%s:%u: GOTO=\"%s\" has no matching label, ignoring", - rule_file->filename, line->line_number, line->goto_label); + log_line_error(rule_file, line->line_number, + "GOTO=\"%s\" has no matching label, ignoring", + line->goto_label); SET_FLAG(line->type, LINE_HAS_GOTO, false); line->goto_label = NULL; - if ((line->type & ~LINE_HAS_LABEL) == 0) { - log_notice("%s:%u: The line takes no effect any more, dropping", - rule_file->filename, line->line_number); - if (line->type == LINE_HAS_LABEL) + if ((line->type & ~(LINE_HAS_LABEL|LINE_IS_REFERENCED)) == 0) { + log_line_notice(rule_file, line->line_number, + "The line takes no effect any more, dropping"); + /* LINE_IS_REFERENCED implies LINE_HAS_LABEL */ + if (line->type & LINE_HAS_LABEL) udev_rule_line_clear_tokens(line); else udev_rule_line_free(line); @@ -1230,6 +1279,7 @@ int udev_rules_parse_file(UdevRules *rules, const char *filename) { line_nr++; line = skip_leading_chars(buf, NULL); + /* Lines beginning with '#' are ignored regardless of line continuation. */ if (line[0] == '#') continue; @@ -1263,7 +1313,7 @@ int udev_rules_parse_file(UdevRules *rules, const char *filename) { } if (ignore_line) - log_error("%s:%u: Line is too long, ignored", filename, line_nr); + log_line_error(rule_file, line_nr, "Line is too long, ignored"); else if (len > 0) (void) rule_add_line(rules, line, line_nr); @@ -1271,10 +1321,35 @@ int udev_rules_parse_file(UdevRules *rules, const char *filename) { ignore_line = false; } + if (continuation) + log_line_error(rule_file, line_nr, + "Unexpected EOF after line continuation, line ignored"); + rule_resolve_goto(rule_file); return 0; } +static void udev_check_rule_line(UdevRuleFile *rule_file, const UdevRuleLine *line) { + /* check for unused labels */ + if (FLAGS_SET(line->type, LINE_HAS_LABEL) && + !FLAGS_SET(line->type, LINE_IS_REFERENCED)) + log_line_warning(rule_file, line->line_number, + "LABEL=\"%s\" is unused.", line->label); +} + +unsigned udev_check_current_rule_file(UdevRules *rules) { + assert(rules); + + UdevRuleFile *rule_file = rules->current_file; + if (!rule_file) + return 0; + + LIST_FOREACH(rule_lines, line, rule_file->rule_lines) + udev_check_rule_line(rule_file, line); + + return rule_file->issues; +} + UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing) { assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX); diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h index 860fe7c8e4..44f30e1c13 100644 --- a/src/udev/udev-rules.h +++ b/src/udev/udev-rules.h @@ -18,6 +18,7 @@ typedef enum { } UdevRuleEscapeType; int udev_rules_parse_file(UdevRules *rules, const char *filename); +unsigned udev_check_current_rule_file(UdevRules *rules); UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing); int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing); UdevRules *udev_rules_free(UdevRules *rules); diff --git a/src/udev/udevadm-verify.c b/src/udev/udevadm-verify.c new file mode 100644 index 0000000000..8819b07238 --- /dev/null +++ b/src/udev/udevadm-verify.c @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "log.h" +#include "pretty-print.h" +#include "strv.h" +#include "udev-rules.h" +#include "udevadm.h" + +static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("udevadm", "8", &link); + if (r < 0) + return log_oom(); + + printf("%s verify [OPTIONS] FILE...\n" + "\n%sVerify udev rules files.%s\n\n" + " -h --help Show this help\n" + " -V --version Show package version\n" + " -N --resolve-names=early|never When to resolve names\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "resolve-names", required_argument, NULL, 'N' }, + {} + }; + + int c; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0) + switch (c) { + case 'h': + return help(); + case 'V': + return print_version(); + case 'N': + arg_resolve_name_timing = resolve_name_timing_from_string(optarg); + if (arg_resolve_name_timing < 0) + return log_error_errno(arg_resolve_name_timing, + "--resolve-names= takes \"early\" or \"never\""); + /* + * In the verifier "late" has the effect of "never", + * and "never" would generate irrelevant diagnostics, + * so map "never" to "late". + */ + if (arg_resolve_name_timing == RESOLVE_NAME_NEVER) + arg_resolve_name_timing = RESOLVE_NAME_LATE; + break; + case '?': + return -EINVAL; + default: + assert_not_reached(); + } + + if (optind == argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No rules file specified."); + + return 1; +} + +static int verify_rules_file(UdevRules *rules, const char *fname) { + int r; + + r = udev_rules_parse_file(rules, fname); + if (r < 0) + return log_error_errno(r, "Failed to parse rules file %s: %m", fname); + + unsigned issues = udev_check_current_rule_file(rules); + unsigned mask = (1U << LOG_ERR) | (1U << LOG_WARNING); + if (issues & mask) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s: udev rules check failed", fname); + + return 0; +} + +static int verify_rules(UdevRules *rules, char **files) { + int r, rv = 0; + + STRV_FOREACH(fp, files) { + r = verify_rules_file(rules, *fp); + if (r < 0 && rv >= 0) + rv = r; + } + + return rv; +} + +int verify_main(int argc, char *argv[], void *userdata) { + _cleanup_(udev_rules_freep) UdevRules *rules = NULL; + int r; + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + rules = udev_rules_new(arg_resolve_name_timing); + if (!rules) + return -ENOMEM; + + return verify_rules(rules, strv_skip(argv, optind)); +} diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c index b742c1a287..273d3c5b29 100644 --- a/src/udev/udevadm.c +++ b/src/udev/udevadm.c @@ -25,6 +25,7 @@ static int help(void) { { "monitor", "Listen to kernel and udev events" }, { "test", "Test an event run" }, { "test-builtin", "Test a built-in command" }, + { "verify", "Verify udev rules files" }, { "wait", "Wait for device or device symlink" }, { "lock", "Lock a block device" }, }; @@ -104,6 +105,7 @@ static int udevadm_main(int argc, char *argv[]) { { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main }, { "wait", VERB_ANY, VERB_ANY, 0, wait_main }, { "lock", VERB_ANY, VERB_ANY, 0, lock_main }, + { "verify", VERB_ANY, VERB_ANY, 0, verify_main }, { "version", VERB_ANY, VERB_ANY, 0, version_main }, { "help", VERB_ANY, VERB_ANY, 0, help_main }, {} diff --git a/src/udev/udevadm.h b/src/udev/udevadm.h index 417611affe..7920a70d5b 100644 --- a/src/udev/udevadm.h +++ b/src/udev/udevadm.h @@ -13,6 +13,7 @@ int monitor_main(int argc, char *argv[], void *userdata); int hwdb_main(int argc, char *argv[], void *userdata); int test_main(int argc, char *argv[], void *userdata); int builtin_main(int argc, char *argv[], void *userdata); +int verify_main(int argc, char *argv[], void *userdata); int wait_main(int argc, char *argv[], void *userdata); int lock_main(int argc, char *argv[], void *userdata); diff --git a/test/units/testsuite-17.11.sh b/test/units/testsuite-17.11.sh new file mode 100755 index 0000000000..ed5da16c07 --- /dev/null +++ b/test/units/testsuite-17.11.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -ex +set -o pipefail + +# Test for udevadm verify. + +udevadm verify -h +udevadm verify --help +udevadm verify -V +udevadm verify --version +udevadm verify /dev/null + +# No rules file specified. +(! udevadm verify) +# unrecognized option '--unknown' +(! udevadm verify --unknown) +# option requires an argument -- 'N' +(! udevadm verify -N) +# --resolve-names= takes "early" or "never" +(! udevadm verify -N now) +# option '--resolve-names' requires an argument +(! udevadm verify --resolve-names) +# --resolve-names= takes "early" or "never" +(! udevadm verify --resolve-names=now) +# Failed to parse rules file .: Is a directory +(! udevadm verify .) +# Failed to parse rules file .: Is a directory +(! udevadm verify /dev/null . /dev/null) + +# UDEV_LINE_SIZE 16384 +printf '%16383s\n' ' ' >sample.rules +udevadm verify sample.rules + +# Failed to parse rules file sample.rules: No buffer space available +printf '%16384s\n' ' ' >sample.rules +(! udevadm verify sample.rules) + +printf 'RUN+="/bin/true"%8175s\\\n' ' ' ' ' >sample.rules +echo >>sample.rules +udevadm verify sample.rules + +printf 'RUN+="/bin/true"%8176s\\\n #\n' ' ' ' ' >sample.rules +echo >>sample.rules +cat >exp <<'EOF' +sample.rules:5 Line is too long, ignored +sample.rules: udev rules check failed +EOF +(! udevadm verify sample.rules 2>err) +diff exp err + +printf '\\\n' >sample.rules +cat >exp <<'EOF' +sample.rules:1 Unexpected EOF after line continuation, line ignored +sample.rules: udev rules check failed +EOF +(! udevadm verify sample.rules 2>err) +diff exp err + +test_syntax_error() { + local rule msg + rule="$1"; shift + msg="$1"; shift + + printf '%s\n' "$rule" > sample.rules + cat >exp <<-EOF + sample.rules:1 $msg + sample.rules: udev rules check failed + EOF + (! udevadm verify sample.rules 2>err) + diff exp err +} + +test_syntax_error '=' 'Invalid key/value pair, ignoring.' +test_syntax_error 'ACTION{a}=="b"' 'Invalid attribute for ACTION.' +test_syntax_error 'ACTION:="b"' 'Invalid operator for ACTION.' +test_syntax_error 'ACTION=="b"' 'The line takes no effect, ignoring.' +test_syntax_error 'DEVPATH{a}=="b"' 'Invalid attribute for DEVPATH.' +test_syntax_error 'DEVPATH:="b"' 'Invalid operator for DEVPATH.' +test_syntax_error 'KERNEL{a}=="b"' 'Invalid attribute for KERNEL.' +test_syntax_error 'KERNEL:="b"' 'Invalid operator for KERNEL.' +test_syntax_error 'KERNELS{a}=="b"' 'Invalid attribute for KERNELS.' +test_syntax_error 'KERNELS:="b"' 'Invalid operator for KERNELS.' +test_syntax_error 'SYMLINK{a}=="b"' 'Invalid attribute for SYMLINK.' +test_syntax_error 'SYMLINK:="%?"' 'Invalid value "%?" for SYMLINK (char 1: invalid substitution type), ignoring.' +test_syntax_error 'NAME{a}=="b"' 'Invalid attribute for NAME.' +test_syntax_error 'NAME-="b"' 'Invalid operator for NAME.' +test_syntax_error 'NAME+="a"' "NAME key takes '==', '!=', '=', or ':=' operator, assuming '='." +test_syntax_error 'NAME:=""' 'Ignoring NAME="", as udev will not delete any network interfaces.' +test_syntax_error 'NAME="%k"' 'Ignoring NAME="%k", as it will take no effect.' +test_syntax_error 'ENV=="b"' 'Invalid attribute for ENV.' +test_syntax_error 'ENV{a}-="b"' 'Invalid operator for ENV.' +test_syntax_error 'ENV{a}:="b"' "ENV key takes '==', '!=', '=', or '+=' operator, assuming '='." +test_syntax_error 'ENV{ACTION}="b"' "Invalid ENV attribute. 'ACTION' cannot be set." +test_syntax_error 'CONST=="b"' 'Invalid attribute for CONST.' +test_syntax_error 'CONST{a}=="b"' 'Invalid attribute for CONST.' +test_syntax_error 'CONST{arch}="b"' 'Invalid operator for CONST.' +test_syntax_error 'TAG{a}=="b"' 'Invalid attribute for TAG.' +test_syntax_error 'TAG:="a"' "TAG key takes '==', '!=', '=', or '+=' operator, assuming '='." +test_syntax_error 'TAG="%?"' 'Invalid value "%?" for TAG (char 1: invalid substitution type), ignoring.' +test_syntax_error 'TAGS{a}=="b"' 'Invalid attribute for TAGS.' +test_syntax_error 'TAGS:="a"' 'Invalid operator for TAGS.' +test_syntax_error 'SUBSYSTEM{a}=="b"' 'Invalid attribute for SUBSYSTEM.' +test_syntax_error 'SUBSYSTEM:="b"' 'Invalid operator for SUBSYSTEM.' +test_syntax_error 'SUBSYSTEM=="bus" NAME="b"' '"bus" must be specified as "subsystem".' +test_syntax_error 'SUBSYSTEMS{a}=="b"' 'Invalid attribute for SUBSYSTEMS.' +test_syntax_error 'SUBSYSTEMS:="b"' 'Invalid operator for SUBSYSTEMS.' +test_syntax_error 'DRIVER{a}=="b"' 'Invalid attribute for DRIVER.' +test_syntax_error 'DRIVER:="b"' 'Invalid operator for DRIVER.' +test_syntax_error 'DRIVERS{a}=="b"' 'Invalid attribute for DRIVERS.' +test_syntax_error 'DRIVERS:="b"' 'Invalid operator for DRIVERS.' +test_syntax_error 'ATTR="b"' 'Invalid attribute for ATTR.' +test_syntax_error 'ATTR{%}="b"' 'Invalid attribute "%" for ATTR (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTR{a}-="b"' 'Invalid operator for ATTR.' +test_syntax_error 'ATTR{a}+="b"' "ATTR key takes '==', '!=', or '=' operator, assuming '='." +test_syntax_error 'ATTR{a}="%?"' 'Invalid value "%?" for ATTR (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SYSCTL=""' 'Invalid attribute for SYSCTL.' +test_syntax_error 'SYSCTL{%}="b"' 'Invalid attribute "%" for SYSCTL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SYSCTL{a}-="b"' 'Invalid operator for SYSCTL.' +test_syntax_error 'SYSCTL{a}+="b"' "SYSCTL key takes '==', '!=', or '=' operator, assuming '='." +test_syntax_error 'SYSCTL{a}="%?"' 'Invalid value "%?" for SYSCTL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTRS=""' 'Invalid attribute for ATTRS.' +test_syntax_error 'ATTRS{%}=="b" NAME="b"' 'Invalid attribute "%" for ATTRS (char 1: invalid substitution type), ignoring.' +test_syntax_error 'ATTRS{a}-="b"' 'Invalid operator for ATTRS.' +test_syntax_error 'ATTRS{device/}!="a" NAME="b"' "'device' link may not be available in future kernels." +test_syntax_error 'ATTRS{../}!="a" NAME="b"' 'Direct reference to parent sysfs directory, may break in future kernels.' +test_syntax_error 'TEST{a}=="b"' "Failed to parse mode 'a': Invalid argument" +test_syntax_error 'TEST{0}=="%" NAME="b"' 'Invalid value "%" for TEST (char 1: invalid substitution type), ignoring.' +test_syntax_error 'TEST{0644}="b"' 'Invalid operator for TEST.' +test_syntax_error 'PROGRAM{a}=="b"' 'Invalid attribute for PROGRAM.' +test_syntax_error 'PROGRAM-="b"' 'Invalid operator for PROGRAM.' +test_syntax_error 'PROGRAM=="%" NAME="b"' 'Invalid value "%" for PROGRAM (char 1: invalid substitution type), ignoring.' +test_syntax_error 'IMPORT="b"' 'Invalid attribute for IMPORT.' +test_syntax_error 'IMPORT{a}="b"' 'Invalid attribute for IMPORT.' +test_syntax_error 'IMPORT{a}-="b"' 'Invalid operator for IMPORT.' +test_syntax_error 'IMPORT{file}=="%" NAME="b"' 'Invalid value "%" for IMPORT (char 1: invalid substitution type), ignoring.' +test_syntax_error 'IMPORT{builtin}!="foo"' 'Unknown builtin command: foo' +test_syntax_error 'RESULT{a}=="b"' 'Invalid attribute for RESULT.' +test_syntax_error 'RESULT:="b"' 'Invalid operator for RESULT.' +test_syntax_error 'OPTIONS{a}="b"' 'Invalid attribute for OPTIONS.' +test_syntax_error 'OPTIONS-="b"' 'Invalid operator for OPTIONS.' +test_syntax_error 'OPTIONS!="b"' 'Invalid operator for OPTIONS.' +test_syntax_error 'OPTIONS+="link_priority=a"' "Failed to parse link priority 'a': Invalid argument" +test_syntax_error 'OPTIONS:="log_level=a"' "Failed to parse log level 'a': Invalid argument" +test_syntax_error 'OPTIONS="a" NAME="b"' "Invalid value for OPTIONS key, ignoring: 'a'" +test_syntax_error 'OWNER{a}="b"' 'Invalid attribute for OWNER.' +test_syntax_error 'OWNER-="b"' 'Invalid operator for OWNER.' +test_syntax_error 'OWNER!="b"' 'Invalid operator for OWNER.' +test_syntax_error 'OWNER+="0"' "OWNER key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'OWNER="nosuchuser"' "Unknown user 'nosuchuser', ignoring" +test_syntax_error 'GROUP{a}="b"' 'Invalid attribute for GROUP.' +test_syntax_error 'GROUP-="b"' 'Invalid operator for GROUP.' +test_syntax_error 'GROUP!="b"' 'Invalid operator for GROUP.' +test_syntax_error 'GROUP+="0"' "GROUP key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'GROUP="nosuchuser"' "Unknown group 'nosuchuser', ignoring" +test_syntax_error 'MODE{a}="b"' 'Invalid attribute for MODE.' +test_syntax_error 'MODE-="b"' 'Invalid operator for MODE.' +test_syntax_error 'MODE!="b"' 'Invalid operator for MODE.' +test_syntax_error 'MODE+="0"' "MODE key takes '=' or ':=' operator, assuming '='." +test_syntax_error 'MODE="%"' 'Invalid value "%" for MODE (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SECLABEL="b"' 'Invalid attribute for SECLABEL.' +test_syntax_error 'SECLABEL{a}="%"' 'Invalid value "%" for SECLABEL (char 1: invalid substitution type), ignoring.' +test_syntax_error 'SECLABEL{a}!="b"' 'Invalid operator for SECLABEL.' +test_syntax_error 'SECLABEL{a}-="b"' 'Invalid operator for SECLABEL.' +test_syntax_error 'SECLABEL{a}:="b"' "SECLABEL key takes '=' or '+=' operator, assuming '='." +test_syntax_error 'RUN=="b"' 'Invalid operator for RUN.' +test_syntax_error 'RUN-="b"' 'Invalid operator for RUN.' +test_syntax_error 'RUN="%"' 'Invalid value "%" for RUN (char 1: invalid substitution type), ignoring.' +test_syntax_error 'RUN{builtin}+="foo"' "Unknown builtin command 'foo', ignoring" +test_syntax_error 'GOTO{a}="b"' 'Invalid attribute for GOTO.' +test_syntax_error 'GOTO=="b"' 'Invalid operator for GOTO.' +test_syntax_error 'NAME="a" GOTO="b"' 'GOTO="b" has no matching label, ignoring' +test_syntax_error 'GOTO="a" GOTO="b" +LABEL="a"' 'Contains multiple GOTO keys, ignoring GOTO="b".' +test_syntax_error 'LABEL{a}="b"' 'Invalid attribute for LABEL.' +test_syntax_error 'LABEL=="b"' 'Invalid operator for LABEL.' +test_syntax_error 'LABEL="b"' 'LABEL="b" is unused.' +test_syntax_error 'a="b"' "Invalid key 'a'" + +echo 'GOTO="a"' >sample.rules +cat >exp <<'EOF' +sample.rules:1 GOTO="a" has no matching label, ignoring +sample.rules:1 The line takes no effect any more, dropping +sample.rules: udev rules check failed +EOF +(! udevadm verify sample.rules 2>err) +diff exp err + +cat >sample.rules <<'EOF' +GOTO="a" +LABEL="a" +EOF +udevadm verify sample.rules + +cat >sample.rules <<'EOF' +GOTO="b" +LABEL="b" +LABEL="b" +EOF +cat >exp <<'EOF' +sample.rules:3 LABEL="b" is unused. +sample.rules: udev rules check failed +EOF +(! udevadm verify sample.rules 2>err) +diff exp err + +exit 0